以前寫了一篇關於react-native-code-push的入門使用篇:微軟的React Native熱更新 - 使用篇,真的是很簡單的使用,能熱更新成功就好了。這一篇經過在項目中實戰所遇到的問題,根據源碼分析它的原理,來更深刻的理解code-push。java
這篇文章是在已經搭建好code-push環境(執行過
npm install --save react-native-code-push@latest
、react-native link react-native-code-push
,並安裝了code-push cli
且成功登錄)爲基礎下寫的,沒有使用CRNA來建立App。react
在真正的項目中,咱們通常會分爲開發版(Test),灰度版(Staging)和發佈版(Production),在Test中我通常是用來跟蹤code-push的執行,在Staging中實際上是和Production是一樣的代碼,可是當要熱修復線上版本時,先會發布熱更新到Staging版,在Staging測事後再經過promoting推到Production中去。android
大體步驟:ios
code-push app add MyAppIOS ios react-native
來建立iOS端的App,或者經過code-push app add MyAppAndroid android react-native
建立Android端的App。code-push app ls
查看是否添加成功,默認會建立兩個部署(deployment)環境:Staging和Production,能夠經過code-push deployment ls MyAppIOS -k
來查看當前App全部的部署,-k
是用來查看部署的key
,這個key
是要方法原生項目中去的。code-push deployment add MyAppIOS Test
,添加成功後,就能夠經過code-push deployment ls MyAppIOS -k
來查看Test
部署環境下的key
了。常用code-push --h來查看能夠執行的操做git
最後結果以下圖所示:
github
在上面有提過須要把部署的key
添加到原生項目中,這樣在不一樣的運行環境下動態的使用對應的部署key
,例如在Staging
下使用Staging
的key
,在Relase
下使用Production
的key
,在Debug
下不使用熱更新(如需在debug環境下測試code-push,能夠在codePush.sync裏的option參數中動態修改部署key
)。npm
在Android中動態部署key,而且在同一設備同時安裝不一樣部署的Android包
有兩種方式:react-native
R.string
來實現一樣的效果,在app/src
中分別添加staging/res/values
和debug/res/values
兩個文件夾,而後複製app/src/main/res/value/strings.xml
粘貼到剛新建的兩個values
目錄下,最後在代碼中獲取key
的方式爲R.string.reactNativeCodePush_androidDeploymentKey
。配置好後可使用
./gradlew assembleStaging
來打包Staging下的apk,輸出目錄在./android/app/build/outputs/apk
下,沒有在gradle中配置簽名安裝(adb install app-staging.apk
)會出現以下錯誤:Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES] React native,關於gradle的buildType的使用:tools.android.com/tech-docs/n…api
在iOS中動態部署key
官方配置入口:github.com/Microsoft/r…promise
在iPhone上同時安裝相同App的不一樣部署包
讓你的iOS應用在不一樣狀態(debug, release)有不一樣的圖標和標題
一、分清楚 Target binary version 和 Label
label
表明發佈的更新版本,
Target binary version
表明app的版本號。
二、使用patch打補丁,修改元數據屬性。
使用場景:例如當你已經發布了一個更新,可是到有些狀況下,好比--des
須要修改,--targetBinaryVersion
寫錯了,好比個人8.6.0
寫成了8.6
,而後在我發佈8.6.1
新版的時候就會拉取8.6
的版本更新,這個時候就能夠code-push patch MyAppAndroid Production --label v4 --targetBinaryVersion 8.6.1
。
三、使用promote將Staging推到Production
使用場景:當你在指定的部署環境下測試更新時,例如Staging
,測試經過後,想把這個更新發布到正式生產環境Production
中,則可使用code-push promote MyAppAndroid Staging Production
,這時能夠修改一些元數據,例如--description
、--targetBinaryVersion
、--rollout
等。
四、使用rollback回滾
使用場景:當你發佈的更新測試沒經過時,能夠回滾到以前的某個版本。code-push rollback MyAppAndroid Production
,當執行這個命令時它會在MyAppAndroid上的Production部署上再次發佈一個release,這個release的代碼和元屬性與Production上倒數第二個版本一致。也能夠經過可選參數--targetRelease
來指定rollback
到的版本,例如code-push rollback MyAppAndroid Production --targetRelase v2
,則會新建一個release,這個release的代碼和元屬性與v2
相同。
注意:這個回滾是主動回滾,與自動回滾不同
五、使用debug查看是否使用了熱更新版本
使用場景:當你想知道code-push的狀態時,好比正在檢查是否有更新包,正在下載,正在安裝,當前加載的
bundle路徑等,對於android可使用code-push debug android
,對於iOS可使用code-push debug ios
注意:debug ios必須在模擬器下才可使用
六、使用deployment h查看更新狀態
使用場景:在發佈更新後,須要查看安裝狀況,能夠經過code-push deployment h MyAppAndroid Production
來查看每一次更新的安裝指標。
七、較難理解的發佈參數
codePush.sync
時,updateDialog
爲true
的狀況下,若是-mandatory
爲false
,則更新提示框會彈出兩個按鈕,一個是【確認更新】,一個是【取消更新】,可是在-mandatory
爲true
的狀況下就只有一個按鈕【確認更新】用戶無法拒絕安裝這個更新。在updateDialog
爲false
的狀況下,-mandatory
就不起做用了,由於都會靜默更新。
注意:mandatory是服務器傳給客戶端的,它是一個「動態」屬性,意思就是當你正在使用版本
v1
的更新,而後如今服務器上有v2
和v3
的更新可用,v2
的mandatory
爲true
,v3
的mandatory
爲false
,此時去check update
,服務器會返回v3
的更新屬性給客戶端,這時服務返回的v3
的mandatory
爲true
,由於v3
在v2
以後發佈的更新,它會被認爲是包含v2
的全部更新信息的,居然v2
有強制更新的需求,那跳過v2
直接更新到v3
的狀況下,v3
也被要求強制更新。可是若是你當前是在使用v2
的更新包,check update
時服務器返回v3
的更新包屬性,此時v3
的mandatory
爲false
,由於對於v2
而言v3
不是強制要更新的。
false
,顧名思義,這個參數的意思就是這個更新包是否讓用戶使用,若是爲true,則不會讓用戶下載這個更新包,使用場景:
code-push promote MyAppAndroid Staging Production --disabled false
來發布更新到正式環境,在對外公佈信息後,使用code-push patch MyAppAndroid Production --disabled true
來讓用戶可使用這個更新。rollout
值小於100
,有三點要注意:
rollout
值被patch
爲100
。rollback
時,rollout
值會被置空(爲100)。promote
去其餘部署時,rollout
會被置空(爲100),能夠從新指定--rollout
。八、理解安裝指標(Install Metrics)數據
先來看下試用過程,如今有兩個機子,分別爲A和B
第一步:發了一個更新包,Install Metrics
中提示No install recorded
表示沒有安裝記錄
v1
打了個
patch
,把
App Version
改成
1.0.0
,而且把元屬性
Disabled
改成
true
Install Metrics
中的
Activite
爲
0%
了(0 of 1),證實在
of
左邊的數是會增降的,of右邊的數是隻會增不會降的,
of
左邊的數表明當前
install
或者
receive
的總人數,當有用戶卸載App,或者使用了更新的更新包時,這個數就會下降。所以它很好的解釋了當前更新包有多少活躍用戶,多少用戶接收過這個安裝包。
Install Metrics
中的
total
並無改變,仍是爲
1
,表明有多少個用戶
install
過這個更新包,這個數字只增不降,注意
total
與
active
的區別。
disabled
爲
true
,所以不會接收這個更新包。
Disabled
改成
true
,讓B
check update
,發現下圖中
active
中
of
右邊的數增長了
1
,表明多了一個用戶
received
v1,可是
of
左邊的數字爲
0
,表明v1沒有活躍用戶,
total
的改變是多了
(1 pending)
,表明有一個用戶
received
v1,可是尚未
install
(也就是
notifyApplicationReady
沒被調用)
check update
,發現
Active
沒有任何改變,由於B之前就接收過v1。
total
中
pending
數爲
2
了,表明有兩個用戶
received
v1。
install
v1,
active
變爲
50%
,能夠看出
installed/received
爲50%。
total
增長了
1
,表明v1多了一次
installed
,一共經歷了
2
次
installed
,
(1 pending)
表明還有一個
received
。
install
v1,
active
變爲
100%
。
total
增長了
1
,表明v1多了一次
installed
,一共經歷了
3
次
installed
,沒有
pending
表明沒有
received
。
rollback
的更新。
App.js
的構造函數中添加以下代碼:
constructor() {
super(...arguments)
throw new Error('roll back')
}複製代碼
而後發個更新出去:code-push release-react MyAppIOS ios -d Staging --dev false --des rollBackTest
此時code-push deployment h MyAppIOS Staging
爲:
check update
,而且把
code-push debug ios
打開(注意debug必須使用模擬器)。發現v2的
total
直接從v1
total
中讀下來,也就是說全部的v1用戶都會
received
v2,
pending
爲
1
表明A
recevied
v2,但沒有
installed
。
installed
v2,發現A會閃退,而後再次進入App,發現
pending
沒有了,可是
total
並無增長,
active
也沒有改變,
pending
的加到
rollbacks
去了。
code-push debug ios
會打印
Update did not finish loading the last time, rolling back to a previous version.
通過上面的測試,大體瞭解了Install metrics
中各個參數的意思,這裏大概總結一下:
installed
這個release或者離開這個release(installed了別的更新包,或者卸載了App),總之有它就知道當前release的活躍用戶量installed
這個release的用戶的數量,這個數量只會增不會減。installed
,所以這一個數值會在release被下載時增加,在installed
時下降。這個指標主要是適配於沒有爲更新配置立馬安裝(mandatory)。若是你爲更新配置了立馬安裝可是仍是有pending,頗有多是你的App啓動時沒有調用notifyApplicationReady
。installing
中發生crash
,code-push將會把它回滾到以前的一個更新包中。
檢查、下載、使用以及rollback更新包
js模塊:
code-push中Javascript API並很少,能夠在JavaScript API查閱。
而快速接入的方法也就兩種,一種是sync
,一種是root-level HOC
。如今來看HOC的源碼:
//CodePush.js 456行
componentDidMount() {
if (options.checkFrequency === CodePush.CheckFrequency.MANUAL) {
//若是是手動檢查更新,直接installed
CodePush.notifyAppReady();
} else {
...
//若是不是手動更新,則每次start app都會去sync
CodePush.sync(options, syncStatusCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback);
if (options.checkFrequency === CodePush.CheckFrequency.ON_APP_RESUME) {
//每次從後臺恢復時sync
ReactNative.AppState.addEventListener("change", (newState) => {
newState === "active" && CodePush.sync(options, syncStatusCallback, downloadProgressCallback);
});
}
}
}複製代碼
能夠看出更新的代碼是sync
:
//CodePush.js 344行
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);複製代碼
在checkForUpdate中會去拿App的版本號,部署key和當前更新包的hash值,確保服務器傳過來對應的更新包,有幾種狀況拿不到更新包,第一種是服務端沒有更新包,第二種是服務端的更新包要求的版本號與當前App版本不符,第三種是服務端的更新包和App當前正在使用的更新包Hash值相同。
//CodePush.js 85行
//PackageMixins.remote(...)執行後返回一個對象包含兩屬性,分別是download和isPending。
//download是一個異步方法用來下載更新包,isPending初始值爲false,表示沒有installed。
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
//會去判斷這個包是不是已經安裝失敗的包(rollback過)
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;複製代碼
拿到remotePackage後判斷這個更新包是否能使用,能使用就去下載:
//CodePush.js 362行
//若是有拿個更新包,可是這個更新包是安裝失敗的包,而且設置中配置忽略安裝失敗的包,則這個更新包會被忽略
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
if (!remotePackage || updateShouldBeIgnored) {
if (updateShouldBeIgnored) {
log("An update is available, but it is being ignored due to having been previously rolled back.");
}
//會去原生端拿當前下載的更新包,若是這個更新包沒有installed,又更新包能夠安裝,若是已經installed就會提示已是最新版本。
const currentPackage = await CodePush.getCurrentPackage();
if (currentPackage && currentPackage.isPending) {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
return CodePush.SyncStatus.UPDATE_INSTALLED;
} else {
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
return CodePush.SyncStatus.UP_TO_DATE;
}
} else{
//若是設置中配置彈提示框,則根據mandatory彈出不一樣的提示框,根據用戶的選擇決定是否下載更新包。
//若是沒有配置彈提示框,則直接下載更新包
...
}複製代碼
下載的代碼:
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
//使用以前提到的download方法來下載更新包。
const localPackage = await remotePackage.download(downloadProgressCallback);
//檢查安裝方式
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//安裝更新
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
return CodePush.SyncStatus.UPDATE_INSTALLED;
};複製代碼
原生模塊(以Android端爲例):
首先尋找jsbundle路徑,getJSBundleFile
中返回了CodePush.getJSBundleFile()
,在這裏面會判斷是否有新下載的更新包,若是比本地新則加載這個更新包,不然加載本地包,
//CodePush.java 143行
public String getJSBundleFileInternal(String assetsBundleFileName) {
this.mAssetsBundleFileName = assetsBundleFileName;
String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName;
//獲取當前可使用的更新包的路徑
String packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName);
if (packageFilePath == null) {
// 當前沒有任何更新包可使用
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
sIsRunningBinaryVersion = true;
return binaryJsBundleUrl;
}
//獲取當前可使用的更新包的配置文件
JSONObject packageMetadata = this.mUpdateManager.getCurrentPackage();
if (isPackageBundleLatest(packageMetadata)) {
//若是當前更新包是最新可用的(版本號相符),使用當前更新包
CodePushUtils.logBundleUrl(packageFilePath);
sIsRunningBinaryVersion = false;
return packageFilePath;
} else {
// 當前App的版本是新的(好比更新包是8.6.0的,如今App是8.6.1)
this.mDidUpdate = false;
if (!this.mIsDebugMode || hasBinaryVersionChanged(packageMetadata)) {
//當App版本號有改變的時候清除全部更新包
this.clearUpdates();
}
//使用本地bundle
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
sIsRunningBinaryVersion = true;
return binaryJsBundleUrl;
}
}複製代碼
在js端的remotePackage.download
中會調用原生的downloadUpdate方法
:
//CodePushNativeModule.java 203行
public void downloadUpdate(final ReadableMap updatePackage, final boolean notifyProgress, final Promise promise) {
//後臺下載任務
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage);
CodePushUtils.setJSONValueForKey(mutableUpdatePackage, CodePushConstants.BINARY_MODIFIED_TIME_KEY, "" + mCodePush.getBinaryResourcesModifiedTime());
//開始下載remotePackage
mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
//下載進度回調
...
});
//獲取remotePackage的信息並返回給js
JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));
} catch (IOException e) {
e.printStackTrace();
promise.reject(e);
} catch (CodePushInvalidUpdateException e) {
e.printStackTrace();
mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
promise.reject(e);
}
return null;
}
};
asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}複製代碼
在js端調用installUpdate
,一共會出現三個hash值,分別是剛下載的更新包的hash值(packageHash),當前使用的hash值(currentPackageHash),之前使用的hash值(previousPackageHash),如今要把prevousPackageHash = currentPackageHash
,currentPackageHash = packageHash
//CodePushUpdateManager.java
public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
//獲取更新包的hash值
String packageHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
JSONObject info = getCurrentPackageInfo();
//獲取當前使用的更新包的hash值
String currentPackageHash = info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null);
if (packageHash != null && packageHash.equals(currentPackageHash)) {
// 若是下載的更新包和當前使用的是同一個更新包,不作處理
return;
}
if (removePendingUpdate) {
//若是當前使用的更新包是下載好但沒有installed的更新包,則把這個更新包移除
String currentPackageFolderPath = getCurrentPackageFolderPath();
if (currentPackageFolderPath != null) {
FileUtils.deleteDirectoryAtPath(currentPackageFolderPath);
}
} else {
//獲取以前的更新包,並移除
String previousPackageHash = getPreviousPackageHash();
if (previousPackageHash != null && !previousPackageHash.equals(packageHash)) {
FileUtils.deleteDirectoryAtPath(getPackageFolderPath(previousPackageHash));
}
//將上一個更新包指向當前更新包
CodePushUtils.setJSONValueForKey(info, CodePushConstants.PREVIOUS_PACKAGE_KEY, info.optString(CodePushConstants.CURRENT_PACKAGE_KEY, null));
}
//設置當前可以使用的更新包爲update package
CodePushUtils.setJSONValueForKey(info, CodePushConstants.CURRENT_PACKAGE_KEY, packageHash);
updateCurrentPackageInfo(info);
}複製代碼
將剛下載的更新包標記爲pending package,isloading爲false:
//CodePushNativeModule.java 411行,
//標記爲pending,而且isLoading爲false
mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);複製代碼
App第一次進入和從新加載bundle時會調用initializeUpdateAfterRestart
,用來判斷是否有pending package,若是有而且isloading爲true(被init過),表明這個pending package在notifyApplicationReady
前崩潰了,所以須要rollback,若是isloading爲false則表明是第一次加載更新包,會將isloading(init)置爲true,用來判斷下次進入時需不須要rollback:
//CodePush.js 177行
void initializeUpdateAfterRestart() {
...
JSONObject pendingUpdate = mSettingsManager.getPendingUpdate();
if (pendingUpdate != null) {
//有新的更新包可用
JSONObject packageMetadata = this.mUpdateManager.getCurrentPackage();
if (!isPackageBundleLatest(packageMetadata) && hasBinaryVersionChanged(packageMetadata)) {
//版本不符
CodePushUtils.log("Skipping initializeUpdateAfterRestart(), binary version is newer");
return;
}
try {
boolean updateIsLoading = pendingUpdate.getBoolean(CodePushConstants.PENDING_UPDATE_IS_LOADING_KEY);
if (updateIsLoading) {
// Pending package已經被init過, 可是 notifyApplicationReady 沒有被調用.
// 所以認爲這是個無效的更新而且rollback.
CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version.");
sNeedToReportRollback = true;
rollbackPackage();
} else {
// 如今有個新的更新包能夠運行,開始init這個更新包
//若是它崩潰了,須要在下一次啓動時rollback
mSettingsManager.savePendingUpdate(pendingUpdate.getString(CodePushConstants.PENDING_UPDATE_HASH_KEY),
/* isLoading */true);
}
} catch (JSONException e) {
// Should not happen.
throw new CodePushUnknownException("Unable to read pending update metadata stored in SharedPreferences", e);
}
}
}複製代碼
rollback的代碼:
//CodePush.java 257行
private void rollbackPackage() {
//將當前使用的更新包標記爲失敗的包
JSONObject failedPackage = mUpdateManager.getCurrentPackage();
mSettingsManager.saveFailedUpdate(failedPackage);
//用以前使用的更新包替換當前使用的更新包
mUpdateManager.rollbackPackage();
//移除pending package
mSettingsManager.removePendingUpdate();
}複製代碼
notifyApplicationReady的代碼:
//CodePushNativeModule.java 498行
public void notifyApplicationReady(Promise promise) {
//移除pending package
mSettingsManager.removePendingUpdate();
promise.resolve("");
}複製代碼
總結:
js端使用checkupdate
用App當前的版本號,當時使用的更新包信息以及部署key傳遞給原生,原生調用codu-push服務器查詢是否有更新包可使用,若是不存在更新包,或者更新包與當前使用的更新包一致,或者版本號不符都不會產生remotePackage。拿到remotePackage後會去原生的本地存儲查詢這個remotePackage的hash是否爲failedPackage,若是是failedPackage則會選擇忽略這個更新包,不然就download這個更新包。
下載好更新包後,將這個更新包標誌位pending package,而且isloading爲false,將previousPacakge置爲currentPackage,currentPackage置爲下載的更新包。
在加載更新包時會判斷這個更新包是不是pending package,若是是則判斷isloading是否爲false,若是爲false則表明這個pending package是第一次加載,若是爲true則表明這個pending被加載後調用notifyApplicationReady前發生崩潰,須要回滾。
若是發生回滾會將pending package置空,將previouPackage賦值給currentPackage。
在正確加載更新包後,應該手動觸發notifyApplicationReady將pending package置空,表明這個更新包被正確installed。
示例:
hash包的管理:
failed package:崩潰的package
pending package:下載好的沒有被installed的package
previous package: 以前使用的package
current package:當前正在使用package
第一步:下載更新包A
pending pacakge = A
isloding = false
previous package = current package
current package = pending package複製代碼
第二步:第一次使用A
pending isloading = true複製代碼
若是在notifyApplicationReady以前發生崩潰走第三步,不然走第四步。
第三步:再次加載bundle,發現pending package還存在,而且isloading爲true,回滾
第四步:pending package不存在,不作任何處理