前言
關(guān)于ReactNative熱更新,我首先是從網(wǎng)上對比了幾個常見方案,然后從中選擇較合適的方案實(shí)現(xiàn)。RN熱更新分為全量熱更新和差量熱更新。全量的好處是實(shí)現(xiàn)邏輯相比差量而言較輕松一些,弊端是全量前端代碼量如果很大的話,網(wǎng)絡(luò)下載耗時較長,就影響了APP的啟動體驗(yàn)了。
而差量熱更新也大致分為2種思路:
- 1.將jsbundle 分離成通用部分和業(yè)務(wù)部分,每次熱更新主要是業(yè)務(wù)部分腳本下發(fā),然后和通用部分腳本合并。
- 2.利用比對工具biff將老版本和新版本比對出一個差異部分(我們稱之為patch"補(bǔ)丁"),客戶端每次下載補(bǔ)丁包,然后和老的部分合并出新的完整腳本。
鑒于差量更新第一種思路,業(yè)務(wù)腳本后續(xù)版本其實(shí)也存在大量重復(fù)的腳本,每次下載全量的業(yè)務(wù)腳本其實(shí)不是徹底的差量更新方案,而且資源文件更新也未能體現(xiàn)。所以,我最終采用了第二種差分方案實(shí)現(xiàn)。
可行性探究
1.biff差分方案最核心的要借助第三方開源的biff。由于合并部分要在客戶端上完成,所以我預(yù)先下載了bsdiff-4.3和bzip2的開源代碼,由于是C語言實(shí)現(xiàn),我們要在iOS平臺編譯,直接拷貝到項(xiàng)目中,由于多處方法名main同名而編譯報錯,所以,我選擇了將該庫c實(shí)現(xiàn)打包成.a靜態(tài)庫。(我把這個靜態(tài)庫放在本文末鏈接里,有需要的同學(xué)可自?。?br>
然后將打包好的靜態(tài)庫導(dǎo)入到項(xiàng)目中(頭文件bspatch.h別忘了),然后在項(xiàng)目的pch文件import bspatch.h即可
使用代碼:
- (NSString *)bspatch:(NSString *)patchPath newVersion:(NSString *)newVersion {
const char *argv[4];
argv[0] = "bspatch";
// oldPath
NSString *oldPath = self.currHotZipPath;
if (!oldPath) {
return nil;
}
argv[1] = [oldPath UTF8String];
// newPath
argv[2] = [[self newZipPath:newVersion] UTF8String];
// patchPath
argv[3] = [patchPath UTF8String];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
int result = BsdiffUntils_bspatch(4, argv);
#pragma clang diagnostic pop
NSString *newZipPath = [NSString stringWithFormat:@"%s", argv[2]];
if (result == 0 && [NSFileManager.defaultManager fileExistsAtPath:newZipPath]) {
//success
return newZipPath;
}else {
return nil;
}
}
2.關(guān)于資源文件
我一開始擔(dān)心資源文件(比如圖片)會找不到路徑,因?yàn)槲覀兏碌男碌膠ip包所有資源文件由于不是在assets目錄下,會擔(dān)心RNImage無法找到,實(shí)際上,我多慮了,因?yàn)槲覀冎付薸ndex.jsbundle的path給RN后,RNImage圖片查找邏輯先是根據(jù)jsbundle的所在目錄下遍歷資源文件,如果找不到,最后才取bundle的Assets找資源文件。而我們的資源文件總是和jsbundle打包在一起的,所以,code和資源文件都可以熱更新。
關(guān)于版本管理
約定:
- 1.所有的完整的RN版本包命名規(guī)則:hot_V[版本號].zip
例如:hot_V1.0.0.zip - 2.所有的補(bǔ)丁包命名規(guī)則:hot_V[老版本號]_V[新版本號].patched
例如:hot_V1.0.0_V1.0.1.patched
表示:1.0.1版本和1.0.0版本差異部分產(chǎn)生的補(bǔ)丁包。
3.不做跨原生版本的補(bǔ)丁
為啥不做跨原生版本的補(bǔ)丁呢?
因?yàn)榭紤]到原生版本更新的原生組件的API,很可能v2.0.1版本的腳本調(diào)用了只僅限于v2.0.0原生版本才有的原生組件,那么如果v1.0.3版本跨native版本更新到了v2.0.1的腳本,導(dǎo)致調(diào)用的native 組件api找不到方法而報錯!4.每次原生版本更新的顆粒度精確到版本數(shù)組的第二位,第3位留給熱更新
我們的版本號預(yù)定格式: x_y_z 3位數(shù)字
x:代表整個APP大的功能升級或者重構(gòu)
y:App 原生版本升級(需要市場審核發(fā)布)
z:補(bǔ)丁包升級
x、y版本升級均需要native版本升級,z標(biāo)識補(bǔ)丁包升級,不需要重新發(fā)布APP市場審核,即熱更新。
邏輯流程圖
核心邏輯代碼
- (void)loadResourceURL {
//1.檢查是否有新的補(bǔ)丁
NSString *url = [NSString stringWithFormat:@"%@/rn_hot/checkPatch.php?appVersion=%@", kBaseURL, self.hotVersion];
[[HttpEngine shareInstance] requestUrl:url complete:^(BOOL succ, HttpEngineResponse * _Nonnull resp) {
if (succ) {
NSDictionary *patchDic = resp.data;
DLog(@"%@", patchDic);
if (![patchDic isKindOfClass:[NSDictionary class]]) {
DLog(@"沒有新的補(bǔ)丁,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
return ;
}
NSString *patchUrl = [patchDic objectForKey:@"patchUrl"];
NSString *newVersion = [patchDic objectForKey:@"newVersion"];
if (patchUrl && [patchUrl isKindOfClass:[NSString class]] && patchUrl.length > 0) {
//2.如果沒有補(bǔ)丁目錄,則創(chuàng)建
NSError *err = nil;
[self createDirIfNotExist:kPatchesDir error:&err];
if (err) {
DLog(@"創(chuàng)建補(bǔ)丁目錄失敗,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
return ;
}
//3.下載補(bǔ)丁文件
[[HttpEngine shareInstance] downloadUrl:patchUrl saveToPath:kPatchesDir complete:^(BOOL succ, NSString * _Nonnull filePath, NSString * _Nonnull error) {
if (succ) {
DLog(@"下載保存到地址:%@",filePath);
//4.補(bǔ)丁合并現(xiàn)有zip生成newzip
NSString *newZipPath = [self bspatch:filePath newVersion:newVersion];
if (newZipPath.length) {
//5.解壓
[SSZipArchive unzipFileAtPath:newZipPath toDestination:kDocumentDir overwrite:YES password:@"" progressHandler:nil completionHandler:^(NSString * _Nonnull path, BOOL succeeded, NSError * _Nullable error) {
if (succeeded && !error) {
//6.升級成功,更新版本號
DLog(@"升級成功,更新版本號");
[NSUserDefaults.standardUserDefaults setObject:newVersion forKey:kAppHotVersion];
[NSUserDefaults.standardUserDefaults synchronize];
//7.繼續(xù)檢查有無新的版本更新
[self loadResourceURL];
}else {
DLog(@"解壓失敗,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
}
}];
}else {
DLog(@"合成補(bǔ)丁失敗,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
}
}else {
DLog(@"下載補(bǔ)丁失敗,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
}
}];
}else {
DLog(@"沒有新的補(bǔ)丁,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
}
}else {
DLog(@"checkPatch接口失敗,使用現(xiàn)有的");
self->_bundleURL = [self loadCurrResource];
}
}];
}
最后在你的宿主RN工程中調(diào)用即可:
值得說明的:
1.上面代碼流程中最后第7步,繼續(xù)遞歸調(diào)用了方法本身。
我的考慮是,雖然本次版本剛升級了,一般來說肯定就是最新的版本了,可以直接用本次zip解壓的腳本啟動APP了。但是當(dāng)?shù)硕鄠€版本后,會出現(xiàn)這樣的場景:
v1.0.0_v1.0.1.patch
v1.0.1_v1.0.2.patch
v1.0.2_v1.0.3.patch
...
假設(shè)服務(wù)器已經(jīng)下發(fā)了3個補(bǔ)丁包了,但是,我們的用戶可能很久沒有打開過APP,很可能他的補(bǔ)丁版本還停留在初始的v1.0.0或者v1.0.1, 對于這兩個版本的用戶,升級一次版本后的版本相應(yīng)的為1.0.1和v1.0.2, 而最新的版本是v1.0.3, 也就是說只有上次停留在v1.0.2的版本只需要一次升級到最新的版本,而其他版本需要多次升級到最新版本。所以這里采用遞歸升級策略。
2.每次APP 原生版本發(fā)布(有別于補(bǔ)丁包發(fā)布),客戶端需要手動保存一份和app原生版本相同的zip格式在項(xiàng)目中(即bundle中),這是因?yàn)椋谝粋€補(bǔ)丁x.x.0_x.x.1.patch包要合并的初始zip必須存在。
關(guān)于服務(wù)端部署
1.需要編寫個服務(wù)端腳本,用于判斷是否存在最新的可用補(bǔ)丁包。需要一個入?yún)ⅲ寒?dāng)前版本號。
邏輯是遍歷服務(wù)端存放補(bǔ)丁的列表文件目錄,根據(jù)存放的補(bǔ)丁文件名稱解析出老版本號-新版本號,然后和入?yún)姹颈葘?,返回補(bǔ)丁包下載鏈接和新版本號。
php代碼邏輯如下:
$appVersion = $_GET['appVersion'];
$fileDir = "hot_patches";
function checkAvaliblePatchesByVersion($version, $fileDir){
$result = '';
//1、首先先讀取文件夾
$temp=scandir($fileDir);
//遍歷文件夾
foreach($temp as $v){
$a = $fileDir.'/'.$v;
if(is_dir($a)){//如果是文件夾則執(zhí)行
if($v=='.' || $v=='..'){//判斷是否為系統(tǒng)隱藏的文件.和.. 如果是則跳過否則就繼續(xù)往下走,防止無限循環(huán)再這里。
continue;
}
return checkAvaliblePatchesByVersion($a);//因?yàn)槭俏募A所以再次調(diào)用自己這個函數(shù),把這個文件夾下的文件遍歷出來
}else{
$ext = pathinfo($a, PATHINFO_EXTENSION);
$baseName = pathinfo($a, PATHINFO_BASENAME);
$dirName = pathinfo($a, PATHINFO_DIRNAME);
$filename = str_replace(strrchr($baseName, "."),"",$baseName);
if ($ext == 'patched') {
//將文件名轉(zhuǎn)成數(shù)組,以_分割
$arr = explode('_', $filename);
if (count($arr) >= 2) {
//倒數(shù)第2個字符串:
$oldVersion = str_replace("V","",strtoupper($arr[count($arr)-2]));
//倒數(shù)第1個字符串:
$newVersion = str_replace("V","",strtoupper($arr[count($arr)-1]));
if ($oldVersion == $version) {
$currDir = 'http://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['PHP_SELF']);
//返回完整的下載URL
$url = $currDir.'/'.$a;
$result = array(
'patchUrl' => $url,
'newVersion' => $newVersion,
);
break;
}
}
}
}
}
return $result;
}
$result = checkAvaliblePatchesByVersion($appVersion, $fileDir);
$response = array(
'code' => 200,
'message' => 'success for request',
'data' => $result,
);
//echo $result."<br>";
echo json_encode($response);
return json_encode($response);
日常補(bǔ)丁更新維護(hù)
1.終端命令行:
biff [oldzip_path] [newzip_path] [生成的補(bǔ)丁輸出目錄]
2.將 生成的補(bǔ)丁包版本放入上圖中的hot_patches目錄下即可
整個RN差量熱更新方案大致就是這樣,也經(jīng)過實(shí)測通過。此間前后花了4天時間,連php腳本都是現(xiàn)學(xué)現(xiàn)用的。特此記錄一下研究過程。
最后,附上我的研究的成果,我上傳至我的github上了:https://github.com/GithubXkw1573/RN_BiffHot
如果我的方案成果對你有所幫助,請給個star吧,謝謝~