前言

React Native App(后称RN App)的UI由JS端的View tree构成,在App运行时会创建相应的原生View tree。从结果看,这和安卓原生开发时用xml布局文件是一样的,最终结果都是由Java对象构成的View tree。View tree中每个节点必须拥有正确的位置和尺寸数据,才能渲染出正确的界面。安卓原生App渲染流程(测量,布局,绘制)中前两步刚好在做这个工作,那么RN App里渲染流程是怎么样的?RN采用的是Flexbox布局(实现体称为Yoga),这种布局方式如何应用到原生渲染流程中?本文先简单介绍安卓平台原生渲染流程,然后在此基础上着重分析RN在安卓平台的渲染流程。本文将从以下几个方面进行阐述:

  1. 安卓原生平台的渲染流程概述
  2. RN渲染流程介绍
    2.1 根据js端view tree,创建平台原生view tree流程概述
    2.2 使用Yoga的计算结果,来跑原生渲染流程
  3. Yoga布局和原生布局共存,安卓自定义View也可以正常工作
  4. 总结

本文的主要读者是有安卓基础同学,在有安卓知识的前提下阅读会更容易一些,比如其中的MessageQueue切换,FrameLayout等控件,原生渲染流程等内容将不会有理解负担,能直接过渡到RN部分的阅读。其他读者可能就需要先建立安卓中这些概念的理解。阅读完本文后,你将会理解RN在安卓平台的渲染流程实现原理,对RN App在渲染时都执行了哪些逻辑有一个具体的概念,理解我们开发时写的MRN代码究竟都做了什么,知其然也知其所以然。在遇到问题时也可从流程上进行分析定位,或者对这个流程的某一步进行修改来满足定制化需求。文中难免有纰漏之处,欢迎大家不吝指出,共同学习成长。

参考源码版本:RN:0.59.8,安卓:28。

1、安卓原生App UI渲染流程简介

GUI程序都用View tree来描述界面内容,不管界面多复杂,元素有多少,都可以收纳在这颗树里。View tree很好的提供了渲染所需的必要信息:位置(含尺寸)和颜色。简单来说,有了这俩信息,系统就可以生成图形库(OpenGL ES或者Skia)所需的绘制指令,绘制出由View tree所表达的一帧画面。虽然我们在写View时可以用{ flex: 1 }(react)或者android:layout_width="match_parent"等来指定组件的尺寸,但最终尺寸属性还是需要被计算成具体的数值。

在App启动过程的onResume阶段,系统会触发ViewRootImpl.performTraversals开始渲染流程,这个函数里会依次触发root view的测量、布局、绘制,最后通知系统渲染到物理屏幕上,这个过程如下图所示。测量遍历用于计算每个节点的尺寸,布局遍历时会参考尺寸进行位置摆放,算出位置数据。经过这两步以后,每个节点就拥有了位置和尺寸,接着就可以遍历绘制每个节点的内容了。view tree中父子节点的遍历衔接主要得益于measure/onMeasure,layout/onLayout,draw/onDraw的设计,如下图中measure过程所示,layout和draw同理。

image

以上就是安卓原生渲染的三个主要步骤,测量和布局其实是为绘制服务,前两步所计算出的位置和尺寸数据在绘制时需要用到,用于生成图形库的绘制指令。这样说来,如果view的位置和尺寸数据已经准备好,测量和布局这两步就可以省去了。这也正是RN在原生渲染流程中的切入点:把view中onMeasure和onLayout的计算逻辑都去掉,同时阻断这两步遍历,通过Yoga来计算位置和尺寸,并将这些数据亲自“交给”view tree中的每个节点,然后只需要一次绘制遍历,就完成了整个渲染流程。下面来看看RN是如何完成这个工作的。

2、RN渲染流程介绍

我们用js写成的view tree势必要翻译成安卓平台的原生view tree,才可以在安卓上正常工作。这个翻译过程并不只是简单的映射而已,RN并不是将映射后的原生view tree直接交给系统,它还接管了测量和布局工作。安卓有自己的布局方式,体现在渲染流程的measure和layout中,RN采用的Flexbox是一种完全不同的布局方式,它如何参与到原生渲染流程中呢?事实上不管哪种布局方式,他们都是为了一个目的:给出view节点的边界数据bounds(left,top,right,bottom),有了边界位置,view的尺寸也就有了,比如width = right - left,因此我们可以猜测两种布局方式的结合点就是View的边界数据bounds,在下面介绍的渲染流程中进行验证。RN在mqt_native线程中执行Flexbox布局计算,计算结果将直接用于渲染流程,省去了主线程中measure和layou的计算量。如果这两个线程是运行的不同的核心上,在执行复杂的布局动画时将会有明显的优势。下面分小节来具体看看这个过程是如何进展的。

2.1、根据js端view tree,创建平台原生view tree流程概述

RN提供的View系列组件,都有相应的原生端组件实现,js端的view tree相当于一个“剧本”,用来描述UI界面,RN会根据这个“剧本”来生成平台原生View tree。对于js端View tree中的每一个节点,都会在native端生成一个ReactShadowNode节点作为对应,同时还会创建一个原生View节点(先不考虑RN的布局优化,可认为他们是一一对应关系)。ReactShadowNode承担了Yoga布局的计算工作,其内部会创建一个YogaNode节点,YogaNode内部再创建c++端的YGNode。YogaNode本身是一个jni承载类,代表的是c++端的YGNode,两者都表示Yoga布局中的一个节点,当Yoga引擎计算完毕后,YGNode中就填充满了尺寸和位置数据,通过jni回设到java端的YogaNode中,留着给原生view使用。最终在native端会生成4颗tree:ReactShadowNode tree, YogaNode tree, YGNode tree, 原生View tree,如下图所示。

image

RN App会在Activity的onCreate阶段创建ReactRootView(其本质是一个FrameLayout),并由此进入RN的世界。下面我们来看看RN是如何创建native端的4颗tree结构的。在RN Bridge完成初始化后,native端通过runApplication()触发执行我们的js业务代码,根据js端view tree会执行一系列的UIManager.createView, UIManager.setChildren, UIManager.manageChildren等函数。通过RN Bridge,这些函数会在nativeQueue中添加一系列的操作,用于创建ReactShadowNode节点,并把js端view的各属性值保存在节点中,最后形成tree结构。根据之前的介绍,此时也会同步生成YoagNode tree和YGNode tree。这个过程如下图中左侧两个queue所示。

image

从上图还可以看出,每一次对ReactShadowNode的操作同时还会有相应的原生View操作,以runnable的形式添加到batchUIQueue中(注意它并不是安卓中和线程相关的queue,仅仅是个队列容器)。batchUIQueue的名字非常恰当的描述了它的作用:保存一系列的原生View操作,最后在App主线程中一次性批处理完。所以当ReactShadowNode tree形成时,所有对应的原生View操作(view创建,view属性赋值,addView形成tree等)都添加到了batchUIQueue中,不过此时还不到执行的时机。

js端view节点的属性可以大致分为两类,一类用于直接作用于原生view,比如背景颜色,透明度等等和布局不相关的;另一类主要是flexbox布局相关的属性,比如flex,margin,padding,alignItems等等,这些会通过ReactShadowNode保存在YogaNode和YGNode中,用于Yoga引擎计算。

到目前为止ReactShadowNode tree已经形成,原生view的操作也已经添加到batchUIQueue中等待被执行。那什么时候会执行呢?答案是尺寸和位置数据计算出来以后。在本次js代码执行完之后,jsQueue里会根据是否是endOfBatch来执行onBatchComplete,它会触发在nativeQueue中执行onBatchComplete,其中会调用dispatchViewUpdates,它的工作主要分为3步:

  1. 启动Yoga引擎对YGNode tree进行计算,履行Flexbox布局协议,并将计算后的结果递归回设到java端YogaNode节点
  2. 根据YogaNode tree中的数据,递归向batchUIQueue中添加runnable:给对应的原生View tree节点设置尺寸和位置(下文还会详细分析)
  3. 将batchUIQueue中所有的runnable,交给App主线程执行,所有的这些runnable都在主线程的一个事件循环中执行完

至此,batchUIQueue的所有view操作在App主线程执行完,UI界面也该显示出来了。其中关键代码如下:

// ----- file: UIImplementation.java
public void dispatchViewUpdates(int batchId) {...updateViewHierarchy();...mOperationsQueue.dispatchViewUpdates(batchId, commitStartTime, mLastCalculateLayoutTime); // 3、执行batchUIQueue中所有的view操作
}protected void updateViewHierarchy() {...calculateRootLayout(cssRoot); // 1、调用YogaNode.calculateLayout启用yoga引擎,并将计算结果递归回设到java端的YogaNode tree的每个节点里...applyUpdatesRecursive(cssRoot, 0f, 0f); // 2、将YogaNode tree节点的数据,递归设置给原生view,将该操作添加到batchUIQueue中,最后统一执行
}protected void applyUpdatesRecursive(...) {...for (int i = 0; i < cssNode.getChildCount(); i++) {applyUpdatesRecursive(...); // 递归}...cssNode.dispatchUpdates(...) --> uiViewOperationQueue.enqueueUpdateLayout(...) // updateLayout具体做什么?看下面小节的分析。
}// ----- file: YogaNode.java
public void calculateLayout(float width, float height) {jni_YGNodeCalculateLayout(mNativePointer, width, height);
}// ----- file: YGJNI.cpp
void jni_YGNodeCalculateLayout(alias_ref<jclass>, jlong nativePointer, jfloat width, jfloat height) {const YGNodeRef root = _jlong2YGNodeRef(nativePointer);YGNodeCalculateLayout( // Yoga引擎执行计算,里面是漫长的c++代码,Flexbox布局协议的实现root,static_cast<float>(width),static_cast<float>(height),YGNodeStyleGetDirection(_jlong2YGNodeRef(nativePointer)));YGTransferLayoutOutputsRecursive(root); // 将计算结果,以递归方式回设给java端每个YogaNode节点
}// ----- UIViewOperationQueue.java
public void dispatchViewUpdates(final int batchId, final long commitStartTime, final long layoutTime) {...UiThreadUtil.runOnUiThread( // 切换到主线程处理原生Viewnew GuardedRunnable(mReactApplicationContext) {@Overridepublic void runGuarded() {flushPendingBatches();}});
}private void flushPendingBatches() {...for (Runnable runnable : runnables) {runnable.run();}
}

2.2、使用Yoga的计算结果,来跑原生渲染流程

上文提到在Yoga计算完毕后,会递归将YogaNode tree中的数据设置给原生View tree,简单的“设置”二字实在是太过敷衍,本小节来详细看看这个过程。首先看下关键代码:

// ----- file: UIImplementation.java
protected void applyUpdatesRecursive(...) {...for (int i = 0; i < cssNode.getChildCount(); i++) {applyUpdatesRecursive(...); // 递归调用}...cssNode.dispatchUpdates(...); // 这种递归方式产生的效果是从叶子节点开始,到根节点的遍历
}// ----- file: ReactShadowNodeImpl.java
public boolean dispatchUpdates(...) {...uiViewOperationQueue.enqueueUpdateLayout( // 添加到batchUIQueue中getParent().getReactTag(),getReactTag(),getScreenX(), // 这些数据都是Yoga计算出来的getScreenY(),getScreenWidth(),getScreenHeight());
}// ----- file: NativeViewHierarchyManager.java
public synchronized void updateLayout(...) {...viewToUpdate.measure( // 看这里View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));...updateLayout(...) --> viewToUpdate.layout(x, y, x + width, y + height); // 再看这里,Yoga输出给View的,就是简单的left, top, right, bottom
}

从上面的代码中可以看到,从YogaNode tree的根节点开始递归处理,但却是反向地将原生View tree对应节点的layout操作添加到batchUIQueue中,即先处理的是叶子结点,最后到根节点。个人分析这里的遍历顺序没有什么分别,top-down或者down-top结果是一样的,毕竟各节点的尺寸和位置已经算好,至于是先设置子节点还是父节点并无区别,最终都是在batchUIQueue中一次性执行完,再进行reqeustLayout,进而执行performTraversals完成渲染,此处若分析的不对还请大神指正。这里的重点是RN亲力亲为的调用了原生View tree里所有节点的measure和layout(仅限和YogaNode tree对应的节点,自定义view group里子节点不在此范围),来完成渲染流程的前两个阶段,将Yoga的计算结果应用进去。上文有提到过,容器节点onMeasure/onLayout会调用子节点的measure/layout,来衔接tree结构父子节点的遍历,这里是否和RN的遍历重复了呢?答案当然是没有重复。RN既然选择亲自遍历所有节点,当然就会有处理:onMeasure和onLayout中的计算逻辑都去掉,并且不调用子节点的measure和layout。代码节选如下:

// ----- file: ReactRootView.java RN原生端的根view,用于承载RN所有的界面
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...setMeasuredDimension(width, height); // 没有调用子节点的measure,仅履行了安卓的约定调用setMeasuredDimension。
}protected void onLayout(boolean changed, int left, int top, int right, int bottom) {// No-op since UIManagerModule handles actually laying out children.// 这里更简单,什么都不做,上面的注释说的很明白了。
}// ----- file: ReactViewGroup.java 这个类表示的是js端的View.js,最基本的容器view。测量和布局均没有任何计算,没有触发子节点。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}protected void onLayout(boolean changed, int left, int top, int right, int bottom) {// No-op since UIManagerModule handles actually laying out children.
}

3、Yoga布局和原生布局共存,安卓自定义View也可以正常工作

RN虽然是自己负责衔接安卓渲染流程的前两步,但是接入自定view group并不需要额外做太多,只需跟安卓自定义view group一样重写onMeasure和onLayout,负责计算子节点的尺寸和位置。如下图所示,自定义节点和RN的节点能够完美合作,“各司其职”。

不过事情总是会有一些遗憾,当自定义view group进行requestLayout时会触发view tree的渲染流程遍历,但是RN的view容器并没有在onMeasure/onLayout里衔接遍历,导致自定义view group界面更新不生效。解决方式比较简单,在自定义view group里重写requestLayout,然后手动调用measure和layout,这样自定义view就可以正常工作了。

// ----- 某自定义view group.java
@Override
public void requestLayout() {super.requestLayout();post(measureAndLayout);
}private final Runnable measureAndLayout = new Runnable() {@Overridepublic void run() {measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));layout(getLeft(), getTop(), getRight(), getBottom());}
};

下面来看一个笔者之前在实际项目中遇到的例子,这个例子是电子答题,主要功能是展示一张题目图片,学生可以在下面手写作答,当答题区域不够时可以增加区域尺寸等等。该功能实现采用的是FrameLayout+自定义ImageView,如下示意图所示,自定义ImageView用于设置题目图片和实现画笔功能,FrameLayout用于移动和缩放ImageView等。初始化时ImageView和FrameLayout尺寸一样,当用户点击增加一屏画布时,在FrameLayout里增加ImageView的高度。这其中没有重写onMeasure和onLayout,FrameLayout和ImageView默认的就够用。

功能在安卓原生侧是正常的,接入到RN以后展示正常,写写画画正常,对画布进行放大缩小(scale)正常,但是点击增加一屏操作却无效。总结一下上述这些现象会发现:

  • onDraw里的操作执行正常,比如画笔操作,setScale和setTranslationX等
  • setLayoutParams操作无效,如设置ImageView高度

也就是说尺寸更新无效,绘制操作有效,原因就是当在View里进行invalidate和setLayoutParams时,都会执行requestLayout向上反馈到ViewRootImpl来触发一次渲染流程,其中RN阻断了measure和layout的遍历,没有阻断draw。解决方式就是在FrameLayout.requestLayout中主动触发measure和layout,完成子View的尺寸刷新。另外,由于FrameLayout是作为自定义View接入到RN的,他的尺寸将会受RN来控制,原生侧无需关心。

4、总结

以上就是RN在安卓端UI渲染流程的介绍,主要描述了我们在js端写的view tree,是如何一步一步转化到原生view的,这也体现了RN确实就是native的一面。笔者一直以来就有一个疑惑,安卓原生View本身有大量的属性,比如width,height,padding,margin等等,RN所采用的Flexbox也有很多的属性,flex,padding,alignItems等等,两者的差距还是很大的,这两套属性该如何对接?乍一想还是很头疼的,大量的属性剪不断理还乱,但从RN源码来看,其实两者的衔接点只是view的边界值而已(left, top, right, bottom)。分析一下可以发现,不管是Flexbox还是安卓原生布局,其他属性存在的价值就是为了计算出left,top,right,bottom。RN参与渲染流程的切入点也正是这里,刚好原生view就有一个layout(l, t, r, b)方法来接收这四个值,并且也刚好这个方法就是渲染流程中的一步,一切都恰到好处。另外,RN在一个子线程中来计算View的布局数据,省去了主线程刷新UI时的前两步遍历,减轻了负担,但从整体上来看UI的连贯性未必就有提升,线程之间的配合成本可能也不低。笔者也是在学习过程中,想法难免有欠缺或错误之处,欢迎各位大神提出宝贵建议。

作者:ipursue
链接:http://events.jianshu.io/p/a8e5c47e4d06
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

React Native UI渲染流程分析(Android)相关推荐

  1. Android UI绘制流程分析(三)measure

    源码版本Android 6.0 请参阅:http://androidxref.com/6.0.1_r10 本文目的是分析从Activity启动到走完绘制流程并显示在界面上的过程,在源码展示阶段为了使跟 ...

  2. 【逆向】UE4 渲染流程分析

    UE4作为当今商业引擎界的大佬,渲染和图形质量一直是首屈一指的水准,但是相对于unity来说UE4基本上是一套完整方案提供,不通过源码修改对渲染进行定制的可能性比较小,而且同时UE4这方面的文档很少, ...

  3. React Native的安装和初始化(android /ios)

    2019独角兽企业重金招聘Python工程师标准>>> 好久没有写东西,最近想学一下React Native,借此写一写笔记 React Native 的安装 使用React Nat ...

  4. React Native之react-native bundle --platform android --dev false --entry-file index.js --bundle失败

    1 问题 react native项目在assert目录下面生成index.android.bundle文件用下面的命令 react-native bundle --platform android ...

  5. 全志 android 编译,全志A20启动代码流程分析 ——Android

    现在的CPU都固化了内部 ROM,内部 ROM中有一般都有一段程序,一般有如下几个功能: 1,初始化,部分外设,如USB,SDCARD 2,初始化DDR(内存)和NandFlash 3,加载boot( ...

  6. 全志android 编译,全志A20启动代码流程分析 ——Android

    现在的CPU都固化了内部 ROM,内部 ROM中有一般都有一段程序,一般有如下几个功能: 1,初始化,部分外设,如USB,SDCARD 2,初始化DDR(内存)和NandFlash 3,加载boot( ...

  7. android 休眠唤醒驱动流程分析,Android 电源管理——gotosleep和userActivity关注

    一.Android power management应用层分析 Android提供了android.os.PowerManager类,该类用于控制设备的电源状态的切换. 该类对外有三个接口函数: 1. ...

  8. android app启动流程分析,Android应用开发之Android 7.0 Launcher3的启动和加载流程分析...

    本文将带你了解Android应用开发Android 7.0 Launcher3的启动和加载流程分析,希望本文对大家学Android有所帮助. Android 7.0 Launcher3的启动和加载流程 ...

  9. Ogre内部渲染流程分析系列

    come from:http://blog.csdn.net/weiqubo/article/details/6956005 要理解OGRE引擎,就要理解其中占很重要位置的 Renderable接口, ...

最新文章

  1. php正则匹配js中变量_PHP正则表达式核心技术 第4节 php查找匹配函数使用心得
  2. 使用链栈来对十进制数进行任意进制的转换
  3. 怎么恢复手机上的照片呢?
  4. 内存版u-boot制作
  5. MySQL三大日志及主从复制的原理
  6. 【JS】使用变量作为object的key-方法汇总
  7. Java里String.split需要注意的用法
  8. visio图标_弱电间机柜原型图整理,可编辑!(Excel,visio,CAD)
  9. .NET技术面试题系列(2) -sql server数据库优化规范
  10. 当BeanUtils遇到泛型
  11. CSRF - 跨站请求伪造
  12. WPF+VB.net制作桌面股票小助手
  13. 190428多线进程编程
  14. 请思考用人单位要的是什么?死记硬背学专业能将你支撑到哪里?
  15. CentOS7安装Jenkins教程
  16. 忠告360安全卫士督导委员:小心沦为周鸿一的工具(zz)
  17. 力扣解题思路:位运算系列
  18. 基于JSP的网上订餐管理系统餐厅餐饮系统
  19. 港科夜闻|海南省教育厅党委书记曹献坤到访香港科大(广州)开展实地调研
  20. c# cad二次开发通过获取excel数据 在CAD绘图,将CAD属性导出到excel

热门文章

  1. 一个Java菜鸟的学习之道~~~
  2. FPGA学习思维导图
  3. 人人开源 / renren-security/小记(二)
  4. yaourt/yay 安装软件出现 parse “XXX“: first path segment in URL cannot contain colon 错误
  5. 群发邮件的方法有哪些?怎样大量群发邮件?
  6. 希望计算机专业同学都知道这些老师
  7. Mac 上怎么双开微信
  8. 关于STM8的程序下载问题:SWIM Error[30006]报错解决办法汇总
  9. 用户注册及APP使用隐私协议
  10. 直角坐标系与极坐标系了解与转换