从设计初衷了解ReactNative - 上篇

369 阅读22分钟

最近跨平台的热度又起来了,我们今天来聊聊比较有名的跨平台方案ReactNative相关的话题,从第一版ReactNative到现在,也接近10年了,10年的漫长岁月整个框架也发生了不少变化。我们也迎来了很多概念,比如新架构、Turbomodule等等,这不免让我产生好奇,为什么RN团队需要朝着这个方向迭代?在翻遍全网文章后,发现并没有我要的答案,因此就有了今天这个系列文章,希望能给还不太了解或者还在观望的技术团队带来更多的认识。本系列将从旧架构起源到新架构的设计做一系列的演化过程进行讲解,也算是记录自己对类RN框架发展认知!

源码的阅读总是枯燥的,很多情况下我们都知道一件事情是怎么做的,源码解析就是这样,但是为什么这么做其实很少有人探究,这是因为所处的“Context”不一样,因为如果不站着设计者的角度上看,其实无法看出整齐全貌。当然我也无法让ReactNative的设计者来跟我讲述为什么要这么设计,不过有趣的是,曾经我也作为一个跨平台框架的开发者,“操盘”着公司内部的跨平台框架的设计,在设计整体框架时,其中碰到的困难与RN团队是一样的,经过两个多月断断续续的代码阅读,我相信自己的一些认知可以帮助大家更加全面的掌握ReactNative框架的本质。

当然,目前ReactNative在移动端上支持Android、iOS、鸿蒙系统,为了时间节约,大部分Native设计代码我都会从Android展开,但是在总体平台思路是一致,这点需要大家注意

UI与事件

回到第一版ReactNative代码时,在2015年,此时正是移动互联网繁荣与机遇的年代,android 与iOS操作系统霸占整体手机市场进90%以上,移动端技术正是兴起的时候,此时ReactNative项目在Facebook孕育下正式开业,而开源的初衷并不是为了解决多端系统人力的问题,(比较有趣,杀手级跨平台框架一开始并不是纯为了解决多端代码编写问题)而是希望把强大的前端设计框架React.js带到移动端,“Bringing modern web techniques to mobile”

因为自身业务的特性,Facebook本身就具备了庞大的前端设计基础建设,面对着相对不那么成熟的移动端界面的编写体系,于是乎,为什么不把React也带到移动端呢?这种想法也孕育而生。第一版React到移动端的方案整体,其实是先在iOS平台上实现的,把React带到移动端,其实主要面临的是以下两个问题,对没错,只有两个,他们分别是UI与事件

UI

想要达成用React.js写出的界面能在移动端侧保持一致,就需要满足一种映射关系的转化,因为Native侧比如Android,他们对于UI的描述语言是不一样的,比如React.js表述文本是Text组件,Android是TextView,各自的设计表述都是存在差别的,但是表述的东西却是一致的,文本其实就是文本

我们把其称为UI描述与UI实现,UI实现有很多种,但是UI的描述却是唯一的

因此RN团队第一步需要解决的,是如何打通UI的描述与UI实现,这也是所有跨平台框架需要解决的第一步。这就需要一个媒介,能够承担起把UI描述功能。

UI描述

我们如何描述一个页面呢?其实换算成自然语言的话,其实也就两部,第一个这个页面有什么,比如一个文本在一个容器里面,这其实是对于页面父子关系的描述,第二个页面的某个具体控件长什么样,这里其实描述的就是单独个体的属性。通过父子关系的描述与个体属性的组合,我们就能够把一个复杂的静态页面描述起来。有趣吧,其实把问题拆解下来就是这两个,接下来我们就得思考了,我们怎么去实现这两个。

(注意:这里我们还是先以旧架构思路讲,后面也有新架构的实现)最简单的实现就是,我们可以定义一个协议呀,比如我们就可以定义以下json数据,一个json就能够包含以上两个概念,比如描述一个容器里面有什么,我们可以设定一个协议,

subView如果当前容器存在子控件,那么这个属性就有值,这里就描述了父子关系
prop这里就表示一个属性,可以有位置信息,比如距离父亲控件左上角的位置,x,y,也可以添加描述自身的属性,这里也分为两种一种是通用属性:比如描述背景色等一种是特殊属性:比如只有文本控件才可以有的文本属性

其实换算下来,就如同类似的json

{
    "viewid" : "container" ,
    "subView" : [
        {
            "viewid" : "View1" ,
            "prop":{
              x:100
              y:100,
              color:xxxx
              xxxx:xxx
            }
            "subView" : [
                {
                    "viewid" : "View1-1" ,
                    "subView" : [

                    ]
                },
                {
                    "viewid" : "View1-2" ,
                    "subView" : [

                    ]
                }
            ]
        },
    ]
}

是不是非常简单,我们就已经把关键的两部解决了,没错ReactNative的旧架构就是通过json的方式去表示一个UI的描述

相当于React.js通过“解析器”把数据React.js涉及的代码转换为了Json,即“序列化”过程(当然这里并不是数据传输的序列化,这是一个类比概念),然后再把Json通过“反序列化”过程转化为了原生控件

如何进行UI描述的“序列化”过程

如何把用React.js编写的内容反序列化是一个重要过程,通过上文,我们可以得到最终的产物,其实就是UI的属性与UI父子关系描述。UI的父子关系在这个过程转化较为简单,因为在开发人员编写一个控件时,本身就会自己声明控件的父子关系,这个过程中最难的是UI属性解析阶段。

在这个过程会遇到如何把JS侧的规则转化为通用UI描述,同时位置关系也非常重要,我们熟知的Yoga其实就是为了解决ReactNative中解析位置等关键属性而引入的。因为本篇关键分析React.js相关内容,就请允许我暂且掠过,我们直接看UI的创建过程吧,

当UI数据解析后,JS侧会调用UIManager.createView进行UI的创建过程

const {UIManager} = NativeModules;
....
createInstance: function(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
    var tag = ReactNativeTagHandles_1.allocateTag(), viewConfig = ReactNativeViewConfigRegistry_1.get(type);
    for (var key in viewConfig.validAttributes) props.hasOwnProperty(key) && deepFreezeAndThrowOnMutationInDev(props[key]);
    var updatePayload = ReactNativeAttributePayload_1.create(props, viewConfig.validAttributes);
    UIManager.createView(tag, viewConfig.uiViewClassName, rootContainerInstance, updatePayload);
    var component = new ReactNativeFiberHostComponent_1(tag, viewConfig);
    return precacheFiberNode$1(internalInstanceHandle, tag), updateFiberProps$1(tag, props), 
    component;
},

UIManager是一个NativeModules(这里我们遇到一个叫NativeModule的概念,不过没关系,我们之后的NativeModule小结还会讲,这里大家知道它其实是一个桥梁的封装,用于JS调用原生方法) createView方法的实现其实并不在JS侧,而是通过一个中间层统一调用,这个中间层callNativeModules,其实现在C++,后面我们也会继续分析为什么设计这个中间层,以及为什么新架构又拿掉它

void JSCExecutor::callNativeModules(Value&& value) {
 ....
  try {
    //这里进行了序列化
    auto calls = value.toJSONString();
    m_delegate->callNativeModules(*this, folly::parseJson(calls), true);
  } catch (...) {
   . ...
  }
}

在这里我们终于看到了,JS侧的数据通过toJSONString方法完成了序列化,之后数据通过JNI调用会再回到Java层

如何进行UI描述的“反序列化”过程

回到上文,通过C++侧调起的函数,最终就会到invoke 方法进行反序列化解析数据,并调用对应的方法

public class JavaMethodWrapper implements NativeModule.NativeMethod 
@Override
public void invoke(JSInstance jsInstance, ReadableNativeArray parameters) {
   ....
    int i = 0, jsArgumentsConsumed = 0;
    try {
      for (; i < mArgumentExtractors.length; i++) {
        mArguments[i] = mArgumentExtractors[i].extractArgument(
          jsInstance, parameters, jsArgumentsConsumed);
        jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded();
      }
    } catch (UnexpectedNativeTypeException e) {
      throw new NativeArgumentsParseException(
        e.getMessage() + " (constructing arguments for " + traceName + " at argument index " +
          getAffectedRange(jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) +
          ")",
        e);
    }

    try {
      mMethod.invoke(mModuleWrapper.getModule(), mArguments);
    } catch (IllegalArgumentException ie) {
     ....
}

最终,NativeModule会调用UIManagerModule走到UI相关创建的过程。

在ReactNative中,有一个最关键的基类,就是ViewManager,ViewManager就承担着“反序列化”构建UI大部分的能力,比如如何创建一个View,即createView方法,如何更新当前View的属性等最基础的方法

public abstract class ViewManager<T extends View, C extends ReactShadowNode>
    extends BaseJavaModule {

  ... 
  /** Creates a view with knowledge of props and state. */
public @NonNull T createView(
      int reactTag,
      @NonNull ThemedReactContext reactContext,
      @Nullable ReactStylesDiffMap props,
      @Nullable StateWrapper stateWrapper,
      JSResponderHandler jsResponderHandler) {
    T view = createViewInstance(reactTag, reactContext, props, stateWrapper);
    if (view instanceof ReactInterceptingViewGroup) {
      ((ReactInterceptingViewGroup) view).setOnInterceptTouchEventListener(jsResponderHandler);
    }
    return view;
  }
  
  ....
public void updateProperties(@NonNull T viewToUpdate, ReactStylesDiffMap props) {
  final ViewManagerDelegate<T> delegate = getDelegate();
  if (delegate != null) {
    ViewManagerPropertyUpdater.updateProps(delegate, viewToUpdate, props);
  } else {
    ViewManagerPropertyUpdater.updateProps(this, viewToUpdate, props);
  }
  onAfterUpdateTransaction(viewToUpdate);
}
....

ViewManager 定义了如何创建一个控件的流程以及更新一个View的过程,那么它是如何知道我们该创建哪个View呢?比如我是创建Text文本控件呢?还是Switch 控件呢?没错,还是协议,它可以是一个数字标识,也可以是文本标识,这里ReactNative采取的就是文本标识className,通过序列化阶段传递的className 找到最终需要创建的ViewManager。

public synchronized void createView(
    ThemedReactContext themedContext,
    int tag,
    String className,
    @Nullable ReactStylesDiffMap initialProps) {
  ...
  通过className 找到我需要创建什么控件
  try {
    ViewManager viewManager = mViewManagers.get(className);
    
    View view =
        viewManager.createView(tag, themedContext, initialProps, null, mJSResponderHandler);
    mTagsToViews.put(tag, view);
    mTagsToViewManagers.put(tag, viewManager);
  } finally {
    Systrace.endSection(Systrace.TRACE_TAG_REACT_VIEW);
  }
}

ViewManager 其实就是承担了如何完成UI描述的过程,这个通过也有很多其他的叫法,比如鸿蒙的Node树也是类似的概念,其实就是完成控件创建以及(父子关系绑定过程,可选,因为可以没有父子关系的叶子控件)

当然,我们如果想要描述父子控件关系,那么也有一个ViewGroupManager,它继承自ViewManager基础上添加如何添加子View的描述关系

public abstract class ViewGroupManager<T extends ViewGroup>
    extends BaseViewManager<T, LayoutShadowNode> implements IViewGroupManager<T> {

 .... 

  @Override
  public void addView(T parent, View child, int index) {
    parent.addView(child, index);
  }

  /** 

有了ViewManager基础概念之后呢,每个控件就可以定义自己的实现以及自己管理类的实现。

至此,我们就完成了静态页面构建中最后一步,UI就完美化成了Native侧的UI表示!

事件

UI的渲染已经完成了,但是到这一步还只是一个普通页面,它只能看并不能相应任何东西,比如点击事件。UI的展示需要事件的驱动去修改对应的展示。如果我们说UI描述是JS侧打通Native侧,那么事件的处理就是Native侧打通JS侧

事件的驱动,我们拿点击事件来举例子,比如有一个按钮,通过我们对UI的描述的理解,点击事件的编写肯定也是绑定在UI侧

<Button title={'Hello'} onPress={() => {console.log("我被点击了")}}/>

那么UI表述部分是哪里的,其实就是这里,描述了一个Button的显示

<Button title={'Hello'} onPress={}/>

我们可以留意到,UI声明的同时也通常会绑定一个事件,onPress就描述了点击事件需要做出的修改,这里以一个函数的形式表面() => {console.log("我被点击了")} ,那么这个函数该怎么被处理呢?函数的触发时机是什么时候呢?这些都是框架开发者需要考虑的。

在ReactNative中,事件可以简单分为两种,一种是View事件,比如Button的点击,这种事件是依赖于UI控件的,还有一个就是普通事件,比如JS侧通过NativeModule等触发的函数,它是需要有callback的,这两种事件都依赖原生平台的处理。

View事件

回到上文,() => {console.log("我被点击了")} 其实就可以理解为一个事件,它需要等到Button点击时被回调。那么这里我们就得考虑一个问题,我们怎么知道这个事件是属于哪个控件的,比如有两个Button监听了通用的事件,当其中一个Button被点击了(原生侧事件),如何把这个事件派发到JS侧对应的事件呢?

在View事件中,事件的触发者必定是原生侧的控件,因为通过UI生成后我们可以知道,UI侧最终生成的东西其实就是Native侧对应的视图,那么事件的接受者肯定就是原生侧的View来触发。

通常原生侧在创建好视图后,也会注册对应的事件,比如点击事件的监听

public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup>
@ReactProp(name = "focusable")

public void setFocusable(final ReactViewGroup view, boolean focusable) {
  if (focusable) {
    view.setOnClickListener(
        new View.OnClickListener() {
          @Override
          public void onClick(View v) {
            final EventDispatcher mEventDispatcher =
                UIManagerHelper.getEventDispatcherForReactTag(
                    (ReactContext) view.getContext(), view.getId());
            if (mEventDispatcher == null) {
              return;
            }
            
            // 通过view.getId() 获取事件传递的目标,这里其实就是获取reacttag
            mEventDispatcher.dispatchEvent(
                new ViewGroupClickEvent(
                    UIManagerHelper.getSurfaceId(view.getContext()), view.getId()));
          }
        });

   ....
  }

事件进行监听后,会通过EventDispatcher 从Native侧把消息传递到JS侧,那么JS侧是如何判断这个事件是属于哪个控件的呢?

这里最重要的就是mViewTag属性,这个属性会在事件构建时把当前View的reactTag传递进去

public abstract class Event<T extends Event> { 
 ...
  private int mViewTag;

那么这个reactTag是在什么时候被设置的呢?其实我们再回到上文,在创建View的时候就会带上

 */
protected @NonNull T createViewInstance(
    int reactTag,
    @NonNull ThemedReactContext reactContext,
    @Nullable ReactStylesDiffMap initialProps,
    @Nullable StateWrapper stateWrapper) {
  T view = null;
 ....
  // 把reactTag设置到View的id里面
  view.setId(reactTag);
  addEventEmitters(reactContext, view);
  if (initialProps != null) {
    updateProperties(view, initialProps);
  }
  ... 
  return view;
}

那么大家有没有猜到,什么时候才会生成reactTag呢?在哪生成呢?其实是在JS侧,我们再把其中的数据流动画一下,大家就能够非常清楚事件传递以及事件是如何被查找的了

JS通过createInstance创建单独UI控件时,就会分配一个自增的id

function createInstance(
  type,
  props,
  rootContainerInstance,
  hostContext,
  internalInstanceHandle
) {
  var tag = allocateTag();
  var viewConfig = getViewConfigForType(type);

 ....
  
  
function allocateTag() {
  var tag = nextReactTag;

  if (tag % 10 === 1) {
    tag += 2;
  }

  nextReactTag = tag + 2;
  return tag;
}

当收到对应的reactTag后,JS侧就可以通过Tag找到对应在JS侧的控件,并分发对应的事件了

ReactNativePrivateInterface.RCTEventEmitter.register({
  receiveEvent: receiveEvent,
  receiveTouches: receiveTouches
});

function _receiveRootNodeIDEvent(rootNodeID, topLevelType, nativeEventParam) {
  var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT,
    inst = getInstanceFromTag(rootNodeID),
    target = null;
  null != inst && (target = inst.stateNode);

在View事件的设计中,最重要的是如何绑定JS事件与原生事件,这里我们看到的是ReactNative采取的是在JS侧生成View的唯一标识,通过把唯一标识传递原生侧,当原生侧响应事件时再在唯一标识带回来触发事件。此机制得益于无论是在JS侧还是原生侧,都建立了一对一的视图关系,才能完成View事件的传递。

普通事件

区别于View事件具备特殊的事件载体,普通事件则显得更加无拘束,比如我们可以通过NativeModule或者TurboModule方式定义一个Native方法,Native方法的调用者是JS侧,在这个过程中很有可能需要一些“回调”信息,那么回调的触发者就是从Native测进行触发。

通过上文我们也有清晰了解到,事件的传递核心就是绑定关系,View的事件正因为有View的载体,我们可以把View的唯一标识进行传递,可是普通的callback可没有,怎么办呢?没事,ReactNative团队的思路就是,每一个callback的生成都绑定一个独立的id

每一个方法都会有一个专属的methodid,如果methodid具备回调,那么同时也会生成一个_callID,这个id会在MessageQueue中以递增的方式存在,并且方法对应的id具备两种,一种是_successCallbacks 与_failureCallbacks,这同样也方便Promise的设计

class MessageQueue {
  _lazyCallableModules: {[key: string]: (void) => {...}, ...};
  _queue: [number[], number[], mixed[], number];
  // 保存callback集合
  _successCallbacks: Map<number, ?(...mixed[]) => void>;
  _failureCallbacks: Map<number, ?(...mixed[]) => void>;
  _callID: number;
  _lastFlush: number;
  _eventLoopStartTime: number;
  _reactNativeMicrotasksCallback: ?() => void;

  _debugInfo: {[number]: [number, number], ...};
  _remoteModuleTable: {[number]: string, ...};
  _remoteMethodTable: {[number]: $ReadOnlyArray<string>, ...};

  __spy: ?(data: SpyData) => void;

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

  invokeCallbackAndReturnFlushedQueue(
    cbID: number,
    args: mixed[],
  ): null | [Array<number>, Array<number>, Array<mixed>, number] {
    this.__guard(() => {
      this.__invokeCallback(cbID, args);
    });

    return this.flushedQueue();
  }

  flushedQueue(): null | [Array<number>, Array<number>, Array<mixed>, number] {
    this.__guard(() => {
      this.__callReactNativeMicrotasks();
    });

    const queue = this._queue;
    this._queue = [[], [], [], this._callID];
    return queue[0].length ? queue : null;
  }

我们拿最简单的NativeModule出发,当构建NativeModule时同样会构造所属于NativeModule的JS方法,通过genMethod构建一对一方法以及对应的callback

function genMethod(moduleID: number, methodID: number, type: MethodType) {
  let fn = null;
  if (type === 'promise') {
    fn = function promiseMethodWrapper(...args: Array<mixed>) {
      // In case we reject, capture a useful stack trace here.
      /* $FlowFixMe[class-object-subtyping] added when improving typing for
       * this parameters */
      const enqueueingFrameError: ExtendedError = new Error();
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          args,
          data => resolve(data),
          errorData =>
            reject(
              updateErrorWithErrorData(
                (errorData: $FlowFixMe),
                enqueueingFrameError,
              ),
            ),
        );
      });
    };
  } else {
    fn = function nonPromiseMethodWrapper(...args: Array<mixed>) {
      const lastArg = args.length > 0 ? args[args.length - 1] : null;
      const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
      const hasSuccessCallback = typeof lastArg === 'function';
      const hasErrorCallback = typeof secondLastArg === 'function';
      hasErrorCallback &&
        invariant(
          hasSuccessCallback,
          'Cannot have a non-function arg after a function arg.',
        );
      // $FlowFixMe[incompatible-type]
      const onSuccess: ?(mixed) => void = hasSuccessCallback ? lastArg : null;
      // $FlowFixMe[incompatible-type]
      const onFail: ?(mixed) => void = hasErrorCallback ? secondLastArg : null;
      // $FlowFixMe[unsafe-addition]
      const callbackCount = hasSuccessCallback + hasErrorCallback;
      const newArgs = args.slice(0, args.length - callbackCount);
      if (type === 'sync') {
        return BatchedBridge.callNativeSyncHook(
          moduleID,
          methodID,
          newArgs,
          onFail,
          onSuccess,
        );
      } else {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          newArgs,
          onFail,
          onSuccess,
        );
      }
    };
  }
  // $FlowFixMe[prop-missing]
  fn.type = type;
  return fn;
}
processCallbacks(
  moduleID: number,
  methodID: number,
  params: mixed[],
  onFail: ?(...mixed[]) => void,
  onSucc: ?(...mixed[]) => void,
): void {
  if (onFail || onSucc) {
   ...
    onFail && params.push(this._callID << 1);
    // eslint-disable-next-line no-bitwise
    onSucc && params.push((this._callID << 1) | 1);
    this._successCallbacks.set(this._callID, onSucc);
    this._failureCallbacks.set(this._callID, onFail);
  }

后续这个callbackId 就会被一起传递到Native侧,就是JavaMethodWrapper对象构建时

有了callbackid后,后续当Native侧需要把callback信息回传时,就可以通过这个id传递给js侧


void CatalystInstanceImpl::jniCallJSCallback(
    jint callbackId,
    NativeArray *arguments) {
  instance_->callJSCallback(callbackId, arguments->consume());
}

JS获取到id后从集合里面把对应的函数取出来然后回调即可,这里我们也可以看到,与View事件不同的是,callback是一次性的,即调用之后就会被map移除,这也符合我们的方法调用直觉

void JSIExecutor::invokeCallback(
    const double callbackId,
    const folly::dynamic &arguments) {
  SystraceSection s("JSIExecutor::invokeCallback", "callbackId", callbackId);
  if (!invokeCallbackAndReturnFlushedQueue_) {
    bindBridge();
  }
  Value ret;
  try {
    ret = invokeCallbackAndReturnFlushedQueue_->call(
        *runtime_, callbackId, valueFromDynamic(*runtime_, arguments));
  } catch (...) {
    std::throw_with_nested(std::runtime_error(
        folly::to<std::string>("Error invoking callback ", callbackId)));
  }
  ....
}
__invokeCallback(cbID: number, args: mixed[]): void {
  this._lastFlush = Date.now();
  this._eventLoopStartTime = this._lastFlush;

  // The rightmost bit of cbID indicates fail (0) or success (1), the other bits are the callID shifted left.
  // eslint-disable-next-line no-bitwise
  const callID = cbID >>> 1;
  // eslint-disable-next-line no-bitwise
  const isSuccess = cbID & 1;
  const callback = isSuccess
    ? this._successCallbacks.get(callID)
    : this._failureCallbacks.get(callID);
    
  ... 
  移除id
  this._successCallbacks.delete(callID);
  this._failureCallbacks.delete(callID);
  处理callback ...
 
  callback(...args);

可以看到,在NativeModule的情况一下,一个方法调用其实是通过多个环节之间的绑定关系完成的,同时呢我们也可以看到,每个Native方法的执行都经过了异步的队列机制,方法的调用与返回值的处理都是通过队列分发。当然,队列的设计并非一定在JS侧,我们在Native侧实现也是可以的,核心就是关系的绑定与查找

新架构下跨语言交互

实现JS与Native交互核心

可能大家接触到ReactNative,都听说过Bridge跟JSI等等的概念,比如大家在很多情况下都能看到一些资料说新架构引入JSI等等提升了多少多少性能。

但是我这里认为,他们其实本质上都是“同一种东西”,可能有读者这个时候就会站出来说,Pika啊,你这不就是扯淡嘛!别人都不是这么说的!别急,我慢慢道来~

大家想一下,我们在上文其实看到很多关于JS与Native交互的代码,其中也不少C++代码,那么他们是怎么跨越不同语言环境调用的呢?其实核心就是C++语言太强大了,JS引擎大部分是由C++代码编写的,我们可以看作JS环境运行的JS引擎,本质上就是C/C++环境,而且JS虚拟机天然就支持调用C++方法,不仅是herme,JSC,还有quickJS引擎等,都支持JS声明一个C++方法,这个我们参照QuickJS引擎翻译,都把其都称为CAPI好了

以JSC或者QuickJS举例子,它们都有一个叫JSContext的类,这个类其实是C++编写的JS引擎所暴露的一个用于操控JS环境的类,JSValue类即属于JS类型的统一包装,在JS环境中有个特殊的对象叫做global,它可以被理解为最顶层的函数域(JS环境中查找方法会通过作用域一级一级查找当前是否存在某个函数然后调用),而我们在C++环境中,我们是可以针对global对象设置一些函数/对象,因为JS引擎本身就属于C++环境,我们用C++调用其实就再合理不过。比如我们可以设置一个nativeFlushQueueImmediate 方法,它的实现在C++侧,如下

void JSIExecutor::initializeRuntime() {
 
  runtime_->global().setProperty(
      *runtime_,
      "nativeFlushQueueImmediate",
      Function::createFromHostFunction(
          *runtime_,
          PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"),
          1,
          [this](
              jsi::Runtime &,
              const jsi::Value &,
              const jsi::Value *args,
              size_t count) {
            if (count != 1) {
              throw std::invalid_argument(
                  "nativeFlushQueueImmediate arg count must be 1");
            }
            callNativeModules(args[0], false);
            return Value::undefined();
          }));

这种方法在JS引擎中,会被标记为CFunction,区别于普通JSFunction,CFunction在调用时会直接调用其对应的C++函数,而不是普通JS方法。如果大家想要了解JS引擎,不妨看看QuickJS引擎设计,它足够对新手友好,也能让我们了解JS引擎的设计全貌。普通的JS方法其实就是一条条的指令,指令也对应着一系列模板C++代码, 只是说CFunction允许我们加入自己的方法。

这其实是JS引擎能跟其他语言通信的基础,因为它本身支持嵌入C/C++函数,且允许我们调用JS方法。有了C/C++后,在Java侧我们就可以通过JNI的方式执行到一些在Java环境下编写的函数

比如JS侧想要调用Java侧一个test方法,它的思想如下

  1. C++侧往JS侧注册一个桩函数,比如我们就叫testProxy好了,它是一个CFuntion

  2. JS调用testProxy方法

  3. C++通过JNI 把函数需要的参数传递至Java

  4. Java执行真正的Java函数,得到结果,并通过JNI传递至C++

  5. C++获取到结果后,把其包装为JSValue传递至JS环境,完成通讯

从上面流程我们可以完整了解到整个通讯过程,而无论是NativeModule还是TurboModule,或者本身的实现都是基于此流程完成的,只是实现颗粒度不同。这里的颗粒度可不是大厂说的黑化,而是真的颗粒度。而其中说的Bridge,或者广义说的JSI,本质上都是对于CAPI的应用

新架构下UI实现的本质

我们说到UI的描述过程中,其实都其实要做一件非常重要的事,就是JS侧需要进行UI的描述“序列化”,而原生侧需要进行“反序列化”。新老架构下,其实需要完成事是一样的,只是实现不同!

聪明读者可能留意到,我一直在序列化上多了几个“”,我们都说无论NativeModule还是TurboModule,本质都是对于CAPI的应用

NativeModule

把信息从JS侧传递至C++,这是通讯过程必须要解决的问题,NativeModule的思想就是采取中间层的思想,把东西传递至原生侧,这也就是为什么会被称为桥的原因。因为NativeModule本身就可以看作一个固定的中间件,通过CAPI的封装好一套协议

这个协议本身就是JSON数据,NativeModule C++侧与NativeModule Java侧就是通过JSon数据进行交互的

void JSCExecutor::callNativeModules(Value&& value) {
 ....
  try {
    //这里进行了序列化
    auto calls = value.toJSONString();
    m_delegate->callNativeModules(*this, folly::parseJson(calls), true);
  } catch (...) {
   . ...
  }
}

当然,我们这里省略一些NativeModule的注册流程,比如如何查找的细节我们就不再这篇过多展开了。这里我们再次确定一个思想,NativeModule就是采取一个中间件的方式,把所有方法统一转发到真正实现层,因此采取一个足够通用的协议是必然的,所以它选择了json。

伴随着ReactNative的多年使用,它的身影遍布了各种复杂场景,使用统一协议json的弊端也慢慢出来了。

首先最主要的是json它是静态的数据结构,并不是具备图灵完备的语言,因此很难在json侧承担逻辑,比如我希望在编写一端文本时修改时并修改一段逻辑,比如我的规则是输入以下文本,当遇到a时就会变成b,输入b时变成a,这个输入是需要即可展示给用户的,比如input:aaaabbbb,output:bbbbaaa。NativeModule能不能做到呢?当然是可以的,但是成本就比较大了,因为我们每输入一个文本时,都需要保存好前面所有文本,再对所有文本进行规则映射,比如我们输入ab再输入b时,是需要把整个字符串都传递给JS侧,因为每一次的刷新都是无法保存状态的。

其次,json数据的确存在序列化与反序列化的开销,对于复杂的ui结构而言,在js侧进行序列化与反序列化就是一个性能损耗,因为通常js侧还额外承担必要的UI计算逻辑

当然,也有说到NativeModule没有懒加载的问题,但是这其实我们是可以自己在NtaiveModule的实现侧实现懒加载,因此这并不是一个非常关键的问题,主要还是单一中间件的思想遇到复杂逻辑时的一些不足

TurboModule

既然单一中间件的思想有这些问题,我们能不能把这些逻辑稍微拆一下呢?没错,这就是TurboModule的核心思想,如果我们说NativeModule是一对多的方法映射,那么TurboModule就完成了一对一的方法映射。

这样它的好处是每个JS方法都对应着一个中间层,并且每一个中间层都只对应一个实现层(java方法),其实就是每个方法都绑定一个CAPI方法,从而达到解耦的目的。正因为没有了耦合的存在,也就不再需要通过统一结构json数据进行序列化了。

无论是NativeModule还是TurboModule,在JS侧本质上都需要注册一个Proxy函数,也可以被称为桩函数,所有JS侧需要调用一个统一的函数进行实现的分发。TurboModule的桩函数就是__turboModuleProxy


void TurboModuleBinding::install(
    jsi::Runtime &runtime,
    TurboModuleBindingMode bindingMode,
    TurboModuleProviderFunctionType &&moduleProvider) {
  runtime.global().setProperty(
      runtime,
      "__turboModuleProxy",
      jsi::Function::createFromHostFunction(
          runtime,
          jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"),
          1,
          [binding =
               TurboModuleBinding(bindingMode, std::move(moduleProvider))](
              jsi::Runtime &rt,
              const jsi::Value &thisVal,
              const jsi::Value *args,
              size_t count) {
            if (count < 1) {
              throw std::invalid_argument(
                  "__turboModuleProxy must be called with at least 1 argument");
            }
            std::string moduleName = args[0].getString(rt).utf8(rt);
            return binding.getModule(rt, moduleName);
          }));
}

最终通过我们注册的moduleProvider_获取到一对一的module

jsi::Value TurboModuleBinding::getModule(
    jsi::Runtime &runtime,
    const std::string &moduleName) const {
  std::shared_ptr<TurboModule> module;
  {
   通过moduleProvider_获取一对一的module
    module = moduleProvider_(moduleName);
  }
  

当然我们这里还是省略TurboModule的注册流程,后续如果再出这个系列的文章我们还会继续分析

无论是NativeModule还是TurboModule,本质都是通过CAPI去完成跨语言环境的事件绑定,这点需要大家注意噢!

总结

本篇到这里已经很长了,这里我们基本上从头过了一遍ReactNative旧架构的实现以及新架构的部分支持,接下来我们还会继续从时间线出发,看看ReactNative究竟改进了什么以及它的技术本质!