Why discuss this issue
Generally speaking, building applications with RN s is sufficient. There are two scenarios that need to be considered:
- Loading time is slower when the JS file size is large (although asynchronous), so it is necessary to break down the size of the JS file
- When integrating RNs using native Android projects, loading RN interfaces may require defining multiple RN modules
One diagram illustrates a summary of the entire process
Possibility
- Understand that RN defines JS modules, registers JS modules, and mechanisms for using JS modules
Define JS modules
Define, create source code, and implement basic RN logic.
The well-defined JS source code is a Babel-compiled JS file that uses a static require method to load different sub-modules.For any:
export default class Example ... import Example from './Example'...
Use the react-native bundle command:
react-native bundle --platform android --dev false --entry-file index.androidtest.js --bundle-output ./test.bundle.js --assets-dest android/app/src/main/res/
Conceptually, something like this has been generated:
__d(function (e, t, r, l) { ... t(12), // react t(24), // react-native t(271), // Example }, 0); ... ;require(0); // The first component/module, usually the one registered with AppRegistry
Between modules, IDs are used to indicate that loading modules is done by require(ID), and relying on parameter t to confirm, so when one JS module loads another module, the existence of the previous module is guaranteed.
Register JS modules
The purpose of registration is to tell RN that another module can be used independently by Android, and to provide a starting point for calling this JS module.
This is accomplished through AppRegistry, and multiple modules can be registered using AppRegistry's registerComponent. From the analysis below, it can be seen that each ReactActivity has a default ReactRootView, each Activity can use its own modules independently, and the UI level of module building will be presented in ReactRootView.
// RN / Libraries / ReactNative / AppRegistry.js registerComponent: function(appKey: string, getComponentFunc: ComponentProvider): string { runnables[appKey] = { run: (appParameters) => renderApplication(getComponentFunc(), appParameters.initialProps, appParameters.rootTag) }; return appKey; },
The real runApplication, located in AppRegistry, does the following:
runApplication is the starting point for running this module.
// RN / Libraries / ReactNative / AppRegistry.js runApplication: function(appKey: string, appParameters: any): void { const msg = 'Running application "' + appKey + '" with appParams: ' + JSON.stringify(appParameters) + '. ' + '__DEV__ === ' + String(__DEV__) + ', development-level warning are ' + (__DEV__ ? 'ON' : 'OFF') + ', performance optimizations are ' + (__DEV__ ? 'OFF' : 'ON'); infoLog(msg); BugReporting.addSource('AppRegistry.runApplication' + runCount++, () => msg); invariant( runnables[appKey] && runnables[appKey].run, 'Application ' + appKey + ' has not been registered. This ' + 'is either due to a require() error during initialization ' + 'or failure to call AppRegistry.registerComponent.' ); runnables[appKey].run(appParameters); },
The code below is actually called to render the first UI component generated
// RN / Libraries / ReactNative / renderApplication.js ReactNative.render( <AppContainer rootTag={rootTag}> <RootComponent {...initialProps} rootTag={rootTag} /> </AppContainer>, rootTag );
Here, appKey, in fact, is the name of the module used when registering.
Run JS module
Let's first look at how to run a JS module, and then look at how to load a JS module in detail. Loading a JS module is a relatively complex part. It also involves whether you can decompose a JS Bundle file, load a small JS Bundle file, and load different JS Bundle files as needed.
The following is the section where you start running the JS module after the Activity is created.
In the Android section, RN-based MainActivity is derived from ReactActivity.
When an Activity is created, ReactActivity creates a ReactActivityDelegate that acts as an agent for many tasks.
protected ReactActivity() { mDelegate = createReactActivityDelegate(); } /** * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. * e.g. "MoviesApp" */ protected @Nullable String getMainComponentName() { return null; } /** * Called at construction time, override if you have a custom delegate implementation. */ protected ReactActivityDelegate createReactActivityDelegate() { return new ReactActivityDelegate(this, getMainComponentName()); }
The default MainActivity, however, provides an overridden getMainComponentName method that provides the name of the current module, which is the name of any module registered with AppRegistry.
When the ReactActivityDelegate was created, the passed module was used throughout the process.
At the same time, the above proxy is used in the onCreate and onActivityResult methods within ReactActivity.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDelegate.onCreate(savedInstanceState); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { mDelegate.onActivityResult(requestCode, resultCode, data); }
These two methods, which are provided by default by Activity for Android, are called automatically by the system, created after, or returned from an activity.
You can then see that loadApp is called in both the onCreate method that creates the ReactActivityDelegate and the onActivityResult method, as follows.
protected void onCreate(Bundle savedInstanceState) { boolean needsOverlayPermission = false; if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Get permission to show redbox in dev builds. if (!Settings.canDrawOverlays(getContext())) { needsOverlayPermission = true; Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName())); FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE); Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show(); ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE); } } if (mMainComponentName != null && !needsOverlayPermission) { loadApp(mMainComponentName); } mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); } public void onActivityResult(int requestCode, int resultCode, Intent data) { if (getReactNativeHost().hasInstance()) { getReactNativeHost().getReactInstanceManager() .onActivityResult(getPlainActivity(), requestCode, resultCode, data); } else { // Did we request overlay permissions? if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Settings.canDrawOverlays(getContext())) { if (mMainComponentName != null) { loadApp(mMainComponentName); } Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show(); } } } }
Inside ReactActivityDelegate, a ReactRootView is created as the default view for ReactActivity.
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); }
Here, you can see the ReactRootView, which calls the startReactApplication to see how it works:
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; mLaunchOptions = launchOptions; if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { 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) { attachToReactInstanceManager(); } }
The code above is complex, so let's take the last step, and when the UI loads and you get a definite Layout, start.
private void attachToReactInstanceManager() { if (mIsAttachedToInstance) { return; } mIsAttachedToInstance = true; Assertions.assertNotNull(mReactInstanceManager).attachMeasuredRootView(this); getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener()); }
Notice here, (mReactInstanceManager).attachMeasuredRootView(this) calls inside XReactInstanceManagerImpl:
/** * Attach given {@param rootView} to a catalyst instance manager and start JS application using * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently * being (re)-created, or if react context has not been created yet, the JS application associated * with the provided root view will be started asynchronously, i.e this method won't block. * This view will then be tracked by this manager and in case of catalyst instance restart it will * be re-attached. */ @Override public void attachMeasuredRootView(ReactRootView rootView) { UiThreadUtil.assertOnUiThread(); mAttachedRootViews.add(rootView); // If react context is being created in the background, JS application will be started // automatically when creation completes, as root view is part of the attached root view list. if (mReactContextInitAsyncTask == null && mCurrentReactContext != null) { attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance()); } }
Looking at the official comment, catalyst instance manager is the gateway to Android-JSC.The so-called react context is the representation of RN's environment in Java, including the initialization of various native modules, packages, etc. If everything is ready, it will go to the following methods:
private void attachMeasuredRootViewToInstance( ReactRootView rootView, CatalystInstance catalystInstance) { Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "attachMeasuredRootViewToInstance"); UiThreadUtil.assertOnUiThread(); // Reset view content as it's going to be populated by the application content from JS rootView.removeAllViews(); rootView.setId(View.NO_ID); UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class); int rootTag = uiManagerModule.addMeasuredRootView(rootView); rootView.setRootViewTag(rootTag); @Nullable Bundle launchOptions = rootView.getLaunchOptions(); WritableMap initialProps = Arguments.makeNativeMap(launchOptions); String jsAppModuleName = rootView.getJSModuleName(); WritableNativeMap appParams = new WritableNativeMap(); appParams.putDouble("rootTag", rootTag); appParams.putMap("initialProps", initialProps); catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); rootView.onAttachedToReactInstance(); Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE); }
As you can see here, the rootView uninstalls all the UI s that have been loaded and then starts running. This is the key step.
catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);
In this step, the official JS module registered with Android-JSC directly calls the JS method, the runApplication method on AppRegistry, to start the JS Bundle and run it.
So far, the call to the JS module has been completed.
- To make a long story short
After the JS module is loaded, the Activity will start running the runApplication method of AppRegistry in the JS module at the time of creation, and use the module name defined in this Activity to identify a module. You can register multiple modules with different names and use them in different activities, but by default all the different modules are in the same module JS Bundle files, loaded in MainApplication, or ReactApplication, by default.
Therefore, for native Android applications, it is entirely possible to derive ReactApplication to use various RN modules, each hosted by a unique ReactActivity.
Each Activity has a unique ReactRootView by default. When running a JS module, it waits for the initialization of the View to complete (an asynchronous process) and the location and size are determined so that the UI components in the RN can be loaded in the correct location.
Before running the JS module, ensure that the JS module is loaded, and that the native environment of the RN is completed, including various official modules, as well as the registration of custom modules, so there will be no problems with the subsequent use of JS.
Another interesting thing is that in ReactActivity, the loadApp method is defined for internal components.
protected final void loadApp(String appKey) { mDelegate.loadApp(appKey); }
Not used, this method can be called for any native module, but there is an interesting usage pattern in ReactTestActivity.
public void loadApp( String appKey, ReactInstanceSpecForTest spec, @Nullable Bundle initialProps, String bundleName, boolean useDevSupport, UIImplementationProvider uiImplementationProvider) { final CountDownLatch currentLayoutEvent = mLayoutEvent = new CountDownLatch(1); mBridgeIdleSignaler = new ReactBridgeIdleSignaler(); ReactInstanceManager.Builder builder = ReactTestHelper.getReactTestFactory().getReactInstanceManagerBuilder() .setApplication(getApplication()) .setBundleAssetName(bundleName) // By not setting a JS module name, we force the bundle to be always loaded from // assets, not the devserver, even if dev mode is enabled (such as when testing redboxes). // This makes sense because we never run the devserver in tests. //.setJSMainModuleName() .addPackage(spec.getAlternativeReactPackageForTest() != null ? spec.getAlternativeReactPackageForTest() : new MainReactPackage()) .addPackage(new InstanceSpecForTestPackage(spec)) .setUseDeveloperSupport(useDevSupport) .setBridgeIdleDebugListener(mBridgeIdleSignaler) .setInitialLifecycleState(mLifecycleState) .setUIImplementationProvider(uiImplementationProvider); mReactInstanceManager = builder.build(); mReactInstanceManager.onHostResume(this, this); Assertions.assertNotNull(mReactRootView).getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { currentLayoutEvent.countDown(); } }); Assertions.assertNotNull(mReactRootView) .startReactApplication(mReactInstanceManager, appKey, initialProps); }
Why is it interesting because ReactInstanceManager involves loading JS modules, and the methods here actually provide a way to load and run JS modules within an Activity.
The default Android section is to load the only JS module through MainApplication (although you can define where the JS is located). If the above method works, you can load different JS modules in each Activity. Here's how the JS is loaded.
How to load a JS module
Loading a JS module is the basis for running a JS Bundle. By default, loading a JS module is done in the current MainApplication.
When the Android section starts, a MainApplication is created, which implements the ReactApplication interface.
The only way this interface needs to provide is:
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new RNChineseToPinyinPackage(), new LinearGradientPackage(), new RCTCameraPackage(), new RNFSPackage(), new RealmReactPackage(), new VectorIconsPackage() ); } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); SoLoader.init(this, /* native exopackage */ false); }
ReactNativeHost is an abstract class, so you can override the original method in your implementation to see what the main methods are.
Used to get ReactInstanceManager, ReactInstanceManger is the main entry to load JS Bundle
public ReactInstanceManager getReactInstanceManager() { if (mReactInstanceManager == null) { mReactInstanceManager = createReactInstanceManager(); } return mReactInstanceManager; } public boolean hasInstance() { return mReactInstanceManager != null; } public void clear() { if (mReactInstanceManager != null) { mReactInstanceManager.destroy(); mReactInstanceManager = null; } } 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(); }
You can see that during the creation of ReactInstanceManager, you can set the JS Bundle as the file location or the module name (the default location is in assets).
The method is to override more than two methods of ReactNativeHost, getJSMainModuleName() or getJSBundleFile.
Then, let's see what ReactInstanceManager.Builder is doing when it calls Builder.build.
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); } }
You can see that the implementation of the real method is in the XReactInstanceManagerImpl, and this step is the key to loading.
(mJSBundleLoader == null && mJSBundleAssetUrl != null) ? JSBundleLoader.createAssetLoader(mApplication, mJSBundleAssetUrl) : mJSBundleLoader,
The parameter passed here, mJSBundleAssetUrl, is where the previously defined JS Bundle is located, and the default is assets://index.android.bundle, whose location can be overridden.
The default creates a JSBundleLoader of type Asset, so what exactly is this Loader?
/** * This loader is recommended one for release version of your app. In that case local JS executor * should be used. JS bundle will be read from assets in native code to save on passing large * strings from java to native memory. */ 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; } }; }
It provides two methods, the most important of which is loadScript. As mentioned earlier, CatalystInstanceImpl is an implementation that provides the Android-JSC interface, and loadScriptFromAssets is its internal implementation.What you need to know is that it takes two parameters, mApplication and mJSBundleAssetUrl, so you can get the location of the current assets and the URL path of the JS Bundle you are already loading, such as assets://index.android.bundle.
Clearly, in subsequent operations, you will use loadScript to load the JS Bundle.
Go back to the XReactInstanceManagerImpl section above, where the public interface using JSBundleLoader is,
/** * Trigger react context initialization asynchronously in a background async task. This enables * applications to pre-load the application JS, and execute global code before * {@link ReactRootView} is available and measured. This should only be called the first time the * application is set up, which is enforced to keep developers from accidentally creating their * application multiple times without realizing it. * * Called from UI thread. */ @Override public void createReactContextInBackground() { Assertions.assertCondition( !mHasStartedCreatingInitialContext, "createReactContextInBackground should only be called when creating the react " + "application for the first time. When reloading JS, e.g. from a new file, explicitly" + "use recreateReactContextInBackground"); mHasStartedCreatingInitialContext = true; recreateReactContextInBackgroundInner(); } /** * Recreate the react application and context. This should be called if configuration has * changed or the developer has requested the app to be reloaded. It should only be called after * an initial call to createReactContextInBackground. * * Called from UI thread. */ public void recreateReactContextInBackground() { Assertions.assertCondition( mHasStartedCreatingInitialContext, "recreateReactContextInBackground should only be called after the initial " + "createReactContextInBackground call."); recreateReactContextInBackgroundInner(); } private void recreateReactContextInBackgroundInner() { UiThreadUtil.assertOnUiThread(); 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(); } private void recreateReactContextInBackgroundFromBundleLoader() { recreateReactContextInBackground( new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()), mBundleLoader); }
From the official comment, the first case is to initialize the RN environment in the Application, the second case is to reinitialize the RN environment after initialization, to use it in the reload, which is the case with Live Reload, which is often used when debugging RN applications.
The two open interfaces above, which are executed internally, are recreateReactContextInBackground, and here,
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; } }
Leave aside the details here to see how these two public API s are actually used in startApplication in ReactRootView and before running JS Bundle,
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { 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) { attachToReactInstanceManager(); }
According to the official statement, the first API can only be used once in an Application. If you want to load dynamically, you need to call the second API. The method of dynamically loading different JS Bundles should depend on the use of the second API. However, the so-called dynamic loading is still loading a whole JS Bundle, not a part.
(TO BE CONTINUE)