前言:前面寫了如何搭建熱更新的本地環(huán)境,今天梳理一下熱更新的流程,在看代碼之前,我們先想一下,實(shí)現(xiàn)更新應(yīng)該需要以下幾個(gè)步驟:
1、向服務(wù)端提交檢查請求
2、服務(wù)端告知本地有新版本,通知本地去更新
3、本地下載新版本
4、安裝新版本
5、加載新版本內(nèi)容
以上應(yīng)該是檢查更新的基本步驟,我們再來看一下對應(yīng)的代碼:
demo中的檢查更新是在App.js文件中執(zhí)行的,我們可以看到執(zhí)行更新主要有以下兩個(gè)方法:
/** Update is downloaded silently, and applied on restart (recommended) */
sync() {
CodePush.sync(
{},
this.codePushStatusDidChange.bind(this),
this.codePushDownloadDidProgress.bind(this)
);
}
/** Update pops a confirmation dialog, and then immediately reboots the app */
syncImmediate() {
CodePush.sync(
{ installMode: CodePush.InstallMode.IMMEDIATE, updateDialog: true },
this.codePushStatusDidChange.bind(this),
this.codePushDownloadDidProgress.bind(this)
);
}
方法中都調(diào)用了CodePush.sync這個(gè)方法,我們看一下這個(gè)方法做了什么:
/**
* Allows checking for an update, downloading it and installing it, all with a single call.
檢查更新、下載、安裝都在這個(gè)方法里啦啦啦啦啦。
*
* @param options Options used to configure the end-user update experience (e.g. show an prompt?, install the update immediately?).
更新的參數(shù)
* @param syncStatusChangedCallback An optional callback that allows tracking the status of the sync operation, as opposed to simply checking the resolved state via the returned Promise.
更新狀態(tài)改變的回調(diào)
* @param downloadProgressCallback An optional callback that allows tracking the progress of an update while it is being downloaded.
下載進(jìn)度的回調(diào)
* @param handleBinaryVersionMismatchCallback An optional callback for handling target binary version mismatch
版本不一致的回調(diào)(可選)
*/
function sync(options?: SyncOptions,
syncStatusChangedCallback?: SyncStatusChangedCallback,
downloadProgressCallback?: DowloadProgressCallback,
handleBinaryVersionMismatchCallback?:
HandleBinaryVersionMismatchCallback): Promise<SyncStatus>;
檢查更新的參數(shù):
export interface SyncOptions {
deploymentKey?: string;
檢查更新的部署密鑰,就是我們在code-push服務(wù)器上創(chuàng)建app時(shí)生成的key,服務(wù)器根據(jù)此來判斷是檢查哪一個(gè)應(yīng)用。
installMode?: CodePush.InstallMode;//安裝模式,默認(rèn)是下一次restart時(shí)安裝,包含:
IMMEDIATE(安裝更新并立刻重啟應(yīng)用),
ON_NEXT_RESTART(安裝更新,但不立馬重啟,直到下一次重新進(jìn)入),
ON_NEXT_RESUME(安裝更新,但是不立馬重新啟動,直到下一次從后臺恢復(fù)到前臺)
ON_NEXT_SUSPEND(下一次處于后臺時(shí))
mandatoryInstallMode?: CodePush.InstallMode;
指定如果更新的版本是標(biāo)識了強(qiáng)制更新怎么更新。具體參數(shù)和上面一樣。
minimumBackgroundDuration?: number;
指定應(yīng)用程序需要在后臺重新啟動應(yīng)用程序之前所需的最小秒數(shù),默認(rèn)是0。
updateDialog?: UpdateDialog;
標(biāo)記檢查到更新是要彈出對話框
}
讓我們再看看CodePush中sync方法具體做了什么處理(代碼在node_modules/react-native-code-push/CodePush.js,本地的module代碼對應(yīng)的是react-native-code-push對應(yīng)的lib庫中的com/microsoft/codepush/react/CodePushNativeModule.java):
const sync = (() => {
let syncInProgress = false;
const setSyncCompleted = () => { syncInProgress = false; };
//上面是互斥處理,保證多次調(diào)用sync只有一個(gè)在檢查更新,其他直接返回正在檢查中的狀態(tài)
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
let syncStatusCallbackWithTryCatch, downloadProgressCallbackkWithTryCatch;
//將syncStatusChangeCallback和downloadProgressCallback放入try catch中。
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
//將syncStatusChangeCallback和downloadProgressCallback放入try catch中。
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackkWithTryCatch = (...args) => {
try {
downloadProgressCallback(...args);
} catch (error) {
log(`An error has occurred: ${error.stack}`);
}
}
}
//檢查更新的互斥邏輯處理
if (syncInProgress) {
typeof syncStatusCallbackWithTryCatch === "function"
? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
syncInProgress = true;
//調(diào)用了syncInternal方法
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackkWithTryCatch, handleBinaryVersionMismatchCallback);
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
我們再看一下syncInternal方法做了什么,這個(gè)里面就包含了所有的流程:
/*
該方法提供了一個(gè)簡單的在線合并檢查、下載、安裝更新
* The syncInternal method provides a simple, one-line experience for
* incorporating the check, download and installation of an update.
*
它將這一些方法組合到一起,并支持強(qiáng)制更新、忽略失敗更新、更新確認(rèn)dialog等
* It simply composes the existing API methods together and adds additional
* support for respecting mandatory updates, ignoring previously failed
* releases, and displaying a standard confirmation UI to the end-user
* when an update is available.
*/
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
let resolvedInstallMode;
//準(zhǔn)備檢查更新的參數(shù)
const syncOptions = {
deploymentKey: null,//這個(gè)地方為null,是因?yàn)閺脑a中獲取的
ignoreFailedUpdates: true,//是否忽略失敗的更新版本
installMode: CodePush.InstallMode.ON_NEXT_RESTART,//安裝模式
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,//強(qiáng)制更新時(shí)的安裝模式
minimumBackgroundDuration: 0,//重啟最小等待時(shí)間
updateDialog: null,//是否要對話框
...options//其他傳遞過來的參數(shù)
};
//更新狀態(tài)同步的回調(diào)
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
? syncStatusChangeCallback
: (syncStatus) => {
switch(syncStatus) {
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
break;
case CodePush.SyncStatus.AWAITING_USER_ACTION:
break;
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
break;
case CodePush.SyncStatus.INSTALLING_UPDATE:
break;
case CodePush.SyncStatus.UP_TO_DATE:
break;
case CodePush.SyncStatus.UPDATE_IGNORED:
break;
case CodePush.SyncStatus.UPDATE_INSTALLED:
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
}
break;
case CodePush.SyncStatus.UNKNOWN_ERROR:
break;
}
};
try {
//通知App已經(jīng)ready。
await CodePush.notifyApplicationReady();
//發(fā)出第一個(gè)同步狀態(tài)。
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
//檢查更新
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
//定義下載的方法
const doDownloadAndInstall = async () => {
...
};
const updateShouldBeIgnored = remotePackage && (remotePackage.failedInstall && syncOptions.ignoreFailedUpdates);
if (!remotePackage || updateShouldBeIgnored) {
//如果沒有更新或者當(dāng)前更新應(yīng)該被忽略的
const currentPackage = await CodePush.getCurrentPackage();
if (currentPackage && currentPackage.isPending) {
//已經(jīng)安裝了 syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
return CodePush.SyncStatus.UPDATE_INSTALLED;
} else {
//沒有可更新的內(nèi)容
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
return CodePush.SyncStatus.UP_TO_DATE;
}
} else if (syncOptions.updateDialog) {
//如果需要對話框,則需要用戶手動點(diǎn)擊下載安裝
...
return await new Promise((resolve, reject) => {
let message = null;
const dialogButtons = [{
text: null,
onPress:() => {
doDownloadAndInstall()
.then(resolve, reject);
}
}];
} else {
//如果不需要對話框,直接下載并安裝
return await doDownloadAndInstall();
}
} catch (error) {
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
log(error.message);
throw error;
}
};
notifyApplicationReady
上面的notifyApplicationReady方法是調(diào)用了notifyApplicationReadyInternal方法,而notifyApplicationReadyInternal方法如下:
async function notifyApplicationReadyInternal() {
await NativeCodePush.notifyApplicationReady();//原生Module的方法
//獲取本地的狀態(tài),并上傳到服務(wù)器
const statusReport = await NativeCodePush.getNewStatusReport();
statusReport && tryReportStatus(statusReport); // Don't wait for this to complete.
return statusReport;
}
NativeCodePush.notifyApplicationReady()原生代碼操作如下:
@ReactMethod
public void notifyApplicationReady(Promise promise) {
mSettingsManager.removePendingUpdate();//清除未更新的hash之
promise.resolve("");
}
NativeCodePush.getNewStatusReport的方法就是根據(jù)上一次檢查的狀態(tài)返回不同的值,此處不做細(xì)述。
tryReportStatus方法如下:
async function tryReportStatus(statusReport, resumeListener) {
log(`statusReport = (${statusReport})`);
const config = await getConfiguration();//獲取配置信息
const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;//當(dāng)前版本的標(biāo)簽或者app的版本
const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;//部署的key
try {
if (statusReport.appVersion) {
const sdk = getPromisifiedSdk(requestFetchAdapter, config);//得到操作的sdk,具體方法在前一個(gè)目錄下code-push/script/acquisition-sdk.js
await sdk.reportStatusDeploy(/* deployedPackage */ null, /* status */ null, previousLabelOrAppVersion, previousDeploymentKey);
} else {
const label = statusReport.package.label;
if (statusReport.status === "DeploymentSucceeded") {
log(`Reporting CodePush update success (${label})`);
} else {
log(`Reporting CodePush update rollback (${label})`);
}
config.deploymentKey = statusReport.package.deploymentKey;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
await sdk.reportStatusDeploy(statusReport.package, statusReport.status, previousLabelOrAppVersion, previousDeploymentKey);
}
NativeCodePush.recordStatusReported(statusReport);//本地保存狀態(tài)報(bào)告
resumeListener && AppState.removeEventListener("change", resumeListener);//移除對于change時(shí)間的監(jiān)聽
} catch (e) {
log(`Report status failed: ${JSON.stringify(statusReport)}`);
NativeCodePush.saveStatusReportForRetry(statusReport);//本地保存狀態(tài)報(bào)告并在resume時(shí)重新調(diào)用tryReportStatus方法
// Try again when the app resumes
if (!resumeListener) {
resumeListener = async (newState) => {
if (newState !== "active") return;
const refreshedStatusReport = await NativeCodePush.getNewStatusReport();
if (refreshedStatusReport) {
tryReportStatus(refreshedStatusReport, resumeListener);
} else {
AppState.removeEventListener("change", resumeListener);
}
};
AppState.addEventListener("change", resumeListener);
}
}
}
notifyApplicationReady方法講解完成
checkForUpdate
狀態(tài)什么的處理完后就開始執(zhí)行checkForUpdate檢查更新了。
async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
/*
//獲取本地配置信息,最終調(diào)用NativeCodePush.getConfiguration方法
*/
const nativeConfig = await getConfiguration();
const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);//和服務(wù)器交互的sdk
const localPackage = await module.exports.getCurrentPackage();//本地的package
let queryPackage;
if (localPackage) {
queryPackage = localPackage;
} else {
queryPackage = { appVersion: config.appVersion };
if (Platform.OS === "ios" && config.packageHash) {
queryPackage.packageHash = config.packageHash;
}
}
//從服務(wù)器檢查更新
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
/*
* 有四種結(jié)果:
* ----------------------------------------------------------------
* 1) 沒有更新.
* 2) 服務(wù)器又更新,但是當(dāng)前原生版本不支持此版本
* 3) 服務(wù)器告訴又更新,但是hsah值和本地是一樣的(理論上不會出現(xiàn))
* 4) 服務(wù)器有更新,但是hash值和當(dāng)前運(yùn)行的二進(jìn)制(差異化的二進(jìn)制hash)是一樣的,這只會發(fā)生在Android手機(jī),我理解的是iOS沒有返回差異化的二進(jìn)制。
*/
if (!update || update.updateAppVersion ||
localPackage && (update.packageHash === localPackage.packageHash) ||
(!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
if (update && update.updateAppVersion) {
//有更新,但是當(dāng)前原生的版本不兼容
if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
handleBinaryVersionMismatchCallback(update)
}
}
return null;
} else {
//和本地對比,返回此更新的信息
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);//根據(jù)此hash判斷是否是失敗的更新(本地會存儲之前更新的狀態(tài))
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
return remotePackage;
}
}
需要有兩點(diǎn)需要注意:
1、remotePackage不僅僅有此版本的信息,同時(shí)也將PackageMixins的屬性包含進(jìn)來了,而PackageMixins對應(yīng)的是package-mixins.js的代碼,里面只有兩個(gè)屬性download下載更新和install安裝更新。里面的邏輯在講到下載時(shí)再細(xì)說。
2、上面提到的和服務(wù)器交互的sdkcode-push/script/acquisition-sdk.js里面有queryUpdateWithCurrentPackage查詢更新,reportStatusDeploy記錄狀態(tài)報(bào)告,reportStatusDownload下載狀態(tài)報(bào)告,這幾個(gè)方法,具體細(xì)節(jié)可以看源代碼,不復(fù)雜。
到目前為止,我們已經(jīng)得到了更新的版本信息。往下執(zhí)行會根據(jù)此版本信息來判斷是否是忽略版本,需要下載。如果需要下載,則會執(zhí)行前面提到的doDownloadAndInstall方法。
doDownloadAndInstall
doDownloadAndInstall里面代碼邏輯很少,如下:
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);//同步狀態(tài)
const localPackage = await remotePackage.download(downloadProgressCallback);//下載
// 安裝模式
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//準(zhǔn)備安裝
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
return CodePush.SyncStatus.UPDATE_INSTALLED;
};
我們在來分布解析下載和安裝的邏輯,
download
download的邏輯比較簡單:
async download(downloadProgressCallback) {
if (!this.downloadUrl) {//下載地址為空,拋出異常
throw new Error("Cannot download an update without a download url");
}
let downloadProgressSubscription;//下載進(jìn)度回調(diào)
if (downloadProgressCallback) {
const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
// CodePushDownloadProgress該事件會有原生下載時(shí)發(fā)出
downloadProgressSubscription = codePushEventEmitter.addListener(
"CodePushDownloadProgress",
downloadProgressCallback
);
}
// Use the downloaded package info. Native code will save the package info
// so that the client knows what the current package version is.
try {
const updatePackageCopy = Object.assign({}, this);
Object.keys(updatePackageCopy).forEach((key) => (typeof updatePackageCopy[key] === 'function') && delete updatePackageCopy[key]);
//調(diào)用本地下載
const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
//記錄下載狀態(tài)
reportStatusDownload && reportStatusDownload(this);
//返回下載package信息
return { ...downloadedPackage, ...local };
} finally {
downloadProgressSubscription && downloadProgressSubscription.remove();
}
},
isPending: false // A remote package could never be in a pending state
};
上面NativeCodePush.downloadUpdate是通過本地下載,本地的源碼如下:
@ReactMethod
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());
//通過 mUpdateManager下載并監(jiān)聽進(jìn)度回調(diào),具mUpdateManager的方法和普通下載一樣,不多細(xì)述。
mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
private boolean hasScheduledNextFrame = false;
private DownloadProgress latestDownloadProgress = null;
@Override
public void call(DownloadProgress downloadProgress) {
if (!notifyProgress) {
return;
}
latestDownloadProgress = downloadProgress;
// If the download is completed, synchronously send the last event.
if (latestDownloadProgress.isCompleted()) {
dispatchDownloadProgressEvent();
return;
}
if (hasScheduledNextFrame) {
return;
}
hasScheduledNextFrame = true;
getReactApplicationContext().runOnUiQueueThread(new Runnable() {
@Override
public void run() {
ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, new ChoreographerCompat.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (!latestDownloadProgress.isCompleted()) {
dispatchDownloadProgressEvent();
}
hasScheduledNextFrame = false;
}
});
}
});
}
//重點(diǎn)是該方法,通過時(shí)間不斷地發(fā)出下載的進(jìn)度狀態(tài)
public void dispatchDownloadProgressEvent() {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
}
}, mCodePush.getPublicKey());
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);
}
通過上面源碼分析一下就可以知道下載流程,還是比較簡單的,重點(diǎn)說一下通知下載進(jìn)度給ReactNative的Component是通過getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());方法,而時(shí)間的event就是CodePushDownloadProgress,這個(gè)上面已經(jīng)提到過了。
下載完成之后就執(zhí)行到
// 安裝模式
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
//準(zhǔn)備安裝
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
方法了,我們再來看一下具體的安裝方法源碼:
const local = {
//傳入?yún)?shù)
installMode安裝模式,默認(rèn)NativeCodePush.codePushInstallModeOnNextRestart,
minimumBackgroundDuration上面提到的,如果是onresume的安裝模式,應(yīng)用從上一次暫停到前臺的時(shí)間間隔開始安裝,默認(rèn)為0。
updateInstalledCallback安裝回調(diào)。
async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, minimumBackgroundDuration = 0, updateInstalledCallback) {
const localPackage = this;
const localPackageCopy = Object.assign({}, localPackage); // In dev mode, React Native deep freezes any object queued over the bridge
//調(diào)用本地的installUpdate方法,就是講bundle文件準(zhǔn)備好,以便重新加載,后面細(xì)述。
await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
updateInstalledCallback && updateInstalledCallback();
if (installMode == NativeCodePush.codePushInstallModeImmediate) {
//如果安裝模式是立即安裝的話,直接重啟
RestartManager.restartApp(false);
} else {
//如果不是,清除已經(jīng)準(zhǔn)備好的重啟任務(wù)
RestartManager.clearPendingRestart();
localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
}
},
isPending: false // A local package wouldn't be pending until it was installed
};
上面是CodePush中的安裝方法,里面有兩個(gè)點(diǎn)需要細(xì)述:
一、本地的NativeCodePush.installUpdate方法:
@ReactMethod
public void installUpdate(final ReadableMap updatePackage, final int installMode, final int minimumBackgroundDuration, final Promise promise) {
AsyncTask<Void, Void, Void> asyncTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mUpdateManager.installPackage(CodePushUtils.convertReadableToJsonObject(updatePackage), mSettingsManager.isPendingUpdate(null));
String pendingHash = CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY);
if (pendingHash == null) {
throw new CodePushUnknownException("Update package to be installed has no hash.");
} else {
mSettingsManager.savePendingUpdate(pendingHash, /* isLoading */false);
}
if (installMode == CodePushInstallMode.ON_NEXT_RESUME.getValue() ||
// We also add the resume listener if the installMode is IMMEDIATE, because
// if the current activity is backgrounded, we want to reload the bundle when
// it comes back into the foreground.
installMode == CodePushInstallMode.IMMEDIATE.getValue() ||
installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue()) {
// Store the minimum duration on the native module as an instance
// variable instead of relying on a closure below, so that any
// subsequent resume-based installs could override it.
CodePushNativeModule.this.mMinimumBackgroundDuration = minimumBackgroundDuration;
if (mLifecycleEventListener == null) {
// Ensure we do not add the listener twice.
mLifecycleEventListener = new LifecycleEventListener() {
private Date lastPausedDate = null;
private Handler appSuspendHandler = new Handler(Looper.getMainLooper());
private Runnable loadBundleRunnable = new Runnable() {
@Override
public void run() {
CodePushUtils.log("Loading bundle on suspend");
loadBundle();
}
};
@Override
public void onHostResume() {
appSuspendHandler.removeCallbacks(loadBundleRunnable);
// As of RN 36, the resume handler fires immediately if the app is in
// the foreground, so explicitly wait for it to be backgrounded first
if (lastPausedDate != null) {
long durationInBackground = (new Date().getTime() - lastPausedDate.getTime()) / 1000;
if (installMode == CodePushInstallMode.IMMEDIATE.getValue()
|| durationInBackground >= CodePushNativeModule.this.mMinimumBackgroundDuration) {
CodePushUtils.log("Loading bundle on resume");
loadBundle();
}
}
}
@Override
public void onHostPause() {
// Save the current time so that when the app is later
// resumed, we can detect how long it was in the background.
lastPausedDate = new Date();
if (installMode == CodePushInstallMode.ON_NEXT_SUSPEND.getValue() && mSettingsManager.isPendingUpdate(null)) {
appSuspendHandler.postDelayed(loadBundleRunnable, minimumBackgroundDuration * 1000);
}
}
@Override
public void onHostDestroy() {
}
};
getReactApplicationContext().addLifecycleEventListener(mLifecycleEventListener);
}
}
promise.resolve("");
return null;
}
};
asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
這個(gè)里面有兩個(gè)知識點(diǎn):
1、是調(diào)用mUpdateManager.installPackage來處理已經(jīng)下載好的bundle文件,里面原理比較簡單,可以自行看一下。每一個(gè)版本的文件命名都是以其hash值命名的。
2、建立一個(gè)監(jiān)聽,根據(jù)安裝模式來判斷是在onHostResume中調(diào)用loadBundle還是在onHostPause中調(diào)用loadBundle。
二、RestartManager這個(gè)類:
RestartManager這個(gè)類的源碼在RestartManager.js中,里面有兩個(gè)要素,一個(gè)是重啟任務(wù)的數(shù)組,這個(gè)不用細(xì)說;一個(gè)是重啟的方法,也是調(diào)用NativeCodePush.restartApp(onlyIfUpdateIsPending)原生的方法,源碼如下:
@ReactMethod
public void restartApp(boolean onlyIfUpdateIsPending, Promise promise) {
// If this is an unconditional restart request, or there
// is current pending update, then reload the app.
if (!onlyIfUpdateIsPending || mSettingsManager.isPendingUpdate(null)) {
loadBundle();
promise.resolve(true);
return;
}
promise.resolve(false);
}
我們看到了里面關(guān)鍵的方法是loadBundle,這個(gè)方法獲取了一個(gè)ReactInstanceManager對象和最后更新下來的bundle文件路徑latestJSBundleFile,然后調(diào)用setJSBundle(instanceManager, latestJSBundleFile)方法完成重啟,這個(gè)方法源碼如下:
private void setJSBundle(ReactInstanceManager instanceManager, String latestJSBundleFile) throws IllegalAccessException {
try {
JSBundleLoader latestJSBundleLoader;
//準(zhǔn)備JSBundleLoader對象
if (latestJSBundleFile.toLowerCase().startsWith("assets://")) {
latestJSBundleLoader = JSBundleLoader.createAssetLoader(getReactApplicationContext(), latestJSBundleFile, false);
} else {
latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile);
}
//通過反射,得到 instanceManager中的mBundleLoader對象
Field bundleLoaderField = instanceManager.getClass().getDeclaredField("mBundleLoader");
bundleLoaderField.setAccessible(true);
//給instanceManager中重新設(shè)置mBundleLoader對象
bundleLoaderField.set(instanceManager, latestJSBundleLoader);
} catch (Exception e) {
CodePushUtils.log("Unable to set JSBundle - CodePush may not support this version of React Native");
throw new IllegalAccessException("Could not setJSBundle");
}
}
上面提到的通過ReactInstanceManager中的JSBundleLoader完成bundle文件的重加載,具體的源碼就不多分析,后續(xù)有時(shí)間可以梳理一下ReactNative如何加載bundle文件的文章。重新加載完成之后,通過調(diào)用instanceManager.recreateReactContextInBackground();重新創(chuàng)建了ReactContext。
至此,一次完整的檢查--下載--安裝--更新流程完成了。
需要注意以下幾個(gè)知識點(diǎn):
1、ReactNative項(xiàng)目在進(jìn)入后臺是會發(fā)出background事件通知,在進(jìn)入前臺時(shí)會發(fā)出active事件通知。
2、熱更新檢查可以通過手動調(diào)用sync方法也可以在主界面的componentDidMount中通過事件觸發(fā)調(diào)用。