Pride's Blog

ReactNative详解

· Pride

一、为什么要学习React Native§

与其说是学习React Native,不如说是深入这一类框架的本源,React Native有着优秀的设计思想,海量的开发者和应用,经过多年的工程领域验证,在一定程度上已经离不开React Native这种开发方案。

1.跨平台发展§

基于WebView实现的跨平台§

基于JS与WebView实现,最早出现的跨平台框架,代表作为:PhoneGap、PWA小程序。第三方应用程序创建 HTML 并将其显示在平台上的 WebView 中,对于操作系统提供的一些服务和API,通过JS Bridge来调用。然而,一个完整HTML5页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加N个数量级,所以这种方案的瓶颈在于WebView对H5页面的渲染。

1

以React Native为代表的一类跨平台§

这类方案也就衍生出了「泛前端」这个概念,也是我在滴滴听到的最多的一个分类,这类方案有React Native、Weex还有我司的Hummer,放弃了使用Webview渲染的模式,采用定制原生UI组件的方式来替代核心的渲染引擎,性能上要比前者好很多,同时又使用了 JavaScript/TypeScript 作为开发语言,拥有更丰富的生态,对于前端开发人员以及客户端开发人员,入门门槛做了一个折中处理;但是与Native的交互都要通过Bridge来实现,而Bridge又是一个异步方案,自此Bridge也就成为了一个性能上的瓶颈。

2

以Flutter为代表的跨平台§

在Flutter种采用了一种叫「自绘引擎」的方案,和过去的几种都不一样;不使用Webview或是原生组件进行渲染,自己设计了一套UI渲染框架,通过跨平台的Skia图形库来实现渲染,容器层只作为一块画布。减少了差异性,同时通信上使用了「平台信道」的方案,在性能上优于前两种方式。Flutter在编译成release版本后,会生成对应的二进制文件,提升了安全性和渲染性能,但是相对的也丧失了一部分动态性。

3

2.React Native的设计§

    React Native的架构主要包含React、JavaScript、Bridge和Native四个部分,从上到下分为四层结构,分别为JS逻辑层、JS引擎层、通信层、原生层。在最顶部的JS代码层提供了React.js的支持,React.js的JSX代码转化为JS代码运行在JavaScriptCore提供的 JavaScript 运行时环境中,通信层将JavaScript 与 Native 层联系起来,来处理和传递消息事件;通信层又可以分为三部分,其中ShadowTree用来定义UI效果和交互功能,Native Modules来提供Native能力和一些API,两者之间使用JSON异步传递消息。

按照上述设计,React Native在运行时会创建三个线程:

3.设计思想§

4.优势§

下面将从启动流程、渲染原理、通信机制三块来剖析React Native的实现原理。

二、启动流程§

简单来说,启动时ReactNative主要做了两件事情:准备环境和调用JS入口函数。

4

三、渲染原理§

1.加载流程§

    在此之前,我们需要了解JS是如何被加载的,JS加载的方式可以是从本地加载与远程地址加载。在前面的启动流程中,说到过在创建绑定上下文之前会去加载JS。

public class ReactInstanceManager {
  /**
   * 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.
   *
   * <p>Called from UI thread.
   */
  @ThreadConfined(UI)
  public void createReactContextInBackground() {
    FLog.d(TAG, "ReactInstanceManager.createReactContextInBackground()");
    UiThreadUtil
        .assertOnUiThread(); // Assert before setting mHasStartedCreatingInitialContext = true
    if (!mHasStartedCreatingInitialContext) {
      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.
   *
   * <p>Called from UI thread.
   */
  @ThreadConfined(UI)
  public void recreateReactContextInBackground() {
    Assertions.assertCondition(
        mHasStartedCreatingInitialContext,
        "recreateReactContextInBackground should only be called after the initial "
            + "createReactContextInBackground call.");
    recreateReactContextInBackgroundInner();
  }

  @ThreadConfined(UI)
  private void recreateReactContextInBackgroundInner() {
    FLog.d(TAG, "ReactInstanceManager.recreateReactContextInBackgroundInner()");
    PrinterHolder.getPrinter()
        .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: recreateReactContextInBackground");
    UiThreadUtil.assertOnUiThread();

    // 开发模式,实时更新bundle
    if (mUseDeveloperSupport && mJSMainModulePath != null) {
      final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();

      if (!Systrace.isTracing(TRACE_TAG_REACT_APPS | TRACE_TAG_REACT_JS_VM_CALLS)) {
        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() {
                          // 加载JS
                          if (packagerIsRunning) {
                            mDevSupportManager.handleReloadJS();
                          // 判断是否处于开发者模式,从dev server中获JSBundle,如果不是就从本地文件获取
                          } else if (mDevSupportManager.hasUpToDateJSBundleInCache()
                              && !devSettings.isRemoteJSDebugEnabled()
                              && !mUseFallbackBundle) {
                            // If there is a up-to-date bundle downloaded from server,
                            // with remote JS debugging disabled, always use that.
                            onJSBundleLoadedFromServer();
                          } else {
                            // If dev server is down, disable the remote JS debugging.
                            devSettings.setRemoteJSDebugEnabled(false);
                            recreateReactContextInBackgroundFromBundleLoader();
                          }
                        }
                      });
                }
              });
        }
        return;
      }
    }

    // 线上读JSBundle
    recreateReactContextInBackgroundFromBundleLoader();
  }

  @ThreadConfined(UI)
  private void recreateReactContextInBackgroundFromBundleLoader() {
    FLog.d(TAG, "ReactInstanceManager.recreateReactContextInBackgroundFromBundleLoader()");
    PrinterHolder.getPrinter()
        .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from BundleLoader");
    recreateReactContextInBackground(mJavaScriptExecutorFactory, mBundleLoader);
  }

}

接着就是handleReloadJS方法的具体实现

  @Override
  public void handleReloadJS() {

    UiThreadUtil.assertOnUiThread();

    ReactMarker.logMarker(
        ReactMarkerConstants.RELOAD,
        getDevSettings().getPackagerConnectionSettings().getDebugServerHost());

    // dismiss redbox if exists
    hideRedboxDialog();

    if (getDevSettings().isRemoteJSDebugEnabled()) {
      PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy");
      showDevLoadingViewForRemoteJSEnabled();
      reloadJSInProxyMode();
    } else {
      PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server");
      // getJSAppBundleName() 方法会获取到mJSAppBundleName
      String bundleURL =
          getDevServerHelper()
              .getDevServerBundleURL(Assertions.assertNotNull(getJSAppBundleName()));
      reloadJSFromServer(bundleURL);
    }
  }

最终通过mJSAppBundleName、platform、dev拼接构建出bundleURL,记录了整个JSBundle的位置信息以及版本信息。

DevSupportManager.handleReloadJS()调用reloadJSFromServer(bundleURL)来加载Bundle,最终通过BundleDownloader来加JSBundle。

public class BundleDownloader {
      public void downloadBundleFromURL(
      final DevBundleDownloadListener callback,
      final File outputFile,
      final String bundleURL,
      final @Nullable BundleInfo bundleInfo,
      Request.Builder requestBuilder) {

    final Request request =
        requestBuilder.url(bundleURL).addHeader("Accept", "multipart/mixed").build();
    mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
    mDownloadBundleFromURLCall.enqueue(
        new Callback() {
          @Override
          public void onFailure(Call call, IOException e) {
            // ignore callback if call was cancelled
            if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
              mDownloadBundleFromURLCall = null;
              return;
            }
            mDownloadBundleFromURLCall = null;

            String url = call.request().url().toString();

            callback.onFailure(
                DebugServerException.makeGeneric(
                    url, "Could not connect to development server.", "URL: " + url, e));
          }

          @Override
          public void onResponse(Call call, final Response response) throws IOException {
            // ignore callback if call was cancelled
            if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
              mDownloadBundleFromURLCall = null;
              return;
            }
            mDownloadBundleFromURLCall = null;

            final String url = response.request().url().toString();

            // Make sure the result is a multipart response and parse the boundary.
            String contentType = response.header("content-type");
            Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
            Matcher match = regex.matcher(contentType);
            try (Response r = response) {
              if (match.find()) {
                processMultipartResponse(url, r, match.group(1), outputFile, bundleInfo, callback);
              } else {
                // In case the server doesn't support multipart/mixed responses, fallback to normal
                // download.
                processBundleResult(
                    url,
                    r.code(),
                    r.headers(),
                    Okio.buffer(r.body().source()),
                    outputFile,
                    bundleInfo,
                    callback);
              }
            }
          }
        });
  }
}

使用OkHttp来处理下载任务,不管是Local Host还是Server Host都进行了统一处理,在Response中将返回数据写入到本地存储:

2.渲染原理§

    React Native 运行时会创建三个线程:JS Thread、Shadow Thread、Main Thread,在这三个线程中分别创建三棵树,JS线程中会创建一个Fiber Tree,Shadow线程中会创建一棵Shadow Tree,在UI线程中则是View Tree;Fiber Tree在JS侧创建,Shadow Tree和View Tree在Native侧创建,RN渲染机制的重点就是这三棵树的创建和同步。

8

流程:

四、通信机制§

在RN中有三个线程:JS线程、UI线程、Shadow线程(Native Modules线程),而在Native Modules线程中,主要用来进行Yoga布局计算,同时也负责C++层和原生通信。我们知道Java可以通过JNI的方式和C++代码实现相互调用,JS 可以通过 JavaScriptCore 实现和C++的相互调用,而JavaScriptCore是由C++实现的JS引擎,所以很自然的,C++成为了连接Java和JS的桥梁。

5

所以RN的通信机制总结起来就是一句话:一个C++实现的桥打通了Java和JS,实现了两者的相互调用。

1.Bridge初始化§

    在ReactNative的启动流程中,会对Bridge进行初始化,这个过程中最关键的就是创建了两张表和建立了两个Bridge;两张表中,一张是JavaScriptModuleRegistry,供Java调用JS,一张是NativeModuleRegistry,供JS调用Java;两Bridge,一个是NativeToJSBridge,提供给Java调JS,一个是JsToNativeBridge提供给JS调Java。

(1)JavaScriptModuleRegistry§

(2)NativeModuleRegistry§

2.Bridge实现流程§

Native调用JS§

6

JS调用Native§

在这里,分为两种调用方法:

异步调用§

整个流程可以分为 JS调用Native 和 Native将执行结果回调到JS侧(与Native调用JS的流程相似)

7

JS调用Native:

3.动态代理§

动态代理和静态代理§

动态代理实现§

动态代理在React Native的实现§

  private static class JavaScriptModuleInvocationHandler implements InvocationHandler {
    private final CatalystInstance mCatalystInstance;
    private final Class<? extends JavaScriptModule> mModuleInterface;
    private @Nullable String mName;

    public JavaScriptModuleInvocationHandler(
        CatalystInstance catalystInstance, Class<? extends JavaScriptModule> moduleInterface) {
      mCatalystInstance = catalystInstance;
      mModuleInterface = moduleInterface;

      if (ReactBuildConfig.DEBUG) {
        Set<String> methodNames = new HashSet<>();
        for (Method method : mModuleInterface.getDeclaredMethods()) {
          if (!methodNames.add(method.getName())) {
            throw new AssertionError(
                "Method overloading is unsupported: "
                    + mModuleInterface.getName()
                    + "#"
                    + method.getName());
          }
        }
      }
    }

    private String getJSModuleName() {
      if (mName == null) {
        // Getting the class name every call is expensive, so cache it
        mName = JavaScriptModuleRegistry.getJSModuleName(mModuleInterface);
      }
      return mName;
    }

    @Override
    public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
        throws Throwable {
      NativeArray jsArgs = args != null ? Arguments.fromJavaArgs(args) : new WritableNativeArray();
      mCatalystInstance.callFunction(getJSModuleName(), method.getName(), jsArgs);
      return null;
    }
  }

React Native的动态代理类在CatalystInstanceImpl被初始化,其继承实现了InvocationHandler接口,每个代理类的实例都关联到了一个handler,当通过代理对象调用一个方法的时候,这个方法的调用就会被转发为InvocationHandler的invoke方法来调用,重写的invoke方法将args转化为NativeArray对象,走到callFunction方法。

@DoNotStrip
public class CatalystInstanceImpl implements CatalystInstance {
  @Override
  public void callFunction(final String module, final String method, final NativeArray arguments) {
    callFunction(new PendingJSCall(module, method, arguments));
  }

  public void callFunction(PendingJSCall function) {
    if (mDestroyed) {
      final String call = function.toString();
      FLog.w(ReactConstants.TAG, "Calling JS function after bridge has been destroyed: " + call);
      return;
    }
    if (!mAcceptCalls) {
      // Most of the time the instance is initialized and we don't need to acquire the lock
      synchronized (mJSCallsPendingInitLock) {
        if (!mAcceptCalls) {
          mJSCallsPendingInit.add(function);
          return;
        }
      }
    }
    function.call(this);
  }

  private native void jniCallJSCallback(int callbackID, NativeArray arguments);
}

方法走到这里,实现逻辑已经从Java转向C++层,这个方法在C++层有个对应的CatalystInstanceImpl.cpp类

void CatalystInstanceImpl::jniCallJSFunction(
    std::string module,
    std::string method,
    NativeArray *arguments) {
  // We want to share the C++ code, and on iOS, modules pass module/method
  // names as strings all the way through to JS, and there's no way to do
  // string -> id mapping on the objc side.  So on Android, we convert the
  // number to a string, here which gets passed as-is to JS.  There, they they
  // used as ids if isFinite(), which handles this case, and looked up as
  // strings otherwise.  Eventually, we'll probably want to modify the stack
  // from the JS proxy through here to use strings, too.
  instance_->callJSFunction(
      std::move(module), std::move(method), arguments->consume());
}

在这里jniCallJSFunction()方法会去调用Instance.cpp中的callJSFunction()方法。

void Instance::callJSFunction(
    std::string &&module,
    std::string &&method,
    folly::dynamic &&params) {
  callback_->incrementPendingJSCalls();
  nativeToJsBridge_->callFunction(
      std::move(module), std::move(method), std::move(params));
}

Instance.cpp的callJSFunction()会继续去调用NativeToJsBridge.cpp的callFunction()方法。

void NativeToJsBridge::callFunction(
    std::string &&module,
    std::string &&method,
    folly::dynamic &&arguments) {
  int systraceCookie = -1;
#ifdef WITH_FBSYSTRACE
  systraceCookie = m_systraceCookie++;
  FbSystraceAsyncFlow::begin(
      TRACE_TAG_REACT_CXX_BRIDGE, "JSCall", systraceCookie);
#endif

  runOnExecutorQueue([this,
                      module = std::move(module),
                      method = std::move(method),
                      arguments = std::move(arguments),
                      systraceCookie](JSExecutor *executor) {
    if (m_applicationScriptHasFailure) {
      LOG(ERROR)
          << "Attempting to call JS function on a bad application bundle: "
          << module.c_str() << "." << method.c_str() << "()";
      throw std::runtime_error(
          "Attempting to call JS function on a bad application bundle: " +
          module + "." + method + "()");
    }

#ifdef WITH_FBSYSTRACE
    FbSystraceAsyncFlow::end(
        TRACE_TAG_REACT_CXX_BRIDGE, "JSCall", systraceCookie);
    SystraceSection s(
        "NativeToJsBridge::callFunction", "module", module, "method", method);
#else
    (void)(systraceCookie);
#endif
    // This is safe because we are running on the executor's thread: it won't
    // destruct until after it's been unregistered (which we check above) and
    // that will happen on this thread
    executor->callFunction(module, method, arguments);
  });
}

会进一步调用JSCExcutor的callFunction方法。

void JSCExecutor::callFunction(
    const std::string &moduleId,
    const std::string &methodId,
    const folly::dynamic &arguments) {
  SystraceSection s(
      "JSCExecutor::callFunction", "moduleId", moduleId, "methodId", methodId);
  if (!callFunctionReturnFlushedQueue_) {
    bindBridge();
  }

  // Construct the error message producer in case this times out.
  // This is executed on a background thread, so it must capture its parameters
  // by value.
  auto errorProducer = [=] {
    std::stringstream ss;
    ss << "moduleID: " << moduleId << " methodID: " << methodId
       << " arguments: " << folly::toJson(arguments);
    return ss.str();
  };

  Value ret = Value::undefined();
  try {
    scopedTimeoutInvoker_(
        [&] {
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));
  } catch (...) {
    std::throw_with_nested(
        std::runtime_error("Error calling " + moduleId + "." + methodId));
  }

  performMicrotaskCheckpoint(*runtime_);

  callNativeModules(ret, true);
}

这里会进一步调用callFunctionReturnFlushedQueueJS_的call方法,而callFunctionReturnFlushedQueueJS_变量是在bindBridge里面实现的,究其根本就是通过WebKit JSC拿到JS层代码相关的对象和方法引用,

void JSCExecutor::bindBridge() {
  std::call_once(bindFlag_, [this] {
    SystraceSection s("JSCExecutor::bindBridge (once)");
    Value batchedBridgeValue =
        runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
    if (batchedBridgeValue.isUndefined() || !batchedBridgeValue.isObject()) {
      throw JSCNativeException(
          "Could not get BatchedBridge, make sure your bundle is packaged correctly");
    }

    Object batchedBridge = batchedBridgeValue.asObject(*runtime_);
    callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
        *runtime_, "callFunctionReturnFlushedQueue");
    invokeCallbackAndReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
        *runtime_, "invokeCallbackAndReturnFlushedQueue");
    flushedQueue_ =
        batchedBridge.getPropertyAsFunction(*runtime_, "flushedQueue");
  });
}

最后到JS层中,体现在MessageQueue的callFunctionReturnFlushedQueue方法

  callFunctionReturnFlushedQueue(
    module: string,
    method: string,
    args: mixed[],
  ): null | [Array<number>, Array<number>, Array<mixed>, number] {
    this.__guard(() => {
      this.__callFunction(module, method, args);
    });

    return this.flushedQueue();
  }
 
  // 走到这里已经是最后的方法执行了
  __callFunction(module: string, method: string, args: mixed[]): void {
    this._lastFlush = Date.now();
    this._eventLoopStartTime = this._lastFlush;
    if (__DEV__ || this.__spy) {
      Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`);
    } else {
      Systrace.beginEvent(`${module}.${method}(...)`);
    }
    if (this.__spy) {
      this.__spy({type: TO_JS, module, method, args});
    }
    // 从JS层的JavaScriptModule注册表中查到AppRegistry.js
    const moduleMethods = this.getCallableModule(module);
    invariant(
      !!moduleMethods,
      `Module ${module} is not a registered callable module (calling ${method}). A frequent cause of the error is that the application entry file path is incorrect.
      This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`,
    );
    invariant(
      !!moduleMethods[method],
      `Method ${method} does not exist on module ${module}`,
    );
    moduleMethods[method].apply(moduleMethods, args);
    Systrace.endEvent();
  }

从JS层的JavaScriptModule中查到AppRegistry.js,并调用其runApplication方法。

    如此整个动态代理逻辑即已实现

五、思考§

在实际的工作中,跨端是为了什么,或者说,跨端方案给我们带来了什么收益

    按照我不多的经验来总结,跨端方案的出现是一个必然,从大的角度来看,总体的收益是降低了研发人员的研发成本与维护成本,从一定程度上保证了双端甚至多端的一致性。但仅此来看是不完整的,细分到各个领域来阐述。

    PWA小程序方案所做的并不是为了更好的性能,甚至于PWA小程序方案在某种程度上是一种性能降级;但大面积的铺开必定有其优势,在我看来,其优势就是能在一定程度上,以及一定规模上打造生态闭环,能有更多的第三方接入能力;以至于微信小程序如今能发展成一种可以与应用市场匹敌的状态;2017年的某天,微信悄然上线了「跳一跳」小游戏,当时的我初学移动开发,还不知道这些对我们来说意味着什么;如今看来,微信小程序在某种程度上替代了移动端,市面上大部分的场景下,都不再需要app的介入。

    类似ReactNative的方案更像是为了消费者领域涉设计的跨端框架,目前市面上绝大多数ToC的App产品均应用此方案来实现,通过原生渲染来保证使用体验的同时,又拥有了高动态性,可以随时下发UI级别的更新,来迭代业务。

    而Flutter方案在我工作中见到最多的就是ToB应用,ToB用户和ToC用户使用体验不同,ToB多数会主动保持软件最新版本,少数也会因为服务强制升级而升级;且ToB用户使用时间较长,需要更稳定的服务以及更高的性能来运行;看起来Flutter就像是为了ToB市场而生的。

    跨端框架百花齐放的今天,未来的移动端开发会是什么样的,以产品的角度来看,一款好的产品会需要强劲的产品力,产品力的定义不只是产品业务,还有一部分是产品的使用体验,在业务全面铺开后,开发人员成本已经是项目最小支出的情况下,未来为了吃掉剩下的一小部分低性能设备市场一定会逐步地将主流程更替为原生开发,进一步实现混合架构。