React Native是怎么渲染出原生组件的

1,808 阅读5分钟

最近工作需要研究了一下React Native 的工作流程,理了一下 React Native 是怎么把控件最终渲染在屏幕上的。
在开始研究这个问题之前,我们缕一下我们的困惑:

  • React、React Native 和 native 的关系
  • React Native 开始渲染逻辑的入口
  • React Native 是怎么更新 UI 的变化的
  • React Native 是怎么创建 native 的 View 并且设置布局、位置和属性的

入口


整个JS 端的逻辑都从默认的 index.js 开始执行,代码也只有一行:
image.png
这里会调用RN的 renderApplication 方法。触发 ReactNativeType 的 render 方法。 ReactNativeType 根据是否是 fabric  实现来决定最终的实现。

接着按照如下的调用顺序执行了一连串建立 dom 树的操作,这部分的操作是按照 React 的 Reconcilation 算法来执行的:

updateContainer
scheduleUpdateOnFiber
flushSyncCallbackQueue
flushSyncCallbackQueueImpl
runWithPriority
performSyncWorkOnRoot
workLoopSync

最后在

function completeUnitOfWork(unitOfWork) {
}

里面执行 completeWork , 内部会根据

workInProgress.tag

来判断当前的操作。创建组件则在 HostComponent 里面:

image.png
这里的关键逻辑就是 创建实例 -> 添加创建的节点 -> 初始化创建的节点。

image.png
这里调用 UIManager 的 createView 创建 View,最后根据 tag、viewConfig 等字段得到 component 对象。
这个 UIManager 在 Android 端对应的是 com.facebook.react.bridge.UIManager 。实现类是: com.facebook.react.uimanager.UIManagerModule 

创建View

Android端调用到 UImanagerModule 后会通过 createView 来创建 View:

image.png

这里传入的参数:

  • tag:js端分配好的view id
  • className:对应的view的类名
  • rootViewTag: 根布局的id
  • props: 属性列表


UIImplementation 创建 View 的按照这个逻辑去执行:

  1. 创建 ReactShadowNode 对象
ReactShadowNode cssNode = createShadowNode(className);
ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
  1. 给 view 节点设置id、类名和根节点的id
cssNode.setReactTag(tag); // Thread safety needed here
cssNode.setViewClassName(className);
cssNode.setRootTag(rootNode.getReactTag());
cssNode.setThemedContext(rootNode.getThemedContext());
  1. 把这个node添加到 ShadowNodeRegistry :
mShadowNodeRegistry.addNode(cssNode);
  1. 根据js端传过来的属性map更新view的属性

    image.png    

  1. 处理创建相关的其他逻辑
handleCreateView(cssNode, rootViewTag, styles);


关于 view 的id, js端有自己的生成规则:

image.png

id 每次加上2,但是个位数是1的会进行保留,用作root的id。所以在 Native 端,root view的id 则每次都是分配的1。

native的布局

看完了创建,我们通过一个实例来看看具体的布局:

image.png

这是一个加入了3个 Text 组件和 1个 Native View的demo,最终运行的时候,我们可以通过 Android Studio 的LayoutInspector 工具来查看布局:

image.png

这里我画出创建的节点树的图:

image.png

可以看到这里实际上布局展示这几个 View 都是在 ReactRootView 下面同一层。在 CreateView 加个断点则会发现,Text 组件其实在 js 端创建了不同的节点,一个Text包括 1个 RCTRawText 和 1个 RCTText ,那么这时候就有一个疑惑了,**为什么创建的Native View 有一些没有显示在屏幕上呢? **答案还在 handleCreateView 里面:

image.png

这里会给 node 打上一个 isLayoutOnly 的标签:
当 node 对应的类名是 RCTView 并且 isLayoutOnlyAndCollapsable 返回 true 的时候, isLayoutOnly 是true。

在添加 View 之前,会再判断一次 getNativeKind :

image.png

当node是虚拟节点或者 isLayoutOnly 是true 的时候,kind 为 NativeKind.NONE , 否则如果是叶子节点的话返回 NativeKine.LEAF , 否则返回 PARENT 。

所以中间很多层 RCTView 只是为了布局的时候使用,RN 已经很聪明的把这些辅助类的节点在实际渲染的时候给移除了。这样也能保证对应到 native 端的时候,做太多无用的层级渲染。

接下来就是把创建操作加入到真正的执行队列里面。RN维护了一个 UIViewOperationQueue 来维护各种关于 View 的操作。

image.png

创建 View 则是: CreateViewOperation 里面执行 NativeViewHierarchyManager 的 createView 。

View view = viewManager.createView(themedContext, null, null, mJSResponderHandler);
mTagsToViews.put(tag, view);
mTagsToViewManagers.put(tag, viewManager);
view.setId(tag);

添加native View


native需要创建的 View 已经创建了,那么这时候如何把创建出来的 View 添加到 ViewGroup 里面去呢?JS 端会从 finalizeInitialChildren 开始执行。

image.png

这里调用了 UIManager 的 setChildren  函数; 同理,会执行 Android 端的

mUIImplementation.setChildren(viewTag, childrenTags);

SetChildrenOperation 中执行操作:

image.png
这里会找到root表示的parent和我们要添加的children view,把 children 添加到 root 里面去。

view的布局和属性


View 创建出来了,也添加到父布局里面了,接下来就是进行布局了。那么 RN 是怎么进行布局的呢?通过断点,我们能找到在开始布局的时候从root开始进行树层级的更新。这里会从jni层开始执行到java层的 NativeRunnable 里面,最后走到 UIManagerModule  的 onBatchComplete 方法:

try {
    mUIImplementation.dispatchViewUpdates(batchId);
} finally {
}


![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83f9a6b9b1224537a84b0ff9b158bf0c~tplv-k3u1fbpfcp-zoom-1.image)

这里会:
  1. 刷新view的层级
  2. 在布局刷新后进行一次批处理
  3. 分发view的更新


执行 updateViewHierarchy  , 每个rootview下面都要执行。当root的measurespec不为空的时候,就执行。

calculateRootLayout(cssRoot);
applyUpdatesRecursive(cssRoot, 0f, 0f);
if (mLayoutUpdateListener != null) {
    mOperationsQueue.enqueueLayoutUpdateFinished(cssRoot,mLayoutUpdateListener);
}
  • 调用YogaNode的 calculate 方法来计算布局
  • 递归更新子组件。先调用dispatchUpdates判断是否改变了尺寸等布局相关的信息,如果改变,分发 OnLayoutEvent 事件去更新。


这里的计算布局其实是调用了 Yoga 的布局计算, Yoga 是 RN 官方独立的一个 Flexbox 布局引擎库。这个库的底层计算逻辑是 C/C++ 跨平台的,性能也比较高。支持了 Flexbox 的各种属性。具体可以参考它的 github: github.com/facebook/yo…

如果hasNewLayout条件成立,则获取绝对位置的坐标来判断是否改变了布局。最后走到applyLayoutBase,这里计算x和y,然后从子view往上开始更新坐标,
ReactShadowNodeImple#dispatchUpdates

image.png

然后调applyLayoutRecursive

applyLayoutRecursive

递归调用会加到屏幕上的view:

image.png

根据tag找到view之后:

image.png

可以看到这里确定了view的宽高和坐标位置:

image.png
到这里,RN 创建出来的View的布局就很清晰了,其实是使用了 Yoga 的计算,得到每个 View 在屏幕上的绝对坐标值。然后利用坐标去执行 View
layout 方法。而最外层的 ReactRootView ,其实就是一个 FrameLayout 的实现。

这里我们用一张图来表示 RN 创建 View的流程:

image.png

总结

这里就分析出了RN是如何把JS的虚拟dom 树转换成 Android 的 View 的。简单总结就是 js 把 virtual dom的结构发给了 native 端,
native 利用 Yoga 的能力比较高效的计算出 View 的实际位置。然后把 View 最终呈现在屏幕上。