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篇-热更新的实现