前提
- 通过react-native官方脚手架初始化项目
- 项目内集成react-native-code-push8.1.0版本
本文的源码主要以js为主,iOS为辅,没有Android代码。原生代码,仅仅停留在原生模块暴露给js使用的方法。这种方法名iOS和Android都是同名的,Android去源码搜索同名方法即可,不影响对本文的理解
流程概括
- 1、检查更新
- 2、下载更新
- 3、安装更新
- 4、埋点上报逻辑
检查更新
起点:
在js端发起,js在页面入口调用 CodePush.sync()
。
// 自调用函数,返回值是一个函数。当外面调用时,即调用的当前函数返回的函数
const sync = (() => {
// 通过一个标记,用来标识当前是否在更新过程中
let syncInProgress = false;
const setSyncCompleted = () => { syncInProgress = false; };
// 返回一个函数
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
// 下述代码都是一些校验相关的逻辑,可以适当略过
let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackWithTryCatch = (...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;
// 核心逻辑都在这个函数里
// 先生成一个promise
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
// 然后调用这个promise函数,这个函数里实现了核心逻辑
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
概括一下这个函数的逻辑如下:
- 添加是否在更新的过程中的标记
- 进入更新过程中
- 把整个更新的逻辑,包装成一个promise,更新结束后修改标记,表示不在更新过程中
- 然后把这个promise返回去
通过上面的梳理,可以看到核心逻辑都在syncPromise
中,接下来我们看一下syncPromise
做了什么。函数的实现在syncInternal
中
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
let resolvedInstallMode;
// 声明更新相关的配置信息
const syncOptions = {
deploymentKey: null,
ignoreFailedUpdates: true,
rollbackRetryOptions: null,
installMode: CodePush.InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
minimumBackgroundDuration: 0,
updateDialog: null,
...options
};
// 更新过程中的回调,通过该回调可以观察更新状态的变更,如果外部没有指定,优先使用内置的逻辑
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
? syncStatusChangeCallback
: (syncStatus) => {
// 每次状态发生变化,都会有日志输出
switch(syncStatus) {
// 1、检查更新
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
log("Checking for update.");
break;
// 2、等待用户操作
case CodePush.SyncStatus.AWAITING_USER_ACTION:
log("Awaiting user action.");
break;
// 3、下载文件
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
log("Downloading package.");
break;
// 4、安装更新
case CodePush.SyncStatus.INSTALLING_UPDATE:
log("Installing update.");
break;
// 5、更新完成
case CodePush.SyncStatus.UP_TO_DATE:
log("App is up to date.");
break;
// 6、用户取消更新
case CodePush.SyncStatus.UPDATE_IGNORED:
log("User cancelled the update.");
break;
// 7、更新安装完毕
case CodePush.SyncStatus.UPDATE_INSTALLED:
// 8、下次启动的时候,应用更新内容
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
log("Update is installed and will be run on the next app restart.");
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
// 9、应用进去后台的若干秒后,应用更新内容
if (syncOptions.minimumBackgroundDuration > 0) {
log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
} else {
// 10、下次resumes的时候,应用更新内容
log("Update is installed and will be run when the app next resumes.");
}
}
break;
// 11、出现未知错误
case CodePush.SyncStatus.UNKNOWN_ERROR:
log("An unknown error occurred.");
break;
}
};
try {
// 发个通知出去,这个通知的本质是一个上报,上报服务器本地的一些部署的信息。
await CodePush.notifyApplicationReady();
// 修改状态为检查更新
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
// 检查更新的逻辑,返回值是新版本的信息。这个信息里通过mixin的方式添加了download的函数,便于后续下载更新
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
// 声明一个函数,内部包含下载更新并安装的逻辑
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
// 下载更新,下载成功后,本次更新就变成了本地包,所以返回值是本地包内容(本地更新的内容)内部会添加install的函数,便于后续安装
const localPackage = await remotePackage.download(downloadProgressCallback);
// Determine the correct install mode based on whether the update is mandatory or not.
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;
};
// 判断当前的更新是否要被忽略
const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);
if (!remotePackage || updateShouldBeIgnored) {
if (updateShouldBeIgnored) {
log("An update is available, but it is being ignored due to having been previously rolled back.");
}
// 获取本地包的信息
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 if (syncOptions.updateDialog) {
// 如果需要展示dialog,让用户来操作如何更新
// updateDialog supports any truthy value (e.g. true, "goo", 12),
// but we should treat a non-object value as just the default dialog
if (typeof syncOptions.updateDialog !== "object") {
syncOptions.updateDialog = CodePush.DEFAULT_UPDATE_DIALOG;
} else {
syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
}
return await new Promise((resolve, reject) => {
let message = null;
let installButtonText = null;
const dialogButtons = [];
if (remotePackage.isMandatory) {
message = syncOptions.updateDialog.mandatoryUpdateMessage;
installButtonText = syncOptions.updateDialog.mandatoryContinueButtonLabel;
} else {
message = syncOptions.updateDialog.optionalUpdateMessage;
installButtonText = syncOptions.updateDialog.optionalInstallButtonLabel;
// Since this is an optional update, add a button
// to allow the end-user to ignore it
dialogButtons.push({
text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
onPress: () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
resolve(CodePush.SyncStatus.UPDATE_IGNORED);
}
});
}
// Since the install button should be placed to the
// right of any other button, add it last
dialogButtons.push({
text: installButtonText,
onPress:() => {
doDownloadAndInstall()
.then(resolve, reject);
}
})
// If the update has a description, and the developer
// explicitly chose to display it, then set that as the message
if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
}
syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
});
} else {
// 开始下载更新并安装
return await doDownloadAndInstall();
}
} catch (error) {
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
log(error.message);
throw error;
}
};
概括一下上述逻辑:
- 声明更新相关的配置信息
syncOptions
- 声明不同状态的回调函数:通过这个回调函数,可以发现,整个更新过程包括多个阶段(检查更新、下载更新、安装更新、应用更新)其中在应用更新有多个时机可供选择(重启app生效、应用进去后台n秒后生效、resumes时生效)
- 发个上报,上报部署的信息,如果之前已经下载了最新版本,没有应用,在第一次应用的时候,会上报一次,后续不会上报
- 开始检查更新
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
- 如果没有更新或这个更新被忽略,直接获取当前本地包的信息
const currentPackage = await CodePush.getCurrentPackage();
,然后检查本地包是正在安装更新中,还是已更新到最新 - 如果需要弹出dialog,就需要让用户来操作如何更新
- 如果有新版本且不需要弹出dialog,则自动开始下载更新并安装
await doDownloadAndInstall()
,下载成功的时候,还会有一个下载成功的上报。
接下来我们需要分别分一下各个关键环节的逻辑
- 检查更新
- 获取本地包信息
- 下载并安装更新
一、检查更新
async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
/*
* 在我们询问服务器是否存在更新之前,
* 我们需要从本机端检索三项信息:部署密钥、应用程序版本(例如 1.0.1)和当前正在运行的更新的哈希值(如果有)。
* 这允许客户端仅接收针对其特定部署和版本且实际上与其已安装的 CodePush 更新不同的更新。
*/
// 从本地获取配置信息
const nativeConfig = await getConfiguration();
/*
* 如果显式提供了部署密钥,
* 那么让我们覆盖从应用程序本机端检索到的部署密钥。
* 这允许在不同的部署中动态“重定向”最终用户(例如内部人员的早期访问部署)。
*/
const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
// Use dynamically overridden getCurrentPackage() during tests.
const localPackage = await module.exports.getCurrentPackage();
/*
* 如果应用程序之前安装了更新,
* 并且该更新针对当前正在运行的同一应用程序版本,
* 那么我们希望使用其包哈希来确定服务器上是否已发布新版本。
* 否则,我们只需将应用程序版本发送到服务器,
* 因为我们对当前二进制版本的任何更新感兴趣,而不管哈希值如何。
*/
let queryPackage;
if (localPackage) {
queryPackage = localPackage;
} else {
queryPackage = { appVersion: config.appVersion };
if (Platform.OS === "ios" && config.packageHash) {
queryPackage.packageHash = config.packageHash;
}
}
// 检查更新的核心逻辑,如果有更新,此时update就有值
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
/*
* checkForUpdate 有四种情况会解析为 null:
* ------------------------------------------------- ----------------
* 1) 服务器说没有更新。 这是最常见的情况。
* 2) 服务器表示有更新,但需要更新的二进制版本(native app版本)。
* 当最终用户运行比可用版本更旧的二进制版本时,就会发生这种情况,
* 并且 CodePush 正在确保他们不会获得可能无法获得的更新 与他们正在运行的内容不兼容。
* 3) 服务器说有更新,但更新的哈希值与当前运行的更新相同。
* 这应该永远不会发生,除非服务器中存在错误,
* 但我们添加此检查只是为了仔细检查客户端应用程序是否能够应对更新检查的潜在问题。
* 4) 服务器说有更新,但更新的哈希值与二进制文件当前运行版本的哈希值相同。
* 这应该只发生在 Android 中 -
* 与 iOS 不同,我们不会将二进制文件的哈希附加到 updateCheck 请求,
* 因为我们希望避免必须针对二进制版本安装差异更新,而这在 Android 上还无法做到。
*/
if (!update || update.updateAppVersion ||
localPackage && (update.packageHash === localPackage.packageHash) ||
(!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
if (update && update.updateAppVersion) {
log("An update is available but it is not targeting the binary version of your app.");
if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
handleBinaryVersionMismatchCallback(update)
}
}
return null;
} else {
// 生成remotePackage,会把download方法添加进去,也会添加下载成功的回调
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
// 然后加一个安装失败的回调,这个回调会直接调用到Native的逻辑
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
// 把key修改为显示指定的key或者是从native获取到的key
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
return remotePackage;
}
}
上述的逻辑概括如下:
- 从native获取配置信息
const nativeConfig = await getConfiguration();
- 获取本地的currentPackage信息
const localPackage = await module.exports.getCurrentPackage();
- 通过本地信息生成网络请求的参数
- 检查远程服务器上是否有更新
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
.没有更新的四种情况:1、远程没有更新。2、远程有更新,但是本地app版本过低。3、远程有更新,但是和当前本地最新版本的内容一致。4、远程有更新,但是和本地内置的内容hash一致。这种只会发生在Android。 - 如果有更新的话,以远程更新的信息为基础,再添加几个字段,然后返回出去。添加的内容主要是下载方法和下载成功的回调,以及是否正在下载的标记
1、从native获取配置
const getConfiguration = (() => {
let config;
return async function getConfiguration() {
if (config) {
return config;
} else if (testConfig) {
return testConfig;
} else {
config = await NativeCodePush.getConfiguration();
return config;
}
}
})();
优先使用缓存,如果没有缓存数据,使用测试数据,没有测试数据,则从native获取config = await NativeCodePush.getConfiguration();
iOS:
RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSDictionary *configuration = [[CodePushConfig current] configuration];
NSError *error;
if (isRunningBinaryVersion) {
// isRunningBinaryVersion will not get set to "YES" if running against the packager.
NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error];
if (error) {
CPLog(@"Error obtaining hash for binary contents: %@", error);
resolve(configuration);
return;
}
if (binaryHash == nil) {
// The hash was not generated either due to a previous unknown error or the fact that
// the React Native assets were not bundled in the binary (e.g. during dev/simulator)
// builds.
resolve(configuration);
return;
}
NSMutableDictionary *mutableConfiguration = [configuration mutableCopy];
[mutableConfiguration setObject:binaryHash forKey:PackageHashKey];
resolve(mutableConfiguration);
return;
}
resolve(configuration);
}
Android
2、获取本地的currentPackage信息
async function getCurrentPackage() {
return await getUpdateMetadata(CodePush.UpdateState.LATEST);
}
async function getUpdateMetadata(updateState) {
let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
if (updateMetadata) {
updateMetadata = {...PackageMixins.local, ...updateMetadata};
updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
}
return updateMetadata;
}
上述逻辑概括:
- 从native获取
updateMetadata
- 然后给这个
updateMetadata
添加一些回调函数:安装失败的回调、第一次运行的回调
对应到原生的代码如下:
/*
* This method is the native side of the CodePush.getUpdateMetadata method.
*/
RCT_EXPORT_METHOD(getUpdateMetadata:(CodePushUpdateState)updateState
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error;
NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];
if (error) {
return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
} else if (package == nil) {
// The app hasn't downloaded any CodePush updates yet,
// so we simply return nil regardless if the user
// wanted to retrieve the pending or running update.
return resolve(nil);
}
// We have a CodePush update, so let's see if it's currently in a pending state.
BOOL currentUpdateIsPending = [[self class] isPendingUpdate:[package objectForKey:PackageHashKey]];
if (updateState == CodePushUpdateStatePending && !currentUpdateIsPending) {
// The caller wanted a pending update
// but there isn't currently one.
resolve(nil);
} else if (updateState == CodePushUpdateStateRunning && currentUpdateIsPending) {
// The caller wants the running update, but the current
// one is pending, so we need to grab the previous.
resolve([CodePushPackage getPreviousPackage:&error]);
} else {
// The current package satisfies the request:
// 1) Caller wanted a pending, and there is a pending update
// 2) Caller wanted the running update, and there isn't a pending
// 3) Caller wants the latest update, regardless if it's pending or not
if (isRunningBinaryVersion) {
// This only matters in Debug builds. Since we do not clear "outdated" updates,
// we need to indicate to the JS side that somehow we have a current update on
// disk that is not actually running.
[package setObject:@(YES) forKey:@"_isDebugOnly"];
}
// Enable differentiating pending vs. non-pending updates
[package setObject:@(currentUpdateIsPending) forKey:PackageIsPendingKey];
resolve(package);
}
}
iOS
/*
* This method isn't publicly exposed via the "react-native-code-push"
* module, and is only used internally to populate the RemotePackage.failedInstall property.
*/
RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
BOOL isFailedHash = [[self class] isFailedHash:packageHash];
resolve(@(isFailedHash));
}
/*
* This method isn't publicly exposed via the "react-native-code-push"
* module, and is only used internally to populate the LocalPackage.isFirstRun property.
*/
RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error;
BOOL isFirstRun = _isFirstRunAfterUpdate
&& nil != packageHash
&& [packageHash length] > 0
&& [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]];
resolve(@(isFirstRun));
}
Android
3、调用接口从服务端获取更新信息
queryUpdateWithCurrentPackage
的实现如下:(代码在code-push这个包里,不在react-native-code-push这个包里)
AcquisitionManager.prototype.queryUpdateWithCurrentPackage = function (currentPackage, callback) {
var _this = this;
if (!currentPackage || !currentPackage.appVersion) {
throw new code_push_error_1.CodePushPackageError("Calling common acquisition SDK with incorrect package"); // Unexpected; indicates error in our implementation
}
// 拼装请求的参数
var updateRequest = {
deployment_key: this._deploymentKey,
app_version: currentPackage.appVersion,
package_hash: currentPackage.packageHash,
is_companion: this._ignoreAppVersion,
label: currentPackage.label,
client_unique_id: this._clientUniqueId
};
// 生成请求的URL
var requestUrl = this._serverUrl + this._publicPrefixUrl + "update_check?" + queryStringify(updateRequest);
// 发送网络请求
this._httpRequester.request(0 /* Http.Verb.GET */, requestUrl, function (error, response) {
if (error) {
callback(error, /*remotePackage=*/ null);
return;
}
if (response.statusCode !== 200) {
var errorMessage = void 0;
if (response.statusCode === 0) {
errorMessage = "Couldn't send request to ".concat(requestUrl, ", xhr.statusCode = 0 was returned. One of the possible reasons for that might be connection problems. Please, check your internet connection.");
}
else {
errorMessage = "".concat(response.statusCode, ": ").concat(response.body);
}
callback(new code_push_error_1.CodePushHttpError(errorMessage), /*remotePackage=*/ null);
return;
}
try {
var responseObject = JSON.parse(response.body);
// 解析数据
var updateInfo = responseObject.update_info;
}
catch (error) {
callback(error, /*remotePackage=*/ null);
return;
}
if (!updateInfo) {
callback(error, /*remotePackage=*/ null);
return;
}
else if (updateInfo.update_app_version) {
callback(/*error=*/ null, { updateAppVersion: true, appVersion: updateInfo.target_binary_range });
return;
}
else if (!updateInfo.is_available) {
callback(/*error=*/ null, /*remotePackage=*/ null);
return;
}
// 生成remotePackage
var remotePackage = {
deploymentKey: _this._deploymentKey,
description: updateInfo.description,
label: updateInfo.label,
appVersion: updateInfo.target_binary_range,
isMandatory: updateInfo.is_mandatory,
packageHash: updateInfo.package_hash,
packageSize: updateInfo.package_size,
downloadUrl: updateInfo.download_url
};
callback(/*error=*/ null, remotePackage);
});
};
概括如下:
- 组装参数
- 发送请求
- 解析数据
- 组装出remotePackage,返回回去
二、下载并安装更新
此处逻辑对应的doDownloadAndInstall
的实现
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
// 下载逻辑
const localPackage = await remotePackage.download(downloadProgressCallback);
// Determine the correct install mode based on whether the update is mandatory or not.
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;
};
概括如下:
- 下载更新
- 安装更新
1、 下载更新
对应
const localPackage = await remotePackage.download(downloadProgressCallback);
的逻辑。
实际的download
实现如下:
async download(downloadProgressCallback) {
if (!this.downloadUrl) {
throw new Error("Cannot download an update without a download url");
}
let downloadProgressSubscription;
// 添加下载进度的监听
if (downloadProgressCallback) {
const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
// Use event subscription to obtain download progress.
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]);
// 调用原生方法,实现下载功能
const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
// 下载成功的上报
if (reportStatusDownload) {
reportStatusDownload(this)
.catch((err) => {
log(`Report download status failed: ${err}`);
});
}
// 次数的返回值,除了会把downloadedPackage信息放进来以为,还会添加install函数,以及是否正在install的标记。下一步可直接调用install
return { ...downloadedPackage, ...local };
} finally {
downloadProgressSubscription && downloadProgressSubscription.remove();
}
},
概括如下:
- 添加对下载进度监听
- 把参数传给原生端,原生实现下载功能
- 当下载完毕后,上报一下下载完成
- 最后把原生的返回值,添加install方法后,返回出去
iOS
/*
* This is native-side of the RemotePackage.download method
*/
RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
notifyProgress:(BOOL)notifyProgress
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSDictionary *mutableUpdatePackage = [updatePackage mutableCopy];
NSURL *binaryBundleURL = [CodePush binaryBundleURL];
if (binaryBundleURL != nil) {
[mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL]
forKey:BinaryBundleDateKey];
}
if (notifyProgress) {
// Set up and unpause the frame observer so that it can emit
// progress events every frame if the progress is updated.
_didUpdateProgress = NO;
self.paused = NO;
}
NSString * publicKey = [[CodePushConfig current] publicKey];
[CodePushPackage
downloadPackage:mutableUpdatePackage
expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]
publicKey:publicKey
operationQueue:_methodQueue
// The download is progressing forward
progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
// Update the download progress so that the frame observer can notify the JS side
_latestExpectedContentLength = expectedContentLength;
_latestReceivedConentLength = receivedContentLength;
_didUpdateProgress = YES;
// If the download is completed, stop observing frame
// updates and synchronously send the last event.
if (expectedContentLength == receivedContentLength) {
_didUpdateProgress = NO;
self.paused = YES;
[self dispatchDownloadProgressEvent];
}
}
// The download completed
doneCallback:^{
NSError *err;
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
if (err) {
return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}
resolve(newPackage);
}
// The download failed
failCallback:^(NSError *err) {
if ([CodePushErrorUtils isCodePushError:err]) {
[self saveFailedUpdate:mutableUpdatePackage];
}
// Stop observing frame updates if the download fails.
_didUpdateProgress = NO;
self.paused = YES;
reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}];
}
Android
2、安装更新
对应
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); });
实际的install
实现如下:
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
// 调用原生方法,实现install的逻辑
await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
updateInstalledCallback && updateInstalledCallback();
// 根据生效时机的配置,操作如何生效
if (installMode == NativeCodePush.codePushInstallModeImmediate) {
// 如果是立即生效,则调用原生方法,立刻生效
NativeCodePush.restartApp(false);
} else {
// 如果不是立即生效,也是调用原生方法,做一些数据的清除操作
NativeCodePush.clearPendingRestart();
localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
}
},
概括:
- 把相关参数传给原生端,原生端实现install功能
- 安装完毕后,根据生效时机,调用不同的后续处理逻辑
- 如果需要立即生效,则调用原生模块提供的
restartApp
方法 - 如果不需要立刻生效,则调用原生模块提供的
clearPendingRestart
方法,清除没用的数据
iOS
/*
* This method is the native side of the LocalPackage.install method.
*/
RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
installMode:(CodePushInstallMode)installMode
minimumBackgroundDuration:(int)minimumBackgroundDuration
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error;
[CodePushPackage installPackage:updatePackage
removePendingUpdate:[[self class] isPendingUpdate:nil]
error:&error];
if (error) {
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
} else {
[self savePendingUpdate:updatePackage[PackageHashKey]
isLoading:NO];
_installMode = installMode;
if (_installMode == CodePushInstallModeOnNextResume || _installMode == CodePushInstallModeOnNextSuspend) {
_minimumBackgroundDuration = minimumBackgroundDuration;
if (!_hasResumeListener) {
// Ensure we do not add the listener twice.
// Register for app resume notifications so that we
// can check for pending updates which support "restart on resume"
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive)
name:UIApplicationDidBecomeActiveNotification
object:RCTSharedApplication()];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:RCTSharedApplication()];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive)
name:UIApplicationWillResignActiveNotification
object:RCTSharedApplication()];
_hasResumeListener = YES;
}
}
// Signal to JS that the update has been applied.
resolve(nil);
}
}
Android
数据上报
除了文件的下载安装等路逻辑以外,还需要注意一些关键信息的上报,当前主要是两个节点
- 下载成功: 表示本地已经将服务器上的更新,下载到本地。
- 部署成功: 表示本地下载的更新内容已经生效。
下载成功
AcquisitionManager.prototype.reportStatusDownload = function (downloadedPackage, callback) {
var url = this._serverUrl + this._publicPrefixUrl + "report_status/download";
var body = {
client_unique_id: this._clientUniqueId,
deployment_key: this._deploymentKey,
label: downloadedPackage.label
};
this._httpRequester.request(2 /* Http.Verb.POST */, url, JSON.stringify(body), function (error, response) {
if (callback) {
if (error) {
callback(error, /*not used*/ null);
return;
}
if (response.statusCode !== 200) {
callback(new code_push_error_1.CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null);
return;
}
callback(/*error*/ null, /*not used*/ null);
}
});
};
部署成功
AcquisitionManager.prototype.reportStatusDeploy = function (deployedPackage, status, previousLabelOrAppVersion, previousDeploymentKey, callback) {
var url = this._serverUrl + this._publicPrefixUrl + "report_status/deploy";
var body = {
app_version: this._appVersion,
deployment_key: this._deploymentKey
};
if (this._clientUniqueId) {
body.client_unique_id = this._clientUniqueId;
}
if (deployedPackage) {
body.label = deployedPackage.label;
body.app_version = deployedPackage.appVersion;
switch (status) {
case AcquisitionStatus.DeploymentSucceeded:
case AcquisitionStatus.DeploymentFailed:
body.status = status;
break;
default:
if (callback) {
if (!status) {
callback(new code_push_error_1.CodePushDeployStatusError("Missing status argument."), /*not used*/ null);
}
else {
callback(new code_push_error_1.CodePushDeployStatusError("Unrecognized status \"" + status + "\"."), /*not used*/ null);
}
}
return;
}
}
if (previousLabelOrAppVersion) {
body.previous_label_or_app_version = previousLabelOrAppVersion;
}
if (previousDeploymentKey) {
body.previous_deployment_key = previousDeploymentKey;
}
callback = typeof arguments[arguments.length - 1] === "function" && arguments[arguments.length - 1];
this._httpRequester.request(2 /* Http.Verb.POST */, url, JSON.stringify(body), function (error, response) {
if (callback) {
if (error) {
callback(error, /*not used*/ null);
return;
}
if (response.statusCode !== 200) {
callback(new code_push_error_1.CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null);
return;
}
callback(/*error*/ null, /*not used*/ null);
}
});
};
上报的本质就是调用api,发送一次请求,把一些参数传给服务器,服务器会做好记录,便于统计。