前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >​React Native是怎么渲染出原生组件的

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

作者头像
烧麦程
发布2022-05-10 20:46:43
2.3K0
发布2022-05-10 20:46:43
举报
文章被收录于专栏:半行代码

最近工作需要研究了一下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=%5Bobject%20Object%5D&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 最终呈现在屏幕上。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半行代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 入口
    • 创建View
      • native的布局
        • 添加native View
      • view的布局和属性
        • 总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档