ReactNative是如何让JS代码『变成』Android控件的?

2017年12月8日 283点热度 0人点赞 0条评论

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-RCTView3-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被放进队列的逻辑能找到答案。

顺着这个思路跟踪到在NativeViewHierarchyOptimizerapplyLayoutRecursive()方法。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、积极主动、喜欢挑战,有强烈的创业精神,能承受高强度的工作压力;


以上如有小伙伴感兴趣,请发送简历到:

[email protected]

图片


12710ReactNative是如何让JS代码『变成』Android控件的?

root

这个人很懒,什么都没留下

文章评论