Kahn's blogs

Android原生app加载RN界面过程

2017/09/19

前言

现有原生Android应用集成RN(React Native Android)开发环境
RN环境 Android原生和JS交互Android RN

两篇博客中介绍了RN集成原生app和原生和js互动的基本原理。其中第一篇现有原生Android应用集成RN(React Native Android)开发环境里面的Activity的例子是从官网上直接copy过来的,能运行起来但是一知半解。这篇blog记录一下RN界面是怎么加载到我们原生Activity上的。

正文

在这个例子中现有原生Android应用集成RN(React Native Android)开发环境,范例Activity使用的是自己原生的纯纯的Activity来加载RN界面。其实RN官方给我们提供了一个com.facebook.react.ReactActivity来让我们继承使用,它继承自24k纯Activity,ReactActivity里面包含了怎么加载RN界面的方法。分析一下这个类,就能知道原生Activity怎么加载RN界面的了。

老规矩,成员要先上代码

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react;

import javax.annotation.Nullable;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;

import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;

/**
* Base Activity for React Native applications.
*/
public abstract class ReactActivity extends Activity
implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {

private final ReactActivityDelegate mDelegate;

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());
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDelegate.onCreate(savedInstanceState);
}

@Override
protected void onPause() {
super.onPause();
mDelegate.onPause();
}

@Override
protected void onResume() {
super.onResume();
mDelegate.onResume();
}

@Override
protected void onDestroy() {
super.onDestroy();
mDelegate.onDestroy();
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mDelegate.onActivityResult(requestCode, resultCode, data);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}

@Override
public void onBackPressed() {
if (!mDelegate.onBackPressed()) {
super.onBackPressed();
}
}

@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}

@Override
public void onNewIntent(Intent intent) {
if (!mDelegate.onNewIntent(intent)) {
super.onNewIntent(intent);
}
}

@Override
public void requestPermissions(
String[] permissions,
int requestCode,
PermissionListener listener) {
mDelegate.requestPermissions(permissions, requestCode, listener);
}

@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults) {
mDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

protected final ReactNativeHost getReactNativeHost() {
return mDelegate.getReactNativeHost();
}

protected final ReactInstanceManager getReactInstanceManager() {
return mDelegate.getReactInstanceManager();
}

protected final void loadApp(String appKey) {
mDelegate.loadApp(appKey);
}
}

非常干净的一个类,它做了什么?

  1. 构造函数里生成一个com.facebook.react.ReactActivityDelegate对象,并把自己(this)和MainComponentName传给这个委托对象
  2. 在Activity的生命周期回调里,都调用了委托对象的对应方法。换句话说就是,把Activity的生命周期都委托的这个对象处理。
  3. 对外公开了几个mDelegate的方法来获取ReactInstanceManager,ReactNativeHost。提供了一个loadApp方法,同样,也是调用了mDelegate的loadApp方法
  4. 实现了两个接口DefaultHardwareBackBtnHandler,PermissionAwareActivity。
    • DefaultHardwareBackBtnHandler 用提供默认的回退键处理方式
    • PermissionAwareActivity 提供了回调权限相关的东西,android从api23(6.0),权限就改了及用机申请的模式

典型的委托模式,所有东西都交给了ReactActivityDelegate来处理。不但简化的Activity的逻辑,而且可以把这个委托对象复用给Fragment。几乎所有逻辑都在ReactActivityDelegate里面了,下面来看下ReactActivityDelegate。

1
2
3
4
5
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
mFragmentActivity = null;
}

ReactActivityDelegate持有Activity的引用和一个字符串类型的mainComponentName
先看oncreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
}

前面的代码都是一些调试用的界面加载,关键代码是loadApp(mMainComponentName);

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

先是创建了一个ReactRootView对象,然后调用ReactRootView对象的startReactApplication方法。再然后,使用持有的activity直接setContentView,搞定!这样就把RN界面加载到activity上了。核心点还是这个ReactRootView,它其实是一个FrameLayout,所以能直接使用setContentView设置到布局上,其实在xml写上它。findbyid出来,然后调用startReactApplication初始化一下,其实是一样的。所以!关键还在startReactApplication上。
看下它的三个参数

1
2
3
4
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle launchOptions)

ReactInstanceManager对象由自己的建造者模式生成,可以设置一些属性。
moduleName就是moduleName,不知道怎么用言语表达。
看下这个方法都干了啥

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
  /**
* Schedule rendering of the react component rendered by the JS application from the given JS
* module (@{param moduleName}) using provided {@param reactInstanceManager} to attach to the
* JS context of that manager. Extra parameter {@param launchOptions} can be used to pass initial
* properties for the react component.
*/
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");

if(mReactInstanceManager != null && mReactInstanceManager.hasStartedCreatingInitialContext()){
mReactInstanceManager.recreateReactContextInBackground();
return;
}
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();
}
}

这个方法先用UiThreadUtil.assertOnUiThread()断言的形式判断了一下是不是在UI线程,如果不是就抛异常中断执行,在RN整个代码中,有大量的断言用法,看到直接忽略就可以了,就是一些数据校验和判断。初始化过程中也都保持在UI线程中执行。方法中先判断持有的mReactInstanceManager为不为空,如果不为空,调用recreateReactContextInBackground,为空的话,再去看看是否已经初始化过了,没有的话调用createReactContextInBackground要去初始化。方法的最后先看了下这个view有没有经过Measured,注释说的很清楚,只有经过Measured的view才能调用attachToReactInstanceManager把自己关联到reactInstanceManager上。ReactRootView的onMeasure方法中其实也有调用attachToReactInstanceManager的代码。
上面说了,如果初始化过了会调用recreateReactContextInBackground,没有初始化调用createReactContextInBackground来初始化。其实这两个方法最终都会调到recreateReactContextInBackgroundInner,只不过里面的断言条件不一样。这两个方法都是manager的,自此就进入了mReactInstanceManager的代码里面了,recreateReactContextInBackgroundInner里面前半部分都是调试模式下的代码,最后又调到了recreateReactContextInBackgroundFromBundleLoader。
recreateReactContextInBackgroundFromBundleLoader里面只有一句代码,调用了recreateReactContextInBackground方法,它有两个参数。

1
2
3
private void recreateReactContextInBackground(
JavaScriptExecutor.Factory jsExecutorFactory,
JSBundleLoader jsBundleLoader)

第一个参数是为了互传数据,互相调用用的,里面是native代码(该native指的是c/c++库),为了加载js生成的Bundle文件用的。首先我们要注意一点,RN是用JS开发的,但是JS相当于RN的源代码。我们原生应用加载的是Bundle文件,Bundle文件相当于RN的源代码(JS代码)编译后的可执行加载的代码。还要注意版本差异,不同版本编译出来的Bundle文件,在android这边使用不同版本的RN库加载Bundle文件会有差异。最好编译版本和加载版本一致。下面说下recreateReactContextInBackground的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}

看代码它对参数进行了一层封装,就是ReactContextInitParams对象。然后调用过了ReactContextInitAsyncTask的执行方法。ReactContextInitAsyncTask是一个AsyncTask。至此,UI线程中的任务就结束了,后面交给了子线程。去看下ReactContextInitAsyncTask中是怎么执行的。ReactContextInitAsyncTask中的onPreExecute是一些清理工作,直接看doInBackground。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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();
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);
}
}

小知识:子线程运行的第一句代码设置为Process.setThreadPriority(Process.xxx);可以调整线程运行级别,xxx为内置级别。请尽量把线程设置的优先级降低,避免抢占UI线程占用CPU资源而造成卡顿。
看下doInBackground都做了什么。从参数0获取之前创建的JsExecutorFactory,然后create一个JavaScriptExecutor,调用createReactContext(jsExecutor, params[0].getJsBundleLoader())返回。这里使用了Result.of这样的返回方式。Result.of是一个封装类,封装了结果和异常信息,主要处理逻辑还在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
92
93
94
95
96
97
  /**
* @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);
//生成一个ReactApplicationContext,这个ReactApplicationContext继承自ReactContext,而ReactContext又继承自ContextWrapper
final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
//创建一个原生module生成器,下面会用这个生成器把我们需要注册的原生module类都加载起来
NativeModuleRegistryBuilder nativeModuleRegistryBuilder = new NativeModuleRegistryBuilder(
reactContext,
mLazyNativeModulesEnabled);
//创建一个JSmodule生成器,下面会用这个生成器把我们需要注册的JSmodule都加载起来,这个加载器是跨语言加载,加载的js
JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();
if (mUseDeveloperSupport) {
reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
}

//下面是一些日志追踪七七八八的
ReactMarker.logMarker(PROCESS_PACKAGES_START);
Systrace.beginSection(
TRACE_TAG_REACT_JAVA_BRIDGE,
"createAndProcessCoreModulesPackage");
//RN核心package加载。RN自己也要注册一些module
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
//注册我们自己的module(包括原生和js)
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的建造者把准备好的东西都set上。
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 {
//上面使用建造者模式数据都准备好了,然后build出对象。CatalystInstanceImpl中有大量的jni调用,用户建立起原生和js交互的桥梁
catalystInstance = catalystInstanceBuilder.build();
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END);
}

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

//初始化reactContext对象,把catalystInstance这玩意穿进去,然后在initializeWithInstance初始化好几个线程队列。
reactContext.initializeWithInstance(catalystInstance);
//开撸!
catalystInstance.runJSBundle();

return reactContext;
}

核心方法调用了catalystInstance的runJSBundle方法。它内部又调用了之前准备好的mJSBundleLoader的loadScript方法。回到最初的起点,我们创建ReactInstanceManager对象的时候,会然让你设置一个setBundleAssetName或者setJSBundleFile,它在build出ReactInstanceManager对象的时候,会通过JSBundleLoader的静态方法createFileLoader或者createAssetLoader创建出一个JSBundleLoader,参数就是我们的bundle文件的路径(这里路径描述不准确,应该是asset文件夹下的文件名或者文件的全路径)。上面就是用这个JSBunldeLoader对象调用的runJSBundle。来看下runJSBundle的实现。

1
2
3
private native void jniSetSourceURL(String sourceURL);
private native void jniLoadScriptFromAssets(AssetManager assetManager, String assetURL);
private native void jniLoadScriptFromFile(String fileName, String sourceURL);

当当当!jni的,跟不下去了。结束~打完收工。

对了,还有最后的,RN的View是怎么挂载到ReactRootView上的。Debug了一下,FaceBook使用了Choreographer机制来传递addview的消息。从Choreographer.doFrame回调到了ChoreographerCompat的内部类FrameCallback中。具体FrameCallback是什么时候注册到Choreographer上去的,还没去看。FrameCallback的实现类为ReactChoreographerDispatcher,这个大哥里面有个callback队列,收到一个doFrame消息,就会执行一遍队列的全部回调,回调的实现在UIViewOperationQueue->DispatchUIFrameCallback中。DispatchUIFrameCallback中会调用flushPendingBatches方法来顺序执行UIViewOperationQueue持有的mDispatchUIRunnables队列的所有runnale,每一个runnable都是一个UIViewOperationQueue对象,一个UIViewOperationQueue对象代表一个事件,持有一些视图啊之类的东西,最后调用mNativeViewHierarchyManager.manageChildren把持有的视图添加到了之前我们准备好的ReactRootView上。呼呼~~具体UIViewOperationQueue对象何时生成,又何时添加到队列中的,这个之前初始化的时候就已经和c/c++代码绑定上了,应该是c/c++那边添进来的。具体时机不详。

总结

  • Activity需要加载的RN的View是ReactRootView,所以,把ReactRootView当成普通的View玩弄即可
  • ReactRootView需要调用startReactApplication激活一下。
  • 基本上所有关于RN配置都在ReactInstanceManager里面,使用ReactInstanceManager的build建造者模式可以构造想要的ReactInstanceManager,一个ReactInstanceManager对应一个bundler文件。在build的时候设置。一个ReactRootView对应一个ReactInstanceManager
  • 知道这些基本就可以开撸了,剩下无非就是下载bundler文件和一些封装调用
  • Facebook的编码风格看起来还是很统一的,大量的build,对象包裹,委托和断言充斥的整个库,最最重要的是他们的过程比较清晰,如果滥用设计模式的话,很有可能比较简单的代码弄得无比繁琐和绕圈子。不能为设计而设计,为了分层而分层。TODO用的也很合理,但是不知道后面的版本到底DO不DO

最后想说如何看待百度要求内部全面停止使用 React / React Native?跟我们有什么关系呢?真上升要去告FaceBook的地步,还是老老实实的用别的技术或者原生吧。小厂还是尽情的用吧。而且,这是前段的事情,俺们一个不沾惹前段的人还是不叽歪了。