weex更新方案探索(三)

created by zhenggl

weex更新方案的探索,总结归档成系列文章:


1. weex更新方案探索(一)【weex更新方案整体思路】
2. weex更新方案探索(二)【weex更新方案vue端实现】
3. weex更新方案探索(三)【weex更新方案IOS端实现】【本篇】
4. weex更新方案探索(四)【weex更新方案Android端实现】
5. weex更新方案探索(五)【weex更新方案服务器端实现】
6. weex更新方案探索(六)【创建工具构建版本配置文件】
7. weex更新方案探索(七)【遗留问题或后续工作】


weex更新方案探索(三) ——weex更新方案IOS端实现

前提

IOS工程配置信息,参考《第一个weex程序》中 配置IOS环境 的部分。
不同之处,本篇需要使用在线js文件,config.xml作如下修改:

<!--////////////////////////【重要】////////////////////////////////////-->
    <!-- 本地 或 在线资源 开关: true使用本地,false使用在线 -->
    <preference name="launch_locally" value="false" />
<!--///////////////////////////////////////////////////////////////////-->


<!--////////////////////////【在线】////////////////////////////////////-->
    <!-- launch_locally:false 使用在线的资源:launch_url不为空,使用launch_url的启动地址;launch_url为空,使用online_start_url:+port -->
    <!-- 公网地址 -->
    <preference name="launch_url" value="http://www.xxx.com/xxx/dist/xxx/login/auto_login.js"/>

目的

C端要实现公用缓存,下载、进度、解压,页面跳转,页面刷新,防篡改机制。
故在原项目WXEventModule中添加相应的方法提供B端调用。


B端如何与C端交互?

这任务就交给我吧——WXEventModule!!

添加以下方法,暴露给B端调用,需要注意的:涉及多次回调结果给B疯掉的,callback要定义为WXModuleKeepAliveCallback。

说明:
涉及到的:LoaderViewController,LandscapeLoaderViewController,是基于WXDemoViewController自定义的扩展横竖屏处理和url拼装等的ViewController,限于篇幅,本文不罗列。
涉及到的:Singleton.h,可自行谷歌或百度一下。
涉及到的:DownloadUtil,FileUtil,ZipUtil,本文会在后面篇幅贴出代码。

WX_EXPORT_METHOD(@selector(openURL:))
- (void)openURL:(id)params {
    [self jumpDeal:params callback:nil isLandscape:NO isJumpToRoot:NO];
}

WX_EXPORT_METHOD(@selector(openURL:callback:))
- (void)openURL:(id)params callback:(WXModuleCallback)callback {
    [self jumpDeal:params callback:callback isLandscape:NO isJumpToRoot:NO];
}

WX_EXPORT_METHOD(@selector(openURLtoLandscape:))
- (void)openURLtoLandscape:(id)params {
    [self jumpDeal:params callback:nil isLandscape:YES isJumpToRoot:NO];
}

WX_EXPORT_METHOD(@selector(openURLtoLandscape:callback:))
- (void)openURLtoLandscape:(id)params callback:(WXModuleCallback)callback {
    [self jumpDeal:params callback:callback isLandscape:YES isJumpToRoot:NO];
}

//设置根导航
WX_EXPORT_METHOD(@selector(openURLToRoot:))
- (void)openURLToRoot:(id)params {
    NSLog(@"WXEventModule.m --- openURLToRoot 设置导航根控制器");
    [self jumpDeal:params callback:nil isLandscape:NO isJumpToRoot:YES];
}

//设置根导航
WX_EXPORT_METHOD(@selector(openURLToRoot:callback:))
- (void)openURLToRoot:(id)params callback:(WXModuleCallback)callback {
    NSLog(@"WXEventModule.m --- openURLToRoot 设置导航根控制器");
    [self jumpDeal:params callback:callback isLandscape:NO isJumpToRoot:YES];
}

- (void)jumpDeal:(id)params callback:(WXModuleCallback)callback isLandscape:(BOOL)isLandscape isJumpToRoot:(BOOL)isJumpToRoot {
    if ([params isKindOfClass:[NSDictionary class]]) {
        [self jumpPageWithDic:(NSDictionary *)params callback:callback isLandscape:isLandscape isJumpToRoot:isJumpToRoot];
    } else if ([params isKindOfClass:[NSString class]]) {
        [self jumpPageWithUrl:(NSString *)params callback:callback isLandscape:isLandscape isJumpToRoot:isJumpToRoot];
    } else {
        [self dealCallback:callback isSuccess:NO log:@"参数为非字典,也非字符串,不处理"];
    }
}

- (void)jumpPageWithDic:(NSDictionary *)dic callback:(WXModuleCallback)callback isLandscape:(BOOL)isLandscape isJumpToRoot:(BOOL)isJumpToRoot {
    NSString *fileName = dic[@"fileName"];
    NSString *url = dic[@"url"];
    NSString *fileMD5 = dic[@"fileMD5"];

    //获取真实的地址
    NSString *newURL = [self getRealUrlPath:url];

    //可以进行页面渲染
    [self jumpPage:fileName url:newURL fileMD5:fileMD5 isLandscape:isLandscape callback:callback isJumpToRoot:isJumpToRoot];
}

- (void)jumpPageWithUrl:(NSString *)url callback:(WXModuleCallback)callback isLandscape:(BOOL)isLandscape isJumpToRoot:(BOOL)isJumpToRoot {
    //获取真实的地址
    NSString *newURL = [self getRealUrlPath:url];

    //可以进行页面渲染
    [self jumpPage:@"" url:newURL fileMD5:nil isLandscape:isLandscape callback:callback isJumpToRoot:isJumpToRoot];
}

- (void)dealCallback:(WXModuleCallback)callback isSuccess:(BOOL)isSuccess log:(NSString *)log {
    NSLog(@"%@", log);
    if (callback) {
        callback(@{ @"isSuccess": isSuccess ? @"true" : @"false" });
    }
}

- (void)jumpPage:(NSString *)fileName url:(NSString *)url fileMD5:(NSString *)fileMD5 isLandscape:(BOOL)isLandscape callback:(WXModuleCallback)callback isJumpToRoot:(BOOL)isJumpToRoot {
    //判断文件是否可以跳转
    BOOL isCanJump = [self checkPageIsCanJump:fileName url:url fileMD5:fileMD5];

    //是否需要判断,过滤掉不是我们域名的url???
    //

    if (isCanJump) {
        [self dealCallback:callback isSuccess:YES log:@"跳转处理"];

        UIViewController *controller = nil;
        if (!isLandscape) {
            controller = [[LoaderViewController alloc] init];
            ((LoaderViewController *)controller).fileName = fileName;
            ((LoaderViewController *)controller).url = [NSURL URLWithString:url];
        } else {
            controller = [[LandscapeLoaderViewController alloc] init];
            ((LandscapeLoaderViewController *)controller).fileName = fileName;
            ((LandscapeLoaderViewController *)controller).url = [NSURL URLWithString:url];
        }

        if (!isJumpToRoot) {
            //普通跳转
            [[weexInstance.viewController navigationController] pushViewController:controller animated:YES];
        } else {
            //跳转到根页面
            NSMutableArray *arr = [[NSMutableArray alloc]initWithArray:[[weexInstance.viewController navigationController] viewControllers]];
            if (arr != nil && arr.count > 0) {
                NSLog(@"WXEventModule.m --- finish 导航里页面数量1个及以上,可以移除所有页面,再添加当前页面进导航");
                if ([WeexSDKManager isTest]) {
                    NSInteger ac = [arr count];
                    for (NSInteger i = ac-1; i >= 0 ; i--) {
                        UIViewController *tmp = [arr objectAtIndex:i];
                        if ([tmp isKindOfClass:[StartViewController class]]) {
                            //测试模式,不去除测试页面
                            continue;
                        } else {
                            [arr removeObject:tmp];
                        }
                    }
                } else {
                    [arr removeAllObjects];
                }

                [arr addObject:controller];
                [[weexInstance.viewController navigationController] setViewControllers:arr];
            } else {
                NSLog(@"WXEventModule.m --- finish 导航里页面数量没有1个及以上,不移除所有页面,添加当前页面进导航");
                [arr addObject:controller];
                [[weexInstance.viewController navigationController] setViewControllers:arr];
            }
        }
    } else {
        [self dealCallback:callback isSuccess:NO log:@"不处理该跳转"];
    }
}

- (NSString *)getRealUrlPath:(NSString *)url {
    NSString *newURL = url;
    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *tmpPrefix = @"weex://";
    //if ([newURL hasPrefix:sandboxPath]) {
    if ([newURL hasPrefix:tmpPrefix]) {
        //本地文件
        //newURL = [NSString stringWithFormat:@"file://%@", url];
        newURL = [newURL substringFromIndex:tmpPrefix.length]; //去除前缀标识
        BOOL isHas = NO;
        if ([[sandboxPath substringFromIndex:sandboxPath.length-1] isEqualToString:@"/"]) {
            //最后有/
            isHas = YES;
            sandboxPath = [sandboxPath substringToIndex:sandboxPath.length-1]; //去除最后的/
        }
        if ([[newURL substringToIndex:1] isEqualToString:@"/"]) {
            //前面有/,不用重复添加
        } else {
            //前面没有/,需要添加/
            newURL = [NSString stringWithFormat:@"/%@", newURL];
        }

        //组装文件路径
        newURL = [NSString stringWithFormat:@"file://%@%@", sandboxPath, newURL]; //拼装成沙盒全路径
    } else if ([newURL hasPrefix:@"file://"]) {
        //本地文件,不用处理
    } else {
        //在线文件
        if ([url hasPrefix:@"//"]) {
            newURL = [NSString stringWithFormat:@"http:%@", url];
        } else if (![url hasPrefix:@"http"]) {
            // relative path
            newURL = [NSURL URLWithString:url relativeToURL:weexInstance.scriptURL].absoluteString;
        }
    }

    return newURL;
}

- (BOOL)checkPageIsCanJump:(NSString *)fileName url:(NSString *)url fileMD5:(NSString *)fileMD5 {
    //判断本地文件的MD5
    BOOL isCanJump = YES;
    NSString *localFilePrefix = @"file://";
    if ([url hasPrefix:localFilePrefix] && fileMD5 && fileMD5.length > 0) {
        //本地文件
        NSString *filePath = [url substringFromIndex:localFilePrefix.length];
        //要将后面?xxx=xxx的去掉
        NSRange r = [filePath rangeOfString:@"?"];
        if (r.location > 0 && r.location < filePath.length && r.length > 0) {
            filePath = [filePath substringToIndex:r.location];
        }
        NSString *locaMD5 = [NSString fileMD5:filePath];
        NSLog(@"fileMD5=%@", fileMD5);
        NSLog(@"locaMD5=%@", locaMD5);
        if ([fileMD5 isEqualToString:locaMD5]) {
            //MD5值一致,文件没被篡改,可以执行
            NSLog(@"!!!!!!!! !!!!!! !!! !! !MD5值一致,文件没被篡改,可以执行");
            isCanJump = YES;
        } else {
            //MD5值不一致,文件被篡改,不执行
            NSLog(@"!!!!!!!! !!!!!! !!! !! !MD5值不一致,文件被篡改,不执行");
            isCanJump = NO;
        }
    } else {
        NSLog(@"不是本地文件,或传入MD5为空,不进行MD5判断");
    }

    return isCanJump;
}


//获取当前页面的fileName
WX_EXPORT_METHOD(@selector(getCurrentPageFileName:))
- (void)getCurrentPageFileName:(WXModuleCallback)callback {
    NSString *isSuccess = @"false";
    NSString *fileName = @"";
    UIViewController *vc = [[weexInstance.viewController navigationController].viewControllers lastObject];
    if (vc) {
        if ([vc isKindOfClass:[LoaderViewController class]]) {
            fileName = ((LoaderViewController *)vc).fileName ? ((LoaderViewController *)vc).fileName : @"";
        } else if ([vc isKindOfClass:[LandscapeLoaderViewController class]]) {
            fileName = ((LandscapeLoaderViewController *)vc).fileName ? ((LandscapeLoaderViewController *)vc).fileName : @"";
        }

        if (fileName.length > 0) {
            isSuccess = @"true";
        }
    }

    if (callback) {
        callback(@{ @"isSuccess": isSuccess, @"fileName": fileName });
    }
}

//更新当前页面
WX_EXPORT_METHOD(@selector(refreshCurrectPage:callback:))
- (void)refreshCurrectPage:(NSDictionary *)params callback:(WXModuleCallback)callback {
    NSString *fileName = [params objectForKey:@"fileName"];
    NSString *url = [params objectForKey:@"url"];
    NSString *fileMD5 = [params objectForKey:@"fileMD5"];

    //url = @"weex:///web/dist/my.js";
    //fileMD5 = @"F280AE512A7F1F6D4E7ABF076C0E61A8";

    NSString *newURL = [self getRealUrlPath:url];

    UIViewController *vc = [[weexInstance.viewController navigationController].viewControllers lastObject];
    [self refreshPage:vc fileName:fileName url:newURL fileMD5:fileMD5 callback:callback];
}

//更新导航里所有页面
WX_EXPORT_METHOD(@selector(refreshAllPages:callback:))
- (void)refreshAllPages:(NSDictionary *)params callback:(WXModuleCallback)callback {
    NSArray *fileNames = [params objectForKey:@"fileNames"];
    NSArray *urls = [params objectForKey:@"urls"];
    NSArray *fileMD5s = [params objectForKey:@"fileMD5s"];

    NSArray *vcs = [weexInstance.viewController navigationController].viewControllers;
    if (vcs && [vcs count] > 0) {
        for (NSInteger j = [vcs count]-1; j >= 0; j--) {
            NSString *n = @"";
            UIViewController *v = [vcs objectAtIndex:j];
            if (v) {
                if ([v isKindOfClass:[LoaderViewController class]]) {
                    n = ((LoaderViewController *)v).fileName;
                } else if ([v isKindOfClass:[LandscapeLoaderViewController class]]) {
                    n = ((LandscapeLoaderViewController *)v).fileName;
                }

                if (n.length > 0) {
                    //处理
                    for (NSInteger i = [fileNames count]-1; i >= 0; i--) {
                        NSString *fileName = [fileNames objectAtIndex:i];
                        NSString *url = [urls objectAtIndex:i];

                        if ([n isEqualToString:fileName]) {
                            //匹配上了,需要更新该v
                            NSString *newURL = [self getRealUrlPath:url];
                            [self refreshPage:v fileName:fileName url:newURL fileMD5:[fileMD5s objectAtIndex:i] callback:^(id result) {
                                //
                            }];
                            break;
                        }
                    }
                } else {
                    //不处理
                    continue;
                }
            } else {
                //不处理
                continue;
            }
        }
    }
}

- (void)refreshPage:(UIViewController *)vc fileName:(NSString *)fileName url:(NSString *)url fileMD5:(NSString *)fileMD5 callback:(WXModuleCallback)callback {
    //判断文件是否可以跳转
    BOOL isCanRefresh = [self checkPageIsCanJump:fileName url:url fileMD5:fileMD5];

    //是否需要判断,过滤掉不是我们域名的url???
    //

    NSString *dealString = @"跳转处理";
    NSString *undealString = @"不处理该跳转";
    BOOL isSuccess = NO;
    BOOL isLandscape = NO;

    if (isCanRefresh) {
        if (vc) {
            if ([vc isKindOfClass:[LoaderViewController class]]) {
                if ([((LoaderViewController *)vc).fileName isEqualToString:fileName]) {
                    isSuccess = YES;
                }
            } else if ([vc isKindOfClass:[LandscapeLoaderViewController class]]) {
                isLandscape = YES;
                if ([((LandscapeLoaderViewController *)vc).fileName isEqualToString:fileName]) {
                    isSuccess = YES;
                }
            }
        }
    }

    //先回调通知B端
    [self dealCallback:callback isSuccess:isSuccess log:(isSuccess ? dealString : undealString)];

    //再处理页面刷新
    if (isSuccess) {
        if (!isLandscape) {
            [((LoaderViewController *)vc) refreshPageWithUrl:url];
        } else {
            [((LandscapeLoaderViewController *)vc) refreshPageWithUrl:url];
        }
    }
}

//下载文件
WX_EXPORT_METHOD(@selector(downloadFiles:callback:))
- (void)downloadFiles:(NSDictionary *)params callback:(WXModuleKeepAliveCallback)callback {
    NSArray *files = params[@"files"];
    NSString *serverUrl = params[@"serverUrl"];
    BOOL isUnzip = NO;
    if (params[@"isUnzip"]) {
        NSNumber *num = (NSNumber *)params[@"isUnzip"];
        if (num) {
            isUnzip = [num boolValue];
        }
    }
    [[DownloadUtil sharedInstance]downloadFiles:files
                                      serverUrl:serverUrl
                                        isUnzip:isUnzip
                                          block:^(BOOL result, NSInteger successCount, NSInteger allCount, NSArray *savePaths, BOOL isUnziped) {
        callback(@{ @"result": @"success",@"data":@{ @"isSuccess": [NSNumber numberWithBool:result], @"successCount": [NSNumber numberWithInteger:successCount], @"allCount": [NSNumber numberWithInteger:allCount], @"savePaths": savePaths, @"isUnziped": [NSNumber numberWithBool:isUnziped]}}, false);
    }
                                  progressBlock:^(double progress) {
        callback(@{ @"result": @"progress", @"data": @{ @"progress": @(progress) } }, true);
    }];
}

//解压文件
WX_EXPORT_METHOD(@selector(unzipFile:callback:))
- (void)unzipFile:(NSString *)file callback:(WXModuleKeepAliveCallback)callback {
    [[ZipUtil sharedInstance] unzipFile:file block:^(BOOL result) {
        callback(@{ @"result": @"success", @"data": @{ @"isSuccess": [NSNumber numberWithBool:result]}}, false);
    }];
}



//获取全局缓存
WX_EXPORT_METHOD(@selector(getGlobelCaches:))
- (void)getGlobelCaches:(WXModuleCallback)callback {
    NSDictionary *dic = [AppDelegate shareInstance].globelCaches;
    if (dic && dic.count > 0) {
        callback(@{ @"result": @"success", @"data": [AppDelegate shareInstance].globelCaches });
    } else {
        //获取为空,B端已经做了从storage中获取的处理,此处理不处理

        callback(@{ @"result": @"success", @"data": [AppDelegate shareInstance].globelCaches });
    }
}

//设置/更新全局缓存
WX_EXPORT_METHOD(@selector(setGlobelCaches:callback:))
- (void)setGlobelCaches:(NSDictionary *)params callback:(WXModuleCallback)callback {
    [AppDelegate shareInstance].globelCaches = [[NSMutableDictionary alloc]initWithDictionary:params];

    //B端已经做了从storage更新数据的处理,此处不处理
    callback(@{ @"result": @"success", @"data": [AppDelegate shareInstance].globelCaches });
}



//获取Web版本号
WX_EXPORT_METHOD(@selector(getWebVersion:))
- (void)getWebVersion:(WXModuleCallback)callback {
    NSString *version = [AppDelegate shareInstance].globelWebVersion;
    if (version && version.length > 0) {
        callback(@{ @"result": @"success", @"data": version });
    } else {
        //获取为空,B端已经做了从storage中获取的处理,此处理不处理

        callback(@{ @"result": @"success", @"data": @"" });
    }
}

//设置/更新Web版本号
WX_EXPORT_METHOD(@selector(setWebVersion:callback:))
- (void)setWebVersion:(NSString *)version callback:(WXModuleCallback)callback {
    [AppDelegate shareInstance].globelWebVersion = version;

    //B端已经做了从storage更新数据的处理,此处不处理
    callback(@{ @"result": @"success", @"data": [AppDelegate shareInstance].globelWebVersion });
}

公用缓存

在C端定义整个app生命周期的变量,提供给B端,即可减轻weex大量的storage操作。

在AppDelegate.h中定义:

@property (strong, nonatomic) NSMutableDictionary *globelCaches;
@property (strong, nonatomic) NSString *globelWebVersion;

+(AppDelegate*)shareInstance;

在AppDelegate.m中声明:

+ (AppDelegate *)shareInstance {
    return (AppDelegate *)[[UIApplication sharedApplication] delegate];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.backgroundColor = [UIColor whiteColor];

    [WeexSDKManager setup];

    [self.window makeKeyAndVisible];

    // Override point for customization after application launch.

    self.globelCaches = [[NSMutableDictionary alloc]init];

    return YES;
}

谁来帮忙下载?

交给DownloadUtil!!

下载辅助类,结合传参isUnzip,若只有一个zip文件下载,isUnzip=ture,则会进行解压操作处理,具体B端传参决定。
下载过程中,会回调进度给调用方。

//
//  DownloadUtil.h
//

#import <Foundation/Foundation.h>
#import "Singleton.h"

typedef void(^DownloadUtilBlock)(BOOL result, NSInteger successCount, NSInteger allCount, NSArray *savePath, BOOL isUnziped);
typedef void(^DownloadUtilProgressBlock)(double progress);

@interface DownloadUtil : NSObject
AS_SINGLETON(DownloadUtil)

- (void)downloadFiles:(NSArray *)files
            serverUrl:(NSString *)serverUrl
              isUnzip:(BOOL)isUnzip
                block:(DownloadUtilBlock)block
        progressBlock:(DownloadUtilProgressBlock)progressBlock;

@end
//
//  DownloadUtil.m
//

#import "DownloadUtil.h"
#import "FileUtil.h"

#import "NSString+Extend.h"

#import "ZipUtil.h"

//web目录名称
static NSString *WEB_LOCAL_FOLDER = @"BRLF_WEB";


@interface DownloadUtil()
@property (nonatomic) BOOL isUnzip;
@property (nonatomic, strong) NSString *serverUrl;
@property (nonatomic, strong) NSArray *downloadFiles;
@property (nonatomic) DownloadUtilBlock downloadBlock;
@property (nonatomic) DownloadUtilProgressBlock progressBlock;
@end


@implementation DownloadUtil
DEF_SINGLETON(DownloadUtil);


- (void)downloadFiles:(NSArray *)files
            serverUrl:(NSString *)serverUrl
              isUnzip:(BOOL)isUnzip
                block:(DownloadUtilBlock)block
        progressBlock:(DownloadUtilProgressBlock)progressBlock {
    self.isUnzip = isUnzip;
    self.serverUrl = serverUrl;
    self.downloadFiles = files;
    self.downloadBlock = block;
    self.progressBlock = progressBlock;

    [self performSelector:@selector(deal) withObject:nil afterDelay:0.1];

}

- (void)deal {
    NSArray *files = self.downloadFiles;
    DownloadUtilBlock block = self.downloadBlock;
    DownloadUtilProgressBlock progress = self.progressBlock;
    BOOL unzipFile = self.isUnzip;

    __weak DownloadUtil *weakSelf = self;
    __block NSInteger successCount = 0;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        //将文件下载到沙盒中
        NSMutableArray *savePaths = [[NSMutableArray alloc]init];
//        NSInteger successCount = 0;
        for (NSInteger i = 0; i < files.count; i++) {
//            NSString *file = [files objectAtIndex:i];
            NSDictionary *dic = [files objectAtIndex:i];
            NSString *fileName = dic[@"fileName"];
            NSString *downloadUrl = dic[@"downloadUrl"];
            NSString *savePath = [weakSelf dealDownloadFile:downloadUrl index:i];
            if (savePath && savePath.length > 0) {
                NSString *ex = @"";
                if (savePath.length > 4) {
                    ex = [savePath substringFromIndex:savePath.length-4];
                }
                NSLog(@"ex=%@", ex);
                if (unzipFile && [ex isEqualToString:@".zip"] && files.count == 1) {
                    //特殊处理zip
                    NSLog(@"特殊处理zip");
                    //只处理一个zip的情况,多zip不适用!!!!
                    NSLog(@"只处理一个zip的情况,多zip不适用!!!!");
                    dispatch_async(dispatch_get_main_queue(), ^{
                        BOOL isHasOneFile = NO;
                        if (files.count == 1) {
                            //只有一个文件的情况
                            isHasOneFile = YES;
                            successCount += 1;

                            //回调进度
                            if (progress) {
                                double p = (double)((double)successCount / (double)(files.count + 1));
                                progress(p);
                            }
                        }
                        NSString *file = savePath;
                        [[ZipUtil sharedInstance] unzipFile:file block:^(BOOL unzipResult) {
                            if (unzipResult) {
                                //解压成功
                                NSLog(@"解压成功");
                                NSDictionary *sdic = @{@"fileName": fileName, @"savePath": savePath};
                                [savePaths addObject:sdic];
                                successCount += 1;
                            } else {
                                //解压失败
                                NSLog(@"解压失败");
                            }

                            //回调进度
                            if (progress) {
                                if (isHasOneFile) {
                                    double p = (double)((double)successCount / (double)(files.count + 1));
                                    progress(p);
                                } else {
                                    double p = (double)((double)successCount / (double)files.count);
                                    progress(p);
                                }
                            }

                            //回调反馈结果
                            if (isHasOneFile) {
                                if (successCount > files.count) {
                                    successCount = files.count;
                                }
                            }

                            BOOL result = NO;
                            if (successCount == files.count) {
                                result = YES;
                            }
                            if (block) {
                                block(result, successCount, files.count, savePaths, unzipResult);
                            }
                        }];
                    });

                    return;
                } else {
                    NSDictionary *sdic = @{@"fileName": fileName, @"savePath": savePath};
                    [savePaths addObject:sdic];
                    successCount += 1;
                }
            }

            //回调进度
            if (progress) {
                double p = (double)((double)successCount / (double)files.count);
                progress(p);
            }
        }

        //回调反馈结果
        BOOL result = NO;
        if (successCount == files.count) {
            result = YES;
        }
        if (block) {
            block(result, successCount, files.count, savePaths, NO);
        }
    });
}

- (NSString *)dealDownloadFile:(NSString *)file index:(NSInteger)index {
    NSString *savePath = @"";
    BOOL isDownOk = NO;

    //下载文件到沙盒中
    NSLog(@"=====================dealDownloadFile[%ld] %@", (long)index, file);

    //有下载地址,可以下载处理
    NSLog(@"正在下载第%ld个文件[%@]", (long)index+1, file);

    //下载处理
    NSString *downloadUrl = [NSString stringWithFormat:@"%@%@", self.serverUrl, file];
    NSLog(@"downlaodUrl=%@", downloadUrl);

    NSData *verData = [NSData dataWithContentsOfURL:[NSURL URLWithString:downloadUrl]];
    if (verData) {
        NSLog(@"下载第%ld个文件[%@] 成功", (long)index+1, file);

        //写文件
        savePath = [FileUtil saveDataToSandBox:WEB_LOCAL_FOLDER fileName:file data:verData];

        //test code
        //打印文件的md5
        //NSString *locaMD5 = [NSString fileMD5:savePath];
        //NSLog(@"_________%@->MD5=%@", file, locaMD5);
        //test code

        /
        //取相对路径
        NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
        savePath = [savePath substringFromIndex:sandboxPath.length];
        /

        if (savePath && savePath.length > 0) {
            //保存成功,下载文件成功
            NSLog(@"保存第%ld个文件[%@] 成功", (long)index+1, file);
            isDownOk = YES;
        } else {
            NSLog(@"保存第%ld个文件[%@] 失败", (long)index+1, file);
        }
    } else {
        NSLog(@"下载第%ld个文件[%@] 失败", (long)index+1, file);
    }

//    return isDownOk;
    return savePath;
}

@end

需要文件处理吗?

FileUtil

//
//  FileUtil.h
//

#import <Foundation/Foundation.h>
#import "Singleton.h"

@interface FileUtil : NSObject
AS_SINGLETON(FileUtil)

/**
 *  @author guoli, 17-10-19 17:10:41
 *
 *  @brief 保存数据到沙盒中
 *  @param dir      沙盒中Documents下的目录名称
 *  @param fileName 文件名称
 *  @param data     要保存的数据
 *  @return 保存的文件路径 //保存操作是否成功
 */
+ (NSString *)saveDataToSandBox:(NSString *)dir fileName:(NSString *)fileName data:(NSData *)data;

/**
 *  @author guoli, 17-10-19 17:10:45
 *
 *  @brief 创建目录
 *  @param createDir 要创建的目录名称(路径)
 */
+ (void)createFolder:(NSString *)createDir;

/**
 *  @author guoli, 17-10-19 17:10:24
 *
 *  @brief 删除沙盒中指定目录及其下所有文件
 *  @param dir 目录名称(路径)
 */
+ (void)deleteSandboxFolderAndAllFiles:(NSString *)dir;

/**
 *  @author guoli, 17-10-19 17:10:22
 *
 *  @brief 删除沙盒中指定文件(或目录)
 *  @param dir      目录名称(路径)
 *  @param fileName 文件名称(带后缀)
 */
+ (void)deleteSandboxFile:(NSString *)dir fileName:(NSString *)fileName;

/**
 *  @author guoli, 17-10-19 18:10:31
 *
 *  @brief 检测目录是否存在
 *  @param dir 目录名称(路径)
 *  @return true为存在,false为不存在
 */
+ (BOOL)checkFolderIsExisted:(NSString *)dir;

/**
 *  @author guoli, 17-10-19 17:10:22
 *
 *  @brief 复制指定路径下的文件到另一指定路径下
 *  @param sourcePath 要复制的原文件所在路径
 *  @param toPath     文件要复制到的所在路径
 *  @return 文件复制操作是否成功
 */
+ (BOOL)copyMissingFile:(NSString *)sourcePath toPath:(NSString *)toPath;

/**
 *  @author guoli, 17-10-19 17:10:46
 *
 *  @brief 复制资源文件到沙盒中
 *  @param resourceName 资源文件名称
 *  @param type         资源文件类型(如: wav,mp3,png,jpg,plist,txt...)
 *  @param dir          沙盒Documents下的目录路径
 *  @return 复制操作是否成功
 */
+ (BOOL)copyResourcesToSandbox:(NSString *)resourceName type:(NSString *)type dir:(NSString *)dir;

/**
 *  @author guoli, 17-10-19 17:10:59
 *
 *  @brief 获取沙盒里的文件绝对路径
 *  @param dir      沙盒Documents下的目录路径
 *  @param fileName 文件名称
 *  @return 返回沙盒里该文件的绝对路径
 */
+ (NSString *)getSandBoxFilePath:(NSString *)dir fileName:(NSString *)fileName;

/**
 *  @author guoli, 17-10-19 17:10:59
 *
 *  @brief 从沙盒指定文件中获取Dic字典数据
 *  @param dir      沙盒Documents下的目录路径
 *  @param fileName 文件名称
 *  @return 返回沙盒里该文件的Dic字典数据
 */
+ (NSDictionary *)getJsonDicData:(NSString *)dir fileName:(NSString *)fileName;

/**
 *  @author guoli, 17-10-19 17:10:51
 *
 *  @brief 获取文件的真实路由
 *  @param fileName   文件名称
 *  @param baseUrlStr 路由前缀
 *  @return 返回包含路由前缀的文件的真实路由
 */
+ (NSString *)getRealUrlString:(NSString *)fileName baseUrlStr:(NSString *)baseUrlStr;

@end
//
//  FileUtil.m
//

#import "FileUtil.h"

@implementation FileUtil
DEF_SINGLETON(FileUtil);

/**
 *  @author guoli, 17-10-19 17:10:41
 *
 *  @brief 保存数据到沙盒中
 *  @param dir      沙盒中Documents下的目录名称
 *  @param fileName 文件名称
 *  @param data     要保存的数据
 *  @return 保存的文件路径 //保存操作是否成功
 */
+ (NSString *)saveDataToSandBox:(NSString *)dir fileName:(NSString *)fileName data:(NSData *)data {
    NSString *savePath = @"";
    BOOL result = NO;

    if (data) {
        if (fileName && fileName.length > 0) {
            NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
            NSString *dirPath = @"";
            if (dir && dir.length > 0) {
                dirPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@", dir]];
            } else {
                dirPath = sandboxPath;
            }

            //创建目录
            //NSLog(@"dirPath=%@", dirPath);
            [FileUtil createFolder:dirPath];

            //创建文件名称中所涉及到的目录
            NSString *tmpDirPath = dirPath;
            NSArray *arr = [fileName componentsSeparatedByString:@"/"];
            for (NSInteger i = 0; i < arr.count; i++) {
                NSString *tmpDir = [arr objectAtIndex:i];
                if (i == arr.count-1) {
                    NSRange r = [tmpDir rangeOfString:@"."];
                    if (r.location > 0) {
                        break;
                    }
                }

                NSString *tmpPath = [NSString stringWithFormat:@"%@/%@", tmpDirPath, tmpDir];
                //NSLog(@"tmpPath=%@", tmpPath);
                [FileUtil createFolder:tmpPath];

                tmpDirPath = tmpPath;
            }


            NSString *filePath = [NSString stringWithFormat:@"%@/%@", dirPath, fileName];
            //NSLog(@"filePath=%@", filePath);
            result = [data writeToFile:filePath atomically:YES];
            //NSLog(@"result=%@", result ? @"YES" : @"NO");

            if (result) {
                savePath = filePath;
            }
        }
    }

//    return result;
    return savePath;
}

/**
 *  @author guoli, 17-10-19 17:10:45
 *
 *  @brief 创建目录
 *  @param createDir 要创建的目录名称(路径)
 */
+ (void)createFolder:(NSString *)createDir {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL existed = [fileManager fileExistsAtPath:createDir];
    if (!existed) {
        NSError *error = nil;
        [fileManager createDirectoryAtPath:createDir withIntermediateDirectories:YES attributes:nil error:&error];
    }
}

/**
 *  @author guoli, 17-10-19 17:10:24
 *
 *  @brief 删除沙盒中指定目录及其下所有文件
 *  @param dir 目录名称(路径)
 */
+ (void)deleteSandboxFolderAndAllFiles:(NSString *)dir {
    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *myDirectory = [NSString stringWithFormat:@"%@/%@", sandboxPath, dir];
    NSArray *fileArray = [fileManager subpathsAtPath:myDirectory];
    if (fileArray && fileArray.count > 0) {
        for (NSInteger i = fileArray.count - 1; i >= 0; i--) {
            NSString *fn = [fileArray objectAtIndex:i];
            NSString *fp = [NSString stringWithFormat:@"%@/%@", myDirectory, fn];
            NSLog(@"%@", fp);

            NSError *error = nil;
            [fileManager removeItemAtPath:fp error:&error];
        }
    }
}

/**
 *  @author guoli, 17-10-19 17:10:22
 *
 *  @brief 删除沙盒中指定文件(或目录)
 *  @param dir      目录名称(路径)
 *  @param fileName 文件名称(带后缀)
 */
+ (void)deleteSandboxFile:(NSString *)dir fileName:(NSString *)fileName {
    NSString *realPath = @"";
    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    if (dir && dir.length > 0) {
        if (fileName && fileName.length > 0) {
            realPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@/%@", dir, fileName]];
        } else {
            realPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@", dir]];
        }
    } else {
        if (fileName && fileName.length > 0) {
            realPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@", fileName]];
        } else {
            NSLog(@"文件名为空,不处理!");
        }
    }

    if (realPath && realPath.length > 0) {
        NSError *error = nil;
        [[NSFileManager defaultManager]removeItemAtPath:realPath error:&error];
    }
}

/**
 *  @author guoli, 17-10-19 18:10:31
 *
 *  @brief 检测目录是否存在
 *  @param dir 目录名称(路径)
 *  @return true为存在,false为不存在
 */
+ (BOOL)checkFolderIsExisted:(NSString *)dir {
    BOOL result = NO;

    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *webPath = @"";
    if (dir && dir.length > 0) {
        webPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@/", dir]];
    } else {
        webPath = [sandboxPath stringByAppendingString:@"/"];
    }

    result = [[NSFileManager defaultManager] fileExistsAtPath:webPath];

    return result;
}

/**
 *  @author guoli, 17-10-19 17:10:22
 *
 *  @brief 复制指定路径下的文件到另一指定路径下
 *  @param sourcePath 要复制的原文件所在路径
 *  @param toPath     文件要复制到的所在路径
 *  @return 文件复制操作是否成功
 */
+ (BOOL)copyMissingFile:(NSString *)sourcePath toPath:(NSString *)toPath {
    BOOL result = YES;

    NSString *finalLocation = [[toPath stringByAppendingPathComponent:sourcePath] lastPathComponent];
    NSFileManager *fileManager = [NSFileManager defaultManager];

    if (![fileManager fileExistsAtPath:finalLocation]) {
        NSError *error = nil;
        [fileManager copyItemAtPath:sourcePath toPath:toPath error:&error];
        if (error) {
            result = NO;
            NSLog(@"copyMissingFile error:%ld, %@", (long)error.code, error.userInfo);
        }
    }

    return result;
}

/**
 *  @author guoli, 17-10-19 17:10:46
 *
 *  @brief 复制资源文件到沙盒中
 *  @param resourceName 资源文件名称
 *  @param type         资源文件类型(如: wav,mp3,png,jpg,plist,txt...)
 *  @param dir          沙盒Documents下的目录路径
 *  @return 复制操作是否成功
 */
+ (BOOL)copyResourcesToSandbox:(NSString *)resourceName type:(NSString *)type dir:(NSString *)dir {
    BOOL result = NO;

    NSString *bundlePath = [[NSBundle mainBundle] pathForResource:resourceName ofType:type];
    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *webPath = @"";
    if (dir && dir.length > 0) {
        webPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@", dir]];
    } else {
        webPath = sandboxPath;
    }

    [FileUtil createFolder:webPath];

    result = [FileUtil copyMissingFile:bundlePath toPath:webPath];

    return result;
}

/**
 *  @author guoli, 17-10-19 17:10:59
 *
 *  @brief 获取沙盒里的文件绝对路径
 *  @param dir      沙盒Documents下的目录路径
 *  @param fileName 文件名称
 *  @return 返回沙盒里该文件的绝对路径
 */
+ (NSString *)getSandBoxFilePath:(NSString *)dir fileName:(NSString *)fileName {
    NSString *webPath = @"";

    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    if (dir && dir.length > 0) {
        if ([dir hasSuffix:@"/"]) {
            dir = [dir substringToIndex:dir.length-1];
        }

        if (fileName && fileName.length > 0) {
            if ([fileName hasPrefix:@"/"]) {
                fileName = [fileName substringFromIndex:1];
            }

            webPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@/%@", dir, fileName]];
        } else {
            webPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@", dir]];
        }
    } else {
        if (fileName && fileName.length > 0) {
            if ([fileName hasPrefix:@"/"]) {
                fileName = [fileName substringFromIndex:1];
            }

            webPath = [sandboxPath stringByAppendingString:[NSString stringWithFormat:@"/%@", fileName]];
        } else {
            webPath = [sandboxPath stringByAppendingString:@"/"];
        }
    }

    return webPath;
}

/**
 *  @author guoli, 17-10-19 17:10:59
 *
 *  @brief 从沙盒指定文件中获取Dic字典数据
 *  @param dir      沙盒Documents下的目录路径
 *  @param fileName 文件名称
 *  @return 返回沙盒里该文件的Dic字典数据
 */
+ (NSDictionary *)getJsonDicData:(NSString *)dir fileName:(NSString *)fileName {
    NSDictionary *jsonDic = nil;

    //从沙盒中获取数据
    NSString *sandboxPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *d = dir.length > 0 ? [NSString stringWithFormat:@"/%@", dir] : @"/";
    NSString *webPath = [sandboxPath stringByAppendingString:d];
    NSString *finalLocation = [webPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.json", fileName]].lastPathComponent;
    NSURL *jsUrl = [NSURL fileURLWithPath:finalLocation];

    //josn转对象
    NSData *jsonData = [NSData dataWithContentsOfURL:jsUrl];
    if (jsonData) {
        NSError *error = nil;
        NSDictionary *jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:&error];
        if (jsonObj && !error) {
            jsonDic = jsonObj;
        }
    }

    return jsonDic;
}

/**
 *  @author guoli, 17-10-19 17:10:51
 *
 *  @brief 获取文件的真实路由
 *  @param fileName   文件名称
 *  @param baseUrlStr 路由前缀
 *  @return 返回包含路由前缀的文件的真实路由
 */
+ (NSString *)getRealUrlString:(NSString *)fileName baseUrlStr:(NSString *)baseUrlStr {
    NSString *filePath = @"";

    if ([fileName hasPrefix:baseUrlStr]) {
        filePath = fileName;
    } else {
        filePath = [NSString stringWithFormat:@"%@%@", baseUrlStr, fileName];
    }

    return filePath;
}

@end

需要解压么?

ZipUtil
需要在pod中导入SSZipArchive

def common
    pod 'SSZipArchive'
end

具体代码:

//
//  ZipUtil.h
//

#import <Foundation/Foundation.h>
#import "Singleton.h"

typedef void(^ZipUtilBlock)(BOOL result);

@interface ZipUtil : NSObject
AS_SINGLETON(ZipUtil)

- (void)unzipFile:(NSString *)zipFile block:(ZipUtilBlock)block;

@end
//
//  ZipUtil.m
//  

#import "ZipUtil.h"
#import "FileUtil.h"
#import "ZipArchive.h"

//web目录名称
static NSString *WEB_LOCAL_FOLDER = @"BRLF_WEB";


@interface ZipUtil()
@property (nonatomic) ZipUtilBlock zipBlock;
@end

@implementation ZipUtil
DEF_SINGLETON(ZipUtil);

- (void)unzipFile:(NSString *)zipFile block:(ZipUtilBlock)block {
    self.zipBlock = block;

    [self performSelector:@selector(start:) withObject:zipFile afterDelay:0.1];
}

- (void)start:(NSString *)zipFile {
    __weak ZipUtil *weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        //解压zipFile到沙盒中
        [weakSelf unzipFileToSandBox:zipFile];
    });
}

/**
 *  @author guoli, 17-10-19 16:10:34
 *
 *  @brief 解压zipFile到沙盒中
 */
- (void)unzipFileToSandBox:(NSString *)zipFile {
    NSString *webDir = WEB_LOCAL_FOLDER;

//    NSString *zipPath = [FileUtil getSandBoxFilePath: [NSString stringWithFormat:@"\%@", webDir] fileName:zipFile];
//    NSString *unzipPath = [FileUtil getSandBoxFilePath: [NSString stringWithFormat:@"\%@", webDir] fileName:@""];
    NSString *zipPath = [FileUtil getSandBoxFilePath: @"" fileName:zipFile];
    NSString *unzipPath = [FileUtil getSandBoxFilePath: [NSString stringWithFormat:@"\%@", webDir] fileName:@""];

    BOOL result = [self unzipToPath:unzipPath zipPath:zipPath];
    NSLog(@"解压:%@ %@", webDir, (result ? @"成功" : @"失败"));

    if (self.zipBlock) {
        self.zipBlock(result);
    }
}

/**
 *  @author guoli, 17-10-19 18:10:29
 *
 *  @brief 解压指定压缩文件到指定路径
 *  @param unzipPath 解压后文件存放的路径
 *  @param zipPath   压缩文件的路径
 *  @return 解压成功 或 失败
 */
- (BOOL)unzipToPath:(NSString *)unzipPath zipPath:(NSString *)zipPath {
    BOOL result = NO;

    NSLog(@"zipPath=%@", zipPath);
    NSLog(@"unzipPath=%@", unzipPath);

    result = [SSZipArchive unzipFileAtPath:zipPath toDestination:unzipPath];
    NSLog(@"解压操作:%@", (result ? @"成功" : @"失败"));

    return result;
}

@end

附上扩展类:

NSString+Extend

//
//  NSString+Extend.h
//

#import <Foundation/Foundation.h>

@interface NSString (Extend)

- (NSString *)md5;

//计算文件的MD5值
+ (NSString *)fileMD5:(NSString *)path;

@end
//
//  NSString+Extend.m
//

#import "NSString+Extend.h"
#import <CommonCrypto/CommonDigest.h>

@implementation NSString (Extend)

- (NSString *)md5 {
    const char *cStr = [self UTF8String];
    unsigned char result[CC_MD5_DIGEST_LENGTH];

    CC_MD5(cStr, (CC_LONG)strlen(cStr), result);

    NSMutableString *hash = [NSMutableString string];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [hash appendFormat:@"%02X", result[i]];
    }

    return [hash uppercaseString];
}

//计算文件的MD5值
+ (NSString *)fileMD5:(NSString *)path {
    NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:path];

    if (handle == nil) {
        return @"ERROR GETTING FILE MD5"; // file didnt exist
    }

    CC_MD5_CTX md5;
    CC_MD5_Init(&md5);
    NSUInteger blockSize = 5 * 1024;
    BOOL done = NO;
    while(!done) {
        NSData* fileData = [handle readDataOfLength: blockSize ];
        CC_MD5_Update(&md5, [fileData bytes], (CC_LONG)[fileData length]);
        if( [fileData length] == 0 ) done = YES;
    }

    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    CC_MD5_Final(digest, &md5);

    NSMutableString *hash = [NSMutableString string];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [hash appendFormat:@"%02X", digest[i]];
    }

    return [hash uppercaseString];
}

@end

UIColor+Extend

//
//  UIColor+Extend.h
//

#import <UIKit/UIKit.h>

@interface UIColor (Extend)

+ (UIColor *)colorFromHexRGB:(NSString *)inColorString;
+ (UIColor *)colorFromHexRGB:(NSString *)inColorString alpha:(CGFloat)alpha;

@end
//
//  UIColor+Extend.m
//

#import "UIColor+Extend.h"

@implementation UIColor (Extend)

+ (UIColor *)colorFromHexRGB:(NSString *)inColorString {
    return [self colorFromHexRGB:inColorString alpha:1.0];
}

+ (UIColor *)colorFromHexRGB:(NSString *)inColorString alpha:(CGFloat)alpha {
    if ([inColorString hasPrefix:@"#"]) {
        inColorString = [inColorString substringFromIndex:1];
    }

    UIColor *result = nil;
    unsigned int colorCode = 0;
    unsigned char redByte, greenByte, blueByte;

    if (nil != inColorString) {
        NSScanner *scanner = [NSScanner scannerWithString:inColorString];
        (void) [scanner scanHexInt:&colorCode]; // ignore error
    }
    redByte = (unsigned char) (colorCode >> 16);
    greenByte = (unsigned char) (colorCode >> 8);
    blueByte = (unsigned char) (colorCode); // masks off high bits
    result = [UIColor
              colorWithRed: (float)redByte / 0xff
              green: (float)greenByte/ 0xff
              blue: (float)blueByte / 0xff
              alpha:alpha];

    return result;
}

@end

好像也没什么好说的,所以就直接贴上代码。


(未完,持续更新中…)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值