【稀饭】react native 实战系列教程之热更新原理分析与实现

前端之家收集整理的这篇文章主要介绍了【稀饭】react native 实战系列教程之热更新原理分析与实现前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

很多人在技术选型的时候,会选择RN是因为它具有热更新,而且这是它的一个特性,所以实现起来会相对比较简单,不像原生那样,原生的热更新是一个大工程。那就目前来看,RN的热更新方案已有的,有微软的CodePush和reactnative中文网的pushy。实话说,这两个我还没有体验过。一来是当初选择RN是因为它不但拥有接近原生的体验感还具有热更新特性,那么就想自己来实现一下热更新,研究一下它的原理;二来,把自己的东西放在别人的服务器上总是觉得不是最好的办法,为什么不自己实现呢?因此,这篇文章便是记录自己的一些研究。

react native加载bundle过程

这篇文章是基于RN android 0.38.1

当我们创建完RN的基础项目后,打开android项目,项目只有MainActivity和MainApplication。

打开MainActivity,只有一个重写方法getMainComponentName,返回主组件名称,它继承于ReactActivity。

我们打开ReactActivity,它使用了代理模式,通过ReactActivityDelegate mDelegate对象将Activity需要处理的逻辑放在了代理对象内部,并通过getMainComponentName方法来设置(匹配)JS端AppRegistry.registerComponent端启动的入口组件。

Activity渲染出界面前,先是调用onCreate,所以我们进入代理对象的onCreate方法

  1. //ReactActivityDelegate.java
  2.  
  3.  
  4. protected void onCreate(Bundle savedInstanceState) {
  5. //判断是否支持dev模式,也就是RN常见的那个红色弹窗
  6. if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
  7. // Get permission to show redBox in dev builds.
  8. if (!Settings.canDrawOverlays(getContext())) {
  9. Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
  10. getContext().startActivity(serviceIntent);
  11. FLog.w(ReactConstants.TAG,REDBox_PERMISSION_MESSAGE);
  12. Toast.makeText(getContext(),REDBox_PERMISSION_MESSAGE,Toast.LENGTH_LONG).show();
  13. }
  14. }
  15.  
  16. if (mMainComponentName != null) {
  17. //加载app
  18. loadApp(mMainComponentName);
  19. }
  20. //android模拟器dev 模式下,双击R重新加载
  21. mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  22. }

上面的代码并没什么实质的东西,主要是调用了loadApp,我们跟进看下

  1. protected void loadApp(String appKey) {
  2. if (mReactRootView != null) {
  3. throw new IllegalStateException("Cannot loadApp while app is already running.");
  4. }
  5. mReactRootView = createRootView();
  6. mReactRootView.startReactApplication(
  7. getReactNativeHost().getReactInstanceManager(),appKey,getLaunchOptions());
  8. getPlainActivity().setContentView(mReactRootView);
  9. }

生成了一个ReactRootView对象,然后调用它的startReactApplication方法,最后setContentView将它设置为内容视图。再跟进startReactApplication里

  1. //ReactRootView.java
  2.  
  3. public void startReactApplication(
  4. ReactInstanceManager reactInstanceManager,String moduleName,@Nullable Bundle launchOptions) {
  5. UiThreadUtil.assertOnUiThread();
  6.  
  7. // TODO(6788889): Use POJO instead of bundle here,apparently we can't just use WritableMap
  8. // here as it may be deallocated in native after passing via JNI bridge,but we want to reuse
  9. // it in the case of re-creating the catalyst instance
  10. Assertions.assertCondition(
  11. mReactInstanceManager == null,"This root view has already been attached to a catalyst instance manager");
  12. //配置项管理
  13. mReactInstanceManager = reactInstanceManager;
  14. //入口组件名称
  15. mJSModuleName = moduleName;
  16. //用于传递给JS端初始组件props参数
  17. mLaunchOptions = launchOptions;
  18. //判断是否已经加载过
  19. if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
  20. //去加载bundle文件
  21. mReactInstanceManager.createReactContextInBackground();
  22. }
  23.  
  24. // We need to wait for the initial onMeasure,if this view has not yet been measured,we set which
  25. // will make this view startReactApplication itself to instance manager once onMeasure is called.
  26. if (mWasMeasured) {
  27. //去渲染ReactRootView
  28. attachToReactInstanceManager();
  29. }
  30. }

startReactApplication传入三个参数,第一个ReactInstanceManager配置项管理类(非常重要);第二个是MainComponentName入口组件名称;第三个是Android Bundle类型,用于传递给JS端初始组件的props参数。首先,会根据ReactInstanceManager的配置去加载bundle过程,然后去渲染ReactRootView,将UI展示出来。现在我们不用去管attachToReactInstanceManager是如何去渲染ReactRootView,我们主要是研究如何加载bundle的,所以,我们跟进createReactContextInBackground,发现它是抽象类ReactInstanceManager的一个抽象方法。那它具体实现逻辑是什么呢?那我们就需要知道ReactInstanceManager的具体类的实例对象是谁了【1】。

好了,现在我们回到ReacActivityDelegate.java的loadApp,在ReactRootView的startReactApplication传入的ReactInstanceManager对象是getReactNativeHost().getReactInstanceManager()

  1. //ReacActivityDelegate.java
  2.  
  3. mReactRootView.startReactApplication(
  4. getReactNativeHost().getReactInstanceManager(),getLaunchOptions());

getReactNativeHost(),又是什么呢?

  1. //从Application获取ReactNativeHost
  2. protected ReactNativeHost getReactNativeHost() {
  3. return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
  4. }

所以我们在打开MainApplication类

  1. public class MainApplication extends Application implements ReactApplication {
  2.  
  3. private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
  4. @Override
  5. protected boolean getUseDeveloperSupport() {
  6. return BuildConfig.DEBUG;
  7. }
  8.  
  9. @Override
  10. protected List<ReactPackage> getPackages() {
  11. return Arrays.<ReactPackage>asList(
  12. new MainReactPackage()
  13. );
  14. }
  15. };
  16.  
  17. @Override
  18. public ReactNativeHost getReactNativeHost() {
  19. return mReactNativeHost;
  20. }
  21. }

MainApplication实现了ReactApplication接口,在getReactNativeHost()方法返回配置好的ReactNativeHost对象。由于我们把项目的Application配置成了MainApplication,所以ReacActivityDelegate的getReactNativeHost方法,返回的就是MainApplication mReactNativeHost对象。接着我们看下ReactNativeHost的getReactInstanceManager()方法,里面直接调用了createReactInstanceManager()方法,所以我们直接看createReactInstanceManager()

  1. //ReactNativeHost.java
  2.  
  3. protected ReactInstanceManager createReactInstanceManager() {
  4. ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
  5. .setApplication(mApplication)
  6. .setJSMainModuleName(getJSMainModuleName())
  7. .setUseDeveloperSupport(getUseDeveloperSupport())
  8. .setRedBoxHandler(getRedBoxHandler())
  9. .setUIImplementationProvider(getUIImplementationProvider())
  10. .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
  11.  
  12. for (ReactPackage reactPackage : getPackages()) {
  13. builder.addPackage(reactPackage);
  14. }
  15.  
  16. String jsBundleFile = getJSBundleFile();
  17. if (jsBundleFile != null) {
  18. builder.setJSBundleFile(jsBundleFile);
  19. } else {
  20. builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
  21. }
  22. return builder.build();
  23. }

createReactInstanceManager()通过使用ReactInstanceManager.Builder构造器来设置一些配置并生成对象。从这里看,我们可以从MainApplication的mReactNativeHost对象来配置ReactInstanceManager,比如JSMainModuleName、UseDeveloperSupport、Packages、JSBundleFile、BundleAssetName等,也可以重写createReactInstanceManager方法,自己手动生成ReactInstanceManager对象。

这里看下jsBundleFile的设置,先判断了getJSBundleFile()是否为null,项目默认是没有重写的,所以默认就是null,那么走builder.setBundleAssetName分支,看下getBundleAssetName(),默认是返回”index.android.bundle”

  1. //builder.setBundleAssetName
  2.  
  3. public Builder setBundleAssetName(String bundleAssetName) {
  4. mJSBundleAssetUrl = (bundleAssetName == null ? null : "assets://" + bundleAssetName);
  5. mJSBundleLoader = null;
  6. return this;
  7. }

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

接着往下看,builder最后调用build()来生成ReactInstanceManager实例对象。我们进去build()方法看下。

  1. //ReactInstanceManager.Builder
  2.  
  3. public ReactInstanceManager build() {
  4. Assertions.assertNotNull(
  5. mApplication,"Application property has not been set with this builder");
  6.  
  7. Assertions.assertCondition(
  8. mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,"JS Bundle File or Asset URL has to be provided when dev support is disabled");
  9.  
  10. Assertions.assertCondition(
  11. mJSMainModuleName != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,"Either MainModuleName or JS Bundle File needs to be provided");
  12.  
  13. if (mUIImplementationProvider == null) {
  14. // create default UIImplementationProvider if the provided one is null.
  15. mUIImplementationProvider = new UIImplementationProvider();
  16. }
  17.  
  18. return new XReactInstanceManagerImpl(
  19. mApplication,mCurrentActivity,mDefaultHardwareBackBtnHandler,(mJSBundleLoader == null && mJSBundleAssetUrl != null) ?
  20. JSBundleLoader.createAssetLoader(mApplication,mJSBundleAssetUrl) : mJSBundleLoader,mJSMainModuleName,mPackages,mUseDeveloperSupport,mBridgeIdleDebugListener,Assertions.assertNotNull(mInitialLifecycleState,"Initial lifecycle state was not set"),mUIImplementationProvider,mNativeModuleCallExceptionHandler,mJSCConfig,mRedBoxHandler,mLazyNativeModulesEnabled,mLazyViewManagersEnabled);
  21. }

从上面看来,XReactInstanceManagerImpl的第四个参数,传入的是一个JSBundleLoader,并且默认是JSBundleLoader.createAssetLoader。

new的是XReactInstanceManagerImpl对象,也就是说,XReactInstanceManagerImpl是抽象类ReactInstanceManager的具体实现类。

好了,在【1】处留下的疑问,我们现在就解决了。也就是,说调用ReactInstanceManager的createReactContextInBackground方法,是去执行XReactInstanceManagerImpl的reateReactContextInBackground方法

进去reateReactContextInBackground方法后,它调用了recreateReactContextInBackgroundInner()一个内部方法,直接看下recreateReactContextInBackgroundInner的实现代码

  1. //XReactInstanceManagerImpl.java
  2.  
  3. private void recreateReactContextInBackgroundInner() {
  4. UiThreadUtil.assertOnUiThread();
  5. //判断是否是dev模式
  6. if (mUseDeveloperSupport && mJSMainModuleName != null) {
  7. final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();
  8. // If remote JS debugging is enabled,load from dev server.
  9. if (mDevSupportManager.hasUpToDateJSBundleInCache() &&
  10. !devSettings.isRemoteJSDebugEnabled()) {
  11. // If there is a up-to-date bundle downloaded from server,
  12. // with remote JS debugging disabled,always use that.
  13. onJSBundleLoadedFromServer();
  14. } else if (mBundleLoader == null) {
  15. mDevSupportManager.handleReloadJS();
  16. } else {
  17. mDevSupportManager.isPackagerRunning(
  18. new DevServerHelper.PackagerStatusCallback() {
  19. @Override
  20. public void onPackagerStatusFetched(final boolean packagerIsRunning) {
  21. UiThreadUtil.runOnUiThread(
  22. new Runnable() {
  23. @Override
  24. public void run() {
  25. if (packagerIsRunning) {
  26. mDevSupportManager.handleReloadJS();
  27. } else {
  28. // If dev server is down,disable the remote JS debugging.
  29. devSettings.setRemoteJSDebugEnabled(false);
  30. recreateReactContextInBackgroundFromBundleLoader();
  31. }
  32. }
  33. });
  34. }
  35. });
  36. }
  37. return;
  38. }
  39.  
  40. recreateReactContextInBackgroundFromBundleLoader();
  41. }

由于我们发布出去的apk包,最后都是关闭了dev模式的,所以dev模式下的bundle加载流程我们先不需要太多的关注,那么mUseDeveloperSupport就是false,它就不会走进if里面,而是调用了recreateReactContextInBackgroundFromBundleLoader()方法。其实,你简单看下if里面的判断和方法调用也能知道,其实它就是去拉取通过react-native start启动起来的packages服务器窗口,再者如果打开了远程调试,那么它就走浏览器代理去拉取bundle。

recreateReactContextInBackgroundFromBundleLoader又调用了recreateReactContextInBackground

  1. private void recreateReactContextInBackground(
  2. JavaScriptExecutor.Factory jsExecutorFactory,JSBundleLoader jsBundleLoader) {
  3. UiThreadUtil.assertOnUiThread();
  4.  
  5. ReactContextInitParams initParams =
  6. new ReactContextInitParams(jsExecutorFactory,jsBundleLoader);
  7. if (mReactContextInitAsyncTask == null) {
  8. // No background task to create react context is currently running,create and execute one.
  9. mReactContextInitAsyncTask = new ReactContextInitAsyncTask();
  10. mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,initParams);
  11. } else {
  12. // Background task is currently running,queue up most recent init params to recreate context
  13. // once task completes.
  14. mPendingReactContextInitParams = initParams;
  15. }
  16. }

到这里,recreateReactContextInBackground使用了ReactContextInitAsyncTask(继承AsyncTask)开启线程去执行,并且将ReactContextInitParams当作参数,传递到了AsyncTask的doInBackground。ReactContextInitParams只是将jsExecutorFactory、jsBundleLoader两个参数封装成一个内部类,方便传递参数。

那么ReactContextInitAsyncTask开启线程去执行了什么?该类也是个内部类,我们直接看它的doInBackground方法

  1. @Override
  2. protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {
  3.  
  4. Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
  5.  
  6. Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
  7. try {
  8. JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
  9. return Result.of(createReactContext(jsExecutor,params[0].getJsBundleLoader()));
  10. } catch (Exception e) {
  11. // Pass exception to onPostExecute() so it can be handled on the main thread
  12. return Result.of(e);
  13. }
  14. }

好像也没处理什么,就是使用ReactContextInitParams传递进来的两个参数,去调用了createReactContext

  1. private ReactApplicationContext createReactContext(
  2. JavaScriptExecutor jsExecutor,JSBundleLoader jsBundleLoader) {
  3. FLog.i(ReactConstants.TAG,"Creating react context.");
  4. ReactMarker.logMarker(CREATE_REACT_CONTEXT_START);
  5. mSourceUrl = jsBundleLoader.getSourceUrl();
  6. List<ModuleSpec> moduleSpecs = new ArrayList<>();
  7. Map<Class,ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
  8. JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();
  9.  
  10. final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
  11. if (mUseDeveloperSupport) {
  12. reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
  13. }
  14.  
  15. ReactMarker.logMarker(PROCESS_PACKAGES_START);
  16. Systrace.beginSection(
  17. TRACE_TAG_REACT_JAVA_BRIDGE,"createAndProcesscoreModulesPackage");
  18. try {
  19. CoreModulesPackage coreModulesPackage =
  20. new CoreModulesPackage(this,mBackBtnHandler,mUIImplementationProvider);
  21. processPackage(
  22. coreModulesPackage,reactContext,moduleSpecs,reactModuleInfoMap,jsModulesBuilder);
  23. } finally {
  24. Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
  25. }
  26.  
  27. // TODO(6818138): Solve use-case of native/js modules overriding
  28. for (ReactPackage reactPackage : mPackages) {
  29. Systrace.beginSection(
  30. TRACE_TAG_REACT_JAVA_BRIDGE,"createAndProcessCustomReactPackage");
  31. try {
  32. processPackage(
  33. reactPackage,jsModulesBuilder);
  34. } finally {
  35. Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
  36. }
  37. }
  38. ReactMarker.logMarker(PROCESS_PACKAGES_END);
  39.  
  40. ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_START);
  41. Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE,"buildNativeModuleRegistry");
  42. NativeModuleRegistry nativeModuleRegistry;
  43. try {
  44. nativeModuleRegistry = new NativeModuleRegistry(moduleSpecs,reactModuleInfoMap);
  45. } finally {
  46. Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
  47. ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END);
  48. }
  49.  
  50. NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
  51. ? mNativeModuleCallExceptionHandler
  52. : mDevSupportManager;
  53. CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()
  54. .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
  55. .setJSExecutor(jsExecutor)
  56. .setRegistry(nativeModuleRegistry)
  57. .setJSModuleRegistry(jsModulesBuilder.build())
  58. .setJSBundleLoader(jsBundleLoader)
  59. .setNativeModuleCallExceptionHandler(exceptionHandler);
  60.  
  61. ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_START);
  62. // CREATE_CATALYST_INSTANCE_END is in JSCExecutor.cpp
  63. Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE,"createCatalystInstance");
  64. final CatalystInstance catalystInstance;
  65. try {
  66. catalystInstance = catalystInstanceBuilder.build();
  67. } finally {
  68. Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
  69. ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END);
  70. }
  71.  
  72. if (mBridgeIdleDebugListener != null) {
  73. catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
  74. }
  75.  
  76. reactContext.initializeWithInstance(catalystInstance);
  77. catalystInstance.runJSBundle();
  78.  
  79. return reactContext;
  80. }

这个方法代码有点多,首先它执行设置了RN自带的和开发者自定义的模块组件(Package\Module),然后同样使用了构造器CatalystInstanceImpl.Builder生成了catalystInstance对象,最后调用了catalystInstance.runJSBundle()。跟进去是一个接口类CatalystInstance,那么我们又要去看它的实现类CatalystInstanceImpl

  1. //CatalystInstanceImpl.java
  2.  
  3. @Override
  4. public void runJSBundle() {
  5. Assertions.assertCondition(!mJSBundleHasLoaded,"JS bundle was already loaded!");
  6. mJSBundleHasLoaded = true;
  7. // incrementPendingJSCalls();
  8. mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
  9.  
  10. synchronized (mJSCallsPendingInitLock) {
  11. // Loading the bundle is queued on the JS thread,but may not have
  12. // run yet. It's save to set this here,though,since any work it
  13. // gates will be queued on the JS thread behind the load.
  14. mAcceptCalls = true;
  15.  
  16. for (PendingJSCall call : mJSCallsPendingInit) {
  17. callJSFunction(call.mExecutorToken,call.mModule,call.mMethod,call.mArguments);
  18. }
  19. mJSCallsPendingInit.clear();
  20. }
  21.  
  22.  
  23. // This is registered after JS starts since it makes a JS call
  24. Systrace.registerListener(mTraceListener);
  25. }

到这里,可以看到mJSBundleLoader调用了loadScript去加载bundle。进去方法看下,发现它又是个抽象类,有两个抽象方法,一个是loadScript加载bundle,一个是getSourceUrl返回bundle的地址,并且提供了4个静态工厂方法

由之前分析知道,JSBundleLoader默认是使用了JSBundleLoader.createAssetLoader来创建的实例

  1. //JSBundleLoader.java
  2.  
  3. public static JSBundleLoader createAssetLoader(
  4. final Context context,final String assetUrl) {
  5. return new JSBundleLoader() {
  6. @Override
  7. public void loadScript(CatalystInstanceImpl instance) {
  8. instance.loadScriptFromAssets(context.getAssets(),assetUrl);
  9. }
  10.  
  11. @Override
  12. public String getSourceUrl() {
  13. return assetUrl;
  14. }
  15. };
  16. }

我们看到loadScript最后是调用了CatalystInstanceImpl的loadScriptFromAssets。跟进去之后发现,它是一个native方法,也就是最后的实现RN把它放在了jni层来完成最后加载bundle的过程。

并且CatalystInstanceImpl不止loadScriptFromAssets一个native方法,它还提供了loadScriptFromFile和loadScriptFromOptimizedBundle。其中前面两个,分别是从android assets目录下加载bundle,另一个是从android SD卡文件夹目录下加载bundle。而loadScriptFromOptimizedBundle是在UnpackingJSBundleLoader类里调用,但是UnpackingJSBundleLoader目前好像是没有用到,有知道它的作用的朋友们可以告知一下。

至此,bundle的加载流程我们已经走一遍了,下面用一张流程图来总结下

加载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

第二种方式是从android文件系统也就是sd卡下去加载bundle。

我们只要事先在sd卡下存放bundle文件,然后在ReactNativeHost的getJSBundleFile返回文件路径即可。

  1. //MainApplication.java
  2.  
  3. private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
  4. @Override
  5. protected boolean getUseDeveloperSupport() {
  6. return BuildConfig.DEBUG;
  7. }
  8.  
  9. @Override
  10. protected List<ReactPackage> getPackages() {
  11. return Arrays.<ReactPackage>asList(
  12. new MainReactPackage()
  13. );
  14. }
  15.  
  16. @Nullable
  17. @Override
  18. protected String getJSBundleFile() {
  19. File bundleFile = new File(getCacheDir()+"/react_native","index.android.bundle");
  20. if(bundleFile.exists()){
  21. return bundleFile.getAbsolutePath();
  22. }
  23. return super.getJSBundleFile();
  24. }
  25.  
  26. @Nullable
  27. @Override
  28. protected String getBundleAssetName() {
  29. return super.getBundleAssetName();
  30. }
  31. };

getJSBundleFile首先会尝试在sd卡目录下

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

看是否存在index.android.bundle文件,如果有,那么就会使用该bundle,如果没有,那么就会返回null,这时候就是去加载assets下的bundle了。

热更新的实现

如果你了解react native bundle命令,那么就会知道,其实该命令分两部分,一部分是生成bundle文件,一部分是生成图片资源。对android的react.gardle来说,也就是app/build.gradle中下面这句

  1. 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包打包出来

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

上传到服务器

然后,在客户端定义一个实体类来存放更新对象

  1. public class AppInfo extends BmobObject{
  2. private String version;//bundle版本
  3. private String updateContent;//更新内容
  4. private BmobFile bundle;//要下载的bundle patch文件
  5. }

然后,程序启动的时候去检测更新

  1. //MainActivity.java
  2.  
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. BmobQuery<AppInfo> query = new BmobQuery<>();
  7. query.setLimit(1);
  8. query.addWhereGreaterThan("version","1.0.0");
  9. query.findObjects(new FindListener<AppInfo>() {
  10. @Override
  11. public void done(List<AppInfo> list,BmobException e) {
  12. if(e == null){
  13. if(list!=null && !list.isEmpty()){
  14. AppInfo info = list.get(0);
  15. File reactDir = new File(getCacheDir(),"react_native");
  16. if(!reactDir.exists()){
  17. reactDir.mkdirs();
  18. }
  19.  
  20. BmobFile patchFile = info.getBundle();
  21. final File saveFile = new File(reactDir,"bundle-patch.zip");
  22. if(saveFile.exists()){
  23. return;
  24. }
  25. //下载bundle-patch.zip文件
  26. patchFile.download(saveFile,new DownloadFileListener() {
  27. @Override
  28. public void done(String s,BmobException e) {
  29. if (e == null) {
  30. System.out.println("下载完成");
  31. //解压patch文件到react_native文件夹下
  32. unzip(saveFile);
  33. } else {
  34. Log.e("bmob",e.toString());
  35. }
  36.  
  37. }
  38.  
  39. @Override
  40. public void onProgress(Integer integer,long l) {
  41. System.out.println("下载中...." + integer);
  42. }
  43. });
  44. }
  45. }else{
  46. Log.e("bmob",e.toString());
  47. }
  48. }
  49. });
  50. }

在MainActivity的onCreate,将当前版本当作是1.0.0,发起检测更新。
当进入应用后,就会从服务端获取到更新对象

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

当将bundle-patch保存完并解压之后,接下去就是加载bundle了。

加载bundle

根据bug的紧急/重要程度,可以把加载bundle的时机分为:立马加载和下次启动加载,我这里将它们分别称为热加载和冷加载。

冷加载

冷加载方式比较简单,不用做任何特殊处理,下载并解压完patch.zip包之后,当应用完全退出之后(应用在后台不算完全退出,应用被杀死才算),用户再次启动应用,就会去加载新的bundle了。

热加载

热加载需要特殊处理一下,处理也很简单,只要在解压unzip之后,调用以下代码即可

  1. //MainActivity.java
  2.  
  3. //清空ReactInstanceManager配置
  4. getReactNativeHost().clear();
  5. //重启activity
  6. recreate();

结合JS端,实现完整热更新流程

热更新的总体思路是,JS端通过Module发起版本检测请求,如果检测到有新版本bundle,就去下载bundle,下载完成后根据更新的紧急程度来决定是冷加载还是热加载。

那么首先我们需要定义一个UpdateCheckModule来建立起JS端和android端之间的检测更新通信。

UpdateCheckModule.java

  1. class UpdateCheckModule extends ReactContextBaseJavaModule {
  2. private static final String TAG = "UpdateCheckModule";
  3. private static final String BUNDLE_VERSION = "CurrentBundleVersion";
  4. private SharedPreferences mSP;
  5.  
  6. UpdateCheckModule(ReactApplicationContext reactContext) {
  7. super(reactContext);
  8. mSP = reactContext.getSharedPreferences("react_bundle",Context.MODE_PRIVATE);
  9. }
  10.  
  11. @Override
  12. public String getName() {
  13. return "UpdateCheck";
  14. }
  15.  
  16. @Nullable
  17. @Override
  18. public Map<String,Object> getConstants() {
  19. Map<String,Object> constants = MapBuilder.newHashMap();
  20. //跟随apk一起打包的bundle基础版本号
  21. String bundleVersion = BuildConfig.BUNDLE_VERSION;
  22. //bundle更新后的当前版本号
  23. String cacheBundleVersion = mSP.getString(BUNDLE_VERSION,"");
  24. if(!TextUtils.isEmpty(cacheBundleVersion)){
  25. bundleVersion = cacheBundleVersion;
  26. }
  27. constants.put(BUNDLE_VERSION,bundleVersion);
  28. return constants;
  29. }
  30.  
  31. @ReactMethod
  32. public void check(String currVersion){
  33. BmobQuery<AppInfo> query = new BmobQuery<>();
  34. query.setLimit(1);
  35. query.addWhereGreaterThan("version",currVersion);
  36. query.findObjects(new FindListener<AppInfo>() {
  37. @Override
  38. public void done(List<AppInfo> list,BmobException e) {
  39. if(e == null){
  40. if(list!=null && !list.isEmpty()){
  41. final AppInfo info = list.get(0);
  42. File reactDir = new File(getReactApplicationContext().getCacheDir(),"react_native");
  43. //获取到更新消息,说明bundle有新版,在解压前先删除掉旧版
  44. deleteDir(reactDir);
  45. if(!reactDir.exists()){
  46. reactDir.mkdirs();
  47. }
  48. final File saveFile = new File(reactDir,"bundle-patch.zip");
  49. BmobFile patchFile = info.getBundle();
  50. //下载bundle-patch.zip文件
  51. patchFile.download(saveFile,new DownloadFileListener() {
  52. @Override
  53. public void done(String s,BmobException e) {
  54. if (e == null) {
  55. log("下载完成");
  56. //解压patch文件到react_native文件夹下
  57. boolean result = unzip(saveFile);
  58. if(result){//解压成功后保存当前最新bundle的版本
  59. mSP.edit().putString(BUNDLE_VERSION,info.getVersion()).apply();
  60. if(info.isImmediately()) {//立即加载bundle
  61. ((ReactApplication) getReactApplicationContext()).getReactNativeHost().clear();
  62. getCurrentActivity().recreate();
  63. }
  64. }else{//解压失败应该删除掉有问题的文件,防止RN加载错误的bundle文件
  65. File reactDir = new File(getReactApplicationContext().getCacheDir(),"react_native");
  66. deleteDir(reactDir);
  67. }
  68. } else {
  69. e.printStackTrace();
  70. log("下载bundle patch失败");
  71. }
  72.  
  73. }
  74.  
  75. @Override
  76. public void onProgress(Integer per,long size) {
  77.  
  78. }
  79. });
  80. }
  81. }else{
  82. e.printStackTrace();
  83. log("获取版本信息失败");
  84. }
  85. }
  86. });
  87. }
  88. }

代码中注释已经解释了其中的重要部分,需要注意的是,AppInfo增加了个boolean型immediately字段,来控制bundle是否立即生效

  1. public class AppInfo extends BmobObject{
  2. private String version;//bundle版本
  3. private String updateContent;//更新内容
  4. private Boolean immediately;//bundle是否立即生效
  5. private BmobFile bundle;//要下载的bundle文件
  6. }

还有在getConstants()方法获取当前bundle版本时,使用BuildConfig.BUNDLE_VERSION来标记和apk一起打包的bundle基础版本号,也就是assets下的bundle版本号,该字段是通过gradle的buildConfigField来定义的。打开app/build.gradle,然后在下面所示的位置添加buildConfigField定义,具体如下:

  1. //省略了其它代码
  2. android{
  3. defaultConfig {
  4. buildConfigField "String","BUNDLE_VERSION",'"1.0.0"'
  5. }
  6. }

接着,不要忘记将自定义的UpdateCheckModule注册到Packages里。如果,你对自定义module还不是很了解,请看这里

最后,就是在JS端使用UpdateCheckModule来发起版本检测更新了。

我们先在XiFan/js/db 创建一个配置文件Config.js

  1. const Config = {
  2. bundleVersion: '1.0.0'
  3. };
  4. export default Config;

代码很简单,Config里面只是定义了个bundleVersion字段,表示当前bundle版本号。
每次要发布新版bundle时,更新下这个文件的bundleVersion即可。

然后,我们在MainScene.js的componentDidMount()函数中发起版本检测更新

  1. //MainScene.js
  2.  
  3. //省略了其他代码
  4. import {
  5. NativeModules
  6. } from 'react-native';
  7. import Config from './db/Config';
  8. var UpdateCheck = NativeModules.UpdateCheck;
  9.  
  10. export default class MainScene extends Component{
  11.  
  12. componentDidMount(){
  13. console.log('当前版本号:'+UpdateCheck.CurrentBundleVersion);
  14. UpdateCheck.check(Config.bundleVersion)
  15. }
  16. }

这样就完成了,基本的bundle更新流程了。

总结

本篇文章主要分析了RN android端bundle的加载过程,并且在分析理解下,实现了完整bundle包的基本热更新,但是这只是热更新的一部分,还有很多方面可以优化,比如:多模块的多bundle热更新、bundle拆分差量更新、热更新的异常回退处理、多版本bundle的动态切换、bundle的更新和apk的更新相结合等等,这也是之后继续研究学习的方向。

最后,这个是项目的github地址 ,本章节的内容是在android分支上开发的,如需查看完整代码,克隆下来后请切换分支。

猜你在找的React相关文章