1.前言
我们知道RN打包后的最终APP,访问的是一个bundle文件,这个bundle文件是所有RN代码打包成的,所以要实现热更新就只需要去更新这个bundle,那么rn是如何bundle的机制是什么样的,我们改在哪替换bundle呢,我们从源码一步步扒。
2.RN加载bundle过程
为了排除其它因素,我们就用最简单的代码来研究
react-native init AwesomeProject
生成的Android项目只有MainActivity和MainApplication两个文件。
打开MainActivity,只有一个重写方法getMainComponentName,返回主组件名称,它继承于ReactActivity。
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "rnandnative";
}
}
接着看ReactActivity,发现好像只是一层壳,实现都在ReactActivityDelegate中。所以对应着ReactActivity的方法看ReactActivityDelegate里是怎么实现的。先是onCreate
protected void onCreate(Bundle savedInstanceState) {
boolean needsOverlayPermission = false;
//判断是否支持dev模式,也就是RN常见的那个红色弹窗
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();
}
这里的getReactNativeHost().getUseDeveloperSupport()返回的其实就是我们在MainApplication中写的:
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
然后看到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);
}
嗯,这里有收获,看到createRootView生成了一个ReactRootView对象
protected ReactRootView createRootView() {
return new ReactRootView(getContext());
}
然后调用它的startReactApplication方法,最后setContentView将它设置为内容视图。所以接着看看startReactApplication干了什么
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,网上看0.4之前的版本createReactContextInBackground是个抽象类,然后绕了一圈在XReactInstanceManagerImpl实现,我目前是0.44,看来优化的代码,直接可以调用了
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();
}
接着看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 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();
}
这里判断是不是dev开发模式,不是话直接调用recreateReactContextInBackgroundFromBundleLoader(),是的话再判断用浏览器代理远程拉bundle,还是拉本地启的package。看看recreateReactContextInBackgroundFromBundleLoader
private void recreateReactContextInBackgroundFromBundleLoader() {
recreateReactContextInBackground(
new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()),
mBundleLoader);
}
又调了onJSBundleLoadedFromServer
private void recreateReactContextInBackground(
JavaScriptExecutor.Factory jsExecutorFactory,
JSBundleLoader jsBundleLoader) {
UiThreadUtil.assertOnUiThread();
final ReactContextInitParams initParams = new ReactContextInitParams(
jsExecutorFactory,
jsBundleLoader);
if (mUseStartupThread) {
if (mCreateReactContextThread == null) {
runCreateReactContextOnNewThread(initParams);
} else {
mPendingReactContextInitParams = initParams;
}
} else {
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;
}
}
}
这里的两个参数:一个是执行js代码的线程池、另外一个是我们bundle文件的加载器(bundle文件可以是我们的本地服务器中的文件(debug模式),也可以是我们assert目录中的bundle文件(发布版)),然后传入ReactContextInitAsyncTask.executeOnExecutor中,看字面就猜的出是一个异步任务,看看ReactContextInitAsyncTask
/*
* Task class responsible for (re)creating react context in the background. These tasks can only
* be executing one at time, see {@link #recreateReactContextInBackground()}.
*/
private final class ReactContextInitAsyncTask extends
AsyncTask<ReactContextInitParams, Void, Result<ReactApplicationContext>> {
@Override
protected void onPreExecute() {
if (mCurrentReactContext != null) {
tearDownReactContext(mCurrentReactContext);
mCurrentReactContext = null;
}
}
@Override
protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {
// TODO(t11687218): Look over all threading
// Default priority is Process.THREAD_PRIORITY_BACKGROUND which means we'll be put in a cgroup
// that only has access to a small fraction of CPU time. The priority will be reset after
// this task finishes: https://android.googlesource.com/platform/frameworks/base/+/d630f105e8bc0021541aacb4dc6498a49048ecea/core/java/android/os/AsyncTask.java#256
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
try {
JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
ReactApplicationContext reactApplicationContext =
createReactContext(jsExecutor, params[0].getJsBundleLoader());
ReactMarker.logMarker(PRE_SETUP_REACT_CONTEXT_START);
return Result.of(reactApplicationContext);
} catch (Exception e) {
// Pass exception to onPostExecute() so it can be handled on the main thread
return Result.of(e);
}
}
@Override
protected void onPostExecute(Result<ReactApplicationContext> result) {
try {
setupReactContext(result.get());
} catch (Exception e) {
mDevSupportManager.handleException(e);
} finally {
mReactContextInitAsyncTask = null;
}
// Handle enqueued request to re-initialize react context.
if (mPendingReactContextInitParams != null) {
recreateReactContextInBackground(
mPendingReactContextInitParams.getJsExecutorFactory(),
mPendingReactContextInitParams.getJsBundleLoader());
mPendingReactContextInitParams = null;
}
}
@Override
protected void onCancelled(Result<ReactApplicationContext> reactApplicationContextResult) {
try {
mMemoryPressureRouter.destroy(reactApplicationContextResult.get());
} catch (Exception e) {
FLog.w(ReactConstants.TAG, "Caught exception after cancelling react context init", e);
} finally {
mReactContextInitAsyncTask = null;
}
}
}
主要看看这个doInBackground,里面调用了createReactContext
/**
* @return instance of {@link ReactContext} configured a {@link CatalystInstance} set
*/
private ReactApplicationContext createReactContext(
JavaScriptExecutor jsExecutor,
JSBundleLoader jsBundleLoader) {
FLog.i(ReactConstants.TAG, "Creating react context.");
ReactMarker.logMarker(CREATE_REACT_CONTEXT_START);
final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
NativeModuleRegistryBuilder nativeModuleRegistryBuilder = new NativeModuleRegistryBuilder(
reactContext,
this,
mLazyNativeModulesEnabled);
JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();
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,
mLazyViewManagersEnabled);
processPackage(coreModulesPackage, nativeModuleRegistryBuilder, 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, nativeModuleRegistryBuilder, 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 = nativeModuleRegistryBuilder.build();
} 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);
}
if (Systrace.isTracing(TRACE_TAG_REACT_APPS | TRACE_TAG_REACT_JSC_CALLS)) {
catalystInstance.setGlobalVariable("__RCTProfileIsProfiling", "true");
}
reactContext.initializeWithInstance(catalystInstance);
catalystInstance.runJSBundle();
return reactContext;
}
代码有点多,首先它执行设置了RN自带的和开发者自定义的模块组件(Package\Module),然后同样使用了构造器CatalystInstanceImpl.Builder生成了catalystInstance对象,最后调用了catalystInstance.runJSBundle()。跟进去是一个接口类CatalystInstance,那么我们又要去看它的实现类CatalystInstanceImpl
@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 safe 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) {
jniCallJSFunction(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来创建的实例
//JSBundleLoader.java
/**
* 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 String loadScript(CatalystInstanceImpl instance) {
instance.loadScriptFromAssets(context.getAssets(), assetUrl);
return assetUrl;
}
};
}
这里调了loadScriptFromAssets
/* package */ void loadScriptFromAssets(AssetManager assetManager, String assetURL) {
mSourceURL = assetURL;
jniLoadScriptFromAssets(assetManager, assetURL);
}
jniLoadScriptFromAssets是一个native方法,也就是最后的实现RN把它放在了jni层来完成最后加载bundle的过程。并且CatalystInstanceImpl不止loadScriptFromAssets一个native方法,它还提供了loadScriptFromFile。一个是从android assets目录下加载bundle,一个是从android SD卡文件夹目录下加载bundle。这样总算追查到底了。
至此,bundle的加载流程我们已经走一遍了,下面用一张流程图来总结下
3.结论
从上面的跟踪,最终知道有两种方式加载bundle
- loadScriptFromAssets
这个是RN的默认方式,访问assets文件夹下的bundle - loadScriptFromFile
第二种方式是从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了。