關(guān)于RN差量熱更新方案實(shí)現(xiàn)

前言

關(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和資源文件都可以熱更新。

RN更新包解壓后的目錄結(jié)構(gòu)

關(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ǔ)丁包。
版本推進(jìn)演示圖稿
  • 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)用即可:


宿主工程調(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);
image.png

日常補(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吧,謝謝~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內(nèi)容