react-native作為跨平臺的語言好處主要體現(xiàn)在跨平臺和能進(jìn)行熱更新脯倚。要實(shí)現(xiàn)熱更新通常想到使用某個第三方組件實(shí)現(xiàn),常用的就是微軟的codepush和RN中文網(wǎng)的react-native-pushly.如果不想使用第三方我們也可以自己實(shí)現(xiàn)获洲。所謂的熱更新就是下載新的bundle壓縮包然后解壓,讓app加載這個補(bǔ)丁包那槽。根據(jù)我的實(shí)踐,一個壓縮的bundle包也只有兩三M周偎,所有只要在原生端下載這個壓縮包然后解壓就行了翁逞。
大概思路:
1.沙盒里建三個文件夾和一個plist文件总珠,一個放補(bǔ)丁包(IOSBundle)屏鳍,一個放下載的壓縮包(BundleZip解壓后會刪除該壓縮包),一個放記錄版本號與補(bǔ)丁號的plist文件(PatchPlist)局服, 判斷存放補(bǔ)丁包的文件夾下是否有下載的補(bǔ)丁钓瞭,同時判斷記錄的版本號與當(dāng)前版本號是否相同,不相同有補(bǔ)丁也不加載淫奔。
2.網(wǎng)絡(luò)請求獲取是否有補(bǔ)丁需要下載山涡,如果有則拿到下載的url進(jìn)行下載,如果沒有則不下載唆迁。網(wǎng)絡(luò)請求使用AFN,解壓用了ZipArchive
3.如果需要下載補(bǔ)丁則先下載壓縮包鸭丛,然后解壓,解壓完成時刪除壓縮包唐责,將獲得的版本號和補(bǔ)丁號寫入plist鳞溉。下次app重啟是就會加載補(bǔ)丁包,熱跟新完成鼠哥。
ps:如果想在上架app Store審核之后用熱更新更新新需求而不重新提交審核熟菲,那圖片就不能放在xcode里了,所有引用圖片的方式需要使用require的方式朴恳,因?yàn)榉旁趚code里的圖片是沒辦法熱更新的〕保現(xiàn)在說的這種更適合修復(fù)線上js寫的邏輯bug,無法修改原生代碼.
在AppDelegate中于颖,啟動的時候判斷沙盒文件夾是否有已經(jīng)下載的補(bǔ)丁包呆贿,如果有就加載。但是如果安裝了新版本且是覆蓋安裝森渐,這樣啟動時加載的會仍然是那個補(bǔ)丁包做入,所以為了防止這種情況冒晰,我們需要比較上次下載補(bǔ)丁時記錄的版本號和當(dāng)前版本號,如果不同母蛛,即使沙盒里有補(bǔ)丁包也不加載
- (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
//開發(fā)包
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
#else
//加載熱修復(fù)下載的bundle
jsCodeLocation = [NSURL URLWithString:iOSBundlePath];
#endif
}else{
#ifdef DEBUG
//開發(fā)包
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];
//因?yàn)槲覍嚎s包托管在leanCould上翩剪,此處是設(shè)置該SDK,如果有自己的后臺則直接請求是否需要更新補(bǔ)丁
[self AVOSCloudSetting];
。彩郊。。
}
//判斷熱更新記錄的版本與當(dāng)前版本是否相同
-(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];//進(jìn)行網(wǎng)絡(luò)請求判斷是否需要下載補(bǔ)丁
}
UpdateDataLoader文件主要是創(chuàng)建文件夾秫逝,plist文件和判斷是否需要下載
#import <Foundation/Foundation.h>
@interface UpdateDataLoader : NSObject
@property (nonatomic, strong) NSDictionary* versionInfo;
+ (UpdateDataLoader *) sharedInstance;
//創(chuàng)建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 <AVOSCloud/AVOSCloud.h>
@implementation UpdateDataLoader
+ (UpdateDataLoader *) sharedInstance
{
static UpdateDataLoader *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[UpdateDataLoader alloc] init];
});
return sharedInstance;
}
//創(chuàng)建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];
}
//此處應(yīng)該將version和patchCode穿給后臺,且要求后臺返回數(shù)據(jù)要按修改時間升序(時間越新越靠前)询枚,拿version到數(shù)據(jù)庫中匹配违帆,與該version相同且大于該patchCode,有滿足條件的數(shù)據(jù)則表明有補(bǔ)丁包需要下載金蜀,這時返回一個數(shù)組刷后,我們?nèi)?shù)組的第一個對象,該對象包含需要下載的url,然后下載渊抄。應(yīng)該我使用了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文件數(shù)據(jù)
-(NSDictionary*)getDataFromIosBundlePlist{
return [NSDictionary dictionaryWithContentsOfFile:[self getVersionPlistPath]];
}
//創(chuàng)建或修改版本信息
-(void)writeAppVersionInfoWithDictiony:(NSDictionary*)dictionary{
NSString* filePath = [self getVersionPlistPath];
[dictionary writeToFile:filePath atomically:YES];
}
@end
下面就是拿到url后下載壓縮包,需要主要的是下載默認(rèn)是掛起的护桦,需要我們手動開啟[downloadTask resume];
#import <Foundation/Foundation.h>
@interface DownLoadTool : NSObject
@property (nonatomic, strong) NSString *zipPath;
+ (DownLoadTool *) defaultDownLoadTool;
//根據(jù)url下載相關(guān)文件
-(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{
//根據(jù)url下載相關(guān)文件
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) {
//獲取下載進(jìn)度
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){
//下載出現(xiàn)錯誤
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
這篇文章參考了別人的文章催享,但是在實(shí)踐過程中發(fā)現(xiàn)有的寫得有問題杭隙,且有的地方考慮不是很全面,所以在此基礎(chǔ)上進(jìn)行了修改完善因妙,當(dāng)然我寫的這種也許也有問題痰憎,有發(fā)現(xiàn)的望提出,我也及時更正兰迫。
參考:
React-Native開發(fā)iOS篇-熱更新的實(shí)現(xiàn)
React-Native開發(fā)iOS篇-熱更新的實(shí)現(xiàn)