在 《CodePush熱更新經常使用命令與注意事項》中簡單介紹過關於codepush的一些經常使用操做及命令。在發佈更新包時,咱們通常會經過以下命令:
javascript
code-push release <appName> <platform> ./bundle-images文件夾路徑/ -d Production -t 1.1.1 --des "更新描述"複製代碼
或者
java
code-push release-react <appName> <platform> -t 1.1.1 -d Production --des "更新描述" -m true (強制更新)複製代碼
兩種方式的區別在於第二條命令會自動打包生成bundle文件和圖片資源。node
假設當前熱更新的版本爲 1.1.0,內容以下:react
資源文件夾/├── drawable-mdpi│ ├── a.png│ └── b.png└── index.iOS.bundle複製代碼
即將要更新的版本爲1.1.1,內容以下:
android
資源文件夾/├── drawable-mdpi│ ├── a.png│ ├── b.png│ └── c.png // 新增圖片 c.png└── index.iOS.bundle複製代碼
若是當前 App 處於 1.1.0版本,而且在以前沒有作過任何codepush相關的更新操做。從 1.1.0 更新到 1.1.1 時,按照咱們所理解的 codepush 差別化更新的特色,在App檢測到 1.1.1 的更新後,會將 c.png 圖片和結構發生變化的 jsbundle 文件下載到本地,並在合適的時機進行加載,渲染。但真相併不是如此!
ios
1. 在 codepush 系統檢測到有新版本,第一次作熱更新加載時,會將全部的資源所有下載到本地。
2. 若是以前用戶是1.0.0 版本經過 codepush 升級到 1.1.0 版本,再升級到 1.1.1 版本,這個時候 codepush 就會下載 diff 以後的 JSBundle 和新增的 c.png 圖片兩個文件。複製代碼
因此在第一次作熱更新操做時,是稍微消耗帶寬流量的。
git
形成這個現象的主要緣由還要從 RN 框架 圖片加載的方式提及,咱們來看看 RN 圖片加載流程的核心代碼,打開 node_modules/react-native/Libraries/Image/AssetSourceResolver.js:
github
/** * jsBundle 文件是否從packager服務加載 */
isLoadedFromServer(): boolean {
return !!this.serverUrl;
}
/** * jsBundle 文件是否從本地文件目錄加載 */
isLoadedFromFileSystem(): boolean {
return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://'));
}
/** * 圖片加載 */
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}複製代碼
在 defaultAsset() 方法中,首先判斷JSBundle文件是不是從 packager 服務加載(調試模式),若是是會直接本地服務加載。 接着對 Android、iOS 兩個不一樣不一樣處理: (1)Android 平臺下,判斷是不是從手機本地目錄下加載,若是是,則調用 drawableFolderInBundle() 方法;反之調用 resourceIdentifierWithoutScale() 方法。 (2)iOS 平臺下直接調用 scaledAssetURLNearBundle() 方法。 咱們首先 Android 平臺下的 drawableFolderInBundle()、resourceIdentifierWithoutScale() 兩個方法
面試
/** * 若是jsbundle從本地文件目錄下加載運行,則會解析相對於其位置的資產 * E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png' */
drawableFolderInBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
}
/** * 與應用捆綁在一塊兒的資產的默認位置,按資源標識符定位 * Android資源系統選擇正確的比例 * E.g. 'assets_awesomemodule_icon' */
resourceIdentifierWithoutScale(): ResolvedAssetSource {
invariant(
Platform.OS === 'android',
'resource identifiers work on Android',
);
return this.fromSource(
assetPathUtils.getAndroidResourceIdentifier(this.asset),
);
}複製代碼
從上述源碼咱們不難發現: (1)若是 JSBundle 文件是從本地文件目錄(File)加載,例如(/sdcard/com.songlcy.myapp/...)之類的目錄,不是從 assets 資源目錄加載的狀況下,會從相對該目錄下的 drawable-xxx 目錄下加載圖片。
react-native
假設當前加載的 JSBundle 的文件路徑是 /sdcard/com.songlcy.myapp/code-push/index.iOS.jsbundle,會從 /sdcard/com.songlcy.myapp/code-push/ 目錄下查找圖片。複製代碼
(2)若是是從 assets 目錄中加載的 JSBundle 文件,這個時候就會從apk包中的 drawable-xxx 目錄中加載圖片。
iOS 平臺下並無作任何區分,直接調用了 scaledAssetURLNearBundle() 方法:
/**
* 直接從 JSBundle 當前目錄下查找 assets 目錄
* E.g. 'file:///sdcard/bundle/assets/AwesomeModule/icon@2x.png'
*/
scaledAssetURLNearBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
return this.fromSource(path + getScaledAssetPath(this.asset));
}複製代碼
分析完圖片的整個加載流程,咱們再回到 codepush 更新。咱們都知道,在當前APP檢測到有更新時,codepush 會將服務器上的JSBundle 及 圖片資源下載到手機本地目錄,因此 JSBundle 文件是從手機系統文件目錄加載,根據RN圖片加載流程,更新的圖片資源也須要放到 JSBundle 所在目錄下。所以在 codepush 第一次更新的時候,須要把全部資源所有下載下來,不然會出現找不到資源的錯誤,加載失敗。一樣這樣作也是爲了方便統一管理,在第二次更新時, codepush 就會作一次 diff-patch,經過比對來實現差別化增量更新。
瞭解了當前 codepush 在第一次更新時所帶來的更新流量開銷,那麼咱們如何優化第一次更新的包體積,使其也能夠作到差別化增量更新呢?經過上面的分析,咱們能夠修改RN圖片加載流程,經過 assets 和 本地目錄結合,在更新後,判斷當前JSBundle所在的本地目錄下是否存在更新以前的資源,若是存在直接加載,不存在,則從apk包中的 drawable-xxx 目錄中加載。此時,咱們就不用上傳全部的圖片資源,只須要上傳更新的資源便可。 修改RN圖片加載流程,咱們能夠直接在源碼中進行修改,也可使用 hook 的方式進行修改,保證了項目與node_modules耦合度下降,核心代碼以下:
import { NativeModules } from 'react-native';
import AssetSourceResolver from "react-native/Libraries/Image/AssetSourceResolver";
import _ from 'lodash';
let iOSRelateMainBundlePath = '';
let _sourceCodeScriptURL = '';
// ios 平臺下獲取 jsbundle 默認路徑
const defaultMainBundePath = AssetsLoad.DefaultMainBundlePath;
function getSourceCodeScriptURL() {
if (_sourceCodeScriptURL) {
return _sourceCodeScriptURL;
}
// 調用Native module獲取 JSbundle 路徑
// RN容許開發者在Native端自定義JS的加載路徑,在JS端能夠調用SourceCode.scriptURL來獲取
// 若是開發者未指定JSbundle的路徑,則在離線環境下返回asset目錄
let sourceCode =
global.nativeExtensions && global.nativeExtensions.SourceCode;
if (!sourceCode) {
sourceCode = NativeModules && NativeModules.SourceCode;
}
_sourceCodeScriptURL = sourceCode.scriptURL;
return _sourceCodeScriptURL;
}
// 獲取bundle目錄下全部drawable 圖片資源路徑
let drawablePathInfos = [];
AssetsLoad.searchDrawableFile(getSourceCodeScriptURL(),
(retArray)=>{
drawablePathInfos = drawablePathInfos.concat(retArray);
});
// hook defaultAsset方法,自定義圖片加載方式
AssetSourceResolver.prototype.defaultAsset = _.wrap(AssetSourceResolver.prototype.defaultAsset, function (func, ...args) {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
if(this.isLoadedFromFileSystem()) {
// 獲取圖片資源路徑
let resolvedAssetSource = this.drawableFolderInBundle();
let resPath = resolvedAssetSource.uri;
// 獲取JSBundle文件所在目錄下的全部drawable文件路徑,並判斷當前圖片路徑是否存在
// 若是存在,直接返回
if(drawablePathInfos.includes(resPath)) {
return resolvedAssetSource;
}
// 判斷圖片資源是否存在本地文件目錄
let isFileExist = AssetsLoad.isFileExist(resPath);
// 存在直接返回
if(isFileExist) {
return resolvedAssetSource;
} else {
// 不存在,則根據資源 Id 從apk包下的drawable目錄加載
return this.resourceIdentifierWithoutScale();
}
} else {
// 則根據資源 Id 從apk包下的drawable目錄加載
return this.resourceIdentifierWithoutScale();
}
} else {
let iOSAsset = this.scaledAssetURLNearBundle();
let isFileExist = AssetsLoad.isFileExist(iOSAsset.uri);
isFileExist = false;
if(isFileExist) {
return iOSAsset;
} else {
let oriJsBundleUrl = 'file://'+ defaultMainBundePath +'/' + iOSRelateMainBundlePath;
iOSAsset.uri = iOSAsset.uri.replace(this.jsbundleUrl, oriJsBundleUrl);
return iOSAsset;
}
}
});複製代碼
實現邏輯其實很簡單:
(1)經過 hook 的方式從新定義 defaultAsset() 方法。
(2)若是是從手機系統文件目錄加載JSBundle文件:
1. 獲取當前圖片資源文件路徑,判斷當前JSBundle目錄下是否存在。若是存在,則直接返回當前資源。
2. 判斷手機本地文件目錄下是否存在該圖片資源,若是存在,則直接返回當前資源。不存在,則從 apk 包中根據資源 Id 來加載圖片資源。
(3)不是從手機系統文件目錄加載JSBundle文件,則直接從 apk 包中根據資源 Id 來加載圖片資源。
通過以上流程在 codepush 第一次更新時,實現資源的差別化增量更新。詳細代碼能夠查看 react-native-code-push-assets