最近工作需要研究了一下React Native 的工作流程,理了一下 React Native 是怎么把控件最终渲染在屏幕上的。 在开始研究这个问题之前,我们缕一下我们的困惑:
整个JS 端的逻辑都从默认的 index.js
开始执行,代码也只有一行:
这里会调用RN的 renderApplication
方法。触发 ReactNativeType
的 render 方法。 ReactNativeType
根据是否是 fabric
实现来决定最终的实现。
接着按照如下的调用顺序执行了一连串建立 dom 树的操作,这部分的操作是按照 React
的 Reconcilation
算法来执行的:
updateContainer
scheduleUpdateOnFiber
flushSyncCallbackQueue
flushSyncCallbackQueueImpl
runWithPriority
performSyncWorkOnRoot
workLoopSync
最后在
function completeUnitOfWork(unitOfWork) {
}
里面执行 completeWork
, 内部会根据
workInProgress.tag
来判断当前的操作。创建组件则在 HostComponent
里面:
这里的关键逻辑就是 创建实例 -> 添加创建的节点 -> 初始化创建的节点。
这里调用 UIManager
的 createView
创建 View,最后根据 tag、viewConfig 等字段得到 component 对象。
这个 UIManager
在 Android 端对应的是 com.facebook.react.bridge.UIManager
。实现类是: com.facebook.react.uimanager.UIManagerModule
Android端调用到 UImanagerModule
后会通过 createView
来创建 View:
这里传入的参数:
UIImplementation
创建 View 的按照这个逻辑去执行:
ReactShadowNode
对象ReactShadowNode cssNode = createShadowNode(className);
ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
cssNode.setReactTag(tag); // Thread safety needed here
cssNode.setViewClassName(className);
cssNode.setRootTag(rootNode.getReactTag());
cssNode.setThemedContext(rootNode.getThemedContext());
ShadowNodeRegistry
:mShadowNodeRegistry.addNode(cssNode);
image.png
handleCreateView(cssNode, rootViewTag, styles);
关于 view 的id, js端有自己的生成规则:
id 每次加上2,但是个位数是1的会进行保留,用作root的id。所以在 Native 端,root view的id 则每次都是分配的1。
看完了创建,我们通过一个实例来看看具体的布局:
这是一个加入了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
里面执行 NativeViewHierarchyManager
的 createView
。
View view = viewManager.createView(themedContext, null, null, mJSResponderHandler);
mTagsToViews.put(tag, view);
mTagsToViewManagers.put(tag, viewManager);
view.setId(tag);
native需要创建的 View 已经创建了,那么这时候如何把创建出来的 View 添加到 ViewGroup 里面去呢?JS 端会从 finalizeInitialChildren
开始执行。
这里调用了 UIManager
的 setChildren
函数; 同理,会执行 Android 端的
mUIImplementation.setChildren(viewTag, childrenTags);
在 SetChildrenOperation
中执行操作:
这里会找到root表示的parent和我们要添加的children view,把 children 添加到 root 里面去。
View 创建出来了,也添加到父布局里面了,接下来就是进行布局了。那么 RN 是怎么进行布局的呢?通过断点,我们能找到在开始布局的时候从root开始进行树层级的更新。这里会从jni层开始执行到java层的 NativeRunnable 里面,最后走到 UIManagerModule
的 onBatchComplete
方法:
try {
mUIImplementation.dispatchViewUpdates(batchId);
} finally {
}
 这里会:
执行 updateViewHierarchy
, 每个rootview下面都要执行。当root的measurespec不为空的时候,就执行。
calculateRootLayout(cssRoot);
applyUpdatesRecursive(cssRoot, 0f, 0f);
if (mLayoutUpdateListener != null) {
mOperationsQueue.enqueueLayoutUpdateFinished(cssRoot,mLayoutUpdateListener);
}
这里的计算布局其实是调用了 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 最终呈现在屏幕上。