寫給原生開發(fā)的code-push 熱更新流程【源碼】【react-native】

前提

  • 通過react-native官方腳手架初始化項目
  • 項目內(nèi)集成react-native-code-push8.1.0版本

本文的源碼主要以js為主另萤,iOS為輔,沒有Android代碼宗苍。原生代碼旁涤,僅僅停留在原生模塊暴露給js使用的方法。這種方法名iOS和Android都是同名的,Android去源碼搜索同名方法即可蠢琳,不影響對本文的理解

流程概括

  • 1、檢查更新
  • 2镜豹、下載更新
  • 3傲须、安裝更新
  • 4、埋點上報邏輯

檢查更新

起點:
在js端發(fā)起趟脂,js在頁面入口調(diào)用 CodePush.sync() 泰讽。

// 自調(diào)用函數(shù),返回值是一個函數(shù)昔期。當(dāng)外面調(diào)用時已卸,即調(diào)用的當(dāng)前函數(shù)返回的函數(shù)
const sync = (() => {
  // 通過一個標(biāo)記,用來標(biāo)識當(dāng)前是否在更新過程中
  let syncInProgress = false;
  const setSyncCompleted = () => { syncInProgress = false; };

    // 返回一個函數(shù)
  return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {

    // 下述代碼都是一些校驗相關(guān)的邏輯硼一,可以適當(dāng)略過
    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);
    }
    // 前述代碼都是一些校驗相關(guān)的邏輯累澡,可以適當(dāng)略過

    // 修改標(biāo)記,開始進入更新的過程
    syncInProgress = true;
    // 核心邏輯都在這個函數(shù)里
    // 先生成一個promise
    const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
    // 然后調(diào)用這個promise函數(shù)般贼,這個函數(shù)里實現(xiàn)了核心邏輯
    syncPromise
      .then(setSyncCompleted)
      .catch(setSyncCompleted);

    return syncPromise;
  };
})();

概括一下這個函數(shù)的邏輯如下:

  • 添加是否在更新的過程中的標(biāo)記
  • 進入更新過程中
  • 把整個更新的邏輯愧哟,包裝成一個promise,更新結(jié)束后修改標(biāo)記,表示不在更新過程中
  • 然后把這個promise返回去
    通過上面的梳理哼蛆,可以看到核心邏輯都在syncPromise中蕊梧,接下來我們看一下syncPromise做了什么。函數(shù)的實現(xiàn)在syncInternal
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
  let resolvedInstallMode;
   // 聲明更新相關(guān)的配置信息
  const syncOptions = {
    deploymentKey: null,
    ignoreFailedUpdates: true,
    rollbackRetryOptions: null,
    installMode: CodePush.InstallMode.ON_NEXT_RESTART,
    mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
    minimumBackgroundDuration: 0,
    updateDialog: null,
    ...options
  };

  // 更新過程中的回調(diào)腮介,通過該回調(diào)可以觀察更新狀態(tài)的變更肥矢,如果外部沒有指定,優(yōu)先使用內(nèi)置的邏輯
  syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
    ? syncStatusChangeCallback
    : (syncStatus) => {
        // 每次狀態(tài)發(fā)生變化叠洗,都會有日志輸出
        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主守、下次啟動的時候,應(yīng)用更新內(nèi)容
            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榄融、應(yīng)用進去后臺的若干秒后参淫,應(yīng)用更新內(nèi)容
              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的時候愧杯,應(yīng)用更新內(nèi)容
                log("Update is installed and will be run when the app next resumes.");
              }
            }
            break;
          // 11涎才、出現(xiàn)未知錯誤
          case CodePush.SyncStatus.UNKNOWN_ERROR:
            log("An unknown error occurred.");
            break;
        }
      };

  try {
    // 發(fā)個通知出去,這個通知的本質(zhì)是一個上報力九,上報服務(wù)器本地的一些部署的信息耍铜。
    await CodePush.notifyApplicationReady();
    // 修改狀態(tài)為檢查更新
    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
    // 檢查更新的邏輯,返回值是新版本的信息跌前。這個信息里通過mixin的方式添加了download的函數(shù)棕兼,便于后續(xù)下載更新
    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

    // 聲明一個函數(shù),內(nèi)部包含下載更新并安裝的邏輯
    const doDownloadAndInstall = async () => {
      syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
      // 下載更新抵乓,下載成功后伴挚,本次更新就變成了本地包,所以返回值是本地包內(nèi)容(本地更新的內(nèi)容)內(nèi)部會添加install的函數(shù)灾炭,便于后續(xù)安裝
      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;
      // 修改狀態(tài)為安裝更新
      syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
      // 安裝更新茎芋, 第三個參數(shù)是安裝成功的回調(diào),安裝成功后蜈出,修改狀態(tài)為已安裝
      await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
      });

      return CodePush.SyncStatus.UPDATE_INSTALLED;
    };

    // 判斷當(dāng)前的更新是否要被忽略
    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) {
        // 修改狀態(tài)為正在安裝中
        syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
        return CodePush.SyncStatus.UPDATE_INSTALLED;
      } else {
        // 修改狀態(tài)為已更新到最新版本
        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;
  }
};

概括一下上述邏輯:

  • 聲明更新相關(guān)的配置信息syncOptions
  • 聲明不同狀態(tài)的回調(diào)函數(shù):通過這個回調(diào)函數(shù),可以發(fā)現(xiàn)铡原,整個更新過程包括多個階段(檢查更新偷厦、下載更新、安裝更新眷蜈、應(yīng)用更新)其中在應(yīng)用更新有多個時機可供選擇(重啟app生效沪哺、應(yīng)用進去后臺n秒后生效沈自、resumes時生效)
  • 發(fā)個上報酌儒,上報部署的信息,如果之前已經(jīng)下載了最新版本枯途,沒有應(yīng)用忌怎,在第一次應(yīng)用的時候,會上報一次酪夷,后續(xù)不會上報
  • 開始檢查更新const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
  • 如果沒有更新或這個更新被忽略榴啸,直接獲取當(dāng)前本地包的信息const currentPackage = await CodePush.getCurrentPackage();,然后檢查本地包是正在安裝更新中晚岭,還是已更新到最新
  • 如果需要彈出dialog鸥印,就需要讓用戶來操作如何更新
  • 如果有新版本且不需要彈出dialog,則自動開始下載更新并安裝await doDownloadAndInstall(),下載成功的時候库说,還會有一個下載成功的上報狂鞋。

接下來我們需要分別分一下各個關(guān)鍵環(huán)節(jié)的邏輯

  • 檢查更新
  • 獲取本地包信息
  • 下載并安裝更新

一、檢查更新


async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
  /*
   * 在我們詢問服務(wù)器是否存在更新之前潜的,
   * 我們需要從本機端檢索三項信息:部署密鑰骚揍、應(yīng)用程序版本(例如 1.0.1)和當(dāng)前正在運行的更新的哈希值(如果有)。
   * 這允許客戶端僅接收針對其特定部署和版本且實際上與其已安裝的 CodePush 更新不同的更新啰挪。
   */
  // 從本地獲取配置信息
  const nativeConfig = await getConfiguration();
  /*
   * 如果顯式提供了部署密鑰信不,
   * 那么讓我們覆蓋從應(yīng)用程序本機端檢索到的部署密鑰。 
   * 這允許在不同的部署中動態(tài)“重定向”最終用戶(例如內(nèi)部人員的早期訪問部署)亡呵。
   */
  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
  const sdk = getPromisifiedSdk(requestFetchAdapter, config);

  // Use dynamically overridden getCurrentPackage() during tests.
  const localPackage = await module.exports.getCurrentPackage();

  /*
   * 如果應(yīng)用程序之前安裝了更新抽活,
   * 并且該更新針對當(dāng)前正在運行的同一應(yīng)用程序版本,
   * 那么我們希望使用其包哈希來確定服務(wù)器上是否已發(fā)布新版本锰什。 
   * 否則酌壕,我們只需將應(yīng)用程序版本發(fā)送到服務(wù)器,
   * 因為我們對當(dāng)前二進制版本的任何更新感興趣歇由,而不管哈希值如何卵牍。
   */
  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) 服務(wù)器說沒有更新糊昙。 這是最常見的情況。
    * 2) 服務(wù)器表示有更新谢谦,但需要更新的二進制版本(native app版本)释牺。
    *     當(dāng)最終用戶運行比可用版本更舊的二進制版本時,就會發(fā)生這種情況回挽,
    *     并且 CodePush 正在確保他們不會獲得可能無法獲得的更新 與他們正在運行的內(nèi)容不兼容没咙。
    * 3) 服務(wù)器說有更新,但更新的哈希值與當(dāng)前運行的更新相同千劈。 
    *     這應(yīng)該永遠(yuǎn)不會發(fā)生祭刚,除非服務(wù)器中存在錯誤,
    *     但我們添加此檢查只是為了仔細(xì)檢查客戶端應(yīng)用程序是否能夠應(yīng)對更新檢查的潛在問題墙牌。
    * 4) 服務(wù)器說有更新涡驮,但更新的哈希值與二進制文件當(dāng)前運行版本的哈希值相同。 
    *     這應(yīng)該只發(fā)生在 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方法添加進去寄月,也會添加下載成功的回調(diào)
    const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
    // 然后加一個安裝失敗的回調(diào),這個回調(diào)會直接調(diào)用到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();
  • 通過本地信息生成網(wǎng)絡(luò)請求的參數(shù)
  • 檢查遠(yuǎn)程服務(wù)器上是否有更新const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);.沒有更新的四種情況:1无牵、遠(yuǎn)程沒有更新剥懒。2、遠(yuǎn)程有更新合敦,但是本地app版本過低初橘。3、遠(yuǎn)程有更新充岛,但是和當(dāng)前本地最新版本的內(nèi)容一致保檐。4、遠(yuǎn)程有更新崔梗,但是和本地內(nèi)置的內(nèi)容hash一致夜只。這種只會發(fā)生在Android。
  • 如果有更新的話蒜魄,以遠(yuǎn)程更新的信息為基礎(chǔ)扔亥,再添加幾個字段,然后返回出去谈为。添加的內(nèi)容主要是下載方法和下載成功的回調(diào)旅挤,以及是否正在下載的標(biāo)記

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;
    }
  }
})();

優(yōu)先使用緩存伞鲫,如果沒有緩存數(shù)據(jù)粘茄,使用測試數(shù)據(jù),沒有測試數(shù)據(jù)秕脓,則從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添加一些回調(diào)函數(shù):安裝失敗的回調(diào)、第一次運行的回調(diào)
    對應(yīng)到原生的代碼如下:

/*
 * 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吠架、調(diào)用接口從服務(wù)端獲取更新信息

queryUpdateWithCurrentPackage的實現(xiàn)如下:(代碼在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
        }
        // 拼裝請求的參數(shù)
        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);
        // 發(fā)送網(wǎng)絡(luò)請求
        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);
                // 解析數(shù)據(jù)
                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);
        });
    };

概括如下:

  • 組裝參數(shù)
  • 發(fā)送請求
  • 解析數(shù)據(jù)
  • 組裝出remotePackage,返回回去

二傍药、下載并安裝更新

此處邏輯對應(yīng)的doDownloadAndInstall的實現(xiàn)

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磺平、 下載更新

對應(yīng)
const localPackage = await remotePackage.download(downloadProgressCallback);
的邏輯。
實際的download實現(xiàn)如下:

 async download(downloadProgressCallback) {
        if (!this.downloadUrl) {
          throw new Error("Cannot download an update without a download url");
        }

        let downloadProgressSubscription;
        // 添加下載進度的監(jiān)聽
        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]);
          // 調(diào)用原生方法怔檩,實現(xiàn)下載功能
          const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
          // 下載成功的上報
          if (reportStatusDownload) {
            reportStatusDownload(this)
            .catch((err) => {
              log(`Report download status failed: ${err}`);
            });
          }
          // 次數(shù)的返回值褪秀,除了會把downloadedPackage信息放進來以為,還會添加install函數(shù)薛训,以及是否正在install的標(biāo)記。下一步可直接調(diào)用install
          return { ...downloadedPackage, ...local };
        } finally {
          downloadProgressSubscription && downloadProgressSubscription.remove();
        }
      },

概括如下:

  • 添加對下載進度監(jiān)聽
  • 把參數(shù)傳給原生端仑氛,原生實現(xiàn)下載功能
  • 當(dāng)下載完畢后乙埃,上報一下下載完成
  • 最后把原生的返回值闸英,添加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馒吴、安裝更新

對應(yīng)
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); });
實際的install實現(xiàn)如下:

 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)用原生方法炫贤,實現(xiàn)install的邏輯
      await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
      updateInstalledCallback && updateInstalledCallback();
      // 根據(jù)生效時機的配置益缎,操作如何生效
      if (installMode == NativeCodePush.codePushInstallModeImmediate) {
        // 如果是立即生效,則調(diào)用原生方法辙喂,立刻生效
        NativeCodePush.restartApp(false);
      } else {
        // 如果不是立即生效,也是調(diào)用原生方法鸠珠,做一些數(shù)據(jù)的清除操作
        NativeCodePush.clearPendingRestart();
        localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
      }
    },

概括:

  • 把相關(guān)參數(shù)傳給原生端巍耗,原生端實現(xiàn)install功能
  • 安裝完畢后,根據(jù)生效時機渐排,調(diào)用不同的后續(xù)處理邏輯
  • 如果需要立即生效炬太,則調(diào)用原生模塊提供的restartApp方法
  • 如果不需要立刻生效,則調(diào)用原生模塊提供的clearPendingRestart方法驯耻,清除沒用的數(shù)據(jù)

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


數(shù)據(jù)上報

除了文件的下載安裝等路邏輯以外亲族,還需要注意一些關(guān)鍵信息的上報,當(dāng)前主要是兩個節(jié)點

  • 下載成功: 表示本地已經(jīng)將服務(wù)器上的更新可缚,下載到本地霎迫。
  • 部署成功: 表示本地下載的更新內(nèi)容已經(jīng)生效。

下載成功

 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);
            }
        });
    };

上報的本質(zhì)就是調(diào)用api帘靡,發(fā)送一次請求女气,把一些參數(shù)傳給服務(wù)器,服務(wù)器會做好記錄测柠,便于統(tǒng)計炼鞠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市轰胁,隨后出現(xiàn)的幾起案子谒主,更是在濱河造成了極大的恐慌,老刑警劉巖赃阀,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霎肯,死亡現(xiàn)場離奇詭異,居然都是意外死亡榛斯,警方通過查閱死者的電腦和手機观游,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驮俗,“玉大人懂缕,你說我怎么就攤上這事⊥醮眨” “怎么了搪柑?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵聋丝,是天一觀的道長。 經(jīng)常有香客問我工碾,道長弱睦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任渊额,我火速辦了婚禮况木,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旬迹。我一直安慰自己火惊,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布舱权。 她就那樣靜靜地躺著矗晃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宴倍。 梳的紋絲不亂的頭發(fā)上张症,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音鸵贬,去河邊找鬼俗他。 笑死,一個胖子當(dāng)著我的面吹牛阔逼,可吹牛的內(nèi)容都是我干的兆衅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嗜浮,長吁一口氣:“原來是場噩夢啊……” “哼羡亩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起危融,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤畏铆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吉殃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辞居,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年蛋勺,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓦灶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抱完,死狀恐怖贼陶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤每界,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布捅僵,位于F島的核電站家卖,受9級特大地震影響眨层,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜上荡,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一趴樱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酪捡,春花似錦叁征、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至永罚,卻和暖如春啤呼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呢袱。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工官扣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羞福。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓惕蹄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親治专。 傳聞我的和親對象是個殘疾皇子卖陵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

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