成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。
Android的绘制优化其实可以分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题,所以它可以认为是卡顿优化的一个子集。对于Android开发来说,写布局可以说是一个比较简单的工作,但是如果想将写的每一个布局的渲染性能提升到比较好的程度,要付出的努力是要远远超过写布局所付出的。由于布局优化这一主题包含的内容太多,因此,笔者将它分为了上、下两篇,本篇,即为深入探索Android布局优化的上篇。本篇包含的主要内容如下所示:
说到Android的布局绘制,那么我们就不得不先从布局的绘制原理开始说起。
Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。
1、CPU与GPU
这里举两个栗子来讲解一些CPU和GPU的作用:
那么,软件绘制和硬件绘制有什么区别呢?我们先看看下图:
这里软件绘制使用的是Skia库(一款在低端设备如手机上呈现高质量的 2D 图形的 跨平台图形框架)进行绘制的,而硬件绘制本质上是使用的OpenGl ES接口去利用GPU进行绘制的。OpenGL是一种跨平台的图形API,它为2D/3D图形处理硬件指定了标准的软件接口。而OpenGL ES是用于嵌入式设备的,它是OpenGL规范的一种形式,也可称为其子集。
并且,由于OpenGl ES系统版本的限制,有很多 绘制API 都有相应的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本的时候,还添加了对Vulkan(一套适用于高性能 3D 图形的低开销、跨平台 API)的支持。Vulan作为下一代图形API以及OpenGL的继承者,它的优势在于大幅优化了CPU上图形驱动相关的性能。
2、Android 图形系统的整体架构
Android官方的架构图如下:
为了比较好的描述它们之间的作用,我们可以把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用分别如下:
在了解完Android图形系统的整体架构之后,我们还需要了解下Android系统的显示原理,关于这块内容可以参考我之前写的Android性能优化之绘制优化的Android系统显示原理一节。
3、RenderThread
在Android系统的显示过程中,虽然我们利用了GPU的图形高性能计算的能力,但是从计算Display到通过GPU绘制到Frame Buffer都在UI线程中完成,此时如果能让GPU在不同的线程中进行绘制渲染图形,那么绘制将会更加地流畅。
于是,在Android 5.0之后,引入了RenderNode和RenderThread的概念,它们的作用如下:
CPU将数据同步给GPU之后,通常不会阻塞等待RenderThread去利用GPU去渲染完视图,而是通知结束之后就返回。加入ReaderThread之后的整个显示调用流程图如下图所示:
在Android 6.0之后,其在adb shell dumpsys gxinfo命令中添加了更加详细的信息,在优化工具一节中我将详细分析下它的使用。
在Android 7.0之后,对HWUI进行了重构,它是用于2D硬件绘图并负责硬件加速的主要模块,其使用了OpenGl ES来进行GPU硬件绘图。此外,Android 7.0还支持了Vulkan,并且,Vulkan 1.1在Android 被引入。
硬件加速存在哪些问题?
我们都知道,硬件加速的原理就是将CPU不擅长的图形计算转换成GPU专用指令。
4、刷新机制
16ms发出VSync信号触发UI渲染,大多数的Android设备屏幕刷新频率为60HZ,如果16ms内不能完成渲染过程,则会产生掉帧现象。
我们都知道,Android手机屏幕的差异化导致了严重的碎片化问题,并且屏幕材质也是用户比较关注的一个重要因素。
首先,我们来了解下主流Android屏幕材质,目前主要有两类:
早在20世纪60年代,随着半导体集成电路的发展,美国人成功研发出了第一块液晶显示屏LCD,而现在大部分最新的高端机使用的都是OLED材质,这是因为相比于LCD屏幕,OLED屏幕在色彩、可弯曲程度、厚度和耗电等方面都有一定的优势。正因为如此,现在主流的全面屏、曲面屏与未来的柔性折叠屏,使用的几乎都是 OLED 材质。当前,好的材质,它的成本也必然会比较昂贵。
1、OLED 屏幕和 LCD 屏幕的区别
如果要明白OLED 屏幕和LCD屏幕的区别,需要了解它们的运行原理,下面,我将分别进行讲解。
屏幕的成像原理
屏幕由无数个点组成,并且,每个点由红绿蓝三个子像素组成,每个像素点通过调节红绿蓝子像素的颜色配比来显示不同的颜色,最终所有的像素点就会形成具体的画面。
LCD背光源与OLED自发光
下面,我们来看下LCD和OLED的总体结构图,如下所示:
LCD的发光原理主要在于背光层Back-light,它通常都会由大量的LED背光灯组成以用于显示白光,之后,为了显示出彩色,在其上面加了一层有颜色的薄膜,白色的背光穿透了有颜色的薄膜后就可以显示出彩色了。但是,为了实现调整红绿蓝光的比例,需要在背光层和颜色薄膜之间加入一个控制阀门,即液晶层liquid crystal,它可以通过改变电压的大小来控制开合的程度,开合大则光多,开合小则光少。
对于OLED来说,它不需要LCD屏幕的背光层和用于控制出光量的液晶层,它就像一个有着无数个小的彩色灯泡组成的屏幕,只需要给它通电就能发光。
LCD的致命缺陷
它的液晶层不能完全关合,如果LCD显示黑色,会有部分光穿过颜色层,所以LCD的黑色实际上是白色和黑色混合而成的灰色。而OLED不一样,OLED显示黑色的时候可以直接关闭区域的像素点。
此外,由于背光层的存在,所以LCD显示器的背光非常容易从屏幕与边框之间的缝隙泄漏出去,即会产生显示器漏光现象。
OLED屏幕的优势
OLED屏幕的劣势
2、屏幕适配方案
我们都知道,Android 的 系统碎片化、机型以及屏幕尺寸碎片化、屏幕分辨率碎片化非常地严重。所以,一个好的屏幕适配方案是很重要的。接下来,我将介绍目前主流的屏幕适配方案。
1、最原始的Android适配方案:dp + 自适应布局或weight比例布局
首先,我们来回顾一下px、dp、dpi、ppi、density等概念:
通常情况下,我们只需要使用dp + 自适应布局(如鸿神的AutoLayout、ConstraintLayout等等)或weight比例布局即可基本解决碎片化问题,当然,这种方式也存在一些问题,比如dpi和ppi的差异所导致在同一分辨率手机上控件大小的不同。
2、宽高限定符适配方案
它就是穷举市面上所有的Android手机的宽高像素值,通过设立一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件,如下图所示:
比如以480x320为基准分辨率:
那么对于800*480的分辨率的dimens文件来说:
此时,如果UI设计界面使用的就是基准分辨率,那么我们就可以按照设计稿上的尺寸填写相对应的dimens去引用,而当APP运行在不同分辨率的手机中时,系统会根据这些dimens去引用该分辨率对应的文件夹下面去寻找对应的值。但是这个方案由一个缺点,就是无法做到向下兼容去使用更小的dimens,比如说800x480的手机就一定要找到800x480的限定符,否则就只能用统一默认的dimens文件了。
3、UI适配框架AndroidAutoLayout的适配方案
因宽高限定符方案的启发,鸿神出品了一款能使用UI适配更加开发高效和适配精准的项目。
项目地址
基本使用步骤如下:
第一步:在你的项目的AndroidManifest中注明你的设计稿的尺寸:
第二步:让你的Activity继承自AutoLayoutActivity。如果你不希望继承AutoLayoutActivity,可以在编写布局文件时,直接使用AutoLinearLayout、Auto***等适配布局即可。
接下来,直接在布局文件里面使用具体的像素值就可以了,因为在APP运行时,AndroidAutoLayout会帮助我们根据不同手机的具体尺寸按比例伸缩。
AndroidAutoLayout在宽高限定符适配的基础上,解决了其dimens不能向下兼容的问题,但是它在运行时会在onMeasure里面对dimens去做变换,所以对于自定义控件或者某些特定的控件需要进行单独适配;并且,整个UI的适配过程都是由框架完成的,以后想替换成别的UI适配方案成本会比较高,而且,不幸的是,项目已经停止维护了。
4、smallestWidth适配方案(sw限定符适配)
smallestWidth即最小宽度,系统会根据当前设备屏幕的 最小宽度 来匹配 values-swdp。
我们都知道,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度。
并且它跟宽高限定符适配原理上是一样,都是系统通过特定的规则来选择对应的文件。它与AndroidAutoLayout一样,同样解决了其dimens不能向下兼容的问题,如果该屏幕的最小宽度是360dp,但是项目中没有values-sw360dp文件夹的话,它就可能找到values-sw320dp这个文件夹,其尺寸规则命名如下图所示:
假如加入我们的设计稿的像素宽度是375,那么其对应的values-sw360dp和values-sw400dp宽度如下所示:
smallestWidth的适配机制由系统保证,我们只需要针对这套规则生成对应的资源文件即可,即使对应的smallestWidth值没有找到完全对应的资源文件,它也能向下兼容,寻找最接近的资源文件。虽然多个dimens文件可能导致apk变大,但是其增加大小范围也只是在300kb-800kb这个区间,这还是可以接受的。这套方案唯一的变数就是选择需要适配哪些最小宽度限定符的文件,如果您生成的 values-swdp 与设备实际的 最小宽度 差别不大,那误差也就在能接受的范围内,如果差别很大,那效果就会很差。最后,总结一下这套方案的优缺点:
优点:
插件地址为自动生成values-sw的项目代码。生成需要的values-swdp文件夹的步骤如下:
缺点:
如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?
此时根据 values-wdp (去掉 sw 中的 s) 去生成一套资源文件即可。
如果想区分屏幕的方向来做适配该怎么办呢?
去根据 屏幕方向限定符 生成一套资源文件,后缀加上 -land 或 -port 即可,如:values-sw360dp-land (最小宽度 360 dp 横向),values-sw400dp-port (最小宽度 720 dp 纵向)。
注意:
如果UI设计上明显更适合使用wrap_content,match_parent,layout_weight等,我们就要毫不犹豫的使用,毕竟,上述都是仅仅针对不得不使用固定宽高的情况,我相信基础的UI适配知识大部分开发者还是具备的。如果不具备的话,请看下方:
5、今日头条适配方案
它的原理是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),通过修改density值的方式,强行把所有不同尺寸分辨率的手机的宽度dp值改成一个统一的值,这样就可以解决所有的适配问题。其对应的重要公式如下:
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
今日头条适配方案默认项目中只能以高或宽中的一个作为基准来进行适配,并不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,来同时进行适配,为什么?
因为,现在中国大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在的全面屏、刘海屏、弹性折叠屏,使这个问题更加严重,不同厂商推出的手机的屏幕高宽比都可能不一致。所以,我们只能以高或宽其中的一个作为基准进行适配,以此避免布局在高宽比不一致的屏幕上出现变形。
它有以下优势:
它的缺点如下所示:
注意:
千万不要在此方案上使用smallestWidth适配方案中直接填写设计图上标注的 px 值的做法,这样会使项目强耦合于这个方案,后续切换其它方案都不得不将所有的 layout 文件都改一遍。
这里推荐一下JessYanCoding的AndroidAutoSize项目,用法如下:
1、首先在项目的build.gradle中添加该库的依赖:
implementation 'me.jessyan:autosize:1.1.2'
2、接着 AndroidManifest 中填写全局设计图尺寸 (单位 dp),如果使用副单位,则可以直接填写像素尺寸,不需要再将像素转化为 dp:
为什么只需在AndroidManifest.xml 中填写一下 meta-data 标签就可实现自动运行?
在 App 启动时,系统会在 App 的主进程中自动实例化声明的 ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作,这个时候我们就可以利用它的 onCreate 方法在其中启动框架。如果项目使用了多进程,调用Application#onCreate 中调用下 ContentProvider#query 就能够使用 ContentProvider 在当前进程中进行实例化。
小结
上述介绍的所有方案并没有哪一个是十分完美的,但我们能清晰的认识到不同方案的优缺点,并将它们的优点相结合,这样才能应付更加复杂的开发需求,创造出最卓越的产品。比如SmallestWidth 限定符适配方案 主打的是稳定性,在运行过程中极少会出现安全隐患,适配范围也可控,不会产生其他未知的影响,而 今日头条适配方案 主打的是降低开发成本、提高开发效率,使用上更灵活,也能满足更多的扩展需求。所以,具体情况具体分析,到底选择哪一个屏幕适配方案还是需要去根据我们项目自身的需求去选择。
1、Systrace
早在深入探索Android启动速度优化一文中我们就了解过Systrace的使用、原理及它作为启动速度分析的用法。而它其实主要是用来分析绘制性能方面的问题。下面我就详细介绍下Systrace作为绘制优化工具有哪些必须关注的点。
1、关注Frames
首先,先在左边栏选中我们当前的应用进程,在应用进程一栏下面有一栏Frames,我们可以看到有绿、黄、红三种不同的小圆圈,如下图所示:
图中每一个小圆圈代表着当前帧的状态,大致的对应关系如下:
并且,选中其中某一帧,我们还可以在视图最下方的详情框看到该帧对应的相关的Alerts报警信息,以帮助我们去排查问题;此外,如果是大于等于Android 5.0的设备(即API Level21),创建帧的工作工作分为UI线程和render线程。而在Android 5.0之前的版本中,创建帧的所有工作都是在UI线程上完成的。接下来,我们看看该帧对应的详情图,如下所示:
对应到此帧,我们发现这里可能有两个绘制问题:Bitmap过大、布局嵌套层级过多导致的measure和layout次数过多,这就需要我们去在项目中找到该帧对应的Bitmap进行相应的优化,针对布局嵌套层级过多的问题去选择更高效的布局方式,这块后面我们会详细介绍。
2、关注Alerts栏
此外,Systrace的显示界面还在在右边侧栏提供了一栏Alert框去显示出它所检测出所有可能有绘制性能问题的地方及对应的数量,如下图所示:
在这里,我们可以将Alert框看做是一个是待修复的Bug列表,通常一个区域的改进可以消除应用程序中的所有类中该类型的警报,所以,不要为这里的警报数量所担忧。
2、Layout Inspector
Layout Inspector是AndroidStudio自带的工具,它的主要作用就是用来查看视图层级结构的。
具体的操作路径为:
点击Tools工具栏 ->第三栏的Layout Inspector -> 选中当前的进程
下面为操作之后打开的Awesome-WanAndroid首页图,如下所示:
其中,最左侧的View Tree就是用来查看视图的层级结构的,非常方便,这是它最主要的功能,中间的是一个屏幕截图,最右边的是一个属性表格,比如我在截图中选中某一个TextView(Kotlin/入门及知识点一栏),在属性表格的text中就可以显示相关的信息,如下图所示:
3、Choreographer
Choreographer是用来获取FPS的,并且可以用于线上使用,具备实时性,但是仅能在Api 16之后使用,具体的调用代码如下:
Choreographer.getInstance().postFrameCallback();
使用Choreographer获取FPS的完整代码如下所示:
private long mStartFrameTime = 0;
private int mFrameCount = 0;
/**
* 单次计算FPS使用160毫秒
*/
private static final long MONITOR_INTERVAL = 160L;
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
/**
* 设置计算fps的单位时间间隔1000ms,即fps/s
*/
private static final long MAX_INTERVAL = 1000L;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
// log输出fps
LogUtils.i("当前实时fps值为: " + fps);
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}
通过以上方式我们就可以实现实时获取应用的界面的FPS了。但是我们需要排除掉页面没有操作的情况,即只在界面存在绘制的时候才做统计。我们可以通过 addOnDrawListener 去监听界面是否存在绘制行为,代码如下所示:
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
当出现丢帧的时候,我们可以获取应用当前的页面信息、View 信息和操作路径上报至 APM后台,以降低二次排查的难度。此外,我们将连续丢帧超过 700 毫秒定义为冻帧,也就是连续丢帧 42 帧以上。这时用户会感受到比较明显的卡顿现象,因此,我们可以统计更有价值的冻帧率。冻帧率就是计算发生冻帧时间在所有时间的占比。通过解决应用中发生冻帧的地方我们就可以大大提升应用的流畅度。
4、Tracer for OpenGL ES 与 GAPID(Graphics API Debugger)
Tracer for OpenGL ES 是 Android 4.1 新增加的工具,它可逐帧、逐函数的记录 App 使用 OpenGL ES 的绘制过程,并且,它可以记录每个 OpenGL 函数调用的消耗时间。当使用Systrace还找不到渲染问题时,就可以去尝试使用它。
而GAPID是 Android Studio 3.1 推出的工具,可以认为是Tracer for OpenGL ES的进化版,它不仅实现了跨平台,而且支持Vulkan与回放。由于它们主要是用于OpenGL相关开发的使用,这里我就不多介绍了。
5、自动化测量 UI 渲染性能的方式
在自动化测试中,我们通常希望通过执行性能测试的自动化脚本来进行线下的自动化检测,那么,有哪些命令可以用于测量UI渲染的性能呢?
我们都知道,dumpsys是一款输出有关系统服务状态信息的Android工具,利用它我们可以获取当前设备的UI渲染性能信息,目前常用的有如下两种命令:
1、gfxinfo
gfxinfo的主要作用是输出各阶段发生的动画与帧相关的信息,命令格式如下:
adb shell dumpsys gfxinfo
这里我以Awesome-WanAndroid项目为例,输出其对应的gfxinfo信息如下所示:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroid
Applications Graphics Acceleration Info:
Uptime: 549887348 Realtime: 549887348
** Graphics info for pid 1722 [json.chao.com.wanandroid] **
Stats since: 549356564232951ns
Total frames rendered: 5210
Janky frames: 193 (3.70%)
50th percentile: 5ms
90th percentile: 9ms
95th percentile: 13ms
99th percentile: 34ms
Number Missed Vsync: 31
Number High input latency: 0
Number Slow UI thread: 153
Number Slow bitmap uploads: 6
Number Slow issue draw commands: 51
HISTOGRAM: 5ms=4254 6ms=131 7ms=144 8ms=87 9ms=80 10ms=83 11ms=108 12ms=57 13ms=29 14ms=17 15ms=17 16ms=14 17ms=20 18ms=15 19ms=15 20ms=17 21ms=9 22ms=14 23ms=8 24ms=9 25ms=4 26ms=5 27ms=4 28ms=4 29ms=1 30ms=2 31ms=4 32ms=3 34ms=6 36ms=5 38ms=7 40ms=8 42ms=0 44ms=3 46ms=3 48ms=5 53ms=2 57ms=0 61ms=3 65ms=0 69ms=1 73ms=1 77ms=0 81ms=0 85ms=0 89ms=1 93ms=1 97ms=0 101ms=0 105ms=0 109ms=0 113ms=1 117ms=0 121ms=0 125ms=0 129ms=0 133ms=0 150ms=2 200ms=0 250ms=2 300ms=1 350ms=1 400ms=0 450ms=1 500ms=0 550ms=1 600ms=0 650ms=0 700ms=0 750ms=0 800ms=0 850ms=0 900ms=0 950ms=0 1000ms=0 1050ms=0 1100ms=0 1150ms=0 1200ms=0 1250ms=0 1300ms=0 1350ms=0 1400ms=0 1450ms=0 1500ms=0 1550ms=0 1600ms=0 1650ms=0 1700ms=0 1750ms=0 1800ms=0 1850ms=0 1900ms=0 1950ms=0 2000ms=0 2050ms=0 2100ms=0 2150ms=0 2200ms=0 2250ms=0 2300ms=0 2350ms=0 2400ms=0 2450ms=0 2500ms=0 2550ms=0 2600ms=0 2650ms=0 2700ms=0 2750ms=0 2800ms=0 2850ms=0 2900ms=0 2950ms=0 3000ms=0 3050ms=0 3100ms=0 3150ms=0 3200ms=0 3250ms=0 3300ms=0 3350ms=0 3400ms=0 3450ms=0 3500ms=0 3550ms=0 3600ms=0 3650ms=0 3700ms=0 3750ms=0 3800ms=0 3850ms=0 3900ms=0 3950ms=0 4000ms=0 4050ms=0 4100ms=0 4150ms=0 4200ms=0 4250ms=0 4300ms=0 4350ms=0 4400ms=0 4450ms=0 4500ms=0 4550ms=0 4600ms=0 4650ms=0 4700ms=0 4750ms=0 4800ms=0 4850ms=0 4900ms=0 4950ms=0
Caches:
Current memory usage / total memory usage (bytes):
TextureCache 5087048 / 59097600
Layers total 0 (numLayers = 0)
RenderBufferCache 0 / 4924800
GradientCache 20480 / 1048576
PathCache 0 / 9849600
TessellationCache 0 / 1048576
TextDropShadowCache 0 / 4924800
PatchCache 0 / 131072
FontRenderer A8 184219 / 1478656
A8 texture 0 184219 / 1478656
FontRenderer RGBA 0 / 0
FontRenderer total 184219 / 1478656
Other:
FboCache 0 / 0
Total memory usage:
6586184 bytes, 6.28 MB
Pipeline=FrameBuilder
Profile data in ms:
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity/android.view.ViewRootImpl@4a2142e (visibility=8)
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.ArticleDetailActivity/android.view.ViewRootImpl@4bccbcf (visibility=8)
View hierarchy:
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity/android.view.ViewRootImpl@4a2142e
151 views, 154.02 kB of display lists
json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.ArticleDetailActivity/android.view.ViewRootImpl@4bccbcf
19 views, 18.70 kB of display lists
Total ViewRootImpl: 2
Total Views: 170
Total DisplayList: 172.73 kB
下面,我将对其中的关键信息进行分析。
帧的聚合分析数据
开始的一栏是统计的当前界面所有帧的聚合分析数据,主要作用是综合查看App的渲染性能以及帧的稳定性。
后续的log数据表明了不同组件的缓存占用信息,帧的建立路径信息以及总览信息等等,参考意义不大。
可以看到,上述的数据只能让我们总体感受到绘制性能的好坏,并不能去定位具体帧的问题,那么,还有更好的方式去获取具体帧的信息吗?
添加framestats去获取最后120帧的详细信息
该命令的格式如下:
adb shell dumpsys gfxinfo framestats
这里还是以Awesome-WanAndroid项目为例,输出项目标签页的帧详细信息:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroid framestats
Applications Graphics Acceleration Info:
Uptime: 603118462 Realtime: 603118462
...
Window: json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity
Stats since: 603011709157414ns
Total frames rendered: 3295
Janky frames: 117 (3.55%)
50th percentile: 5ms
90th percentile: 9ms
95th percentile: 14ms
99th percentile: 32ms
Number Missed Vsync: 17
Number High input latency: 3
Number Slow UI thread: 97
Number Slow bitmap uploads: 13
Number Slow issue draw commands: 20
HISTOGRAM: 5ms=2710 6ms=75 7ms=81 8ms=70...
---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
0,603111579233508,603111579233508,9223372036854775807,0,603111580203105,603111580207688,603111580417688,603111580651698,603111580981282,603111581033157,603111581263417,603111583942011,603111584638678,1590000,259000,
0,603111595904553,603111595904553,9223372036854775807,0,603111596650344,603111596692428,603111596828678,603111597073261,603111597301386,603111597362376,603111597600292,603111600584667,603111601288261,1838000,278000,
...,
---PROFILEDATA---
...
这里我们只需关注其中的PROFILEDATA一栏,因为它表明了最近120帧每个帧的状态信息。
因为其中的数据是以csv格式显示的,我们将PROFILEDATA中的数据全部拷贝过来,然后放入一个txt文件中,接着,把.txt后缀改为.csv,使用WPS表格工具打开,如下图所示:
从上图中,我们看到输出的第一行是对应的输出数据列的格式,下面我将详细进行分析。
Flags:
IntendedVsync:
Vsync:
OldestInputEvent:
NewestInputEvent:
HandleInputStart:
AnimationStart:
PerformTraversalsStart:
DrawStart:
SyncQueued:
SyncStart:
IssueDrawCommandsStart:
SwapBuffers:
FrameCompleted:
综上,我们可以利用这些数据计算获取我们在自动化测试中想关注的因素,比如帧耗时、该帧调用View.draw方法所消耗的时间。framestats和帧耗时信息等一般2s收集一次,即一次120帧。为了精确控制收集数据的时间窗口,如将数据限制为特定的动画,可以重置计数器,重新聚合统计的信息,对应命令如下:
adb shell dumpsys gfxinfo reset
2、SurfaceFlinger
我们都知道,在Android 4.1以后,系统使用了三级缓冲机制,即此时有三个Graphic Buffer,那么如何查看每个Graphic Buffer占用的内存呢?
答案是使用SurfaceFlinger,命令如下所示:
adb shell dumpsys SurfaceFlinger
输出的结果非常多,因为包含很多系统应用和界面的相关信息,这里我们仅过滤出Awesome-WanAndroid应用对应的信息:
+ Layer 0x7f5a92f000 (json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0)
layerStack= 0, z= 21050, pos=(0,0), size=(1080,2280), crop=( 0, 0,1080,2280), finalCrop=( 0, 0, -1, -1), isOpaque=1, invalidate=0, dataspace=(deprecated) sRGB Linear Full range, pixelformat=RGBA_8888 alpha=0.000, flags=0x00000002, tr=[1.00, 0.00][0.00, 1.00]
client=0x7f5dc23600
format= 1, activeBuffer=[1080x2280:1088, 1], queued-frames=0, mRefreshPending=0
mTexName=386 mCurrentTexture=0
mCurrentCrop=[0,0,0,0] mCurrentTransform=0
mAbandoned=0
- BufferQueue mMaxAcquiredBufferCount=1 mMaxDequeuedBufferCount=2
mDequeueBufferCannotBlock=0 mAsyncMode=0
default-size=[1080x2280] default-format=1 transform-hint=00 frame-counter=51
FIFO(0):
Slots:
// 序号 // 表明是否使用的状态 // 对象地址 // 当前负责第几帧 // 手机屏幕分辨率大小
>[00:0x7f5e05a5c0] state=ACQUIRED 0x7f5b1ca580 frame=51 [1080x2280:1088, 1]
[02:0x7f5e05a860] state=FREE 0x7f5b1ca880 frame=49 [1080x2280:1088, 1]
[01:0x7f5e05a780] state=FREE 0x7f5b052a00 frame=50 [1080x2280:1088, 1]
在Slots中,显示的是缓冲区相关的信息,可以看到,此时App使用的是00号缓冲区,即第一个缓冲区。
接着,在SurfaceFlinger命令输出log的最下方有一栏Allocated buffers,这这里可以使用当前缓冲区对应的对象地址去查询其占用的内存大小。具体对应到我们这里的是0x7f5b1ca580,匹配到的结果如下所示:
0x7f5b052a00: 9690.00 KiB | 1080 (1088) x 2280 | 1 | 1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0
0x7f5b1ca580: 9690.00 KiB | 1080 (1088) x 2280 | 1 | 1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0
0x7f5b1ca880: 9690.00 KiB | 1080 (1088) x 2280 | 1 | 1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#0
可以看到,这里每一个Graphic Buffer都占用了9MB多的内存,通常分辨率越大,单个Graphic Buffer占用的内存就越多,如1080 x 1920的手机屏幕,一般占用8160kb的内存大小。此外,如果应用使用了其它的Surface,如SurfaceView或TextureView(两者一般用在opengl进行图像处理或视频处理的过程中),这个值会更大。如果当App退到后台,系统就会将这部分内存回收。
了解了常用布局优化常用的工具与命令之后,我们就应该开始着手进行优化了,但在开始之前,我们还得对Android的布局加载原理有比较深入的了解。
1、为什么要了解Android布局加载原理?
知其然知其所以然,不仅要明白在平时开发过程中是怎样对布局API进行调用,还要知道它内部的实现原理是什么。明白具体的实现原理与流程之后,我们可能会发现更多可优化的点。
2、布局加载源码分析
我们都知道,Android的布局都是通过setContentView()这个方法进行设置的,那么它的内部肯定实现了布局的加载,接下来,我们就详细分析下它内部的实现原理与流程。
以Awesome-WanAndroid项目为例,我们在通用Activity基类的onCreate方法中进行了布局的设置:
setContentView(getLayoutId());
点进去,发现是调用了AppCompatActivity的setContentView方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
这里的setContentView其实是AppCompatDelegate这个代理类的抽象方法:
/**
* Should be called instead of {@link Activity#setContentView(int)}}
*/
public abstract void setContentView(@LayoutRes int resId);
在这个抽象方法的左边,会有一个绿色的小圆圈,点击它就可以查看到对应的实现类与方法,这里的实现类是AppCompatDelegateImplV9,实现方法如下所示:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
setContentView方法中主要是获取到了content父布局,移除其内部所有视图之后并最终调用了LayoutInflater对象的inflate去加载对应的布局。接下来,我们关注inflate内部的实现:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
这里只是调用了inflate另一个的重载方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 1
final XmlResourceParser parser = res.getLayout(resource);
try {
// 2
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在注释1处,通过Resources的getLayout方法获取到了一个XmlResourceParser对象,继续跟踪下getLayout方法:
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
这里继续调用了loadXmlResourceParser方法,注意第二个参数传入的为layout,说明此时加载的是一个Xml资源布局解析器。我们继续跟踪loadXmlResourceParse方法:
@NonNull
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
// 1
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
在注释1处,如果值类型为字符串的话,则调用了ResourcesImpl实例的loadXmlResourceParser方法。我们首先看看这个方法的注释:
/**
* Loads an XML parser for the specified file.
*
* @param file the path for the XML file to parse
* @param id the resource identifier for the file
* @param assetCookie the asset cookie for the file
* @param type the type of resource (used for logging)
* @return a parser for the specified XML file
* @throws NotFoundException if the file could not be loaded
*/
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
...
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
...
return block.newParser();
...
}
注释的意思说明了这个方法是用于加载指定文件的Xml解析器,这里我们之间查看关键的mAssets.openXmlBlockAsset方法,这里的mAssets对象是AssetManager类型的,看看AssetManager实例的openXmlBlockAsset方法做了什么处理:
/**
* {@hide}
* Retrieve a non-asset as a compiled XML file. Not for use by
* applications.
*
* @param cookie Identifier of the package to be opened.
* @param fileName Name of the asset to retrieve.
*/
/*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName)
throws IOException {
synchronized (this) {
if (!mOpen) {
throw new RuntimeException("Assetmanager has been closed");
}
// 1
long xmlBlock = openXmlAssetNative(cookie, fileName);
if (xmlBlock != 0) {
XmlBlock res = new XmlBlock(this, xmlBlock);
incRefsLocked(res.hashCode());
return res;
}
}
throw new FileNotFoundException("Asset XML file: " + fileName);
}
可以看到,最终是调用了注释1处的openXmlAssetNative方法,这是定义在AssetManager中的一个Native方法:
private native final long openXmlAssetNative(int cookie, String fileName);
与此同时,我们可以猜到读取Xml文件肯定是通过IO流的方式进行的,而openXmlBlockAsset方法后抛出的IOException异常也验证了我们的想法。因为涉及到IO流的读取,所以这里是Android布局加载流程一个耗时点 ,也有可能是我们后续优化的一个方向。
分析完Resources实例的getLayout方法的实现之后,我们继续跟踪inflate方法的注释2处:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 1
final XmlResourceParser parser = res.getLayout(resource);
try {
// 2
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
infalte的实现代码如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
...
// 1
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 2
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
...
}
...
}
...
}
可以看到,infalte内部是通过XmlPull解析的方式对布局的每一个节点进行创建对应的视图的。首先,在注释1处会判断节点是否是merge标签,如果是,则对merge标签进行校验,如果merge节点不是当前布局的父节点,则抛出异常。然后,在注释2处,通过createViewFromTag方法去根据每一个标签创建对应的View视图。我们继续跟踪下createViewFromTag方法的实现:
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
在createViewFromTag方法中,首先会判断mFactory2是否存在,存在就会使用mFactory2的onCreateView方法区创建视图,否则就会调用mFactory的onCreateView方法,接下来,如果此时的tag是一个Fragment,则会调用mPrivateFactory的onCreateView方法,否则的话,最终都会调用LayoutInflater实例的createView方法:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 1
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
// 2
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
...
}
...
// 3
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
}
...
}
LayoutInflater的createView方法中,首先,在注释1处,使用类加载器创建了对应的Class实例,然后在注释2处根据Class实例获取到了对应的构造器实例,并最终在注释3处通过构造器实例constructor的newInstance方法创建了对应的View对象。可以看到,在视图节点的创建过程中采用到了反射,我们都知道反射是比较耗性能的,过多的反射可能会导致布局加载过程变慢,这个点可能是后续优化的一个方向。
最后,我们来总结下Android中的布局加载流程:
从以上分析可知,在Android的布局加载流程中,性能瓶颈主要存在两个地方:
3、LayoutInflater.Factory分析
在前面分析的View的创建过程中,我们明白系统会优先使用Factory2和Factory去创建对应的View,那么它们究竟是干什么的呢?
其实LayoutInflater.Factory是layoutInflater中创建View的一个Hook,Hook即挂钩,我们可以利用它在创建View的过程中加入一些日志或进行其它更高级的定制化处理:比如可以全局替换自定义的TextView等等。
接下来,我们查看下Factory2的实现:
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; note that this may be null.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
可以看到,Factory2是直接继承于Factory,继续跟踪下Factory的源码:
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
*
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
onCreateView方法中的第一个参数就是指的tag名字,比如TextView等等,我们还注意到Factory2比Factory的onCreateView方法多一个parent的参数,这是当前创建的View的父View。看来,Factory2比Factory功能要更强大一些。
最后,我们总结下Factory与Factory2的区别:
1、常规方式
如果要获取每个界面的加载耗时,我们就必需在setContentView方法前后进行手动埋点。但是它有如下缺点:
2、AOP
关于AOP的使用,我在《深入探索Android启动速度优化》一文的AOP(Aspect Oriented Programming)打点部分已经详细讲解过了,这里就不再赘述,还不了解的同学可以点击上面的链接先去学习下AOP的使用。
我们要使用AOP去获取界面布局的耗时,那么我们的切入点就是setContentView方法,声明一个@Aspect注解的PerformanceAop类,然后,我们就可以在里面实现对setContentView进行切面的方法,如下所示:
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}
为了获取方法的耗时,我们必须使用@Around注解,这样第一个参数ProceedingJoinPoint就可以提供proceed方法去执行我们的setContentView方法,在此方法的前后就可以获取setContentView方法的耗时。后面的execution表明了在setContentView方法执行内部去调用我们写好的getSetContentViewTime方法,后面括号内的*是通配符,表示匹配任何Activity的setContentView方法,并且方法参数的个数和类型不做限定。
完成AOP获取界面布局耗时的方法之后,重装应用,打开几个Activity界面,就可以看到如下的界面布局加载耗时日志:
2020-01-01 12:20:17.605 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 174
2020-01-01 12:20:58.010 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 13
2020-01-01 12:21:27.058 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 44
2020-01-01 12:21:31.128 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 61
2020-01-01 12:23:09.805 12297-12297/json.chao.com.wanandroid I/WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 22
可以看到,Awesome-WanAndroid项目里面各个界面的加载耗时一般都在几十毫秒作用,加载慢的界面可能会达到100多ms,当然,不同手机的配置不一样,但是,这足够让我们发现哪些界面布局的加载比较慢。
3、LayoutInflaterCompat.setFactory2
上面我们使用了AOP的方式监控了Activity的布局加载耗时,那么,如果我们需要监控每一个控件的加载耗时,该怎么实现呢?
答案是使用LayoutInflater.Factory2,我们在基类Activity的onCreate方法中直接使用LayoutInflaterCompat.setFactory2方法对Factory2的onCreateView方法进行重写,代码如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件的加载耗时,
// 也可以做全局的自定义控件替换处理,比如:将TextView全局替换为自定义的TextView。
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.equals(name, "TextView")) {
// 生成自定义TextView
}
long time = System.currentTimeMillis();
// 1
View view = getDelegate().createView(parent, name, context, attrs);
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
// 2、setFactory2方法需在super.onCreate方法前调用,否则无效
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
unBinder = ButterKnife.bind(this);
mActivity = this;
ActivityCollector.getInstance().addActivity(this);
onViewCreated();
initToolbar();
initEventAndData();
}
这样我们就实现了利用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件加载耗时的处理,后续我们可以将这些数据上传到我们自己的APM服务端,作为监控数据可以分析出哪些控件加载比较耗时。当然,这里我们也可以做全局的自定义控件替换处理,比如在上述代码中,我们可以将TextView全局替换为自定义的TextView。
然后,我们注意到这里我们使用getDelegate().createView方法来创建对应的View实例,跟踪进去发现这里的createView是一个抽象方法:
public abstract View createView(@Nullable View parent, String name, @NonNull Context context,
@NonNull AttributeSet attrs);
它对应的实现方法为AppCompatDelegateImplV9对象的createView方法,代码如下所示:
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
这里最终又调用了AppCompatViewInflater对象的createView方法:
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
在AppCompatViewInflater对象的createView方法中系统根据不同的tag名字创建出了对应的AppCompat兼容控件。看到这里,我们明白了Android系统是使用了LayoutInflater的Factor2/Factory结合了AppCompat兼容类来进行高级版本控件的兼容适配的。
接下来,我们注意到注释1处,setFactory2方法需在super.onCreate方法前调用,否则无效,这是为什么呢?
这里可以先大胆猜测一下,可能是因为在super.onCreate()方法中就需要将Factory2实例存储到内存中以便后续使用。下面,我们就跟踪一下super.onCreate()的源码,看看是否如我们所假设的一样。AppCompatActivity的onCreate方法如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
第一行的delegate实例的installViewFactory()方法就吸引了我们的注意,因为它包含了一个敏感的关键字“Factory“,这里我们继续跟踪进installViewFactory()方法:
public abstract void installViewFactory();
这里一个是抽象方法,点击左边绿色圆圈,可以看到这里具体的实现类为AppCompatDelegateImplV9,其实现的installViewFactory()方法如下所示:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
可以看到,如果我们在super.onCreate()方法前没有设置LayoutInflater的Factory2实例的话,这里就会设置一个默认的Factory2。最后,我们再来看下默认Factory2的onCreateView方法的实现:
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 1、First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// 2、If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
在注释1处,我们首先会尝试让Activity的Facotry实例去加载对应的View实例,如果Factory不能够处理它,在注释2处,就会调用createView方法去创建对应的View,AppCompatDelegateImplV9类的createView方法的实现上面我们已经分析过了,此处就不再赘述了。
在本篇文章中,我们主要对Android的布局绘制以及加载原理、优化工具、全局监控布局和控件的加载耗时进行了全面的讲解,这为大家学习《深入探索Android布局优化(下)》打下了良好的基础。下面,总结一下本篇文章涉及的五大主题:
下篇,我们将进入布局优化的实战环节,敬请期待~
深入探索Android布局优化(下)