React Native 代码阅读(二):JS Bundle 的加载原理(Android)

2019-01-31

介绍

JS Bundle 在 React Native 中是如何加载运行的呢?在本文中,我们通过 JSBundleLoader 类作为切入点,对这个过程一探究竟。

JSBundleLoader 类的作用是:它存储 JS 包信息,并允许通过 ReactBridge 加载包。

这个类本身很简单,是一个抽象类:

public abstract class JSBundleLoader {

    public abstract String loadScript(CatalystInstanceImpl instance);

}

其中:

  • 它的内部很简单,只有一个方法 loadScript
  • 接收参数 CatalystInstanceImpl,CatalystInstanceImpl 提供了各种加载能力供 JSBundleLoader 调用

Loader 的种类

在 JSBundleLoader 中通过静态方法的形式对外暴露了几种 Loader 的创建方法,有如下几类:

  • AssetLoader:
    • JS Bundle 放在 APP 的 assets 目录下
    • JS Bundle 会使用 Native 代码(C/C++)进行加载,之所以不在 Java 加载,因为 JS Bundle 是很大的字符串,这样可以在 Java-Native 之相互传递时的内存开销
  • FileLoader
    • JS Bundle 放在文件系统当中
  • NetworkLoader
    • 向服务器请求 Bundle
    • 有两种请求方式:请求全量包、请求差分包

AssetLoader

介绍

在本节中,我们主要来看 AssetLoader。

在上一节,ReactInstanceManagerBuilder 中的 build 一步,会创建 AssetLoader:

public ReactInstanceManager build() {
    ...
    return new ReactInstanceManager(
        mApplication,
        mCurrentActivity,
        mDefaultHardwareBackBtnHandler,
        mJavaScriptExecutorFactory == null
            ? new JSCJavaScriptExecutorFactory(appName, deviceName)
            : mJavaScriptExecutorFactory,
        (mJSBundleLoader == null && mJSBundleAssetUrl != null)
            ? JSBundleLoader.createAssetLoader(
                mApplication, mJSBundleAssetUrl, false /*Asynchronous*/)
            : mJSBundleLoader,
        mJSMainModulePath,

其中,createAssetLoader 的签名为:

public static JSBundleLoader createAssetLoader(
    final Context context,
    final String assetUrl,
    final boolean loadSynchronously) {

其中:

  • context:Android Context
  • assetUrl:JS 包在 Assets 目录下的地址
  • loadSynchronously:同步加载还是异步加载,在这里是异步加载

createAssetLoader

我们来看 createAssetLoader 的内部实现:

public static JSBundleLoader createAssetLoader(
    final Context context,
    final String assetUrl,
    final boolean loadSynchronously) {
    return new JSBundleLoader() {
        @Override
        public String loadScript(CatalystInstanceImpl instance) {
            instance.loadScriptFromAssets(context.getAssets(), assetUrl, loadSynchronously);
            return assetUrl;
        }
    };
}

其中:

  • 返回了一个匿名内部类,并实现了 loadScript 方法
  • loadScript 内部,则是调用 CatalystInstanceImpl 提供的 loadScriptFromAssets 能力
  • 也就是说,loader 本身没干事,真正干事的是 CatalystInstanceImpl

CatalystInstanceImpl.loadScriptFromAssets

我们进入 CatalystInstanceImpl.loadScriptFromAssets,实际加载的逻辑都在这里面。这个方法的定义如下:

/* package */ void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously) {
    mSourceURL = assetURL;
    jniLoadScriptFromAssets(assetManager, assetURL, loadSynchronously);
}

由此可以看出,它首先把 URL 保存下来,之后实际加载是调用 JNI 方法,进入 Native 层来实现的。

我们进入到 Native 层方法,位于 ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp。首先看方法签名:

void CatalystInstanceImpl::jniLoadScriptFromAssets(
    jni::alias_ref<JAssetManager::javaobject> assetManager,
    const std::string& assetURL,
    bool loadSynchronously) {

其中:

  • jni::alias_ref<JAssetManager::javaobject> 是 fbjni 提供的一种 Java 类映射方法

首先我们通过下面代码,拿到 AssetManager 在 C++ 中的实例:

auto manager = extractAssetManager(assetManager);

通过下面方法读取 Bundle 的内容:

auto script = loadScriptFromAssets(manager, sourceURL);

其中,loadScriptFromAssets 方法并不复杂,我们略过。

现在我们读取的包还是字符串,下面要对它进行解析:

instance_->loadScriptFromString(std::move(script), sourceURL, loadSynchronously);

其中:

  • instance_ 是 Native 层 CatalystInstanceImpl 类的成员,类型为 Instance(位于 ReactCommon/cxxreact/Instance.h)。

Instance.loadScriptFromString

这个方法的签名如下:

void Instance::loadScriptFromString(std::unique_ptr<const JSBigString> string,
                                    std::string sourceURL,
                                    bool loadSynchronously) {

其中:

  • string 是我们的 Bundle 的字符串
  • loadSynchronously 加载方式这里是同步加载

这会调用:

loadApplicationSync(nullptr, std::move(string), std::move(sourceURL));

我们进入 loadApplicationSync:

nativeToJsBridge_->loadApplicationSync(std::move(bundleRegistry), std::move(string),
                                       std::move(sourceURL));

其中:

  • 第一个参数我们实际传入的是 nullptr
  • nativeToJsBridge_ 是 Instance 类的成员,其类型为 NativeToJsBridge。
  • NativeToJsBridge 类的作用是处理 Native 调 JS。

NativeToJsBridge.loadApplicationSync

方法签名为:

void NativeToJsBridge::loadApplicationSync(
    std::unique_ptr<RAMBundleRegistry> bundleRegistry,
    std::unique_ptr<const JSBigString> startupScript,
    std::string startupScriptSourceURL) {

其中:

  • bundleRegistry 实际传入的是 nullptr
  • startupScript 是我们 Bundle 的字符串

方法内部调用了:

m_executor->loadApplicationScript(std::move(startupScript), std::move(startupScriptSourceURL));

其中:

  • m_executor 是 NativeToJsBridge 类的成员,类型为 JSExecutor
  • JSExecutor 的作用是在 JS 环境中执行

JSExecutor

Native 层的 JSExecutor 位于 ReactCommon/cxxreact/JSExecutor.h,需要注意的是它是一个抽象类。

它的实现类是:JSCExecutor:位于 ReactCommon/cxxreact/JSCExecutor.h

这里设计 JSExecutor 这样一个隔离层的意思是 JS 引擎可隔离,React Native 选用的 JS 引擎是 JavaScriptCore,但是通过隔离层,我们完全可以换用其他的引擎。

JSCExecutor.loadApplicationScript

这里我们来看 JSCExecutor 的 loadApplicationScript 方法,方法签名:

void JSCExecutor::loadApplicationScript(
    std::unique_ptr<const JSBigString> script,
    std::string sourceURL) {

其中两个参数:包的内容以及包的路径。

核心的方法是这一句:

evaluateScript(m_context, jsScript, jsSourceURL);

这个方法的定义位于 ReactCommon/jschelpers/JSCHelpers.cpp:

JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef sourceURL) {
  JSValueRef exn, result;
  result = JSC_JSEvaluateScript(context, script, NULL, sourceURL, 0, &exn);
  if (result == nullptr) {
    throw JSException(context, exn, sourceURL);
  }
  return result;
}

到这里,我们已经进入了 JavaScriptCore 的世界中了。

JSC_JSEvaluateScript_

下面我们进入 JavaScriptCore 的源码。JSC_JSEvaluateScript_ 定义在 JavaScriptCore.h 中,它是一个宏:

#define JSC_JSEvaluateScript(...) __jsc_wrapper(JSEvaluateScript, __VA_ARGS__)

__jsc_wrapper 也是一个宏:

#define __jsc_wrapper(method, ctx, ...) method(ctx, ## __VA_ARGS__)

最终来到了 JSBase.h 中的:

JS_EXPORT JSValueRef JSEvaluateScript(
                JSContextRef ctx,          // JavaScript Runtime Context
                JSStringRef script,        // JS Bundle
                JSObjectRef thisObject,    // 用作 this 的对象,这里是 NULL
                JSStringRef sourceURL,     // 路径,也就是我们的 bundle path
                int startingLineNumber,    // 如果报错了,抛出异常的行号
                JSValueRef* exception);    // 用于抛异常的指针

这个方法的作用就是执行 JS 脚本。

小结

至此,我们就一路从 Java 层的 BundleLoader,到了 Native 层加载 Bundle 代码,再从 React Native 的代码进入 JavaScriptCore 的代码。完整地走了一趟 JS Bundle 从加载到执行的全过程。

外部调用

介绍

前面我们对 JSBundleLoader 内部完成了一探究竟。在本节中,我们把 JSBundleLoader 当做一个黑盒,看看外面是如何使用它的。

还记得 CataLystInstanceBuilder 创建了 AssetLoader 的实例吗?

JSBundleLoader 是作为 CatalystInstance 的成员进行持有的。我们回到 Java 世界中的 CatalystInstanceImpl.java。

JSBundleLoader 在其中作为一个成员:

private final JSBundleLoader mJSBundleLoader;

它在 CatalystInstanceImpl 中是如何使用的呢?

位于 runJSBundle 方法中:

@Override
public void runJSBundle() {
    mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
    ...
}

下面我们由内而外地去梳理。

CatalystInstanceImpl.runJSBundle

这个方法在哪里被调用了呢?

我们找到了在 ReactInstanceManager.createReactContext 中。这与我们系列中的第一篇已经呼应上了。

ReactInstanceManager.createReactContext

这个方法在哪里被调用了呢?

答案是在 ReactInstanceManager.runCreateReactContextOnNewThread 中。

我们再往上跟:

  • ReactInstanceManager.recreateReactContextInBackground
    • ReactInstanceManager.recreateReactContextInBackgroundFromBundleLoader
      • ReactInstanceManager.recreateReactContextInBackgroundInner
        • ReactInstanceManager.createReactContextInBackground
          • ReactRootView.startReactApplication
            • MainActivity: mReactRootView.startReactApplication

这样,我们又反向地把第一篇中的路径反走了一遍,一正一反,对整个过程有了更深的认识了。

结论

在本文中,我们从 JSBundleLoader 入手,将 JS Bundle 的加载、执行流程完整地分析了一遍。