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

2022-05-10 20:46:43 浏览数 (3)

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

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

入口

整个JS 端的逻辑都从默认的 index.js 开始执行,代码也只有一行:

这里会调用RN的 renderApplication 方法。触发 ReactNativeType 的 render 方法。 ReactNativeType根据是否是 fabric 实现来决定最终的实现。 接着按照如下的调用顺序执行了一连串建立 dom 树的操作,这部分的操作是按照 ReactReconcilation 算法来执行的:

代码语言:javascript复制
updateContainer
scheduleUpdateOnFiber
flushSyncCallbackQueue
flushSyncCallbackQueueImpl
runWithPriority
performSyncWorkOnRoot
workLoopSync

最后在

代码语言:javascript复制
function completeUnitOfWork(unitOfWork) {
}

里面执行 completeWork , 内部会根据

代码语言:javascript复制
workInProgress.tag

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

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

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

创建View

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

这里传入的参数:

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

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

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

image.png

  1. 处理创建相关的其他逻辑
代码语言:javascript复制
handleCreateView(cssNode, rootViewTag, styles);

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

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

native的布局

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

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

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

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

这里会给 node 打上一个 isLayoutOnly 的标签: 当 node 对应的类名是 RCTView 并且 isLayoutOnlyAndCollapsable 返回 true 的时候, isLayoutOnly 是true。 在添加 View 之前,会再判断一次 getNativeKind :

当node是虚拟节点或者 isLayoutOnly 是true 的时候,kind 为 NativeKind.NONE , 否则如果是叶子节点的话返回 NativeKine.LEAF , 否则返回 PARENT 。 所以中间很多层 RCTView 只是为了布局的时候使用,RN 已经很聪明的把这些辅助类的节点在实际渲染的时候给移除了。这样也能保证对应到 native 端的时候,做太多无用的层级渲染。 接下来就是把创建操作加入到真正的执行队列里面。RN维护了一个 UIViewOperationQueue 来维护各种关于 View 的操作。

创建 View 则是: CreateViewOperation 里面执行 NativeViewHierarchyManagercreateView

代码语言:javascript复制
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 开始执行。

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

代码语言:javascript复制
mUIImplementation.setChildren(viewTag, childrenTags);

SetChildrenOperation 中执行操作:

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

view的布局和属性

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

代码语言:javascript复制
try {
    mUIImplementation.dispatchViewUpdates(batchId);
} finally {
}

![image.png](https://cdn.nlark.com/yuque/0/2020/png/153347/1598502207164-07a2b879-0762-4a88-83ce-d135b08f9131.png#align=left&display=inline&height=349&margin=[object Object]&name=image.png&originHeight=698&originWidth=1698&size=153423&status=done&style=none&width=849) 这里会:

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

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

代码语言:javascript复制
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:https://github.com/facebook/yoga 如果hasNewLayout条件成立,则获取绝对位置的坐标来判断是否改变了布局。最后走到applyLayoutBase,这里计算x和y,然后从子view往上开始g更新坐标, ReactShadowNodeImple#dispatchUpdates

image.png

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

根据tag找到view之后:

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

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

总结

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

0 人点赞