加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-15 07:36:21 所属栏目:百科 来源:网络整理
导读:很多人在技术选型的时候,会选择RN是因为它具有热更新,而且这是它的一个特性,所以实现起来会相对比较简单,不像原生那样,原生的热更新是一个大工程。那就目前来看,RN的热更新方案已有的,有微软的CodePush和reactnative中文网的pushy。实话说,这两个我

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

react native加载bundle过程

这篇文章是基于RNAndroid0.38.1

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

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

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

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
//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,我们跟进看下

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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); }

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    //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传入三个参数,第一个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
  • 2
  • 3
  • 4
  • 5
  • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    //ReacActivityDelegate.java mReactRootView.startReactApplication( getReactNativeHost().getReactInstanceManager(),getLaunchOptions());

    getReactNativeHost(),又是什么呢?

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

    所以我们在打开MainApplication类

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } 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()

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    //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构造器来设置一些配置并生成对象。从这里看,我们可以从MainApplication的mReactNativeHost对象来配置ReactInstanceManager,比如JSMainModuleName、UseDeveloperSupport、Packages、JSBundleFile、BundleAssetName等,也可以重写createReactInstanceManager方法,自己手动生成ReactInstanceManager对象。

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    //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()方法看下。

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    //ReactInstanceManager.Builder public ReactInstanceManager build() { Assertions.assertNotNull( mApplication,0); box-sizing: border-box;">"Application property has not been set with this builder"); Assertions.assertCondition( mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != "JS Bundle File or Asset URL has to be provided when dev support is disabled"); Assertions.assertCondition( mJSMainModuleName != null || mJSBundleAssetUrl != "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(); } new XReactInstanceManagerImpl( mApplication,mCurrentActivity,mDefaultHardwareBackBtnHandler,(mJSBundleLoader == null && mJSBundleAssetUrl != null) ? JSBundleLoader.createAssetLoader(mApplication,mJSBundleAssetUrl) : mJSBundleLoader,mJSMainModuleName,mPackages,mUseDeveloperSupport,mBridgeIdleDebugListener,Assertions.assertNotNull(mInitialLifecycleState,0); box-sizing: border-box;">"Initial lifecycle state was not set"),mUIImplementationProvider,mNativeModuleCallExceptionHandler,mJSCConfig,mRedBoxHandler,mLazyNativeModulesEnabled,mLazyViewManagersEnabled); }

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

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

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

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    //XReactInstanceManagerImpl.java void recreateReactContextInBackgroundInner() { UiThreadUtil.assertOnUiThread(); //判断是否是dev模式 if (mUseDeveloperSupport && mJSMainModuleName != 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 void onPackagerStatusFetched(final boolean packagerIsRunning) { UiThreadUtil.runOnUiThread( new Runnable() { @Override 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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    void recreateReactContextInBackground( JavaScriptExecutor.Factory jsExecutorFactory,JSBundleLoader jsBundleLoader) { UiThreadUtil.assertOnUiThread(); ReactContextInitParams initParams = new ReactContextInitParams(jsExecutorFactory,jsBundleLoader); if (mReactContextInitAsyncTask == // 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两个参数封装成一个内部类,方便传递参数。

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    private ReactApplicationContext createReactContext( JavaScriptExecutor jsExecutor,JSBundleLoader jsBundleLoader) { FLog.i(ReactConstants.TAG,0); box-sizing: border-box;">"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,0); box-sizing: border-box;">"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,0); box-sizing: border-box;">"createAndProcessCustomReactPackage"); try { processPackage( reactPackage,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,0); box-sizing: border-box;">"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,0); box-sizing: border-box;">"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自带的和开发者自定义的模块组件(PackageModule),然后同样使用了构造器CatalystInstanceImpl.Builder生成了catalystInstance对象,最后调用了catalystInstance.runJSBundle()。跟进去是一个接口类CatalystInstance,那么我们又要去看它的实现类CatalystInstanceImpl

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    //CatalystInstanceImpl.java void runJSBundle() { Assertions.assertCondition(!mJSBundleHasLoaded,0); box-sizing: border-box;">"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。进去方法看下,发现它又是个抽象类,有两个抽象方法,一个是loadScript加载bundle,一个是getSourceUrl返回bundle的地址,并且提供了4个静态工厂方法。

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

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

    我们看到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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    //MainApplication.java return Arrays.<ReactPackage>asList( new MainReactPackage() ); } @Nullable protected String getJSBundleFile() { File bundleFile = new File(getCacheDir()+"/react_native",0); box-sizing: border-box;">"index.android.bundle"); if(bundleFile.exists()){ return bundleFile.getAbsolutePath(); } super.getJSBundleFile(); } protected String getBundleAssetName() { super.getBundleAssetName(); } };

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

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

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

    热更新的实现

    如果你了解React Nativebundle命令,那么就会知道,其实该命令分两部分,一部分是生成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
  • 2
  • 3
    • 1
    • 2
    • 3
    react-native bundle --platform android --dev false --r eset-cache --entry-file index.android.js --bundle-output F:GrayReactNativeXiF anbundleindex.android.bundle --assets-dest F:XiFanbundle

    上传到服务器

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

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

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

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

    //MainActivity.java
    
    //清空ReactInstanceManager配置
    getReactNativeHost().clear();
    //重启activity
    recreate();

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

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

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

    UpdateCheckModule.java

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

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • private Boolean immediately;//bundle是否立即生效 //要下载的bundle文件 }

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

    //省略了其它代码
    android{
        defaultConfig {
            buildConfigField "String",0); box-sizing: border-box;">"BUNDLE_VERSION",0); box-sizing: border-box;">'"1.0.0"'
        }
    }

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

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

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

      
      
  • 1
  • 2
  • 3
  • 4
    • 1
    • 2
    • 3
    • 4
    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包的基本热更新,但是这只是热更新的一部分,还有很多方面可以优化,比如:多模块的多bundle热更新、bundle拆分差量更新、热更新的异常回退处理、多版本bundle的动态切换、bundle的更新和apk的更新相结合等等,这也是之后继续研究学习的方向。

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

    (编辑:李大同)

    【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

      推荐文章
        热点阅读