前言
大家好,了解过React Native的同学都知道,RN为提供极佳的交互体验,会将RN组件转换为原生视图再渲染,作为一个Androider,比较好奇RN组件是怎么做的映射呢?中间会调用LayoutInflater吗?带着这一系列疑问开始了探索之旅。
为了方面描述,文中RN指代前端JS/TS页面,Native指代Android原生端。由于多数公司项目都是旧架构,因此源码逻辑分析也以RN旧架构为主。
一、寻找切入点
- 在原生中承接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中添加视图。
- 接着找一下
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,记录调用的方法和参数。
createView和setChildren的方法签名如下:
createView(int tag, String className, int rootViewTag, ReadableMap props)
setChildren(int viewTag, ReadableArray childrenTags)
根据打出来的log来看,createView和setChildren是互相掺杂着调用的,createView之后开始setChildren,这里为了直观,根据签名整理一下,将它们分开存放:
createView方法:
| 调用方法 | tag | className | rootViewTag | props |
|---|---|---|---|---|
| createView | 3 | RCTRawText | 11 | {"text":"Hello, World"} |
| createView | 5 | RCTText | 11 | {"fontSize":20,"allowFontScaling":true,"textAlign":"center","ellipsizeMode":"tail","isHighlighted":false,"selectionColor":null} |
| createView | 7 | RCTView | 11 | {"flex":1,"justifyContent":"center","backgroundColor":-1} |
| createView | 9 | RCTView | 11 | {"collapsable":true,"flex":1,"pointerEvents":"box-none"} |
| createView | 13 | RCTView | 11 | {"flex":1,"pointerEvents":"box-none"} |
setChildren方法:
| 调用方法 | viewTag | childrenTags |
|---|---|---|
| setChildren | 5 | [3] |
| setChildren | 7 | [5] |
| setChildren | 9 | [7] |
| setChildren | 13 | [9] |
| setChildren | 11 | [13] |
上表给人的感觉tag就像是唯一标识,先调用createView创建原生视图并设置tag,接着通过setChildren设置父子关系,最后再与根视图绑定。
如果与设想一致,那么RN页面对应的Native视图应该是这样:
借助CodeLocator观测实际原生视图绘制,发现原生除ReactRootView外其实只渲染了两层,并且证实tag确实是view的id。
| tag | className | 属性 | 原生视图 | 备注 |
|---|---|---|---|---|
| 13 | RCTView | {"flex":1,"pointerEvents":"box-none"} | x | RN默认增加的节点 |
| 9 | RCTView | {"collapsable":true,"flex":1,"pointerEvents":"box-none"} | x | RN默认增加的节点 |
| 7 | RCTView | {"flex":1,"justifyContent":"center","backgroundColor":-1} | ReactViewGroup | View标签 |
| 5 | RCTText | {"fontSize":20,"allowFontScaling":true,"textAlign":"center","ellipsizeMode":"tail","isHighlighted":false,"selectionColor":null} | ReactTextView | Text标签 |
| 3 | RCTRawText | {"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组件 | className | ShadowNode | View |
|---|---|---|---|
| Text、Button | RCTText | ReactTextShadowNode | ReactTextView |
| View | RCTView | LayoutShadowNode | ReactViewGroup |
| Image | RCTImageView | LayoutShadowNode | ReactImageView |
step2: 获取根节点
所有ShadowNode节点都会存储到mShadowNodeRegistry,根据rootViewTag获取到rootNode。
step3: 设置节点信息
新创建的ShadowNode对象存储tag、className和rootTag等信息。
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: 创建任务,压入待执行队列
- handleCreateView
protected void handleCreateView(
ReactShadowNode cssNode, int rootViewTag, @Nullable ReactStylesDiffMap styles) {
// 1. 非虚拟节点
if (!cssNode.isVirtual()) {
mNativeViewHierarchyOptimizer.handleCreateView(cssNode, cssNode.getThemedContext(), styles);
}
}
虚拟节点:是个辅助节点,不会映射为原生视图和YogaNode。
比如标签会拆解className为RCTText和RCTRawText两个ShadowNode,RCTRawText只是存储文字,当真正渲染的时候RCTText的节点可以通过父子关系直接从RCTRawText节点中取文本数据,因此RCTRawText不必参与原生视图转换,它就是虚拟节点。
public class ReactRawTextShadowNode extends ReactShadowNodeImpl {
// ...
@Override
public boolean isVirtual() {
return true;
}
}
- 非虚拟节点时调用
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也算是布局属性,具体见链接。
这也解释了上文部分节点无法映射为原生视图的原因。
| tag | className | 属性 | 原生视图 | 备注 |
|---|---|---|---|---|
| 13 | RCTView | {"flex":1,"pointerEvents":"box-none"} | x | 仅用于布局的节点 |
| 9 | RCTView | {"collapsable":true,"flex":1,"pointerEvents":"box-none"} | x | 仅用于布局的节点 |
| 7 | RCTView | {"flex":1,"justifyContent":"center","backgroundColor":-1} | ReactViewGroup | 有backgroundColor非布局属性 |
| 5 | RCTText | {"fontSize":20,"allowFontScaling":true,"textAlign":"center","ellipsizeMode":"tail","isHighlighted":false,"selectionColor":null} | ReactTextView | Text标签 |
| 3 | RCTRawText | {"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);
}
在真正执行时会调用ViewManger的createViewInstance方法去new View,之后为View设置id和属性。
2. setChildren
职责:关联ShadowNode、YogaNode、Native 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如果想操纵视图位置的话,必然会调用View的onLayout,在ReactViewGroup#onLayout中断点,观察调用堆栈。
和CreateViewOperation和ManageChildrenOperation相同,布局任务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引擎管理,计算完成之后自底向上逐级调用View的measure和layout来实现布局。