ReactNative是如何让JS代码『变成』Android控件的?
编写的JS代码是如何『变』成一个个Android的控件的呢?JS的FlexBox布局是如何『转译』成Android的布局的呢?
在找到这两个问题答案前,先介绍下RN的渲染引擎——Yoga。
Yoga
随着这几年前端技术的崛起,作为前端UI骨架的布局系统也在其中占据了越来越重要的位置。不管是在移动端、桌面端还是Web端,特别是不同设备的屏幕大小和分辨率千变万化,如何构建良好的布局系统以便应付这些变化已经变得越来越重要。
目前,各个平台都有自己的一套解决方案。iOS平台有自动布局系统,Android有容器布局系统,而Web端有基于CSS的布局系统。多种布局系统共存所带来的弊端是很明显的,平台间的共享变得很困难,而每个平台都需要专人来开发维护,增加了开发成本。
Facebook在这个问题上没有少下功夫。首先,Facebook在React Native里引入了一种跨平台的基于CSS的布局系统,它实现了Flexbox规范。基于这个布局系统,不同团队终于可以走到一起,一起解决缺陷,改进性能,让这个系统更加地贴合Flexbox规范。
随着这个系统的不断完善,Facebook决定对它进行重启发布,并取名Yoga。
Yoga是基于C实现的。之所以选择C,首先当然是从性能方面考虑的。基于C实现的Yoga比之前Java实现在性能上提升了33%。其次,使用C实现可以更容易地跟其它平台集成。到目前为止,Yoga已经有以下几个平台的绑定:Java(Android)、Objective-C(UIKit)、C#(.NET)。
Yoga使用方式
目前Yoga的1.5.0在Android上已经支持直接使用xml布局了,但接下来我要追踪的View渲染原理肯定不会在Android上生成静态xml布局。所以这里还得看下如何使用Java代码编写Yoga布局。
这里我直接引用官网的代码示例。
YogaNode root = new YogaNode();
root.setWidth(500);
root.setHeight(300);
root.setAlignItems(CENTER);
root.setJustifyContent(CENTER);
root.setPadding(ALL, 20);
YogaNode text = new YogaNode();
text.setWidth(200);
text.setHeight(25);
YogaNode image = new YogaNode();
image.setWidth(50);
image.setHeight(50);
image.setPositionType(ABSOLUTE);
image.setPosition(END, 20);
image.setPosition(TOP, 20);
root.addChildAt(text, 0);
root.addChildAt(image, 1);
简而言之,创建子节点并设置显示属性后放入根节点即完成了Yoga的布局。
渲染原理
如何布局
我的思路是要找到这个问题的答案,首先要先搞清楚View如何被创建?
之前看到过一篇文章React-Native 源码分析二-JSX如何渲染成原生页面,这里作者直接把我引导到UIManagerModule
,帮我少走了不少弯路。这个类里有好多被声明为@ReactMethod
的方法。
public class UIManagerModule extends ReactContextBaseJavaModule {
··· @ReactMethod
public void createView()
@ReactMethod public void updateView()
@ReactMethod public void manageChildren()
@ReactMethod public void measure()
···
}
以上我只列举了一部分,但通过方法名称即可了解这里是JS和Native关于页面渲染的切口。跟踪createView()方法,发现了ShaowNode的创建。
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
ReactShadowNode cssNode = createShadowNode(className);
ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
cssNode.setReactTag(tag);
cssNode.setViewClassName(className);
cssNode.setRootNode(rootNode);
cssNode.setThemedContext(rootNode.getThemedContext());
mShadowNodeRegistry.addNode(cssNode);
···
}
这里只是把创建好的ShaowNode放到ShadowNodeRegistry
里。ShadowNodeRegistry
的核心是一个存放ReactShadowNode
的列表。
SparseArray<ReactShadowNode>
那么肯定会有逻辑来遍历这个列表来处理单个ReactShadowNode
。
接下来我用JS写了一个简单的布局来跟踪具体布局流程。
<View style={{flex:1}}>
<View style={{flex:1}}>
<Text style={{flex:1}}>显示1</Text>
</View>
<View style={{flex:1}}>
<Text style={{flex:1}}>显示2</Text>
</View></View>
按照之前上面createView()里的逻辑,ShadowNodeRegistry
里会有所有View的ReactShadowNode
。
但是发现里面有10个ReactShadowNode
。多出来的几个我们后面分析,继续跟踪布局流程。
接下来会通过递归的方式将各个ReactShadowNode
以树的方式连接起来。
这里要说的一点是RCTRawText
仅包含Text的文案,和RCTText
是一一绑定的,但在创建View的过程中也会被封装成一个ReactShadowNode
。后来经过测试,发现2-RCTView
和3-RTCView
除了承载要显示的内容之外,还会承载一些框架自己的View,比如经常看到的黄色提示框。所以去除2、3、7和10之后,整个树状结构和JS代码里的布局结构就一一对应起来了。
本以为在Native的View层级也是这样,但通过Layout Inspector发现比这个简单。
那么4、5和8去哪了?这个问题我后面再跟踪,继续看View是怎样布局的。
我觉得这里要简要说明下Native对于View的处理流程。Native接收到JS的渲染请求后,会将其封装为类型为ViewOperation
的操作原子类。这些原子类会被暂时缓存到一个列表中。当UIManagerModule
的onBatchComplete()被调用才会轮询缓存列表执行操作。简单来说onBatchComplete()是Native和JS完成一次通讯后才会被调用,这块流程可以参阅Native和JS通讯原理了解。
View的创建,尺寸计算、更新坐标等细节操作均被封装成了不同的ViewOperation
。在UIViewOperationQueue
里可以看到详细类型。这里我只列举几个。
private final class UpdateLayoutOperation extends ViewOperation {
··· @Override
public void execute() {
Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "updateLayout", mTag);
mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight);
}
} private final class CreateViewOperation extends ViewOperation {
··· @Override
public void execute() {
Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag);
mNativeViewHierarchyManager.createView(
mThemedContext,
mTag,
mClassName,
mInitialProps);
}
} private final class ManageChildrenOperation extends ViewOperation {
··· @Override
public void execute() {
mNativeViewHierarchyManager.manageChildren(
mTag,
mIndicesToRemove,
mViewsToAdd,
mTagsToDelete);
}
}
回到刚才的问题,发现View的布局最终由UpdateLayoutOperation
执行。从execute()一路跟下去,最终发现在NativeViewHierarchyManager
的updateLayout()方法里找到了View的布局逻辑。
private void updateLayout(View viewToUpdate, int x, int y, int width, int height) {
···
viewToUpdate.layout(x, y, x + width, y + height);
···
}
至于x和y是怎样被计算的,暂且不去研究,但追踪到这里可以明确,View的布局并没有使用Android视图容器组件的特性,而是通过计算View的x、y坐标后,直接使用layout()方法在父控件里定位,这也就变相解释了ReactRootView
的父类是FrameLayout
的用意。
接下来要弄明白的一个问题是那些最终没有展示在屏幕上的ReactShadowNode
去哪了?猜测应该有类似『过滤器』的逻辑在某个环节把他们过滤掉了。
带着这个问题继续研究。既然View的布局是由任务队列中的UpdateLayoutOperation
执行,那么也许跟踪UpdateLayoutOperation
被放进队列的逻辑能找到答案。
顺着这个思路跟踪到在NativeViewHierarchyOptimizer
的applyLayoutRecursive()
方法。UpdateLayoutOperation
被放进任务队列果然有『前置条件』。
private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) { if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) { int tag = toUpdate.getReactTag();
mUIViewOperationQueue.enqueueUpdateLayout(
toUpdate.getNativeParent().getReactTag(),
tag,
x,
y,
toUpdate.getScreenWidth(),
toUpdate.getScreenHeight()); return;
}
···
}
问题焦点进入到ReactShadowNode
的isLayoutOnly()方法里。这个方法只是返回了一个布尔值。
public final boolean isLayoutOnly() { return mIsLayoutOnly;
}
那么就搜一下mIsLayoutOnly什么时候被赋值的。
/**
* Sets whether this node only contributes to the layout of its children without doing any
* drawing or functionality itself.
*/
public final void setIsLayoutOnly(boolean isLayoutOnly) {
···
mIsLayoutOnly = isLayoutOnly;
}
这个方法的注释正是我要找的问题的表象。继续搜这个方法被调用的地方,被带到了NativeViewHierarchyOptimizer
的handleCreateView()。
public void handleCreateView(
ReactShadowNode node,
ThemedReactContext themedContext,
@Nullable ReactStylesDiffMap initialProps) {
··· boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) &&
isLayoutOnlyAndCollapsable(initialProps);
node.setIsLayoutOnly(isLayoutOnly);
···
}
ViewProps.VIEW_CLASS_NAME
的值为RCTView
,这也就是我一直在找的被弄丢的4、5和8的节点类型。到此,这个问题的答案就找到了。
但是还有个小问题,总不能所有的RCTView
都被过滤掉吧?
为了验证这个问题,我在JS代码里给第二个Text的父View设置一个背景色。
<View style={{flex:1}}>
<View style={{flex:1}}>
<Text style={{flex:1}}>显示1</Text>
</View>
<View style={{flex:1,backgroundColor:'#223344'}}>
<Text style={{flex:1}}>显示2</Text>
</View></View>
然后再次进入handleCreateView()里,发现『8-RCTView』的isLayoutOnly这次变成了false。原因就是『&&』后面的isLayoutOnlyAndCollapsable()。
这个方法的返回值是根据View的属性判断的,核心逻辑是ViewProps
的isLayoutOnly()。ViewProps
里保存了一个名为LAYOUT_ONLY_PROPS
的静态属性列表,具体值可以到这个类里详细看,这里我就简单概括下,里面包括了设置margin、padding、position等一系列属性。isLayoutOnly()会判断View的属性所有的key都是LAYOUT_ONLY_PROPS
里的属性的话,返回true。我刚才添加的backgroundColor
不属于这个列表里的属性,自然这个方法返回false,所以『8-RCTView』就会被绘制到屏幕上。
综上,RN会将所有JS编写的View封装成ShaowNode,并自动过滤不需要显示的View以减少嵌套层级。最终计算View的坐标值然后绘制到ReactRootView封装的FrameLayout上。
怎样给View分配ID
前面跟踪了View的绘制流程,众所周知,Android上所有的View都有一个ID。那么RN是怎样给View分配ID的呢?
一路跟进创建View的方法,终于在在NativeViewHierarchyManager
的createView()方法中发现了设置id的代码。
public void createView(
ThemedReactContext themedContext, int tag,
String className,
@Nullable ReactStylesDiffMap initialProps) {
···
ViewManager viewManager = mViewManagers.get(className);
View view = viewManager.createView(themedContext, mJSResponderHandler);
view.setId(tag);
···
}
接下来把焦点放到tag是怎样传进来的。一层层网上扒代码,跟踪到UIManagerModule
的createView()方法后,发现了非常熟悉的@ReactMethod
,这就说明View的id并不是在Native生成的,看来得到JS代码里寻找答案。
根据ReactMethod的映射规则,通过在JS代码里全局搜索『createView』,发现ReactNativeBaseComponent
调用了createView。
var tag = ReactNativeTagHandles.allocateTag();
UIManager.createView(
tag, this.viewConfig.uiViewClassName,
nativeTopRootTag,
updatePayload,
);
这里发现ReactNativeTagHandles
负责生成tag,通过方法名allocateTag猜测难道tag是自动递增生成的?进去一看,果不其然。
var INITIAL_TAG_COUNT = 1;var ReactNativeTagHandles = {
tagsStartAt: INITIAL_TAG_COUNT,
tagCount: INITIAL_TAG_COUNT,
allocateTag: function(): number { // Skip over root IDs as those are reserved for native
while (this.reactTagIsNativeTopRootID(ReactNativeTagHandles.tagCount)) {
ReactNativeTagHandles.tagCount++;
} var tag = ReactNativeTagHandles.tagCount;
ReactNativeTagHandles.tagCount++; return tag;
},
reactTagIsNativeTopRootID: function(reactTag: number): boolean { // We reserve all tags that are 1 mod 10 for native root views
return reactTag % 10 === 1;
},
};module.exports = ReactNativeTagHandles;
整个类逻辑比较简单,就是每次通过++的方式将tag的值递增,而且发现ReactRootView的tag恒定为1,那么在Native是不是这样呢?我用JS编写一个简单的页面。
<View style={{flex:1}}>
<View style={{flex:1}}>
<Text style={{flex:1}}>点击显示</Text>
</View>
<View style={{flex:1}}>
<Text style={{flex:1}}>点击显示</Text>
</View></View>
然后用工具Layout Inspector来验证。
可以看到ReactRootView的id果然为1,然后两个ReactTextView的id分别为6和9。
怎样给View设置属性
为了跟踪属性设置流程,我修改了JS代码,给第一个Text背景和文字添加了颜色。
<View style={{flex:1}}>
<View style={{flex:1}}>
<Text style={{flex:1,color:'#661100',backgroundColor:'#998800'}}>点击显示</Text>
</View>
<View style={{flex:1}}>
<Text style={{flex:1}}>点击显示</Text>
</View></View>
发现在UIManagerModule
的createView()接收到了tag为5,props值为{{"allowFontScaling":true,"ellipsizeMode":"tail","accessible":true,"backgroundColor":-6715392,"color":-10088192,"flex":1} }
的调用。通过backgroundColor和color即可判定这是我要跟踪的View。
根据刚才是跟踪View绘制流程的思路,背景色的绘制肯定会被封装成某个ViewOperation
,然后等待任务队列轮询。顺着这个思路,发现背景色的更改会被CreateViewOperation
的execute()执行,最终会进入ViewManagerPropertyUpdater
的updateProps()。
public static <T extends ReactShadowNode> void updateProps(T node, ReactStylesDiffMap props) {
ShadowNodeSetter<T> setter = findNodeSetter(node.getClass());
ReadableMap propMap = props.mBackingMap;
ReadableMapKeySetIterator iterator = propMap.keySetIterator(); while (iterator.hasNextKey()) {
String key = iterator.nextKey();
setter.setProperty(node, key, props);
}
}
这里的propMap即为JS传进来的View的属性。
KEY | VALUE |
---|---|
allowFontScaling | true |
ellipsizeMode | tail |
accessible | true |
color | -10088192 |
backgroundColor | -6715392 |
flex | 1 |
updateProps()会遍历这个Map,将Key传进setter里进一步处理。那么继续跟进去看setProperty()。
@Override
public void setProperty(T manager, V v, String name, ReactStylesDiffMap props) {
ViewManagersPropertyCache.PropSetter setter = mPropSetters.get(name); if (setter != null) {
setter.updateViewProp(manager, v, props);
}
}
这里根据Key获取其对应的ViewManagersPropertyCache.PropSetter
来处理View的属性。那么mPropSetters
应该是类似静态注册表的存在。那么就看看mPropSetters
是怎样初始化的。
mPropSetters = ViewManagersPropertyCache.getNativePropSettersForViewManagerClass(viewManagerClass);
继续跟进getNativePropSettersForViewManagerClass()
static Map<String, PropSetter> getNativePropSettersForViewManagerClass(
Class<? extends ViewManager> cls) {
···
props = new HashMap<>(
getNativePropSettersForViewManagerClass(
(Class<? extends ViewManager>) cls.getSuperclass()));
extractPropSettersFromViewManagerClassDefinition(cls, props);
CLASS_PROPS_CACHE.put(cls, props); return props;
}
核心逻辑是extractPropSettersFromViewManagerClassDefinition()方法,因为源码较长,就简要介绍下。这个方法里会递归扫描ViewManager
及其父类中声明了ReactProp的方法,然后将其放到mPropSetters
中。现在我跟踪的是Text组件,对应的是ReactTextViewManager。
public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTextShadowNode> {
···@ReactProp(name = ViewProps.ELLIPSIZE_MODE)@ReactProp(name = ViewProps.TEXT_ALIGN_VERTICAL)@ReactPropGroup(names = {
ViewProps.BORDER_RADIUS,
ViewProps.BORDER_TOP_LEFT_RADIUS,
ViewProps.BORDER_TOP_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_LEFT_RADIUS
}, defaultFloat = YogaConstants.UNDEFINED)@ReactPropGroup(names = { "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor"
}, customType = "Color")
···
}
但我并没有找到backgroundColor和Color对应的声明。别急,不是还会递归父类么?果然在父类BaseViewManager
找到了。
@ReactProp(name = PROP_BACKGROUND_COLOR, defaultInt = Color.TRANSPARENT, customType = "Color") public void setBackgroundColor(T view, int backgroundColor) {
view.setBackgroundColor(backgroundColor);
}
其根本逻辑还是Native的View的setBackgroundColor()方法。
那么还有一个问题,我始终没有找到color的声明。猜测color应该也是以@ReactProp声明的,只不过没有在ViewManager的类里。突然想到所有JS的View都会在Native被封装为ShadowNode,每个ViewManager都会有个createShadowNodeInstance()方法来建立和ShadowNode的关系。ReactTextViewManager
对应的是ReactTextShadowNode
。在这个类里不仅找到了color的声明,还找到了非常熟悉的fontSize属性的声明。
public class ReactTextShadowNode extends LayoutShadowNode {
···@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = UNSET)public void setFontSize(float fontSize) {
mFontSizeInput = fontSize; if (fontSize != UNSET) {
fontSize = mAllowFontScaling ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize))
: (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize));
}
mFontSize = (int) fontSize;
markUpdated();
}@ReactProp(name = ViewProps.COLOR)public void setColor(@Nullable Integer color) {
mIsColorSet = (color != null); if (mIsColorSet) {
mColor = color;
}
markUpdated();
}
···
}
但是说好的调用setTextColor()的,别只赋值给mColor就不管了~
好吧,那就全局搜一下mColor是怎么用的。在ReactTextShadowNode
的这个方法里找到了答案。
private static void buildSpannedFromTextCSSNode(
ReactTextShadowNode textShadowNode,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {
···
ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textShadowNode.mColor)));
ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textShadowNode.mFontSize)));
···
}
原来并没有按照我猜测的调用setTextColor()套路出牌,而是使用SpannableString
来实现文本效果。
总结以上结论,Native保存了一份JS属性和Native控件属性设置的映射表,所有flexbox的属性设置都是由Native控件的属性设置API实现。
通过跟踪源码,发现渲染流程及原理远不止本文所述,所以本文可能有理解误差,欢迎指正。
部门招聘
高级Java开发工程师
工作职责:
1、负责58同城APP,58同镇等相关后端研发工作;
2、负责基础平台的架构设计,核心代码开发;
3、调研并掌握业内通用技术方案,引入项目迭代,提升研发效率;
职位要求:
1、3年以上Java互联网项目开发经验;
2、Java基础扎实,编码规范,程序具备较高的健壮性,熟悉常用设计模式;
3、对MVC框架、RPC框架、基础服务组件等有深入的研究;
4、掌握Linux环境下的网络编程、多线程编程,数据结构和算法能力良好;
5、对高并发高可用系统设计有深入的实践经验;
6、具有高度的责任心、勇于承担责任,能承受较强的工作压力;
7、积极主动,敢于接受挑战,有较强的团队合作精神;
高级前端研发工程师
工作职责:
1、负责58同城App前端产品研发;
2、负责58同城前端无线产品某一技术方向,人才培养;
3、前端研发所需类库、框架、脚手架搭建;
4、交互模式调研及创新(React,ReactNative);
职位要求:
1、计算机及相关专业本科以上学历;
2、3年以上前端开发经验,负责过复杂应用的前端设计和开发 ;
3、精通web前端技术(js/css/html),熟悉主流框架类库的设计实现、w3c标准,熟悉ES6/7优先;
4、熟悉前端模块化开发方式(commonjs/webpack …);
5、熟悉移动端开发、自适应布局和开发调试工具,熟悉hybrid app开发;
6、掌握一门后端语言(node/java/php...),对前后端合作模式有深入理解;
7、有良好的产品意识和团队合作意识,能够和产品、UI交互部门协作完成产品面向用户端的呈现;
8、有技术理想,致力于用技术去推动和改变前端研发;
9、熟悉Vue/React/ReactNative优先,有BAT等公司经验优先;
高级Android开发工程师
岗位描述:
1、负责58同城App的研发工作;
2、肩负平台化任务(插件框架,Walle,Hybrid,WubaRN) ;
3、维护和开发服务库,公共库的工作;
4、调研Android前端技术;
5、提升开发效率和应用性能;
职位要求:
1、2年以上的Android开发工作经验;
2、精通Java语言,精通Android Studio开发,了解Gradle编译;
3、精通常用算法、数据结构和架构设计;
4、了解Android性能限制及优化方案;
5、了解常用的开源工具:Volley,RxJava,Fresco等等;
6、了解git, maven等等工具;
7、有插件开发经验,Hybrid开发经验,ReactNative开发经验优先;
8、积极主动、喜欢挑战,有强烈的创业精神,能承受高强度的工作压力;
以上如有小伙伴感兴趣,请发送简历到:
文章评论