代码实现react-native热更新(iOS)

react-native作为跨平台的语言好处主要体现在跨平台和能进行热更新。要实现热更新通常想到使用某个第三方组件实现,常用的就是微软的codepush和RN中文网的react-native-pushly.如果不想使用第三方我们也可以自己实现。所谓的热更新就是下载新的bundle压缩包然后解压,让app加载这个补丁包。根据我的实践,一个压缩的bundle包也只有两三M,所有只要在原生端下载这个压缩包然后解压就行了。
大概思路:
1.沙盒里建三个文件夹和一个plist文件,一个放补丁包(IOSBundle),一个放下载的压缩包(BundleZip解压后会删除该压缩包),一个放记录版本号与补丁号的plist文件(PatchPlist), 判断存放补丁包的文件夹下是否有下载的补丁,同时判断记录的版本号与当前版本号是否相同,不相同有补丁也不加载。

2.网络请求获取是否有补丁需要下载,如果有则拿到下载的url进行下载,如果没有则不下载。网络请求使用AFN,解压用了ZipArchive

3.如果需要下载补丁则先下载压缩包,然后解压,解压完成时删除压缩包,将获得的版本号和补丁号写入plist。下次app重启是就会加载补丁包,热跟新完成。

ps:如果想在上架app Store审核之后用热更新更新新需求而不重新提交审核,那图片就不能放在xcode里了,所有引用图片的方式需要使用require的方式,因为放在xcode里的图片是没办法热更新的。现在说的这种更适合修复线上js写的逻辑bug,无法修改原生代码.

在AppDelegate中,启动的时候判断沙盒文件夹是否有已经下载的补丁包,如果有就加载。但是如果安装了新版本且是覆盖安装,这样启动时加载的会仍然是那个补丁包,所以为了防止这种情况,我们需要比较上次下载补丁时记录的版本号和当前版本号,如果不同,即使沙盒里有补丁包也不加载

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   NSURL *jsCodeLocation;
   NSString* iOSBundlePath = [[[UpdateDataLoader sharedInstance] iOSFileBundlePath] stringByAppendingString:@"/bundle/index.ios.jsbundle"];
  if ([[NSFileManager defaultManager] fileExistsAtPath:iOSBundlePath] && [self compareAppVersionIsSame]) {
#ifdef DEBUG
    //开发包
    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
#else
    //加载热修复下载的bundle
    jsCodeLocation = [NSURL URLWithString:iOSBundlePath];
#endif
  }else{
#ifdef DEBUG
     //开发包
     jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
#else
     //离线包
     jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"bundle/index.ios" withExtension:@"jsbundle"];
#endif
  }
  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"ReactFlyApp"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
//因为我将压缩包托管在leanCould上,此处是设置该SDK,如果有自己的后台则直接请求是否需要更新补丁
[self AVOSCloudSetting];
。。。
}

//判断热更新记录的版本与当前版本是否相同
-(BOOL)compareAppVersionIsSame{
  NSString *currentAppVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
  NSDictionary *dic = [[UpdateDataLoader sharedInstance]getDataFromIosBundlePlist];
  NSString *recordAppVersion = @"";
  if(dic !=nil && dic[@"appVerion"]){
    recordAppVersion = dic[@"appVerion"] ;
  }
  return [currentAppVersion isEqualToString:recordAppVersion];
}

-(void)AVOSCloudSetting{
  [[UpdateDataLoader sharedInstance] createHotFixBundlePath]; //在沙盒新建三个文件夹和一个plist文件,如果有则不新建
  [AVOSCloud setApplicationId:AVOSCloudID clientKey:AVOSCloudKey];
#ifdef DEBUG
  [AVOSCloud setAllLogsEnabled:YES];
#else
  [AVOSCloud setAllLogsEnabled:FALSE];
#endif
  [[UpdateDataLoader sharedInstance] downloadNewBundle];//进行网络请求判断是否需要下载补丁
}

UpdateDataLoader文件主要是创建文件夹,plist文件和判断是否需要下载

#import 

@interface UpdateDataLoader : NSObject

@property (nonatomic, strong) NSDictionary* versionInfo;
+ (UpdateDataLoader *) sharedInstance;

//创建bundle路径
-(void)createHotFixBundlePath;

//检查下载热更新包
-(void)downloadNewBundle;

-(void)writeAppVersionInfoWithDictiony:(NSDictionary*)info;

-(NSString*)iOSFileBundlePath;

-(NSDictionary*)getDataFromIosBundlePlist;
@end

----------------------------------------------------------------------------------------= = ------------------------------

#import "UpdateDataLoader.h"
#import "DownLoadTool.h"
#import "Network.h"
#import "AFNetworking.h"
#import 
@implementation UpdateDataLoader

+ (UpdateDataLoader *) sharedInstance
{
  static UpdateDataLoader *sharedInstance = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sharedInstance = [[UpdateDataLoader alloc] init];
  });
  return sharedInstance;
}

//创建bundle路径
-(void)createHotFixBundlePath{
  if([[NSFileManager defaultManager]fileExistsAtPath:[self getVersionPlistPath]]){
    return;
  }
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
  NSString *path = [paths lastObject];
  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSString *directryPath = [path stringByAppendingPathComponent:@"IOSBundle"];
  [fileManager createDirectoryAtPath:directryPath withIntermediateDirectories:YES attributes:nil error:nil];
  NSString *bundleZipPath = [path stringByAppendingPathComponent:@"BundleZip"];
  [fileManager createDirectoryAtPath:bundleZipPath withIntermediateDirectories:YES attributes:nil error:nil];
  NSString *PatchPlistPath = [path stringByAppendingPathComponent:@"PatchPlist"];
  [fileManager createDirectoryAtPath:PatchPlistPath withIntermediateDirectories:YES attributes:nil error:nil];
  NSString *filePath = [PatchPlistPath stringByAppendingPathComponent:@"Version.plist"];
  [fileManager createFileAtPath:filePath contents:nil attributes:nil];
}

//获取版本信息,有下载就下载新的热更新包
-(void)downloadNewBundle{
  NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
  NSDictionary *dic = [self getDataFromIosBundlePlist];
  int patchCode = 0;
  if(dic !=nil && dic[@"patchCode"]){
    patchCode = [dic[@"patchCode"] intValue];
  }
//此处应该将version和patchCode穿给后台,且要求后台返回数据要按修改时间升序(时间越新越靠前),拿version到数据库中匹配,与该version相同且大于该patchCode,有满足条件的数据则表明有补丁包需要下载,这时返回一个数组,我们取数组的第一个对象,该对象包含需要下载的url,然后下载。应该我使用了leanCloud,所以代码是这样
 AVQuery *fileQuery = [AVQuery queryWithClassName:@"hot_update_file"];
  [fileQuery whereKey:@"patchCode" greaterThan:@(patchCode)];
  [fileQuery whereKey:@"version" equalTo:version];
  [fileQuery orderByDescending:@"createdAt"];
  [fileQuery findObjectsInBackgroundWithBlock:^(NSArray *lists, NSError *error) {
    if(error==nil){
      if(lists!=nil && lists.count>0){
        AVObject *avObject = lists.firstObject;
        if(avObject !=nil){
          NSDictionary *dic = (NSDictionary *)[avObject objectForKey:@"localData"];
          NSString *patchCode = (NSString *)dic[@"patchCode"];
          if(dic[@"pathfile"]!=nil){
            AVFile *file = dic[@"pathfile"];
            NSString *downLoadURL = (NSString *)file.url;
            [[DownLoadTool defaultDownLoadTool] downLoadWithUrl:downLoadURL patchCode:patchCode serverAppVersion:version];
          }
        }
      }
    }
  }];
}

//获取Bundle 路径
-(NSString*)iOSFileBundlePath{
  NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString* path = [paths objectAtIndex:0];
  NSString* filePath = [path stringByAppendingPathComponent:@"/IOSBundle"];
  return  filePath;
}

//获取版本信息储存的文件路径
-(NSString*)getVersionPlistPath{
  NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString* path = [paths objectAtIndex:0];
  NSString* filePath = [path stringByAppendingPathComponent:@"/PatchPlist/Version.plist"];
  return filePath;
}

//读取plist文件数据
-(NSDictionary*)getDataFromIosBundlePlist{
  return [NSDictionary dictionaryWithContentsOfFile:[self getVersionPlistPath]];
}

//创建或修改版本信息
-(void)writeAppVersionInfoWithDictiony:(NSDictionary*)dictionary{
  NSString* filePath  = [self getVersionPlistPath];
  [dictionary writeToFile:filePath atomically:YES];
}
@end

下面就是拿到url后下载压缩包,需要主要的是下载默认是挂起的,需要我们手动开启[downloadTask resume];

#import 

@interface DownLoadTool : NSObject
@property (nonatomic, strong) NSString *zipPath;

+ (DownLoadTool *) defaultDownLoadTool;

//根据url下载相关文件
-(void)downLoadWithUrl:(NSString*)url patchCode:(NSString *)patchCode serverAppVersion:(NSString *)serverVersion;

@end

----------------------------------------------------------------------------------------= = ------------------------------


#import "DownLoadTool.h"
#import "ZipArchive.h"
#import "AFURLSessionManager.h"
#import "UpdateDataLoader.h"

@implementation DownLoadTool
+ (DownLoadTool *) defaultDownLoadTool{
  static DownLoadTool *sharedInstance = nil;
  static dispatch_once_t onceToken;
  
  dispatch_once(&onceToken, ^{
    sharedInstance = [[DownLoadTool alloc] init];
  });
  
  return sharedInstance;
}

-(void)downLoadWithUrl:(NSString*)url patchCode:(NSString *)patchCode serverAppVersion:(NSString *)serverVersion{
  //根据url下载相关文件
  NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
  AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
  NSURL *URL = [NSURL URLWithString:url];
  NSURLRequest *request = [NSURLRequest requestWithURL:URL];
  NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
    //获取下载进度
    NSLog(@"Progress is %f", downloadProgress.fractionCompleted);
  } destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    //有返回值的block,返回文件存储路径
    NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    NSURL* targetPathUrl = [documentsDirectoryURL URLByAppendingPathComponent:@"BundleZip"];
    return [targetPathUrl URLByAppendingPathComponent:[response suggestedFilename]];
    
  } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    if(error){
      //下载出现错误
      NSLog(@"%@",error);
    }else{
      //下载成功
      if([filePath absoluteString].length>7){
        self.zipPath = [[filePath absoluteString] substringFromIndex:7];
      }
      //下载成功后更新本地存储信息
      NSDictionary*infoDic=@{@"patchCode":serverVersion,@"appVerion":serverVersion,};
      [[UpdateDataLoader sharedInstance] writeAppVersionInfoWithDictiony:infoDic];
      //解压并删除压缩包
        [self unZip];
    }
  }];
  [downloadTask resume];
}

//解压压缩包
-(void)unZip{
  if (self.zipPath == nil) {
    return;
  }
  //检查Document里有没有bundle文件夹
  NSString* path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
  NSString* bundlePath = [path stringByAppendingPathComponent:@"/IOSBundle"];
   dispatch_queue_t _opQueue = dispatch_queue_create("cn.reactnative.hotupdate", DISPATCH_QUEUE_SERIAL);
  dispatch_async(_opQueue, ^{
    BOOL isDir;
    //如果有,则删除后解压,如果没有则直接解压
    if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:&isDir]&&isDir) {
      [[NSFileManager defaultManager] removeItemAtPath:bundlePath error:nil];
    }
    NSString *zipPath = self.zipPath;
    NSString *destinationPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]stringByAppendingString:@"/IOSBundle"];
    [SSZipArchive unzipFileAtPath:zipPath toDestination:destinationPath progressHandler:^(NSString * _Nonnull entry, unz_file_info zipInfo, long entryNumber, long total) {
    } completionHandler:^(NSString * _Nonnull path, BOOL succeeded, NSError * _Nullable error) {
      //删除压缩包
      NSError* merror = nil;
      [[NSFileManager defaultManager] removeItemAtPath:self.zipPath error:&merror];
    }];
  });
}
@end

至此我们的热更新就完成了,我亲测这种方法是可以完成热更新的。demo
这篇文章参考了别人的文章,但是在实践过程中发现有的写得有问题,且有的地方考虑不是很全面,所以在此基础上进行了修改完善,当然我写的这种也许也有问题,有发现的望提出,我也及时更正。
参考:
React-Native开发iOS篇-热更新的实现
React-Native开发iOS篇-热更新的实现

你可能感兴趣的:(代码实现react-native热更新(iOS))