React-Native 實現(xiàn)增量熱更新的思路(1)

所謂熱更新就是在不重新安裝的前提下進(jìn)行代碼和資源的更新九秀,相信在整個宇宙中還不存在覺得熱更新不重要的程序猿碎乃。

增量熱更新就更牛逼了杨耙,只需要把修改過和新增的代碼和資源推送給用戶下載即可晋辆,增量部分的代碼和資源都比較小穿稳,所以整個熱更新流程可以在用戶無感的情況下完成茴她,我已經(jīng)想不到更好的更新方式可以讓我裝更大的逼了寻拂。

一.實現(xiàn)腳本的熱更新
1.為什么可以熱更新

簡單地說,因為RN是使用腳本語言來編寫的丈牢,所謂腳本語言就是不需要編譯就可以運行的語言祭钉,也就是“即讀即運行”。我們在“讀”之前將之替換成新版本的腳本己沛,運行時執(zhí)行的便是新的邏輯了慌核,稍微抽象一下,圖片資源是不是也是“即讀即運行”申尼?所以腳本本質(zhì)上和圖片資源一樣垮卓,都是可以進(jìn)行熱更新的。

2.RN加載腳本的機(jī)制

要實現(xiàn)RN的腳本熱更新师幕,我們要搞明白RN是如何去加載腳本的粟按。
在編寫業(yè)務(wù)邏輯的時候,我們會有許多個js文件们衙,打包的時候RN會將這些個js文件打包成一個叫index.android.bundle(ios的是index.ios.bundle)的文件钾怔,所有的js代碼(包括rn源代碼、第三方庫蒙挑、業(yè)務(wù)邏輯的代碼)都在這一個文件里宗侦,啟動App時會第一時間加載bundle文件,所以腳本熱更新要做的事情就是替換掉這個bundle文件忆蚀。

3.生成bundle文件

我們在RN項目根目執(zhí)行以下命令來得到bundle文件和圖片資源:

react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false

其中--entry是入口js文件矾利,android系統(tǒng)就是index.android.js,ios系統(tǒng)就是index.ios.js馋袜,--bundle-output就是生成的bundle文件路徑男旗,--platform是平臺,--assets-dest是圖片資源的輸出目錄欣鳖,這個在后面的圖片增量更新中會用到察皇,--dev表示是否是開發(fā)版本,打正式版的安裝包時我們將其賦值為false。
生成的bundle文件體積還是不小的什荣,空項目的話恐怕至少也有900K矾缓,所以我們將其打成zip包并放到web服務(wù)器上以供客戶端去下載。

4.下載bundle文件

下載文件可以使用原生語言來寫稻爬,也可以使用js實現(xiàn)嗜闻,我個人推薦使用React Native FileTransfer來實現(xiàn)下載功能。
實現(xiàn)方法很簡單:

import FileTransfer from 'react-native-file-transfer';

let fileTransfer = new FileTransfer();
fileTransfer.onprogress = (progress) => {
  console.log(parseInt(progress.loaded * 100 / progress.total))
};
// url:新版本bundle的zip的url地址
// bundlePath:存在新版本bundle的路徑
// unzipJSZipFile:下載完成后執(zhí)行的回調(diào)方法桅锄,這里是解壓縮zip
fileTransfer.download(url, bundlePath, unzipJSZipFile, (err) => {
    console.log(err);
  }, true
);

解壓縮的工作我們可以使用react-native-zip來完成琉雳。

import Zip from 'react-native-zip';

function unzipJSZipFile() {
  // zipPath:zip的路徑
 // documentPath:解壓到的目錄
  Zip.unzip(zipPath, documentPath, (err)=>{
    if (err) {
      // 解壓失敗
    } else {
      // 解壓成功,將zip刪除
      fs.unlink(zipPath).then(() => {
        // 通過解壓得到的補丁文件生成最新版的jsBundle
      });
    }
  });
}

解壓成功后友瘤,我們使用react-native-fs來將zip刪除翠肘。

5.替換bundle文件

安裝包中的bundle文件是在asset目錄下的,而asset目錄我們是沒有寫權(quán)限的商佑,所以我們不能修改安裝包中的bundle文件锯茄。好在RN中提供了修改讀取bundle路徑的方法。以android為例(ios的類似)茶没,在ReactActivity類中有這么一個方法:

/**
 * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
 * from a custom path. By default it is loaded from Android assets, from a path specified
 * by {@link getBundleAssetName}.
 * e.g. "file://sdcard/myapp_cache/index.android.bundle"
 */
protected @Nullable String getJSBundleFile() {
  return null;
}

該方法返回了一個自定義的bundle文件路徑肌幽,如果返回默認(rèn)值null,RN會讀取asset里的bundle抓半。我們在MainActivity類中重寫這個方法喂急,返回可寫目錄一下的bundle文件路徑:

@Override
protected @Nullable String getJSBundleFile() {
    String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
    File file = new File(jsBundleFile);
    return file != null && file.exists() ? jsBundleFile : null;
}

如果可寫目錄下沒有bundle文件,還是返回null笛求,RN依然讀取的是asset中的bundle廊移,如果可寫目錄下存在bundle,RN就會讀取可寫目錄下的bundle文件探入。

我們將下載好的zip解壓到getFilesDir().getAbsolutePath()目錄下狡孔,再次啟動App時便會讀取該目錄下的bundle文件了,以后再有新版本的bundle文件蜂嗽,依然是下載苗膝、解壓并覆蓋掉這個bundler文件,至此植旧,我們便完成了代碼的熱更新工作辱揭。

6.圖片不見了

當(dāng)我們使用可寫目錄下的bundle文件時會出現(xiàn)一個很嚴(yán)重的問題:所有的本地圖片資源都無法顯示了。

我們的圖片資源都是通過require來獲取的:

<Image source={require('./imgs/test.png')} />

為了找到圖片消失的原因病附,我們打開image.android.js或者image.ios.js问窃,找到渲染圖片的方法:

render: function() {
  var source = resolveAssetSource(this.props.source);
  var loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
  // ...
}

原來是通過resolveAssetSource方法來獲取資源,那么找到resolveAssetSource方法:

function resolveAssetSource(source: any): ?ResolvedAssetSource {
  if (typeof source === 'object') {
    return source;
  }

  var asset = AssetRegistry.getAssetByID(source);
  if (asset) {
    return assetToImageSource(asset);
  }

  return null;
}

function assetToImageSource(asset): ResolvedAssetSource {
  var devServerURL = getDevServerURL();
  return {
    __packager_asset: true,
    width: asset.width,
    height: asset.height,
    uri: devServerURL ? getPathOnDevserver(devServerURL, asset) : getPathInArchive(asset),
    scale: pickScale(asset.scales, PixelRatio.get()),
  };
}

又發(fā)現(xiàn)是通過getPathInArchive方法來獲取資源的完沪,那么繼續(xù)找到getPathInArchive方法:

/**
 * Returns the path at which the asset can be found in the archive
 */
function getPathInArchive(asset) {
  var offlinePath = getOfflinePath();
  if (Platform.OS === 'android') {
    if (offlinePath) {
      // E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
      return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
    }
    // E.g. 'assets_awesomemodule_icon'
    // The Android resource system picks the correct scale.
    return assetPathUtils.getAndroidResourceIdentifier(asset);
  } else {
    // E.g. '/assets/AwesomeModule/icon@2x.png'
    return offlinePath + getScaledAssetPath(asset);
  }
}

該方法的邏輯是如果有離線腳本域庇,那么就從該腳本所在目錄里尋找圖片資源,否則就從asset中讀取圖片資源,所謂離線腳本就是我們剛剛下載并解壓的bundle文件较剃,而我們并沒有將圖片資源放在這個目錄下咕别,所以所有的圖片都不見了技健。
找到原因就好辦了写穴,我們在使用bundle命令生成bundle文件的時候也將圖片資源輸出出來了,那打包bundle文件的時候我們將所有圖片也一并打包進(jìn)zip雌贱,客戶端下載zip并解壓縮后啊送,客戶端可寫目錄下也就有了所有的圖片資源,這樣就即實現(xiàn)了腳本的熱更新又實現(xiàn)了圖片的熱更新欣孤。

二.減小更新包體積

將一個完整bundle文件和所有圖片都打成zip馋没,zip的體積讓人不敢直視。

1.增量更新圖片

每一次的版本更新我們都將所有圖片裝進(jìn)zip包未免有點太任性了降传,其實我們只需要將修改過和新增的圖片資源放進(jìn)zip就行了篷朵。
我們修改一下獲取圖片資源的方法里的邏輯:

/**
 * Returns the path at which the asset can be found in the archive
 */
function getPathInArchive(asset) {
  var offlinePath = getOfflinePath();
  if (Platform.OS === 'android') {
    if (offlinePath) {
      // 熱更新修改  開始
      if(global.patchList){
        let picName = `${asset.name}.${asset.type}`;
        for (let i = 0; i < global.patchList.length; i++) {
          if(global.patchList[i].endsWith(picName)){
            return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
          }
        }
      }
      // 熱更新修改  結(jié)束
      // E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
      // return 'file://' + offlinePath + getAssetPathInDrawableFolder(asset);
    }
    // E.g. 'assets_awesomemodule_icon'
    // The Android resource system picks the correct scale.
    return assetPathUtils.getAndroidResourceIdentifier(asset);
  } else {
    // E.g. '/assets/AwesomeModule/icon@2x.png'
    return offlinePath + getScaledAssetPath(asset);
  }
}

其中g(shù)lobal.patchList是一個數(shù)組,里面放的是自安裝包版本以來所有修改過和新增的圖片名婆排,如果訪問的圖片名在這個數(shù)組中就從離線腳本所在目錄里尋找圖片資源声旺,否則還是從asset中尋找圖片資源。
我們在打包zip的時候段只,就只裝修改過和新增的圖片腮猖,并將這些圖片名記錄在更新配置文件里,客戶端去讀取更新配置文件時將配置中的圖片名讀取到并生成global.patchList赞枕,這樣我們的更新包就小了許多了澈缺。
這么做的缺點就是每次更新RN版本的時候,都需要修改下RN的源碼炕婶,不過我覺得這點小麻煩還是可以接受的姐赡,畢竟已上線的產(chǎn)品,我們還是以穩(wěn)定為主柠掂,能不升級RN就不升級RN项滑。

2.增量更新腳本

bundle文件的體積,我們也得想想辦法去減少它陪踩。
有兩種思路:

  1. 分離bundle杖们。bundle里存放了RN源碼、第三方庫代碼和業(yè)務(wù)邏輯代碼肩狂,其中頻繁更新的就只有業(yè)務(wù)邏輯代碼摘完,所以我們將RN源碼和第三方庫代碼打包成一個bundle,業(yè)務(wù)邏輯打包成一個bundle傻谁,熱更新的時候就只更新業(yè)務(wù)邏輯的bundle即可孝治。

  2. 打包補丁文件。我們可以使用bsdiff對比兩個版本的bundle文件得到差異文件,也就是“補丁”谈飒,客戶端下載好補丁文件岂座,將其與本地的bundle進(jìn)行融合從而得到最新版本的bundle文件。

這里重點講解第二個思路的做法杭措。

  1. 生成補丁费什。

我們從bsdiff官網(wǎng)上下載到最新的源碼,然后進(jìn)行編譯就得到可執(zhí)行的二進(jìn)制文件了手素。

如果是win系統(tǒng)鸳址,可以直接到我的百度網(wǎng)盤下載,下載密碼:zq1x泉懦。解壓下載好的zip稿黍,使用命令行進(jìn)入到bsdiff的目錄,輸入命令:

bsdiff a.txt b.txt c.pat

上面的命令就是生成a.txt崩哩、b.txt兩個文件的補丁c.pat巡球。

如果是linux系統(tǒng),可以依次執(zhí)行以下命令:

yum install bzip2-devel
wget http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
tar zxvf bsdiff-4.3.tar.gz
cd bsdiff-4.3

編譯完成后邓嘹,會在目錄下生成2個二進(jìn)制文件:bsdiff酣栈、bspatch,這2個二進(jìn)制文件可以直接使用吴超,不過推薦拷貝到/usr/local/sbin/下:

cp bsdiff /usr/local/sbin/
cp bspatch /usr/local/sbin/

這樣就可以在命令行中直接使用了:

bsdiff a.txt b.txt c.pat
  1. 使用補丁钉嘹。
    得到了補丁文件,下一步就會使用補丁了鲸阻,拿上面的a.txt跋涣、b.txt、c.pat做測試:
bspatch a.txt d.txt c.pat

得到文件d.txt鸟悴,將其開打看看是否和b.txt一樣陈辱,如果一樣,說明測試成功细诸。

  1. 在RN中使用bsdiff沛贪。
    待續(xù)。震贵。利赋。
三.制作一鍵熱更新工具

待續(xù)。猩系。媚送。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寇甸,隨后出現(xiàn)的幾起案子塘偎,更是在濱河造成了極大的恐慌疗涉,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吟秩,死亡現(xiàn)場離奇詭異咱扣,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涵防,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門闹伪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人武学,你說我怎么就攤上這事祭往。” “怎么了火窒?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驮肉。 經(jīng)常有香客問我熏矿,道長,這世上最難降的妖魔是什么离钝? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任票编,我火速辦了婚禮,結(jié)果婚禮上卵渴,老公的妹妹穿的比我還像新娘慧域。我一直安慰自己,他們只是感情好浪读,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布昔榴。 她就那樣靜靜地躺著,像睡著了一般碘橘。 火紅的嫁衣襯著肌膚如雪互订。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天痘拆,我揣著相機(jī)與錄音仰禽,去河邊找鬼。 笑死纺蛆,一個胖子當(dāng)著我的面吹牛吐葵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播桥氏,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼温峭,長吁一口氣:“原來是場噩夢啊……” “哼识颊!你這毒婦竟也來了奕坟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤月杉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抠艾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苛萎,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡检号,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了齐苛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翘盖。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖凹蜂,靈堂內(nèi)的尸體忽然破棺而出馍驯,到底是詐尸還是另有隱情玛痊,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布擂煞,位于F島的核電站,受9級特大地震影響蝗拿,放射性物質(zhì)發(fā)生泄漏官辽。R本人自食惡果不足惜蛹磺,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一同仆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧俗批,春花似錦、人聲如沸辛慰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戚篙,卻和暖如春溺职,著一層夾襖步出監(jiān)牢的瞬間岔擂,已是汗流浹背浪耘。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留七冲,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓状原,卻偏偏與公主長得像苗踪,于是被迫代替她去往敵國和親削锰。 傳聞我的和親對象是個殘疾皇子通铲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

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