Android的绘制优化其实能够分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而致使应用卡顿的问题,因此它能够认为是卡顿优化的一个子集。对于Android开发来讲,写布局能够说是一个比较简单的工做,可是若是想将写的每个布局的渲染性能提高到比较好的程度,要付出的努力是要远远超过写布局所付出的。因为布局优化这一主题包含的内容太多,所以,笔者将它分为了上、下两篇,本篇,即为深刻探索Android布局优化的上篇。本篇包含的主要内容以下所示:html
说到Android的布局绘制,那么咱们就不得不先从布局的绘制原理开始提及。java
Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。node
这里举两个栗子来说解一些CPU和GPU的做用:android
那么,软件绘制和硬件绘制有什么区别呢?咱们先看看下图:git
这里软件绘制使用的是Skia库(一款在低端设备如手机上呈现高质量的 2D 图形的 跨平台图形框架)进行绘制的,而硬件绘制本质上是使用的OpenGl ES接口去利用GPU进行绘制的。OpenGL是一种跨平台的图形API,它为2D/3D图形处理硬件指定了标准的软件接口。而OpenGL ES是用于嵌入式设备的,它是OpenGL规范的一种形式,也可称为其子集。github
而且,因为OpenGl ES系统版本的限制,有不少 绘制API 都有相应的 Android API level 的限制,此外,在Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本的时候,还添加了对Vulkan(一套适用于高性能 3D 图形的低开销、跨平台 API)的支持。Vulan做为下一代图形API以及OpenGL的继承者,它的优点在于大幅优化了CPU上图形驱动相关的性能。shell
Android官方的架构图以下:json
为了比较好的描述它们之间的做用,咱们能够把应用程序图形渲染过程看成一次绘画过程,那么绘画过程当中 Android 的各个图形组件的做用分别以下:api
在了解完Android图形系统的总体架构以后,咱们还须要了解下Android系统的显示原理,关于这块内容能够参考我以前写的Android性能优化之绘制优化的Android系统显示原理一节。缓存
在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专用指令。
16ms发出VSync信号触发UI渲染,大多数的Android设备屏幕刷新频率为60HZ,若是16ms内不能完成渲染过程,则会产生掉帧现象。
咱们都知道,Android手机屏幕的差别化致使了严重的碎片化问题,而且屏幕材质也是用户比较关注的一个重要因素。
首先,咱们来了解下主流Android屏幕材质,目前主要有两类:
早在20世纪60年代,随着半导体集成电路的发展,美国人成功研发出了第一块液晶显示屏LCD,而如今大部分最新的高端机使用的都是OLED材质,这是由于相比于LCD屏幕,OLED屏幕在色彩、可弯曲程度、厚度和耗电等方面都有必定的优点。正由于如此,如今主流的全面屏、曲面屏与将来的柔性折叠屏,使用的几乎都是 OLED 材质。当前,好的材质,它的成本也必然会比较昂贵。
若是要明白OLED 屏幕和LCD屏幕的区别,须要了解它们的运行原理,下面,我将分别进行讲解。
屏幕由无数个点组成,而且,每一个点由红绿蓝三个子像素组成,每一个像素点经过调节红绿蓝子像素的颜色配比来显示不一样的颜色,最终全部的像素点就会造成具体的画面。
下面,咱们来看下LCD和OLED的整体结构图,以下所示:
LCD的发光原理主要在于背光层Back-light,它一般都会由大量的LED背光灯组成以用于显示白光,以后,为了显示出彩色,在其上面加了一层有颜色的薄膜,白色的背光穿透了有颜色的薄膜后就能够显示出彩色了。可是,为了实现调整红绿蓝光的比例,须要在背光层和颜色薄膜之间加入一个控制阀门,即液晶层liquid crystal,它能够经过改变电压的大小来控制开合的程度,开合大则光多,开合小则光少。
对于OLED来讲,它不须要LCD屏幕的背光层和用于控制出光量的液晶层,它就像一个有着无数个小的彩色灯泡组成的屏幕,只须要给它通电就能发光。
它的液晶层不能彻底关合,若是LCD显示黑色,会有部分光穿过颜色层,因此LCD的黑色其实是白色和黑色混合而成的灰色。而OLED不同,OLED显示黑色的时候能够直接关闭区域的像素点。
此外,因为背光层的存在,因此LCD显示器的背光很是容易从屏幕与边框之间的缝隙泄漏出去,即会产生显示器漏光现象。
有机材料的。而且,因为OLED单独点亮的功能,会使每一个像素点工做的时间不同,这样,在屏幕老化时就会致使色彩显示不均匀,即产生烧屏现象。
咱们都知道,Android 的 系统碎片化、机型以及屏幕尺寸碎片化、屏幕分辨率碎片化很是地严重。因此,一个好的屏幕适配方案是很重要的。接下来,我将介绍目前主流的屏幕适配方案。
首先,咱们来回顾一下px、dp、dpi、ppi、density等概念:
一般状况下,咱们只须要使用dp + 自适应布局(如鸿神的AutoLayout、ConstraintLayout等等)或weight比例布局便可基本解决碎片化问题,固然,这种方式也存在一些问题,好比dpi和ppi的差别所致使在同一分辨率手机上控件大小的不一样。
它就是穷举市面上全部的Android手机的宽高像素值,经过设立一个基准的分辨率,其余分辨率都根据这个基准分辨率来计算,在不一样的尺寸文件夹内部,根据该尺寸编写对应的dimens文件,以下图所示:
好比以480x320为基准分辨率:
那么对于800*480的分辨率的dimens文件来讲:
此时,若是UI设计界面使用的就是基准分辨率,那么咱们就能够按照设计稿上的尺寸填写相对应的dimens去引用,而当APP运行在不一样分辨率的手机中时,系统会根据这些dimens去引用该分辨率对应的文件夹下面去寻找对应的值。可是这个方案由一个缺点,就是没法作到向下兼容去使用更小的dimens,好比说800x480的手机就必定要找到800x480的限定符,不然就只能用统一默认的dimens文件了。
因宽高限定符方案的启发,鸿神出品了一款能使用UI适配更加开发高效和适配精准的项目。
基本使用步骤以下:
第一步:在你的项目的AndroidManifest中注明你的设计稿的尺寸:
<meta-data android:name="design_width" android:value="768"> </meta-data> <meta-data android:name="design_height" android:value="1280"> </meta-data>
第二步:让你的Activity继承自AutoLayoutActivity。若是你不但愿继承AutoLayoutActivity,能够在编写布局文件时,直接使用AutoLinearLayout、Auto*等适配布局便可。
接下来,直接在布局文件里面使用具体的像素值就能够了,由于在APP运行时,AndroidAutoLayout会帮助咱们根据不一样手机的具体尺寸按比例伸缩。
AndroidAutoLayout在宽高限定符适配的基础上,解决了其dimens不能向下兼容的问题,可是它在运行时会在onMeasure里面对dimens去作变换,因此对于自定义控件或者某些特定的控件须要进行单独适配;而且,整个UI的适配过程都是由框架完成的,之后想替换成别的UI适配方案成本会比较高,并且,不幸的是,项目已经中止维护了。
smallestWidth即最小宽度,系统会根据当前设备屏幕的 最小宽度 来匹配 values-sw<N>dp。
咱们都知道,移动设备都是容许屏幕能够旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是由于这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度。
而且它跟宽高限定符适配原理上是同样,都是系统经过特定的规则来选择对应的文件。它与AndroidAutoLayout同样,一样解决了其dimens不能向下兼容的问题,若是该屏幕的最小宽度是360dp,可是项目中没有values-sw360dp文件夹的话,它就可能找到values-sw320dp这个文件夹,其尺寸规则命名以下图所示:
假如加入咱们的设计稿的像素宽度是375,那么其对应的values-sw360dp和values-sw400dp宽度以下所示:
smallestWidth的适配机制由系统保证,咱们只须要针对这套规则生成对应的资源文件便可,即便对应的smallestWidth值没有找到彻底对应的资源文件,它也能向下兼容,寻找最接近的资源文件。虽然多个dimens文件可能致使apk变大,可是其增长大小范围也只是在300kb-800kb这个区间,这仍是能够接受的。这套方案惟一的变数就是选择须要适配哪些最小宽度限定符的文件,若是您生成的 values-sw<N>dp 与设备实际的 最小宽度 差异不大,那偏差也就在能接受的范围内,若是差异很大,那效果就会不好。最后,总结一下这套方案的优缺点:
优势:
插件地址为自动生成values-sw<N>的项目代码。生成须要的values-sw<N>dp文件夹的步骤以下:
缺点:
若是想让屏幕宽度随着屏幕的旋转而作出改变该怎么办呢?
此时根据 values-w<N>dp (去掉 sw 中的 s) 去生成一套资源文件便可。
若是想区分屏幕的方向来作适配该怎么办呢?
去根据 屏幕方向限定符 生成一套资源文件,后缀加上 -land 或 -port 便可,如:values-sw360dp-land (最小宽度 360 dp 横向),values-sw400dp-port (最小宽度 720 dp 纵向)。
注意:
若是UI设计上明显更适合使用wrap_content,match_parent,layout_weight等,咱们就要绝不犹豫的使用,毕竟,上述都是仅仅针对不得不使用固定宽高的状况,我相信基础的UI适配知识大部分开发者仍是具有的。若是不具有的话,请看下方:
<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/14/16fa19bf2363f9e1?w=240&h=240&f=gif&s=29989" width=30%>
</div>
它的原理是根据屏幕的宽度或高度动态调整每一个设备的 density (每 dp 占当前设备屏幕多少像素),经过修改density值的方式,强行把全部不一样尺寸分辨率的手机的宽度dp值改为一个统一的值,这样就能够解决全部的适配问题。其对应的重要公式以下:
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
今日头条适配方案默认项目中只能以高或宽中的一个做为基准来进行适配,并不像 AndroidAutoLayout 同样,高以高为基准,宽以宽为基准,来同时进行适配,为何?
由于,如今中国大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是如今的全面屏、刘海屏、弹性折叠屏,使这个问题更加严重,不一样厂商推出的手机的屏幕高宽比均可能不一致。因此,咱们只能以高或宽其中的一个做为基准进行适配,以此避免布局在高宽比不一致的屏幕上出现变形。
它有如下优点:
它的缺点以下所示:
注意:
千万不要在此方案上使用smallestWidth适配方案中直接填写设计图上标注的 px 值的作法,这样会使项目强耦合于这个方案,后续切换其它方案都不得不将全部的 layout 文件都改一遍。
这里推荐一下JessYanCoding的AndroidAutoSize项目,用法以下:
一、首先在项目的build.gradle中添加该库的依赖:
implementation 'me.jessyan:autosize:1.1.2'
二、接着 AndroidManifest 中填写全局设计图尺寸 (单位 dp),若是使用副单位,则能够直接填写像素尺寸,不须要再将像素转化为 dp:
<manifest> <application> <meta-data android:name="design_width_in_dp" android:value="360"/> <meta-data android:name="design_height_in_dp" android:value="640"/> </application> </manifest>
为何只需在AndroidManifest.xml 中填写一下 meta-data 标签就可实现自动运行?
在 App 启动时,系统会在 App 的主进程中自动实例化声明的 ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,能够作一些初始化的工做,这个时候咱们就能够利用它的 onCreate 方法在其中启动框架。若是项目使用了多进程,调用Application#onCreate 中调用下 ContentProvider#query 就可以使用 ContentProvider 在当前进程中进行实例化。
上述介绍的全部方案并无哪个是十分完美的,但咱们能清晰的认识到不一样方案的优缺点,并将它们的优势相结合,这样才能应付更加复杂的开发需求,创造出最卓越的产品。好比SmallestWidth 限定符适配方案 主打的是稳定性,在运行过程当中极少会出现安全隐患,适配范围也可控,不会产生其余未知的影响,而 今日头条适配方案 主打的是下降开发成本、提升开发效率,使用上更灵活,也能知足更多的扩展需求。因此,具体状况具体分析,到底选择哪个屏幕适配方案仍是须要去根据咱们项目自身的需求去选择。
早在深刻探索Android启动速度优化一文中咱们就了解过Systrace的使用、原理及它做为启动速度分析的用法。而它其实主要是用来分析绘制性能方面的问题。下面我就详细介绍下Systrace做为绘制优化工具备哪些必须关注的点。
首先,先在左边栏选中咱们当前的应用进程,在应用进程一栏下面有一栏Frames,咱们能够看到有绿、黄、红三种不一样的小圆圈,以下图所示:
图中每个小圆圈表明着当前帧的状态,大体的对应关系以下:
而且,选中其中某一帧,咱们还能够在视图最下方的详情框看到该帧对应的相关的Alerts报警信息,以帮助咱们去排查问题;此外,若是是大于等于Android 5.0的设备(即API Level21),建立帧的工做工做分为UI线程和render线程。而在Android 5.0以前的版本中,建立帧的全部工做都是在UI线程上完成的。接下来,咱们看看该帧对应的详情图,以下所示:
对应到此帧,咱们发现这里可能有两个绘制问题:Bitmap过大、布局嵌套层级过多致使的measure和layout次数过多,这就须要咱们去在项目中找到该帧对应的Bitmap进行相应的优化,针对布局嵌套层级过多的问题去选择更高效的布局方式,这块后面咱们会详细介绍。
此外,Systrace的显示界面还在在右边侧栏提供了一栏Alert框去显示出它所检测出全部可能有绘制性能问题的地方及对应的数量,以下图所示:
在这里,咱们能够将Alert框看作是一个是待修复的Bug列表,一般一个区域的改进能够消除应用程序中的全部类中该类型的警报,因此,不要为这里的警报数量所担心。
Layout Inspector是AndroidStudio自带的工具,它的主要做用就是用来查看视图层级结构的。
具体的操做路径为:
点击Tools工具栏 ->第三栏的Layout Inspector -> 选中当前的进程
下面为操做以后打开的Awesome-WanAndroid首页图,以下所示:
其中,最右侧的View Tree就是用来查看视图的层级结构的,很是方便,这是它最主要的功能,中间的是一个屏幕截图,最右边的是一个属性表格,好比我在截图中选中某一个TextView(Kotlin/入门及知识点一栏),在属性表格的text中就能够显示相关的信息,以下图所示:
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了。
Tracer for OpenGL ES 是 Android 4.1 新增长的工具,它可逐帧、逐函数的记录 App 使用 OpenGL ES 的绘制过程,而且,它能够记录每一个 OpenGL 函数调用的消耗时间。当使用Systrace还找不到渲染问题时,就能够去尝试使用它。
而GAPID是 Android Studio 3.1 推出的工具,能够认为是Tracer for OpenGL ES的进化版,它不只实现了跨平台,并且支持Vulkan与回放。因为它们主要是用于OpenGL相关开发的使用,这里我就很少介绍了。
在自动化测试中,咱们一般但愿经过执行性能测试的自动化脚原本进行线下的自动化检测,那么,有哪些命令能够用于测量UI渲染的性能呢?
咱们都知道,dumpsys是一款输出有关系统服务状态信息的Android工具,利用它咱们能够获取当前设备的UI渲染性能信息,目前经常使用的有以下两种命令:
gfxinfo的主要做用是输出各阶段发生的动画与帧相关的信息,命令格式以下:
adb shell dumpsys gfxinfo <PackageName>
这里我以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 <PackageName> 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 <PackageName> reset
咱们都知道,在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的布局加载原理有比较深刻的了解。
<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/14/16fa19bf9091f057?w=250&h=250&f=gif&s=91876" width=30%>
</div>
知其然知其因此然,不只要明白在平时开发过程当中是怎样对布局API进行调用,还要知道它内部的实现原理是什么。明白具体的实现原理与流程以后,咱们可能会发现更多可优化的点。
咱们都知道,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("<merge /> 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的布局加载流程中,性能瓶颈主要存在两个地方:
在前面分析的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; <em>note that this may be null</em>. * @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. * * <p> * 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的区别:
若是要获取每一个界面的加载耗时,咱们就必需在setContentView方法先后进行手动埋点。可是它有以下缺点:
关于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,固然,不一样手机的配置不同,可是,这足够让咱们发现哪些界面布局的加载比较慢。
上面咱们使用了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; } }); // 二、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) { // 一、First let the Activity's Factory try and inflate the view final View view = callActivityOnCreateView(parent, name, context, attrs); if (view != null) { return view; } // 二、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布局优化(下)》打下了良好的基础。下面,总结一下本篇文章涉及的五大主题:
下篇,咱们将进入布局优化的实战环节,敬请期待~
一、国内Top团队大牛带你玩转Android性能分析与优化 第五章 布局优化
六、骚年你的屏幕适配方式该升级了!-smallestWidth 限定符适配方案
十二、GAPID-Graphics API Debugger
1八、Test UI performance-gfxinfo
1九、使用dumpsys gfxinfo 测UI性能(适用于Android6.0之后)
2五、[[Google Flutter 团队出品] 深刻了解 Flutter 的高性能图形渲染](https://www.bilibili.com/vide...
若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!
<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/7/16f7dc32595031fa?w=1080&h=1457&f=jpeg&s=93345" width=20%><img src="https://user-gold-cdn.xitu.io/2020/1/7/16f7dc3259518ecd?w=990&h=1540&f=jpeg&s=110691" width=20%>
</div>
欢迎关注个人微信:
bcce5360
微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。
<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/7/16f7dc352011e1fe?w=1013&h=1920&f=jpeg&s=86819" width=35%>
</div>
2千人QQ群, Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~