React Native 代码阅读(十三):View 是如何创建的

2019-03-27

前言

在上一篇中我们学习了 Yoga,这为分析 React Native 的视图层打了很好的基础。在本篇中我们分析一个 View 在 JavaScript 中写下,是如何被展示成 Android 原生的 View 的。

首先我们在 React Native 应用的 JavaScript 组件中定义一个简单的组件:

export default () => {
    return <View style={{
        backgroundColor: 'pink',
        width: 200, 
        height: 200}}/>        
}

这个组件会经由 JS → Native Bridge 转换为一下消息:

UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])

其中:

  • Bridge message 中包含的属性就是我们在 JavaScript 中定义的属性

UIManagerModule.createView

这条 message 最终会触发到 com.facebook.react.uimanager.UIManagerModule#createView 方法,它的参数签名为:

public void createView(int tag, String className, int rootViewTag, ReadableMap props) {

各个参数的值为:

参数
tag 343
className RCTView
rootViewTag 31
props { NativeMap: {"height":200,"width":200,"backgroundColor":-16181} }

其中:

  • tag 与 rootViewTag 与前面 message 中的值是一致的

UIImplementation.createView

UIManagerModule.createView 会继续调用 com.facebook.react.uimanager.UIImplementation#createView,它的方法签名与前者一致:

public void createView(int tag, String className, int rootViewTag, ReadableMap props) {

这里面这些参数都有什么作用呢?在我们继续分析的过程中会一一揭晓。

下面我们来看在这个方法中都做了哪些事情:

创建 ReactShadowNode

首先会创建 ReactShadowNode:

ReactShadowNode cssNode = createShadowNode(className);
ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);

其中:

  • className 是 RCTView,UIImplementation 会用它从 mViewManagers 找出已经注册的 ViewManager,并执行这个 Manager 的 createShadowNodeInstance 方法
  • 在本例中实际调用的是 com.facebook.react.uimanager.ViewGroupManager#createShadowNodeInstance
  • rootViewTag 是什么呢?它是每个节点的标识,在 UIImplementation 中 mShadowNodeRegistry 是盛放所有 Node 的地方。可以将 mShadowNodeRegistry 看做一个数组,将 rootViewTag 看做数组的 index,通过 index 向数组中获取 ReactShadowNode,在上面代码中获取到的是跟布局的 Node

之后对创建的新节点进行一些设置:

// 保存 tag
cssNode.setReactTag(tag);
// 保存类名
cssNode.setViewClassName(className);
// 保存父布局
cssNode.setRootTag(rootNode.getReactTag());
// 保存 ReactContext
cssNode.setThemedContext(rootNode.getThemedContext());

当这些步骤都完成后,将新 node 添加到 mShadowNodeRegistry 中:

mShadowNodeRegistry.addNode(cssNode);

之后保存 props 信息:

ReactStylesDiffMap styles = null;
if (props != null) {
    styles = new ReactStylesDiffMap(props);
    cssNode.updateProperties(styles);
}

UIImplementation.handleCreateView

之后会调用 handleCreateView 方法:

protected void handleCreateView(
        ReactShadowNode cssNode,
        int rootViewTag,
        @Nullable ReactStylesDiffMap styles) {
    if (!cssNode.isVirtual()) {
        mNativeViewHierarchyOptimizer.handleCreateView(cssNode, cssNode.getThemedContext(), styles);
    }
}

其中:

  • 如果新 node 不是 virtual node(有实际对应的 Native View),则会调用 mNativeViewHierarchyOptimizer 的 handleCreateView

NativeViewHierarchyOptimizer.handleCreateView

在 NativeViewHierarchyOptimizer.handleCreateView 中也没有真正创建视图,而是向队列中入队一个操作:

mUIViewOperationQueue.enqueueCreateView(
    themedContext,
    node.getReactTag(),
    node.getViewClass(),
    initialProps);

UIViewOperationQueue

UIViewOperationQueue 是一个任务队列,用于执行 JS batch bridge 中执行的指令。

还记得我们操作 JS 侧的 BatchedBridge 时的用法吗?常用的方法是:

  • 创建一系列 message,如视图创建、属性更新
  • 调用 flushedQueue 方法
  • 在 JS 侧给我们的感觉是,一调 flushedQueue,前面的这些 message 就会批量执行

在 Native 层,实现这个任务队列和批量执行的类想必已经猜到了,就是 UIViewOperationQueue。其中批量执行的方法对应于它的 flushPendingBatches 方法。

成员变量

下面先让我们看下 UIViewOperationQueue 的成员变量。

成员变量 类型 说明
mNativeViewHierarchyManager NativeViewHierarchyManager 布局相关的实际操作
mDispatchUIFrameCallback DispatchUIFrameCallback UI 更新回调
mOperations ArrayList<UIOperation> 任务队列
mDispatchUIRunnables ArrayList<Runnable> 积攒的一批一批任务

UIViewOperationQueue.enqueueCreateView

enqueueCreateView 方法会创建一个 CreateViewOperation,并插入 mOperations 中。

我们来看下 CreateViewOperation 的代码:

private final class CreateViewOperation extends ViewOperation {

    private final ThemedReactContext mThemedContext;
    private final String mClassName;
    private final @Nullable ReactStylesDiffMap mInitialProps;

    public CreateViewOperation(
        ThemedReactContext themedContext,
        int tag,
        String className,
        @Nullable ReactStylesDiffMap initialProps) {
            super(tag);
            mThemedContext = themedContext;
            mClassName = className;
            mInitialProps = initialProps;
            Systrace.startAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag);
    }

    @Override
    public void execute() {
        Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag);
        mNativeViewHierarchyManager.createView(
            mThemedContext,
            mTag,
            mClassName,
            mInitialProps);
    }
}

其中:

  • 需要核心关注的是 execute 方法。任务队列在实际执行中会调用它,在内部通过 mNativeViewHierarchyManager 时间真正的视图创建操作

NativeViewHierarchyManager.createView

下面我们就来看是如何真正创建 View 的。

// 取出 ViewManager
ViewManager viewManager = mViewManagers.get(className);

// 创建视图
// 实际调用 com.facebook.react.views.view.ReactViewManager#createViewInstance
View view = viewManager.createView(themedContext, mJSResponderHandler);

// 保存视图
// 还是通过 tag 关联,现在跟 tag 关联的有 ReactShadowNode 和 View
mTagsToViews.put(tag, view);
// viewManager 和 tag 关联
mTagsToViewManagers.put(tag, viewManager);

// 用 Android id 来保存 React Node Tag
view.setId(tag);

// 更新 Props
if (initialProps != null) {
    viewManager.updateProperties(view, initialProps);
}

在这里问一个问题:经过上面的代码后,我们创建的这个 View 会出现在屏幕上吗?

答案是并没有,我们虽然创建了 View 的实例,但是它还并没有被摆到屏幕上。

那么在哪里被摆到屏幕上呢?在此我们知道肯定会有一个统一的地方,统一的时机,来遍历视图树,将 View 摆放到正确的位置上。

manageChildren message

让我们再回到 JS → Native Bridge 上面,前面我们分析了 UIManager.createView 消息的解析。

当绘制一个新 React 组件时,会创建多条 UIManager.createView 和 UIManager.setChildren 命令。

在这些描述绘制的 message 发完后,最后会发一条 manageChildren 消息:

UIManager.manageChildren([209,[],[],[299],[1],[]])

这条消息会触发最终的页面绘制。

这条消息将会触发到 com.facebook.react.uimanager.UIManagerModule#manageChildren 方法。

它的方法签名如下:

public void manageChildren(
    int viewTag,
    @Nullable ReadableArray moveFrom,
    @Nullable ReadableArray moveTo,
    @Nullable ReadableArray addChildTags,
    @Nullable ReadableArray addAtIndices,
    @Nullable ReadableArray removeFrom) {

结合文档和前面的 message log,我们来看各个参数的作用:

参数名称 作用
viewTag 209 父 View 的 Tag
moveFrom [] 父 View 中要移出的视图
moveTo [] 与 moveFrom 已启用,移出后放到父 View 的序列
addChildTags [299] 要添加进入父 View 的视图
addAtIndices [1] 与 addChildTags 一起用,添加到的序列
removeFrom [] 需要从父视图中删除的序列的视图

从中我们可以得出结论,需要做的是向 209 中添加一个 tag 为 299 的 View。

经过一段与前面相同的辗转(这里虽然带过,但是内部的内容很多,ShadowTree 的层级创建就是在这一过程中),最后会向任务队列中插入一个 ManageChildrenOperation:

private final class ManageChildrenOperation extends ViewOperation {

  private final @Nullable int[] mIndicesToRemove;
  private final @Nullable ViewAtIndex[] mViewsToAdd;
  private final @Nullable int[] mTagsToDelete;

  public ManageChildrenOperation(
      int tag,
      @Nullable int[] indicesToRemove,
      @Nullable ViewAtIndex[] viewsToAdd,
      @Nullable int[] tagsToDelete) {
    super(tag);
    mIndicesToRemove = indicesToRemove;
    mViewsToAdd = viewsToAdd;
    mTagsToDelete = tagsToDelete;
  }

  @Override
  public void execute() {
    mNativeViewHierarchyManager.manageChildren(
        mTag,
        mIndicesToRemove,
        mViewsToAdd,
        mTagsToDelete);
  }
}

其中:最终执行的是 mNativeViewHierarchyManager.manageChildren,它会调用 ViewGroup 的 addView 方法,进行实际的 View 层级关联。

结论

本文是对 View 是如何创建的这个大过程的一个粗粒度的梳理,其中有很多步骤是不详尽的。其中有些过程可能梳理地不对,如有错误之处欢迎指正。

在后续的文章中会逐渐细化这些过程。