探秘React Native:从视图创建到布局渲染的流程解析

1,053 阅读13分钟

前言

大家好,了解过React Native的同学都知道,RN为提供极佳的交互体验,会将RN组件转换为原生视图再渲染,作为一个Androider,比较好奇RN组件是怎么做的映射呢?中间会调用LayoutInflater吗?带着这一系列疑问开始了探索之旅。

为了方面描述,文中RN指代前端JS/TS页面,Native指代Android原生端。由于多数公司项目都是旧架构,因此源码逻辑分析也以RN旧架构为主。

一、寻找切入点

  1. 在原生中承接RN页面的根节点是ReactRootView,如果涉及添加视图必然会调用addView,因此可以在addView中打个断点,观测调用的堆栈。

新建DebugReactRootView,是为了方便调试(直接断点View.java大概率会错行)。

class DebugReactRootView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ReactRootView(context, attrs) {

    override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
        super.addView(child, index, params)
    }
}

RN样式仅是View中包含一个“Hello, World”的Text。

const HelloWorld = () => {
  return (
    <View style={{flex: 1, justifyContent: 'center', backgroundColor: '#fff'}}>
      <Text style={{fontSize: 20, textAlign: 'center', margin: 10}}>Hello, World			</Text>
    </View>
  );
};

debug观测整个添加链路:

许多朋友对Choreographer不陌生,它与帧渲染相关,FrameCallback是指当新的一帧开始时的回调。

在Android中更新UI并不是立即触发的,而是等待新的一帧开始时批量触发,一般一帧耗时16.66ms,也就是说如果你想更新一个文字,可能16.66ms后才开始重绘。

从堆栈大体可以看出,新的一帧开始时开始批量执行UIViewOperationQueue中存储的Operation,其中有一项任务ManageChildrenOperation ,便是往ReactRootView中添加视图。

  1. 接着找一下ManageChildrenOperation是何时被创建并添加到队列中的,发现只有一处对象创建,即在enqueueManageChildren方法中,接着断点。

看到消息来自com.facebook.jni包中的NativeRunnable

JNI是指Java与C/C++交互调用的接口,RN与Native通信原理本质就是JS和Java借助C++层互通消息。

综上形成一种推论:视图相关的消息从C++传递给Java,经过若干步骤以任务的形式存储到任务队列中,待到下一帧绘制时再做视图操作。

二、RN会往原生传递那些消息呢?

从上图看到JavaModuleWrapper最先接收到RN的消息。

public class JavaModuleWrapper {

    // 原生支持调用的方法
    private final ArrayList<NativeModule.NativeMethod> mMethods;
    // xxxx
    @DoNotStrip
    public void invoke(int methodId, ReadableNativeArray parameters) {
        // 找到方法并执行
        mMethods.get(methodId).invoke(mJSInstance, parameters);
    }
}

JavaModuleWrapper中打印log,记录调用的方法和参数。

createViewsetChildren的方法签名如下:

createView(int tag, String className, int rootViewTag, ReadableMap props)

setChildren(int viewTag, ReadableArray childrenTags)

根据打出来的log来看,createViewsetChildren是互相掺杂着调用的,createView之后开始setChildren,这里为了直观,根据签名整理一下,将它们分开存放:

createView方法:

调用方法tagclassNamerootViewTagprops
createView3RCTRawText11{"text":"Hello, World"}
createView5RCTText11{"fontSize":20,"allowFontScaling":true,"textAlign":"center","ellipsizeMode":"tail","isHighlighted":false,"selectionColor":null}
createView7RCTView11{"flex":1,"justifyContent":"center","backgroundColor":-1}
createView9RCTView11{"collapsable":true,"flex":1,"pointerEvents":"box-none"}
createView13RCTView11{"flex":1,"pointerEvents":"box-none"}

setChildren方法:

调用方法viewTagchildrenTags
setChildren5[3]
setChildren7[5]
setChildren9[7]
setChildren13[9]
setChildren11[13]

上表给人的感觉tag就像是唯一标识,先调用createView创建原生视图并设置tag,接着通过setChildren设置父子关系,最后再与根视图绑定。

如果与设想一致,那么RN页面对应的Native视图应该是这样:

借助CodeLocator观测实际原生视图绘制,发现原生除ReactRootView外其实只渲染了两层,并且证实tag确实是view的id。

tagclassName属性原生视图备注
13RCTView{"flex":1,"pointerEvents":"box-none"}xRN默认增加的节点
9RCTView{"collapsable":true,"flex":1,"pointerEvents":"box-none"}xRN默认增加的节点
7RCTView{"flex":1,"justifyContent":"center","backgroundColor":-1}ReactViewGroupView标签
5RCTText{"fontSize":20,"allowFontScaling":true,"textAlign":"center","ellipsizeMode":"tail","isHighlighted":false,"selectionColor":null}ReactTextViewText标签
3RCTRawText{"text":"Hello, World"}x纯文字

从上表看出tag 3、9、13并没有渲染为原生视图。看起来像是某种优化策略导致的,带着这个疑问一起阅读下addView和setChildren方法。

三、两个方法

1. createView

职责:创建ShadowNode、创建Native View,并解析RN组件属性。

// 例:
// tag: 7; className: RCTView; rootViewTag: 21; 
// props: {"flex":1,"justifyContent":"center","backgroundColor":-1}
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
  synchronized (uiImplementationThreadLock) {
    // step1  
    ReactShadowNode cssNode = createShadowNode(className);
    // step2  
    ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
    // step3  
    cssNode.setReactTag(tag);
    cssNode.setViewClassName(className);
    cssNode.setRootTag(rootNode.getReactTag());
    cssNode.setThemedContext(rootNode.getThemedContext());
    mShadowNodeRegistry.addNode(cssNode);
    // step4
    ReactStylesDiffMap styles = null;
    if (props != null) {
      styles = new ReactStylesDiffMap(props);
      cssNode.updateProperties(styles);
    }
    // step5
    handleCreateView(cssNode, rootViewTag, styles);
  }
}
step1:创建ShadowNode节点
protected ReactShadowNode createShadowNode(String className) {
  ViewManager viewManager = mViewManagers.get(className);
  return viewManager.createShadowNodeInstance(mReactContext);
}

根据className获取ViewManager,从ViewManager中获取ShadowNode节点,不熟悉这部分的参考Android 原生UI组件 · React Native 中文网

常见的RN组件与ShadowNode之间的映射关系:

RN组件classNameShadowNodeView
Text、ButtonRCTTextReactTextShadowNodeReactTextView
ViewRCTViewLayoutShadowNodeReactViewGroup
ImageRCTImageViewLayoutShadowNodeReactImageView
step2: 获取根节点

所有ShadowNode节点都会存储到mShadowNodeRegistry,根据rootViewTag获取到rootNode

step3: 设置节点信息

新创建的ShadowNode对象存储tagclassNamerootTag等信息。

step4: 记录布局信息

updateProperties本质会调用updateProps

public static <T extends ReactShadowNode> void updateProps(T node, ReactStylesDiffMap props) {
    ShadowNodeSetter<T> setter = findNodeSetter(node.getClass());
    Iterator<Map.Entry<String, Object>> iterator = props.mBackingMap.getEntryIterator();
    while (iterator.hasNext()) {
        Map.Entry<String, Object> entry = iterator.next();
        setter.setProperty(node, entry.getKey(), entry.getValue());
    }
}

该方法使用访问者设计模式,寻找合适能处理属性的ShadowNodeSetter,默认情况下是FallbackShadowNodeSetter,开发者也可以根据自己的需要自定义。布局相关的属性值(如height,width,padding等)默认通过反射最终交给ShadowNode中的YogaNode

public void setLayoutDirection(YogaDirection direction) {
    mYogaNode.setDirection(direction);
}

注意, Yoga是一个跨平台的布局引擎,基于Flexbox算法可以自动计算父子组件的布局关系,支持 Android、iOS等多个平台,能够让React Native在不同平台上有一致的布局表现。

这里的布局参数值都交给了Yoga统一管理,在真正绘制时Yoga会同步告诉View具体的位置和大小。

step5: 创建任务,压入待执行队列
  1. handleCreateView
protected void handleCreateView(
        ReactShadowNode cssNode, int rootViewTag, @Nullable ReactStylesDiffMap styles) {
    // 1. 非虚拟节点
    if (!cssNode.isVirtual()) {
        mNativeViewHierarchyOptimizer.handleCreateView(cssNode, cssNode.getThemedContext(), styles);
    }
}

虚拟节点:是个辅助节点,不会映射为原生视图和YogaNode。

比如标签会拆解className为RCTTextRCTRawText两个ShadowNode,RCTRawText只是存储文字,当真正渲染的时候RCTText的节点可以通过父子关系直接从RCTRawText节点中取文本数据,因此RCTRawText不必参与原生视图转换,它就是虚拟节点。

public class ReactRawTextShadowNode extends ReactShadowNodeImpl {
  // ...
  @Override
  public boolean isVirtual() {
    return true;
  }
}
  1. 非虚拟节点时调用mNativeViewHierarchyOptimizer.handleCreateView
public void handleCreateView(
        ReactShadowNode node,
        ThemedReactContext themedContext,
        @Nullable ReactStylesDiffMap initialProps) {

    // RN官方自己debug用的,该分支不会走到的  
    if (!ENABLED) {
        // xxx
        return;
    }

    // 当前节点是否仅用于布局
    // 节点是RCTView 并且 属性都是布局属性
    boolean isLayoutOnly =
            node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME)
                    && isLayoutOnlyAndCollapsable(initialProps);
    node.setIsLayoutOnly(isLayoutOnly);

    // 是否是虚拟节点 或者 仅用于布局的节点
    if (node.getNativeKind() != NativeKind.NONE) {
        // 将创建View的事件放入队列中    
        mUIViewOperationQueue.enqueueCreateView(
                themedContext, node.getReactTag(), node.getViewClass(), initialProps);
    }
}

// UIViewOperationQueue.java
public void enqueueCreateView(
        ThemedReactContext themedContext,
        int viewReactTag,
        String viewClassName,
        @Nullable ReactStylesDiffMap initialProps) {
    synchronized (mNonBatchedOperationsLock) {
        mCreateViewCount++;
        mNonBatchedOperations.addLast(
                new CreateViewOperation(themedContext, viewReactTag, viewClassName, initialProps));
    }
}

除了“仅用于布局的节点或者虚拟节点”外,其它情况都会将CreateViewOperation压入待执行队列之中,去建立ShadowNode 对应的native视图。

先解释一下NativeKind:

@Override
public NativeKind getNativeKind() {
    return isVirtual() || isLayoutOnly()
            ? NativeKind.NONE
            : hoistNativeChildren() ? NativeKind.LEAF : NativeKind.PARENT;
}
  • NONE:是虚拟节点或者仅用来布局的节点,这两种都不会转换为真实的Native控件
  • LEAF:能转换Native控件,并且是叶子节点。
  • PARENT:能转换Native控件,并且该控件内包含子控件。

上文已经解释过虚拟节点,再来说一下仅用于布局的节点:

如果所有的属性都是布局属性,便认为这个节点只是用于约束子布局,而布局属性既然已经全部交给了yoga管理,那么该节点就并不需要真正绘制。以下属性均是布局属性:

public static final String ALIGN_ITEMS = "alignItems";
public static final String ALIGN_SELF = "alignSelf";
public static final String ALIGN_CONTENT = "alignContent";
public static final String DISPLAY = "display";
public static final String BOTTOM = "bottom";
public static final String COLLAPSABLE = "collapsable";
public static final String FLEX = "flex";
public static final String FLEX_GROW = "flexGrow";
public static final String FLEX_SHRINK = "flexShrink";
public static final String FLEX_BASIS = "flexBasis";
public static final String FLEX_DIRECTION = "flexDirection";
public static final String FLEX_WRAP = "flexWrap";
public static final String ROW_GAP = "rowGap";
public static final String COLUMN_GAP = "columnGap";
public static final String GAP = "gap";
public static final String HEIGHT = "height";
public static final String JUSTIFY_CONTENT = "justifyContent";
public static final String LEFT = "left";

public static final String MARGIN = "margin";
public static final String MARGIN_VERTICAL = "marginVertical";
public static final String MARGIN_HORIZONTAL = "marginHorizontal";
public static final String MARGIN_LEFT = "marginLeft";
public static final String MARGIN_RIGHT = "marginRight";
public static final String MARGIN_TOP = "marginTop";
public static final String MARGIN_BOTTOM = "marginBottom";
public static final String MARGIN_START = "marginStart";
public static final String MARGIN_END = "marginEnd";

public static final String PADDING = "padding";
public static final String PADDING_VERTICAL = "paddingVertical";
public static final String PADDING_HORIZONTAL = "paddingHorizontal";
public static final String PADDING_LEFT = "paddingLeft";
public static final String PADDING_RIGHT = "paddingRight";
public static final String PADDING_TOP = "paddingTop";
public static final String PADDING_BOTTOM = "paddingBottom";
public static final String PADDING_START = "paddingStart";
public static final String PADDING_END = "paddingEnd";

public static final String POSITION = "position";
public static final String RIGHT = "right";
public static final String TOP = "top";
public static final String WIDTH = "width";
public static final String START = "start";
public static final String END = "end";

除此以外还有些其它判断,如pointerEvents=box-none、collapsable=true也算是布局属性,具体见链接

这也解释了上文部分节点无法映射为原生视图的原因。

tagclassName属性原生视图备注
13RCTView{"flex":1,"pointerEvents":"box-none"}x仅用于布局的节点
9RCTView{"collapsable":true,"flex":1,"pointerEvents":"box-none"}x仅用于布局的节点
7RCTView{"flex":1,"justifyContent":"center","backgroundColor":-1}ReactViewGroup有backgroundColor非布局属性
5RCTText{"fontSize":20,"allowFontScaling":true,"textAlign":"center","ellipsizeMode":"tail","isHighlighted":false,"selectionColor":null}ReactTextViewText标签
3RCTRawText{"text":"Hello, World"}x虚拟节点
step6: CreateViewOperation执行
private final class CreateViewOperation extends ViewOperation {
    @Override
    public void execute() {
      mNativeViewHierarchyManager.createView(mThemedContext, mTag, mClassName, mInitialProps);
    }
}

// ViewManager.java
protected @NonNull T createViewInstance(
    int reactTag,
    @NonNull ThemedReactContext reactContext,
    @Nullable ReactStylesDiffMap initialProps,
    @Nullable StateWrapper stateWrapper) {
    T view = null;
    // ViewManger中支持循环利用View,默认是false
    @Nullable Stack<T> recyclableViews = getRecyclableViewStack(reactContext.getSurfaceId());
    if (recyclableViews != null && !recyclableViews.empty()) {
        view = recycleView(reactContext, recyclableViews.pop());
    } else {
        // ⭐️ 默认走这里,调用createViewInstance
        view = createViewInstance(reactContext);
    }
    // 将tag设置为id
    view.setId(reactTag);
    addEventEmitters(reactContext, view);
    if (initialProps != null) {
        // 设置除布局以外的属性,如setBackgroundColor、setText
        updateProperties(view, initialProps);
    }
} 

// ReactTextViewManager.java
public ReactTextView createViewInstance(ThemedReactContext context) {
   return new ReactTextView(context);
}

在真正执行时会调用ViewMangercreateViewInstance方法去new View,之后为View设置id和属性。

2. setChildren

职责:关联ShadowNodeYogaNodeNative View之间的父子关系。

// 例:
// viewTag:7;childrenTags:[5]
public void setChildren(int viewTag, ReadableArray childrenTags) {
    // 取出parent节点
    ReactShadowNode cssNodeToManage = mShadowNodeRegistry.getNode(viewTag);
    for (int i = 0; i < childrenTags.size(); i++) {
        // 取出子节点  
        ReactShadowNode cssNodeToAdd = mShadowNodeRegistry.getNode(childrenTags.getInt(i));
        // step1:关联父子节点
        cssNodeToManage.addChildAt(cssNodeToAdd, i);
    }
    // step2:关联Native View的父子关系
    mNativeViewHierarchyOptimizer.handleSetChildren(cssNodeToManage, childrenTags);
}
step1: 关联Node的父子节点
public void addChildAt(ReactShadowNodeImpl child, int i) {
    // 关联父子节点
    mChildren.add(i, child);
    child.mParent = this;

    // 如果当前非yoga叶子节点就关联父子节点
    if (mYogaNode != null && !isYogaLeafNode()) {
        YogaNode childYogaNode = child.mYogaNode;
        mYogaNode.addChildAt(childYogaNode, i);
    }
    //...
}

关联父子节点,除此之外还关联Yoga节点,让Yoga感知RN各节点父子关系,方便统一管理RN视图的布局。

step2: 关联Native View视图的父子关系

最后关联的结果与原生实际创建和绘制的视图一致。

public void handleSetChildren(ReactShadowNode nodeToManage, ReadableArray childrenTags) {
    for (int i = 0; i < childrenTags.size(); i++) {
        // 子节点  
        ReactShadowNode nodeToAdd = mShadowNodeRegistry.getNode(childrenTags.getInt(i));
        addNodeToNode(nodeToManage, nodeToAdd, i);
    }
}

遍历所有的子Node,并addNodeToNode方法。

private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) {
    int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index));
    if (parent.getNativeKind() != NativeKind.PARENT) {
        // 寻找NativeKind为NativeKind.PARENT,找不到return
        NodeIndexPair result = walkUpUntilNativeKindIsParent(parent, indexInNativeChildren);
        if (result == null) {
            return;
        }
        parent = result.node;
        indexInNativeChildren = result.index;
    }
    // child是Native节点?
    if (child.getNativeKind() != NativeKind.NONE) {
        // 是,则关联父子关系  
        addNativeChild(parent, child, indexInNativeChildren);
    } else {
        // 否,判断父节点是否要与孙子节点关联
        addNonNativeChild(parent, child, indexInNativeChildren);
    }
}

private void addNativeChild(ReactShadowNode parent, ReactShadowNode child, int index) {
    parent.addNativeChildAt(child, index);
    mUIViewOperationQueue.enqueueManageChildren(
            parent.getReactTag(),
            null,
            new ViewAtIndex[] {new ViewAtIndex(child.getReactTag(), index)},
            null);

    if (child.getNativeKind() != NativeKind.PARENT) {
        addGrandchildren(parent, child, index + 1);
    }
}

// UIViewOperationQueue.java
public void enqueueManageChildren(
        int reactTag,
        @Nullable int[] indicesToRemove,
        @Nullable ViewAtIndex[] viewsToAdd,
        @Nullable int[] tagsToDelete) {
    mOperations.add(
            new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete));
}

核心思路是:非Native的节点(即NativeKind.NONE)不做关联操作。

从当前父节点出发,逐层找到一个Native父节点作为parent。

  • 如果当前节点是Native节点,那么父子关联(addNativeChild),如果子节点本身就是叶子节点了,那么它的子孙Native节点(复杂情况)要关联到当前parent下(addGrandchildren)。
  • 如果当前节点不是Native节点,那么它的子孙Native节点要关联到当前parent下(addGrandchildren)。

感兴趣的同学可以自行翻阅源码NativeViewHierarchyOptimizer

step3:ManageChildrenOperation执行
private final class ManageChildrenOperation extends ViewOperation {
    @Override
    public void execute() {
        mNativeViewHierarchyManager.manageChildren(
                mTag, mIndicesToRemove, mViewsToAdd, mTagsToDelete);
    }
}

// NativeViewHierarchyManager.java
public synchronized void manageChildren(
        final int tag,
        @Nullable int[] indicesToRemove,
        @Nullable ViewAtIndex[] viewsToAdd,
        @Nullable int[] tagsToDelete) {
    // 父View  
    final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag);
    final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag);
    // ...
    if (viewsToAdd != null) {
        for (int i = 0; i < viewsToAdd.length; i++) {
            ViewAtIndex viewAtIndex = viewsToAdd[i];
            View viewToAdd = mTagsToViews.get(viewAtIndex.mTag);
            // ...
            // ⭐️
            viewManager.addView(viewToManage, viewToAdd, normalizedIndex);
        }
    }
}

// ViewGroupManager.java
@Override
public void addView(T parent, View child, int index) {
    parent.addView(child, index);
}

mangeChildren方法是同时支持删除和增加View的,当viewsToAdd不为空时最终是会调用viewManager.addView,本质与常用的addView完全一致。

四、布局定位

上面提到Yoga引擎可以操纵RN对应原生视图的布局,是怎么做到的呢?

1. 切入

Yoga如果想操纵视图位置的话,必然会调用ViewonLayout,在ReactViewGroup#onLayout中断点,观察调用堆栈。

CreateViewOperationManageChildrenOperation相同,布局任务UpdateLayoutOperation被放到了任务队列中,等待新的一帧开始执行,同样断点UpdateLayoutOperation被创建处。

观察发现核心逻辑在applyUpdatesRecursive,该方法采用后序遍历自底往上摆放View的位置。

2. 任务生成

protected void applyUpdatesRecursive(ReactShadowNode cssNode, float absoluteX, float absoluteY) {
    if (!cssNode.hasUpdates()) {
        return;
    }

    Iterable<? extends ReactShadowNode> cssChildren = cssNode.calculateLayoutOnChildren();
    if (cssChildren != null) {
        // 后序遍历
        for (ReactShadowNode cssChild : cssChildren) {
            applyUpdatesRecursive(
                    cssChild, absoluteX + cssNode.getLayoutX(), absoluteY + cssNode.getLayoutY());
        }
    }

    int tag = cssNode.getReactTag();
    if (!mShadowNodeRegistry.isRootNode(tag)) {
        // ⭐️ 调用cssNode.dispatchUpdates
        boolean frameDidChange =
                cssNode.dispatchUpdates(
                        absoluteX, absoluteY, mOperationsQueue, mNativeViewHierarchyOptimizer);

        // ...
    }
    cssNode.markUpdateSeen();
    mNativeViewHierarchyOptimizer.onViewUpdatesCompleted(cssNode);
}

// ReactShadowNodeImpl.java
public boolean dispatchUpdates(
        float absoluteX,
        float absoluteY,
        UIViewOperationQueue uiViewOperationQueue,
        @Nullable NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
    if (mNodeUpdated) {
        onCollectExtraUpdates(uiViewOperationQueue);
    }

    if (hasNewLayout()) {
        // 从yogaNode中获取相对父布局的x, y坐标, 宽高 
        float layoutX = getLayoutX();
        float layoutY = getLayoutY();
        // 计算占据屏幕的像素点,它的长和宽必须是整数个像素点,长宽起点也必须是像素点的起点
        int newAbsoluteLeft = Math.round(absoluteX + layoutX);
        int newAbsoluteTop = Math.round(absoluteY + layoutY);
        int newAbsoluteRight = Math.round(absoluteX + layoutX + getLayoutWidth());
        int newAbsoluteBottom = Math.round(absoluteY + layoutY + getLayoutHeight());

        int newScreenX = Math.round(layoutX);
        int newScreenY = Math.round(layoutY);
        int newScreenWidth = newAbsoluteRight - newAbsoluteLeft;
        int newScreenHeight = newAbsoluteBottom - newAbsoluteTop;
        // 校验位置是否发生了变化
        boolean layoutHasChanged =
                newScreenX != mScreenX
                        || newScreenY != mScreenY
                        || newScreenWidth != mScreenWidth
                        || newScreenHeight != mScreenHeight;

        mScreenX = newScreenX;
        mScreenY = newScreenY;
        mScreenWidth = newScreenWidth;
        mScreenHeight = newScreenHeight;

        if (layoutHasChanged) {
            // TODO: T26400974 ReactShadowNode should not depend on nativeViewHierarchyOptimizer
            if (nativeViewHierarchyOptimizer != null) {
                nativeViewHierarchyOptimizer.handleUpdateLayout(this);
            } else {
                // ⭐️ 压入任务队列
                // getScreenX() 几乎等价 getLayoutX()
                // getScreenY() 几乎等价 getLayoutY()
                // getScreenWidth() 几乎等价 getLayoutWidth()
                // getScreenHeight() 几乎等价 getLayoutHeight()
                // 宽、高、相对坐标都是有YogaNode产生  
                uiViewOperationQueue.enqueueUpdateLayout(
                        getParent().getReactTag(),
                        getReactTag(),
                        getScreenX(),
                        getScreenY(),
                        getScreenWidth(),
                        getScreenHeight());
            }
        }

        return layoutHasChanged;
    } else {
        return false;
    }
}

// UIViewOperationQueue.java
public void enqueueUpdateLayout(
        int parentTag, int reactTag, int x, int y, int width, int height) {
    // ⭐️
    mOperations.add(new UpdateLayoutOperation(parentTag, reactTag, x, y, width, height));
}

该过程边后序遍历边记录屏幕坐标,计算出每个节点的视图宽高和相对坐标之后将UpdateLayoutOperation压入队列。

3. 任务执行

private final class UpdateLayoutOperation extends ViewOperation {
    @Override
    public void execute() {
      mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight);
    }
  }

  public synchronized void updateLayout(
      int parentTag, int tag, int x, int y, int width, int height) {
    try {
      View viewToUpdate = resolveView(tag);
      // ⭐️ 1. 测量  
      viewToUpdate.measure(
          View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
          View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));

      ViewParent parent = viewToUpdate.getParent();
      if (parent instanceof RootView) {
        parent.requestLayout();
      }

      // Check if the parent of the view has to layout the view, or the child has to lay itself out.
      if (!mRootTags.get(parentTag)) {
        ViewManager parentViewManager = mTagsToViewManagers.get(parentTag);
        IViewManagerWithChildren parentViewManagerWithChildren;
        if (parentViewManager instanceof IViewManagerWithChildren) {
          parentViewManagerWithChildren = (IViewManagerWithChildren) parentViewManager;
        } else {
          throw new IllegalViewOperationException(
              "Trying to use view with tag "
                  + parentTag
                  + " as a parent, but its Manager doesn't implement IViewManagerWithChildren");
        }
        if (parentViewManagerWithChildren != null
            && !parentViewManagerWithChildren.needsCustomLayoutForChildren()) {
          // ⭐️ 2. 布局  
          updateLayout(viewToUpdate, x, y, width, height);
        }
      } else {
        updateLayout(viewToUpdate, x, y, width, height);
      }
    } finally {
      Systrace.endSection(Systrace.TRACE_TAG_REACT_VIEW);
    }
  }

  private void updateLayout(View viewToUpdate, int x, int y, int width, int height) {
    if (mLayoutAnimationEnabled && mLayoutAnimator.shouldAnimateLayout(viewToUpdate)) {
      mLayoutAnimator.applyLayoutUpdate(viewToUpdate, x, y, width, height);
    } else {
      // ⭐️  
      viewToUpdate.layout(x, y, x + width, y + height);
    }
  }
3.1. 测量

调用view的measure方法将Yoga测量的结果传入,ReactNative的原生组件使用setMeasuredDimension设置宽高,不做任何多余的逻辑判断;由于每一个视图都由Yoga管控,因此ReactViewGroup也不需要再去测量子View。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);

    setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
3.2. 布局

调用View的layout方法,由于layout方法中已经设置好上下左右四个位置了,因此onLayout中也无需做任何处理。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // No-op since UIManagerModule handles actually laying out children.
}

总结

ReactNative以tag作为唯一标识与ShadowNode建立映射关系,通过操作ShadowNode来实现创建视图、添加视图,所有的布局属性统一交由Yoga引擎管理,计算完成之后自底向上逐级调用Viewmeasurelayout来实现布局。