多多色-多人伦交性欧美在线观看-多人伦精品一区二区三区视频-多色视频-免费黄色视屏网站-免费黄色在线

國內最全IT社區平臺 聯系我們 | 收藏本站
阿里云優惠2
您當前位置:首頁 > php開源 > 綜合技術 > [置頂] 【稀飯】react native 實戰系列教程之熱更新原理分析與實現

[置頂] 【稀飯】react native 實戰系列教程之熱更新原理分析與實現

來源:程序員人生   發布時間:2017-02-06 08:48:46 閱讀次數:4711次

很多人在技術選型的時候,會選擇RN是由于它具有熱更新,而且這是它的1個特性,所以實現起來會相對照較簡單,不像原生那樣,原生的熱更新是1個大工程。那就目前來看,RN的熱更新方案已有的,有微軟的CodePush和reactnative中文網的pushy。實話說,這兩個我還沒有體驗過。1來是當初選擇RN是由于它不但具有接近原生的體驗感還具有熱更新特性,那末就想自己來實現1下熱更新,研究1下它的原理;2來,把自己的東西放在他人的服務器上總是覺得不是最好的辦法,為何不自己實現呢?因此,這篇文章便是記錄自己的1些研究。

react native加載bundle進程

這篇文章是基于RN android 0.38.1

當我們創建完RN的基礎項目后,打開android項目,項目只有MainActivity和MainApplication。

打開MainActivity,只有1個重寫方法getMainComponentName,返回主組件名稱,它繼承于ReactActivity。

我們打開ReactActivity,它使用了代理模式,通過ReactActivityDelegate mDelegate對象將Activity需要處理的邏輯放在了代理對象內部,并通過getMainComponentName方法來設置(匹配)JS端AppRegistry.registerComponent端啟動的入口組件。

Activity渲染出界眼前,先是調用onCreate,所以我們進入代理對象的onCreate方法

//ReactActivityDelegate.java


protected void onCreate(Bundle savedInstanceState) {
    //判斷是不是支持dev模式,也就是RN常見的那個紅色彈窗
    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(getContext())) {
        Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        getContext().startActivity(serviceIntent);
        FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
      }
    }

    if (mMainComponentName != null) {
      //加載app
      loadApp(mMainComponentName);
    }
    //android摹擬器dev 模式下,雙擊R重新加載
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  }

上面的代碼并沒甚么實質的東西,主要是調用了loadApp,我們跟進看下

protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

生成了1個ReactRootView對象,然后調用它的startReactApplication方法,最后setContentView將它設置為內容視圖。再跟進startReactApplication里

//ReactRootView.java

public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle launchOptions) {
    UiThreadUtil.assertOnUiThread();

    // TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
    // here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
    // it in the case of re-creating the catalyst instance
    Assertions.assertCondition(
        mReactInstanceManager == null,
        "This root view has already been attached to a catalyst instance manager");
    //配置項管理
    mReactInstanceManager = reactInstanceManager;
    //入口組件名稱
    mJSModuleName = moduleName;
    //用于傳遞給JS端初始組件props參數
    mLaunchOptions = launchOptions;
    //判斷是不是已加載過
    if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
      //去加載bundle文件
      mReactInstanceManager.createReactContextInBackground();
    }

    // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which
    // will make this view startReactApplication itself to instance manager once onMeasure is called.
    if (mWasMeasured) {
      //去渲染ReactRootView
      attachToReactInstanceManager();
    }
  }

startReactApplication傳入3個參數,第1個ReactInstanceManager配置項管理類(非常重要);第2個是MainComponentName入口組件名稱;第3個是Android Bundle類型,用于傳遞給JS端初始組件的props參數。首先,會根據ReactInstanceManager的配置去加載bundle進程,然后去渲染ReactRootView,將UI展現出來。現在我們不用去管attachToReactInstanceManager是如何去渲染ReactRootView,我們主要是研究如何加載bundle的,所以,我們跟進createReactContextInBackground,發現它是抽象類ReactInstanceManager的1個抽象方法。那它具體實現邏輯是甚么呢?那我們就需要知道ReactInstanceManager的具體類的實例對象是誰了【1】。

好了,現在我們回到ReacActivityDelegate.java的loadApp,在ReactRootView的startReactApplication傳入的ReactInstanceManager對象是getReactNativeHost().getReactInstanceManager()

//ReacActivityDelegate.java

mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());

getReactNativeHost(),又是甚么呢?

//從Application獲得ReactNativeHost
protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
  }

所以我們在打開MainApplication類

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    protected boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage()
      );
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
      return mReactNativeHost;
  }
}

MainApplication實現了ReactApplication接口,在getReactNativeHost()方法返回配置好的ReactNativeHost對象。由于我們把項目的Application配置成了MainApplication,所以ReacActivityDelegate的getReactNativeHost方法,返回的就是MainApplication mReactNativeHost對象。接著我們看下ReactNativeHost的getReactInstanceManager()方法,里面直接調用了createReactInstanceManager()方法,所以我們直接看createReactInstanceManager()

//ReactNativeHost.java

protected ReactInstanceManager createReactInstanceManager() {
    ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
      .setApplication(mApplication)
      .setJSMainModuleName(getJSMainModuleName())
      .setUseDeveloperSupport(getUseDeveloperSupport())
      .setRedBoxHandler(getRedBoxHandler())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

    for (ReactPackage reactPackage : getPackages()) {
      builder.addPackage(reactPackage);
    }

    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
      builder.setJSBundleFile(jsBundleFile);
    } else {
      builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
    }
    return builder.build();
  }

createReactInstanceManager()通過使用ReactInstanceManager.Builder構造器來設置1些配置并生成對象。從這里看,我們可以從MainApplication的mReactNativeHost對象來配置ReactInstanceManager,比如JSMainModuleName、UseDeveloperSupport、Packages、JSBundleFile、BundleAssetName等,也能夠重寫createReactInstanceManager方法,自己手動生成ReactInstanceManager對象。

這里看下jsBundleFile的設置,先判斷了getJSBundleFile()是不是為null,項目默許是沒有重寫的,所以默許就是null,那末走builder.setBundleAssetName分支,看下getBundleAssetName(),默許是返回”index.android.bundle”

//builder.setBundleAssetName

public Builder setBundleAssetName(String bundleAssetName) {
      mJSBundleAssetUrl = (bundleAssetName == null ? null : "assets://" + bundleAssetName);
      mJSBundleLoader = null;
      return this;
    }

所以,默許情況下,mJSBundleAssetUrl=”assets://index.android.bundle”,mJSBundleLoader = null。

接著往下看,builder最后調用build()來生成ReactInstanceManager實例對象。我們進去build()方法看下。

//ReactInstanceManager.Builder

public ReactInstanceManager build() {
      Assertions.assertNotNull(
        mApplication,
        "Application property has not been set with this builder");

      Assertions.assertCondition(
        mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,
        "JS Bundle File or Asset URL has to be provided when dev support is disabled");

      Assertions.assertCondition(
        mJSMainModuleName != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,
        "Either MainModuleName or JS Bundle File needs to be provided");

      if (mUIImplementationProvider == null) {
        // create default UIImplementationProvider if the provided one is null.
        mUIImplementationProvider = new UIImplementationProvider();
      }

      return new XReactInstanceManagerImpl(
        mApplication,
        mCurrentActivity,
        mDefaultHardwareBackBtnHandler,
        (mJSBundleLoader == null && mJSBundleAssetUrl != null) ?
          JSBundleLoader.createAssetLoader(mApplication, mJSBundleAssetUrl) : mJSBundleLoader,
        mJSMainModuleName,
        mPackages,
        mUseDeveloperSupport,
        mBridgeIdleDebugListener,
        Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"),
        mUIImplementationProvider,
        mNativeModuleCallExceptionHandler,
        mJSCConfig,
        mRedBoxHandler,
        mLazyNativeModulesEnabled,
        mLazyViewManagersEnabled);
    }

從上面看來,XReactInstanceManagerImpl的第4個參數,傳入的是1個JSBundleLoader,并且默許是JSBundleLoader.createAssetLoader。

new的是XReactInstanceManagerImpl對象,也就是說,XReactInstanceManagerImpl是抽象類ReactInstanceManager的具體實現類。

好了,在【1】處留下的疑問,我們現在就解決了。也就是,說調用ReactInstanceManager的createReactContextInBackground方法,是去履行XReactInstanceManagerImpl的reateReactContextInBackground方法。

進去reateReactContextInBackground方法后,它調用了recreateReactContextInBackgroundInner()1個內部方法,直接看下recreateReactContextInBackgroundInner的實現代碼

//XReactInstanceManagerImpl.java

private void recreateReactContextInBackgroundInner() {
    UiThreadUtil.assertOnUiThread();
    //判斷是不是是dev模式
    if (mUseDeveloperSupport && mJSMainModuleName != null) {
      final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();
      // If remote JS debugging is enabled, load from dev server.
      if (mDevSupportManager.hasUpToDateJSBundleInCache() &&
          !devSettings.isRemoteJSDebugEnabled()) {
        // If there is a up-to-date bundle downloaded from server,
        // with remote JS debugging disabled, always use that.
        onJSBundleLoadedFromServer();
      } else if (mBundleLoader == null) {
        mDevSupportManager.handleReloadJS();
      } else {
        mDevSupportManager.isPackagerRunning(
            new DevServerHelper.PackagerStatusCallback() {
              @Override
              public void onPackagerStatusFetched(final boolean packagerIsRunning) {
                UiThreadUtil.runOnUiThread(
                    new Runnable() {
                      @Override
                      public void run() {
                        if (packagerIsRunning) {
                          mDevSupportManager.handleReloadJS();
                        } else {
                          // If dev server is down, disable the remote JS debugging.
                          devSettings.setRemoteJSDebugEnabled(false);
                          recreateReactContextInBackgroundFromBundleLoader();
                        }
                      }
                    });
              }
            });
      }
      return;
    }

    recreateReactContextInBackgroundFromBundleLoader();
  }

由于我們發布出去的apk包,最后都是關閉了dev模式的,所以dev模式下的bundle加載流程我們先不需要太多的關注,那末mUseDeveloperSupport就是false,它就不會走進if里面,而是調用了recreateReactContextInBackgroundFromBundleLoader()方法。其實,你簡單看下if里面的判斷和方法調用也能知道,其實它就是去拉取通過react-native start啟動起來的packages服務器窗口,再者如果打開了遠程調試,那末它就走閱讀器代理去拉取bundle。

recreateReactContextInBackgroundFromBundleLoader又調用了recreateReactContextInBackground

private void recreateReactContextInBackground(
      JavaScriptExecutor.Factory jsExecutorFactory,
      JSBundleLoader jsBundleLoader) {
    UiThreadUtil.assertOnUiThread();

    ReactContextInitParams initParams =
        new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
    if (mReactContextInitAsyncTask == null) {
      // No background task to create react context is currently running, create and execute one.
      mReactContextInitAsyncTask = new ReactContextInitAsyncTask();
      mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, initParams);
    } else {
      // Background task is currently running, queue up most recent init params to recreate context
      // once task completes.
      mPendingReactContextInitParams = initParams;
    }
  }

到這里,recreateReactContextInBackground使用了ReactContextInitAsyncTask(繼承AsyncTask)開啟線程去履行,并且將ReactContextInitParams當作參數,傳遞到了AsyncTask的doInBackground。ReactContextInitParams只是將jsExecutorFactory、jsBundleLoader兩個參數封裝成1個內部類,方便傳遞參數。

那末ReactContextInitAsyncTask開啟線程去履行了甚么?該類也是個內部類,我們直接看它的doInBackground方法。

@Override
    protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {

      Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);

      Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
      try {
        JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
        return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader()));
      } catch (Exception e) {
        // Pass exception to onPostExecute() so it can be handled on the main thread
        return Result.of(e);
      }
    }

好像也沒處理甚么,就是使用ReactContextInitParams傳遞進來的兩個參數,去調用了createReactContext

private ReactApplicationContext createReactContext(
      JavaScriptExecutor jsExecutor,
      JSBundleLoader jsBundleLoader) {
    FLog.i(ReactConstants.TAG, "Creating react context.");
    ReactMarker.logMarker(CREATE_REACT_CONTEXT_START);
    mSourceUrl = jsBundleLoader.getSourceUrl();
    List<ModuleSpec> moduleSpecs = new ArrayList<>();
    Map<Class, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
    JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();

    final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
    if (mUseDeveloperSupport) {
      reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
    }

    ReactMarker.logMarker(PROCESS_PACKAGES_START);
    Systrace.beginSection(
        TRACE_TAG_REACT_JAVA_BRIDGE,
        "createAndProcessCoreModulesPackage");
    try {
      CoreModulesPackage coreModulesPackage =
        new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider);
      processPackage(
        coreModulesPackage,
        reactContext,
        moduleSpecs,
        reactModuleInfoMap,
        jsModulesBuilder);
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    }

    // TODO(6818138): Solve use-case of native/js modules overriding
    for (ReactPackage reactPackage : mPackages) {
      Systrace.beginSection(
          TRACE_TAG_REACT_JAVA_BRIDGE,
          "createAndProcessCustomReactPackage");
      try {
        processPackage(
          reactPackage,
          reactContext,
          moduleSpecs,
          reactModuleInfoMap,
          jsModulesBuilder);
      } finally {
        Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }
    ReactMarker.logMarker(PROCESS_PACKAGES_END);

    ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_START);
    Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "buildNativeModuleRegistry");
    NativeModuleRegistry nativeModuleRegistry;
    try {
       nativeModuleRegistry = new NativeModuleRegistry(moduleSpecs, reactModuleInfoMap);
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END);
    }

    NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
        ? mNativeModuleCallExceptionHandler
        : mDevSupportManager;
    CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()
        .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
        .setJSExecutor(jsExecutor)
        .setRegistry(nativeModuleRegistry)
        .setJSModuleRegistry(jsModulesBuilder.build())
        .setJSBundleLoader(jsBundleLoader)
        .setNativeModuleCallExceptionHandler(exceptionHandler);

    ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_START);
    // CREATE_CATALYST_INSTANCE_END is in JSCExecutor.cpp
    Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "createCatalystInstance");
    final CatalystInstance catalystInstance;
    try {
      catalystInstance = catalystInstanceBuilder.build();
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END);
    }

    if (mBridgeIdleDebugListener != null) {
      catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
    }

    reactContext.initializeWithInstance(catalystInstance);
    catalystInstance.runJSBundle();

    return reactContext;
  }

這個方法代碼有點多,首先它履行設置了RN自帶的和開發者自定義的模塊組件(Package\Module),然后一樣使用了構造器CatalystInstanceImpl.Builder生成了catalystInstance對象,最后調用了catalystInstance.runJSBundle()。跟進去是1個接口類CatalystInstance,那末我們又要去看它的實現類CatalystInstanceImpl

//CatalystInstanceImpl.java

@Override
  public void runJSBundle() {
    Assertions.assertCondition(!mJSBundleHasLoaded, "JS bundle was already loaded!");
    mJSBundleHasLoaded = true;
    // incrementPendingJSCalls();
    mJSBundleLoader.loadScript(CatalystInstanceImpl.this);

    synchronized (mJSCallsPendingInitLock) {
      // Loading the bundle is queued on the JS thread, but may not have
      // run yet.  It's save to set this here, though, since any work it
      // gates will be queued on the JS thread behind the load.
      mAcceptCalls = true;

      for (PendingJSCall call : mJSCallsPendingInit) {
        callJSFunction(call.mExecutorToken, call.mModule, call.mMethod, call.mArguments);
      }
      mJSCallsPendingInit.clear();
    }


    // This is registered after JS starts since it makes a JS call
    Systrace.registerListener(mTraceListener);
  }

到這里,可以看到mJSBundleLoader調用了loadScript去加載bundle。進去方法看下,發現它又是個抽象類,有兩個抽象方法,1個是loadScript加載bundle,1個是getSourceUrl返回bundle的地址,并且提供了4個靜態工廠方法。

由之前分析知道,JSBundleLoader默許是使用了JSBundleLoader.createAssetLoader來創建的實例

//JSBundleLoader.java

public static JSBundleLoader createAssetLoader(
      final Context context,
      final String assetUrl) {
    return new JSBundleLoader() {
      @Override
      public void loadScript(CatalystInstanceImpl instance) {
        instance.loadScriptFromAssets(context.getAssets(), assetUrl);
      }

      @Override
      public String getSourceUrl() {
        return assetUrl;
      }
    };
  }

我們看到loadScript最后是調用了CatalystInstanceImpl的loadScriptFromAssets。跟進去以后發現,它是1個native方法,也就是最后的實現RN把它放在了jni層來完成最后加載bundle的進程。

并且CatalystInstanceImpl不止loadScriptFromAssets1個native方法,它還提供了loadScriptFromFile和loadScriptFromOptimizedBundle。其中前面兩個,分別是從android assets目錄下加載bundle,另外一個是從android SD卡文件夾目錄下加載bundle。而loadScriptFromOptimizedBundle是在UnpackingJSBundleLoader類里調用,但是UnpackingJSBundleLoader目前好像是沒有用到,有知道它的作用的朋友們可以告知1下。

至此,bundle的加載流程我們已走1遍了,下面用1張流程圖來總結下

這里寫圖片描述

加載bundle文件的幾個途徑

從上面的分析進程,我們可以得出,bundle的加載路徑來源取決于JSBundleLoader的loadScript,而loadScript又調用了CatalystInstanceImpl的loadScriptFromAssets或loadScriptFromFile,所以,加載bundle文件的途徑本質上有兩種方式

  • loadScriptFromAssets

從android項目下的assets文件夾下去加載,這也是RN發布版的默許加載方式,也就是在cmd命令行下使用gradlew assembleRelease 命令打包簽名后的apk里面的assets就包括有bundle文件

這里寫圖片描述

這里寫圖片描述

如果你打包后發現里面沒有bundle文件,那末你將它安裝到系統里,運行也是會報錯的

react native gradle assembleRelease打包運行失敗,沒有生成bundle文件

  • loadScriptFromFile

第2種方式是從android文件系統也就是sd卡下去加載bundle。

我們只要事前在sd卡下寄存bundle文件,然后在ReactNativeHost的getJSBundleFile返回文件路徑便可。

//MainApplication.java

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    protected boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
              new MainReactPackage()
      );
    }

    @Nullable
    @Override
    protected String getJSBundleFile() {
      File bundleFile = new File(getCacheDir()+"/react_native","index.android.bundle");
      if(bundleFile.exists()){
        return bundleFile.getAbsolutePath();
      }
      return super.getJSBundleFile();
    }

    @Nullable
    @Override
    protected String getBundleAssetName() {
      return super.getBundleAssetName();
    }
  };

getJSBundleFile首先會嘗試在sd卡目錄下

data/data/<package-name>/cache/react_native/

看是不是存在index.android.bundle文件,如果有,那末就會使用該bundle,如果沒有,那末就會返回null,這時候候就是去加載assets下的bundle了。

熱更新的實現

如果你了解react native bundle命令,那末就會知道,其實該命令分兩部份,1部份是生成bundle文件,1部份是生成圖片資源。對android的react.gardle來講,也就是app/build.gradle中下面這句

apply from: "../../node_modules/react-native/react.gradle"

該腳本就是去履行react native bundle命令,它將生成的bundle文件放在assets下,且將生成的圖片資源放在drawable下。

但是當我們自定義getJSBundleFile路徑以后,bundle的所有加載進程都是在該目錄下,包括圖片資源,所以我們服務器上寄存的應當是個bundle patch,包括bundle文件和圖片資源。關于RN的圖片熱更新問題,可以看這個React-Native 圖片熱更新初探

有了前面的分析和了解后,那末就能夠自己動手來實現bundle的熱更新了。

那末熱更新主要包括
- bundle patch從服務器下載到sd卡
- 程序中加載bundle

接下來,進行摹擬版本更新:將舊版本中‘我的’tab的列表中‘觀看歷史’item去掉,也就是新版本中不再有‘觀看歷史’功能,效果以下

更新之前以下:

這里寫圖片描述

更新并加載bundle以后以下:

這里寫圖片描述

bundle patch的下載

我這里服務器使用的bmob后臺,將要更新的bundle文件寄存在服務器上。

先將去掉‘觀看歷史’后的新版本bundle patchs打包出來,上傳到服務器上(bmob)。

通過react-native bundle命令手動將patchs包打包出來

react-native bundle --platform android --dev false --r
eset-cache --entry-file index.android.js --bundle-output F:\Gray\ReactNative\XiF
an\bundle\index.android.bundle --assets-dest F:\Gray\ReactNative\XiFan\bundle

這里寫圖片描述

這里寫圖片描述

上傳到服務器

這里寫圖片描述

然后,在客戶端定義1個實體類來寄存更新對象

public class AppInfo extends BmobObject{
    private String version;//bundle版本
    private String updateContent;//更新內容
    private BmobFile bundle;//要下載的bundle patch文件
}

然后,程序啟動的時候去檢測更新

//MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BmobQuery<AppInfo> query = new BmobQuery<>();
    query.setLimit(1);
    query.addWhereGreaterThan("version","1.0.0");
    query.findObjects(new FindListener<AppInfo>() {
        @Override
        public void done(List<AppInfo> list, BmobException e) {
            if(e == null){
               if(list!=null && !list.isEmpty()){
                   AppInfo info = list.get(0);
                   File reactDir = new File(getCacheDir(),"react_native");
                   if(!reactDir.exists()){
                       reactDir.mkdirs();
                   }

                   BmobFile patchFile = info.getBundle();
                   final File saveFile = new File(reactDir,"bundle-patch.zip");
                   if(saveFile.exists()){
                        return;
                    }
                   //下載bundle-patch.zip文件
                   patchFile.download(saveFile, new DownloadFileListener() {
                       @Override
                       public void done(String s, BmobException e) {
                           if (e == null) {
                               System.out.println("下載完成");
                               //解壓patch文件到react_native文件夾下
                                unzip(saveFile);
                           } else {
                               Log.e("bmob", e.toString());
                           }

                       }

                       @Override
                       public void onProgress(Integer integer, long l) {
                           System.out.println("下載中...." + integer);
                       }
                   });
               }
            }else{
                Log.e("bmob",e.toString());
            }
        }
    });
}

在MainActivity的onCreate,將當前版本當作是1.0.0,發起檢測更新。
當進入利用后,就會從服務端獲得到更新對象

更新對象

然后將bundle-patch文件保存到data/data/com.xifan/cache/react_native sd卡路徑下

這里寫圖片描述

當將bundle-patch保存完并解壓以后,接下去就是加載bundle了。

加載bundle

根據bug的緊急/重要程度,可以把加載bundle的時機分為:立馬加載和下次啟動加載,我這里將它們分別稱為熱加載和冷加載。

冷加載

冷加載方式比較簡單,不用做任何特殊處理,下載并解壓完patch.zip包以后,當利用完全退出以后(利用在后臺不算完全退出,利用被殺死才算),用戶再次啟動利用,就會去加載新的bundle了。

熱加載

熱加載需要特殊處理1下,處理也很簡單,只要在解壓unzip以后,調用以下代碼便可

//MainActivity.java

//清空ReactInstanceManager配置
getReactNativeHost().clear();
//重啟activity
recreate();

結合JS端,實現完全熱更新流程

熱更新的整體思路是,JS端通過Module發起版本檢測要求,如果檢測到有新版本bundle,就去下載bundle,下載完成后根據更新的緊急程度來決定是冷加載還是熱加載。

那末首先我們需要定義1個UpdateCheckModule來建立起JS端和android端之間的檢測更新通訊。

UpdateCheckModule.java

class UpdateCheckModule extends ReactContextBaseJavaModule {
    private static final String TAG = "UpdateCheckModule";
    private static final String BUNDLE_VERSION = "CurrentBundleVersion";
    private SharedPreferences mSP;

    UpdateCheckModule(ReactApplicationContext reactContext) {
        super(reactContext);
        mSP = reactContext.getSharedPreferences("react_bundle", Context.MODE_PRIVATE);
    }

    @Override
    public String getName() {
        return "UpdateCheck";
    }

    @Nullable
    @Override
    public Map<String, Object> getConstants() {
        Map<String,Object> constants = MapBuilder.newHashMap();
        //跟隨apk1起打包的bundle基礎版本號
        String bundleVersion = BuildConfig.BUNDLE_VERSION;
        //bundle更新后確當前版本號
        String cacheBundleVersion = mSP.getString(BUNDLE_VERSION,"");
        if(!TextUtils.isEmpty(cacheBundleVersion)){
            bundleVersion = cacheBundleVersion;
        }
        constants.put(BUNDLE_VERSION,bundleVersion);
        return constants;
    }

    @ReactMethod
    public void check(String currVersion){
        BmobQuery<AppInfo> query = new BmobQuery<>();
        query.setLimit(1);
        query.addWhereGreaterThan("version",currVersion);
        query.findObjects(new FindListener<AppInfo>() {
            @Override
            public void done(List<AppInfo> list, BmobException e) {
                if(e == null){
                    if(list!=null && !list.isEmpty()){
                        final AppInfo info = list.get(0);
                        File reactDir = new File(getReactApplicationContext().getCacheDir(),"react_native");
                        //獲得到更新消息,說明bundle有新版,在解壓前先刪除掉舊版
                        deleteDir(reactDir);
                        if(!reactDir.exists()){
                            reactDir.mkdirs();
                        }
                        final File saveFile = new File(reactDir,"bundle-patch.zip");
                        BmobFile patchFile = info.getBundle();
                        //下載bundle-patch.zip文件
                        patchFile.download(saveFile, new DownloadFileListener() {
                            @Override
                            public void done(String s, BmobException e) {
                                if (e == null) {
                                    log("下載完成");
                                    //解壓patch文件到react_native文件夾下
                                    boolean result = unzip(saveFile);
                                    if(result){//解壓成功后保存當前最新bundle的版本
                                        mSP.edit().putString(BUNDLE_VERSION,info.getVersion()).apply();
                                        if(info.isImmediately()) {//立即加載bundle
                                            ((ReactApplication) getReactApplicationContext()).getReactNativeHost().clear();
                                            getCurrentActivity().recreate();
                                        }
                                    }else{//解壓失敗應當刪除掉有問題的文件,避免RN加載毛病的bundle文件
                                        File reactDir = new File(getReactApplicationContext().getCacheDir(),"react_native");
                                        deleteDir(reactDir);
                                    }
                                } else {
                                    e.printStackTrace();
                                    log("下載bundle patch失敗");
                                }

                            }

                            @Override
                            public void onProgress(Integer per, long size) {

                            }
                        });
                    }
                }else{
                    e.printStackTrace();
                    log("獲得版本信息失敗");
                }
            }
        });
    }
}

代碼中注釋已解釋了其中的重要部份,需要注意的是,AppInfo增加了個boolean型immediately字段,來控制bundle是不是立即生效

public class AppInfo extends BmobObject{
    private String version;//bundle版本
    private String updateContent;//更新內容
    private Boolean immediately;//bundle是不是立即生效
    private BmobFile bundle;//要下載的bundle文件
}

還有在getConstants()方法獲得當前bundle版本時,使用BuildConfig.BUNDLE_VERSION來標記和apk1起打包的bundle基礎版本號,也就是assets下的bundle版本號,該字段是通過gradle的buildConfigField來定義的。打開app/build.gradle,然后在下面所示的位置添加buildConfigField定義,具體以下:

//省略了其它代碼
android{
    defaultConfig {
        buildConfigField "String","BUNDLE_VERSION",'"1.0.0"'
    }
}

接著,不要忘記將自定義的UpdateCheckModule注冊到Packages里。如果,你對自定義module還不是很了解,請看這里

最后,就是在JS端使用UpdateCheckModule來發起版本檢測更新了。

我們先在XiFan/js/db 創建1個配置文件Config.js

const Config = {
    bundleVersion: '1.0.0'
};
export default Config;

代碼很簡單,Config里面只是定義了個bundleVersion字段,表示當前bundle版本號。
每次要發布新版bundle時,更新下這個文件的bundleVersion便可。

然后,我們在MainScene.js的componentDidMount()函數中發起版本檢測更新

//MainScene.js

//省略了其他代碼
import {
    NativeModules
} from 'react-native';
import Config from './db/Config';
var UpdateCheck = NativeModules.UpdateCheck;

export default class MainScene extends Component{

    componentDidMount(){
        console.log('當前版本號:'+UpdateCheck.CurrentBundleVersion);
        UpdateCheck.check(Config.bundleVersion)
    }
}

這樣就完成了,基本的bundle更新流程了。

總結

本篇文章主要分析了RN android端bundle的加載進程,并且在分析理解下,實現了完全bundle包的基本熱更新,但是這只是熱更新的1部份,還有很多方面可以優化,比如:多模塊的多bundle熱更新、bundle拆分差量更新、熱更新的異常回退處理、多版本bundle的動態切換、bundle的更新和apk的更新相結合等等,這也是以后繼續研究學習的方向。

最后,這個是項目的github地址 ,本章節的內容是在android分支上開發的,如需查看完全代碼,克隆下來后請切換分支。

生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈
程序員人生
------分隔線----------------------------
分享到:
------分隔線----------------------------
關閉
程序員人生
主站蜘蛛池模板: 欧美a级黄色 | 日本大臿亚洲香蕉大片 | 欧美成人性色大片在线观看 | 欧美性猛交xxxxx按摩国内 | 自拍偷拍图 | 激情影院在线视频永久观看 | 成人精品一区二区三区校园激情 | 欧美综合视频在线 | 久久国产一久久高清 | 欧美一区视频 | 欧美黑粗特黄午夜大片 | 精品动漫第一页 | 免费大黄网站在线观 | 一区二区三区日韩 | 欧美free性 | 国产成人啪精品午夜在线播放 | 欧美性视频一区二区三区 | 久久受www免费人成_看片中文 | 2022国产成人精品福利网站 | 亚洲欧美日韩人成 | 欧亚精品一区二区三区 | 免费爱爱网 | 一区二区中文字幕在线观看 | 久久久不卡国产精品一区二区 | 亚洲欧美日韩在线观看播放 | 波多野结衣一级视频 | 久久久久久免费一区二区三区 | 欧美一级日韩一级 | 久久精品一区二区 | 69国产成人综合久久精 | 日韩国产欧美成人一区二区影院 | 毛片大全网站 | 欧美不卡在线视频 | 国产乱通伦 | 日韩欧美在线第一页 | 欧美精品在线观看 | 国产美女激情 | 欧洲区二区三区四区 | 在线视频一区二区三区 | 国产福利在线网址成人 | 男女在线免费视频 |