深刻探索Android布局优化(上)

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

Android的绘制优化其实能够分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而致使应用卡顿的问题,因此它能够认为是卡顿优化的一个子集。对于Android开发来讲,写布局能够说是一个比较简单的工做,可是若是想将写的每个布局的渲染性能提高到比较好的程度,要付出的努力是要远远超过写布局所付出的。因为布局优化这一主题包含的内容太多,所以,笔者将它分为了上、下两篇,本篇,即为深刻探索Android布局优化的上篇。本篇包含的主要内容以下所示:html

  • 一、绘制原理
  • 二、屏幕适配
  • 三、优化工具
  • 四、布局加载原理
  • 五、获取界面布局耗时

说到Android的布局绘制,那么咱们就不得不先从布局的绘制原理开始提及。java

1、绘制原理

Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。node

一、CPU与GPU

  • CPU负责计算显示内容,包括Measure、Layout、Record、Execute等操做。在UI绘制上的缺陷在于容易显示重复的视图组件,这样不只带来重复的计算操做,并且会占用额外的GPU资源。
  • GPU负责栅格化(用于将UI元素绘制到屏幕上,即将UI组件拆分到不一样的像素上显示)。

这里举两个栗子来说解一些CPU和GPU的做用:android

  • 一、文字的显示首先通过CPU换算成纹理,而后再传给GPU进行渲染。
  • 二、而图片的显示首先是通过CPU的计算,而后加载到内存当中,最后再传给GPU进行渲染。

那么,软件绘制和硬件绘制有什么区别呢?咱们先看看下图:git

image

这里软件绘制使用的是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 图形系统的总体架构

Android官方的架构图以下:json

image

为了比较好的描述它们之间的做用,咱们能够把应用程序图形渲染过程看成一次绘画过程,那么绘画过程当中 Android 的各个图形组件的做用分别以下:api

  • 画笔:Skia 或者 OpenGL。咱们能够用 Skia去绘制 2D 图形,也能够用 OpenGL 去绘制 2D/3D 图形。
  • 画纸:Surface。全部的元素都在 Surface 这张画纸上进行绘制和渲染。在 Android 中,Window 是 View 的容器,每一个窗口都会关联一个 Surface。而 WindowManager 则负责管理这些窗口,而且把它们的数据传递给 SurfaceFlinger。
  • 画板:Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,在 Android 4.1 以前使用的是双缓冲机制,而在 Android 4.1 以后使用的是三缓冲机制。
  • 显示:SurfaceFlinger。它将 WindowManager 提供的全部 Surface,经过硬件合成器 Hardware Composer 合成并输出到显示屏。

在了解完Android图形系统的总体架构以后,咱们还须要了解下Android系统的显示原理,关于这块内容能够参考我以前写的Android性能优化之绘制优化的Android系统显示原理一节。缓存

三、RenderThread

在Android系统的显示过程当中,虽然咱们利用了GPU的图形高性能计算的能力,可是从计算Display到经过GPU绘制到Frame Buffer都在UI线程中完成,此时若是能让GPU在不一样的线程中进行绘制渲染图形,那么绘制将会更加地流畅。

因而,在Android 5.0以后,引入了RenderNode和RenderThread的概念,它们的做用以下:

  • RenderNode:进一步封装了Display和某些View的属性。
  • RenderThread:渲染线程,负责执行全部的OpenGl命令,其中的RenderNode保存有渲染帧的全部信息,能在主线程有耗时操做的前提下保证动画流畅。

CPU将数据同步给GPU以后,一般不会阻塞等待RenderThread去利用GPU去渲染完视图,而是通知结束以后就返回。加入ReaderThread以后的整个显示调用流程图以下图所示:

image

在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专用指令。

  • 一、其中的OpenGl API调用和Graphic Buffer缓冲区至少会占用几MB以上的内存,内存消耗较大
  • 二、有些OpenGl的绘制API尚未支持,特别是比较低的Android系统版本,而且因为Android每个版本都会对渲染模块进行一些重构,致使了在硬件加速绘制过程当中会出现一些不可预知的Bug。如在Android 5.0~7.0机型上出现的libhwui.so崩溃问题,须要使用inline Hook、GOT Hook等native调试手段去进行分析定位,可能的缘由是ReaderThread与UI线程的sync同步过程出现了差错,而这种状况通常都是有多个相同的视图绘制而致使的,好比View的复用、多个动画同时播放

四、刷新机制

16ms发出VSync信号触发UI渲染,大多数的Android设备屏幕刷新频率为60HZ,若是16ms内不能完成渲染过程,则会产生掉帧现象。

2、屏幕适配

咱们都知道,Android手机屏幕的差别化致使了严重的碎片化问题,而且屏幕材质也是用户比较关注的一个重要因素。

首先,咱们来了解下主流Android屏幕材质,目前主要有两类:

  • LCD(Liquid Crystal Display):液晶显示器。
  • OLED(Organic Light-Emitting Diode ):有机发光二极管。

早在20世纪60年代,随着半导体集成电路的发展,美国人成功研发出了第一块液晶显示屏LCD,而如今大部分最新的高端机使用的都是OLED材质,这是由于相比于LCD屏幕,OLED屏幕在色彩、可弯曲程度、厚度和耗电等方面都有必定的优点。正由于如此,如今主流的全面屏、曲面屏与将来的柔性折叠屏,使用的几乎都是 OLED 材质。当前,好的材质,它的成本也必然会比较昂贵。

一、OLED 屏幕和 LCD 屏幕的区别

若是要明白OLED 屏幕和LCD屏幕的区别,须要了解它们的运行原理,下面,我将分别进行讲解。

屏幕的成像原理

屏幕由无数个点组成,而且,每一个点由红绿蓝三个子像素组成,每一个像素点经过调节红绿蓝子像素的颜色配比来显示不一样的颜色,最终全部的像素点就会造成具体的画面。

LCD背光源与OLED自发光

下面,咱们来看下LCD和OLED的整体结构图,以下所示:

image

LCD的发光原理主要在于背光层Back-light,它一般都会由大量的LED背光灯组成以用于显示白光,以后,为了显示出彩色,在其上面加了一层有颜色的薄膜,白色的背光穿透了有颜色的薄膜后就能够显示出彩色了。可是,为了实现调整红绿蓝光的比例,须要在背光层和颜色薄膜之间加入一个控制阀门,即液晶层liquid crystal,它能够经过改变电压的大小来控制开合的程度,开合大则光多,开合小则光少

对于OLED来讲,它不须要LCD屏幕的背光层和用于控制出光量的液晶层,它就像一个有着无数个小的彩色灯泡组成的屏幕,只须要给它通电就能发光。

LCD的致命缺陷

它的液晶层不能彻底关合,若是LCD显示黑色,会有部分光穿过颜色层,因此LCD的黑色其实是白色和黑色混合而成的灰色。而OLED不同,OLED显示黑色的时候能够直接关闭区域的像素点。

此外,因为背光层的存在,因此LCD显示器的背光很是容易从屏幕与边框之间的缝隙泄漏出去,即会产生显示器漏光现象。

OLED屏幕的优点
  • 一、因为没有有背光层和液晶层的存在,因此它的厚度更薄,其弯曲程度能够达到180%
  • 二、对比度(白色比黑色的比值)更高,使其画面颜色越浓;相较于LCD来讲,OLED是油画,色彩纯而细腻,而LCD是水彩笔画,色彩朦胧且淡
  • 三、OLED每一个像素点都是独立的,因此OLED能够单独点亮某些像素点,即能实现单独点亮。而LCD只能控制整个背光层的开关。而且,因为OLED单独点亮的功能,使其耗电程度大大下降
  • 四、OLED的屏幕响应时间很快,不会形成画面残留以至形成视觉上的拖影现象。而LCD则会有严重的拖影现象。
OLED屏幕的劣势
  • 一、因为OLED是有机材料,致使其寿命是不如LCD的 有机材料的。而且,因为OLED单独点亮的功能,会使每一个像素点工做的时间不同,这样,在屏幕老化时就会致使色彩显示不均匀,即产生烧屏现象。
  • 二、因为OLED就不能采起控制电压的方式去调整亮度,因此目前只能经过不断的开关开关开关去进行调光。
  • 三、OLED的屏幕像素点排列方式不如LCD的紧凑,因此在分辨率相同的状况下,OLED的屏幕是不如LCD清楚的。即OLED的像素密度较低

二、屏幕适配方案

咱们都知道,Android 的 系统碎片化、机型以及屏幕尺寸碎片化、屏幕分辨率碎片化很是地严重。因此,一个好的屏幕适配方案是很重要的。接下来,我将介绍目前主流的屏幕适配方案。

一、最原始的Android适配方案:dp + 自适应布局或weight比例布局

首先,咱们来回顾一下px、dp、dpi、ppi、density等概念:

  • px:像素点,px = density * dp。
  • ppi:像素密度,每英寸所包含的像素数目,屏幕物理参数,不可调整,dpi没有人为调整时 = ppi。
  • dpi:像素密度,在系统软件上指定的单位尺寸的像素数量,可人为调整,dpi没有人为调整时 = ppi。
  • dp:density-independent pixels,即密度无关像素,基于屏幕物理分辨率的一个抽象的单位,以dp为尺寸单位的控件,在不一样分辨率和尺寸的手机上表明了不一样的真实像素,好比在分辨率较低的手机中,可能1dp = 1px,而在分辨率较高的手机中,可能1dp=2px,这样的话,一个64*64dp的控件,在不一样的手机中就能表现出差很少的大小了,px = dp * (dpi / 160)。
  • denstiy:密度,屏幕上每平方英寸所包含的像素点个数,density = dpi / 160。

一般状况下,咱们只须要使用dp + 自适应布局(如鸿神的AutoLayout、ConstraintLayout等等)或weight比例布局便可基本解决碎片化问题,固然,这种方式也存在一些问题,好比dpi和ppi的差别所致使在同一分辨率手机上控件大小的不一样

二、宽高限定符适配方案

它就是穷举市面上全部的Android手机的宽高像素值,经过设立一个基准的分辨率,其余分辨率都根据这个基准分辨率来计算,在不一样的尺寸文件夹内部,根据该尺寸编写对应的dimens文件,以下图所示:

image

好比以480x320为基准分辨率:

  • 宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320。
  • 高度为480,将任何分辨率的高度整分为480份,取值为y1-y480。

那么对于800*480的分辨率的dimens文件来讲:

  • x1=(480/320)*1=1.5px
  • x2=(480/320)*2=3px

image

此时,若是UI设计界面使用的就是基准分辨率,那么咱们就能够按照设计稿上的尺寸填写相对应的dimens去引用,而当APP运行在不一样分辨率的手机中时,系统会根据这些dimens去引用该分辨率对应的文件夹下面去寻找对应的值。可是这个方案由一个缺点,就是没法作到向下兼容去使用更小的dimens,好比说800x480的手机就必定要找到800x480的限定符,不然就只能用统一默认的dimens文件了。

三、UI适配框架AndroidAutoLayout的适配方案

因宽高限定符方案的启发,鸿神出品了一款能使用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适配方案(sw限定符适配)

smallestWidth即最小宽度,系统会根据当前设备屏幕的 最小宽度 来匹配 values-swdp。

咱们都知道,移动设备都是容许屏幕能够旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是由于这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度。

而且它跟宽高限定符适配原理上是同样,都是系统经过特定的规则来选择对应的文件。它与AndroidAutoLayout同样,一样解决了其dimens不能向下兼容的问题,若是该屏幕的最小宽度是360dp,可是项目中没有values-sw360dp文件夹的话,它就可能找到values-sw320dp这个文件夹,其尺寸规则命名以下图所示:

image

假如加入咱们的设计稿的像素宽度是375,那么其对应的values-sw360dp和values-sw400dp宽度以下所示:

image

image

smallestWidth的适配机制由系统保证,咱们只须要针对这套规则生成对应的资源文件便可,即便对应的smallestWidth值没有找到彻底对应的资源文件,它也能向下兼容,寻找最接近的资源文件。虽然多个dimens文件可能致使apk变大,可是其增长大小范围也只是在300kb-800kb这个区间,这仍是能够接受的。这套方案惟一的变数就是选择须要适配哪些最小宽度限定符的文件,若是您生成的 values-swdp 与设备实际的 最小宽度 差异不大,那偏差也就在能接受的范围内,若是差异很大,那效果就会不好。最后,总结一下这套方案的优缺点:

优势:

  • 一、稳定且无性能损耗。
  • 二、可经过选择须要哪些最小宽度限定符文件去控制适配范围。
  • 三、在自动生成values-sw的插件基础下,学习成本较低。

插件地址为自动生成values-sw的项目代码。生成须要的values-swdp文件夹的步骤以下:

  • 一、clone该项目到本地,以Android项目打开。
  • 二、DimenTypes文件中写入你但愿适配的sw尺寸,默认的这些尺寸可以覆盖几乎全部手机适配需求。
  • 三、DimenGenerator文件中填写设计稿的尺寸(DESIGN_WIDTH是设计稿宽度,DESIGN_HEIGHT是设计稿高度)。
  • 四、执行lib module中的DimenGenerator.main()方法,当前地址下会生成相应的适配文件,把相应的文件连带文件夹拷贝到正在开发的项目中。

缺点:

  • 一、侵入性高,后续切换其余屏幕适配方案需修改大量 dimens 引用。
  • 二、覆盖更多不一样屏幕的机型须要生成更多的资源文件,使APK体积变大。
  • 三、不能自动支持横竖屏切换时的适配,如要支持需使用 values-wdp 或 屏幕方向限定符 再生成一套资源文件,又使APK体积变大。

若是想让屏幕宽度随着屏幕的旋转而作出改变该怎么办呢?

此时根据 values-wdp (去掉 sw 中的 s) 去生成一套资源文件便可。

若是想区分屏幕的方向来作适配该怎么办呢?

去根据 屏幕方向限定符 生成一套资源文件,后缀加上 -land 或 -port 便可,如:values-sw360dp-land (最小宽度 360 dp 横向),values-sw400dp-port (最小宽度 720 dp 纵向)。

注意:

若是UI设计上明显更适合使用wrap_content,match_parent,layout_weight等,咱们就要绝不犹豫的使用,毕竟,上述都是仅仅针对不得不使用固定宽高的状况,我相信基础的UI适配知识大部分开发者仍是具有的。若是不具有的话,请看下方:

五、今日头条适配方案

它的原理是根据屏幕的宽度或高度动态调整每一个设备的 density (每 dp 占当前设备屏幕多少像素),经过修改density值的方式,强行把全部不一样尺寸分辨率的手机的宽度dp值改为一个统一的值,这样就能够解决全部的适配问题。其对应的重要公式以下:

当前设备屏幕总宽度(单位为像素)/  设计图总宽度(单位为 dp) = density
复制代码

今日头条适配方案默认项目中只能以高或宽中的一个做为基准来进行适配,并不像 AndroidAutoLayout 同样,高以高为基准,宽以宽为基准,来同时进行适配,为何?

由于,如今中国大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是如今的全面屏、刘海屏、弹性折叠屏,使这个问题更加严重,不一样厂商推出的手机的屏幕高宽比均可能不一致。因此,咱们只能以高或宽其中的一个做为基准进行适配,以此避免布局在高宽比不一致的屏幕上出现变形。

它有如下优点:

  • 一、使用成本低,操做简单,使用该方案后在页面布局时不须要额外的代码和操做。
  • 二、侵入性低,和项目彻底解耦,在项目布局时不会依赖哪怕一行该方案的代码,并且使用的仍是 Android 官方的 API,意味着当你遇到什么问题没法解决,想切换为其余屏幕适配方案时,基本不须要更改以前的代码,整个切换过程几乎在瞬间完成,试错成本接近于 0。
  • 三、可适配三方库的控件和系统的控件(不止是是 Activity 和 Fragment,Dialog、Toast 等全部系统控件均可以适配),因为修改的 density 在整个项目中是全局的,因此只要一次修改,项目中的全部地方都会受益。
  • 四、不会有任何性能的损耗。
  • 五、不涉及私有API。

它的缺点以下所示:

  • 一、适配范围不可控,只能一刀切的将整个项目进行适配,这种将全部控件都强行使用咱们项目自身的设计图尺寸进行适配的方案会有问题:当某个系统控件或三方库控件的设计图尺寸和和咱们项目自身的设计图尺寸差距越大时,该系统控件或三方库控件的适配效果就越差。比较好的解决方案就是按 Activity 为单位,取消当前 Activity 的适配效果,改用其余的适配方案。
  • 二、对旧项目的UI适配兼容性不够。

注意:

千万不要在此方案上使用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 限定符适配方案 主打的是稳定性,在运行过程当中极少会出现安全隐患,适配范围也可控,不会产生其余未知的影响,而 今日头条适配方案 主打的是下降开发成本、提升开发效率,使用上更灵活,也能知足更多的扩展需求。因此,具体状况具体分析,到底选择哪个屏幕适配方案仍是须要去根据咱们项目自身的需求去选择。

3、优化工具

一、Systrace

早在深刻探索Android启动速度优化一文中咱们就了解过Systrace的使用、原理及它做为启动速度分析的用法。而它其实主要是用来分析绘制性能方面的问题。下面我就详细介绍下Systrace做为绘制优化工具备哪些必须关注的点。

一、关注Frames

首先,先在左边栏选中咱们当前的应用进程,在应用进程一栏下面有一栏Frames,咱们能够看到有绿、黄、红三种不一样的小圆圈,以下图所示:

image

图中每个小圆圈表明着当前帧的状态,大体的对应关系以下:

  • 正常:绿色。
  • 丢帧:黄色。
  • 严重丢帧:红色。

而且,选中其中某一帧,咱们还能够在视图最下方的详情框看到该帧对应的相关的Alerts报警信息,以帮助咱们去排查问题;此外,若是是大于等于Android 5.0的设备(即API Level21),建立帧的工做工做分为UI线程和render线程。而在Android 5.0以前的版本中,建立帧的全部工做都是在UI线程上完成的。接下来,咱们看看该帧对应的详情图,以下所示:

image

对应到此帧,咱们发现这里可能有两个绘制问题:Bitmap过大、布局嵌套层级过多致使的measure和layout次数过多,这就须要咱们去在项目中找到该帧对应的Bitmap进行相应的优化,针对布局嵌套层级过多的问题去选择更高效的布局方式,这块后面咱们会详细介绍。

二、关注Alerts栏

此外,Systrace的显示界面还在在右边侧栏提供了一栏Alert框去显示出它所检测出全部可能有绘制性能问题的地方及对应的数量,以下图所示:

image

在这里,咱们能够将Alert框看作是一个是待修复的Bug列表,一般一个区域的改进能够消除应用程序中的全部类中该类型的警报,因此,不要为这里的警报数量所担心。

二、Layout Inspector

Layout Inspector是AndroidStudio自带的工具,它的主要做用就是用来查看视图层级结构的。

具体的操做路径为:

点击Tools工具栏 ->第三栏的Layout Inspector -> 选中当前的进程
复制代码

下面为操做以后打开的Awesome-WanAndroid首页图,以下所示:

image

其中,最右侧的View Tree就是用来查看视图的层级结构的,很是方便,这是它最主要的功能,中间的是一个屏幕截图,最右边的是一个属性表格,好比我在截图中选中某一个TextView(Kotlin/入门及知识点一栏),在属性表格的text中就能够显示相关的信息,以下图所示:

image

三、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了。

四、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相关开发的使用,这里我就很少介绍了。

五、自动化测量 UI 渲染性能的方式

在自动化测试中,咱们一般但愿经过执行性能测试的自动化脚原本进行线下的自动化检测,那么,有哪些命令能够用于测量UI渲染的性能呢?

咱们都知道,dumpsys是一款输出有关系统服务状态信息的Android工具,利用它咱们能够获取当前设备的UI渲染性能信息,目前经常使用的有以下两种命令:

一、gfxinfo

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的渲染性能以及帧的稳定性。

  • Graphics info for pid 1722 [json.chao.com.wanandroid] -> 说明了当前提供的是Awesome-WanAndroid应用界面的帧信息,对应的进程id为1722。
  • Total frames rendered 5210 -> 本次dump的数据搜集了5210帧的信息。
  • Janky frames: 193 (3.70%) -> 5210帧中有193帧发生了Jank,即单帧耗时时间超过了16ms,卡顿的几率为3.70%。
  • 50th percentile: 5ms -> 全部帧耗时排序后,其中前50%最大的耗时帧的耗时为5ms。
  • 90th percentile: 9ms -> 同上,依次类推。
  • 95th percentile: 13ms -> 同上,依次类推。
  • 99th percentile: 34ms -> 同上,依次类推。
  • Number Missed Vsync: 31 -> 垂直同步失败的帧数为31。
  • Number High input latency: 0 -> 处理input耗时的帧数为0。
  • Number Slow UI thread: 153 -> 因UI线程的工做而致使耗时的帧数为153。
  • Number Slow bitmap uploads: 6 -> 因bitmap加载致使耗时的帧数为6。
  • Number Slow issue draw commands: 51 -> 因绘制问题致使耗时的帧数为51。
  • HISTOGRAM: 5ms=4254 6ms=131 7ms=144 8ms=87... -> 直方图数据列表,说明了耗时0~5ms的帧数为4254,耗时5~6ms的帧数为131,后续的数据依次类推便可。

后续的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表格工具打开,以下图所示:

image

从上图中,咱们看到输出的第一行是对应的输出数据列的格式,下面我将详细进行分析。

Flags:

  • Flags为0则可计算得出该帧耗时:FrameCompleted - IntendedVsync。
  • Flags为非0则表示绘制时间超过16ms,为异常帧。

IntendedVsync:

  • 帧的预期Vsync时刻,若是预期的Vsync时刻与现实的Vsync时刻不一致,则代表UI线程中有耗时工做致使其没法响应Vsync信号。

Vsync:

  • 花费在Vsync监听器和帧绘制的时间,好比Choreographer frame回调、动画、View.getDrawingTime等待。
  • 理解Vsync:Vsync避免了在屏幕刷新时,把数据从后台缓冲区复制到帧缓冲区所消耗的时间。

OldestInputEvent:

  • 输入队列中最旧输入事件的时间戳,若是没有输入事件,则此列数据都为Long.MAX_VALUE。
  • 一般用于framework层开发。

NewestInputEvent:

  • 输入队列中最新输入时间的时间戳,若是没有输入事件,则此列数据都为0。
  • 计算App大体的延迟添加时间:FrameCompleted - NewestInputEvent。
  • 一般用于framework层开发。

HandleInputStart:

  • 将输入事件分发给App对应的时间戳时刻。
  • 用于测量App处理输入事件的时间:AnimationStart - HandleInputStart。当值大于2ms时,说明程序花费了很长的时间来处理输入事件,好比View.onTouchEvent等事件。注意在Activity切换或产生点击事件时此值通常都比较大,此时是能够接受的。

AnimationStart:

  • 运行Choreographer(舞蹈编排者)注册动画的时间戳。
  • 用来评估全部运行的全部动画器(ObjectAnimator、ViewPropertyAnimator、经常使用转换器)须要多长时间:AnimationStart - PerformTraversalsStart。当值大于2ms时,请查看此时是否执行的是自定义动画且动画是否有耗时操做。

PerformTraversalsStart:

  • 执行布局递归遍历开始的时间戳。
  • 用于获取measure、layout的时间:DrawStart - PerformTraversalsStart。(注意滚动或动画期间此值应接近于0)。

DrawStart:

  • draw阶段开始的时间戳,它记录了任何无效视图的DisplayList的起点。
  • 用于获取视图数中全部无效视图调用View.draw方法所需的时间:SyncStart - DrawStart。
  • 在此过程当中,硬件加速模块中的DisplayList发挥了重要做用,Android系统仍然使用invalidate()调用draw()方法请求屏幕更新和渲染视图,可是对实际图形的处理方式有所不一样。**Android系统并无当即执行绘图命令,而是将它们记录在DisplayList中,该列表包含视图层次结构绘图所需的全部信息。相对于软件渲染的另外一个优化是,Android系统仅须要记录和更新DispalyList,以显示被invalidate() 标记为dirty的视图。只需从新发布先前记录的Displaylist,便可从新绘制还没有失效的视图。**此时的硬件绘制模型主要包括三个过程:刷新视图层级、记录和更新DisplayList、绘制DisplayList。相对于软件绘制模型的刷新视图层级、而后直接去绘制视图层级的两个步骤,虽然多了一个步骤,可是节省了不少没必要要的绘制开销。

SyncQueued:

  • sync请求发送到RenderThread线程的时间戳。
  • 获取sync就绪所花费的时间:SyncStart - SyncQueued。若是值大于0.1ms,则说明RenderThread正在忙于处理不一样的帧。

SyncStart:

  • 绘图的sync阶段开始的时间戳。
  • IssueDrawCommandsStart - SyncStart > 0.4ms左右则代表有许多新的位图须要上传至GPU。

IssueDrawCommandsStart:

  • 硬件渲染器开始GPU发出绘图命令的时间戳。
  • 用于观察App此时绘制时消耗了多少GPU:FrameCompleted - IssueDrawCommandsStart。

SwapBuffers:

  • eglSwapBuffers被调用时的时间戳。
  • 一般用于Framework层开发。

FrameCompleted:

  • 当前帧完成绘制的时间戳。
  • 获取当前帧绘制的总时间:FrameCompleted - IntendedVsync。

综上,咱们能够利用这些数据计算获取咱们在自动化测试中想关注的因素,好比帧耗时、该帧调用View.draw方法所消耗的时间。framestats和帧耗时信息等通常2s收集一次,即一次120帧。为了精确控制收集数据的时间窗口,如将数据限制为特定的动画,能够重置计数器,从新聚合统计的信息,对应命令以下:

adb shell dumpsys gfxinfo <PackageName> reset
复制代码
二、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的布局加载原理有比较深刻的了解。

4、布局加载原理

一、为何要了解Android布局加载原理?

知其然知其因此然,不只要明白在平时开发过程当中是怎样对布局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中的布局加载流程:

  • 一、在setContentView方法中,会经过LayoutInflater的inflate方法去加载对应的布局。
  • 二、inflate方法中首先会调用Resources的getLayout方法去经过IO的方式去加载对应的Xml布局解析器到内存中。
  • 三、接着,会经过createViewFromTag根据每个tag建立具体的View对象。
  • 四、它内部主要是按优先顺序为Factory2和Factory的onCreatView、createView方法进行View的建立,而createView方法内部采用了构造器反射的方式实现。

从以上分析可知,在Android的布局加载流程中,性能瓶颈主要存在两个地方:

  • 一、布局文件解析中的IO过程。
  • 二、建立View对象时的反射过程。

三、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; <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的区别:

  • 一、Factory2继承与Factory。
  • 二、Factory2比Factory的onCreateView方法多一个parent的参数,即当前建立View的父View。

5、获取界面布局耗时

一、常规方式

若是要获取每一个界面的加载耗时,咱们就必需在setContentView方法先后进行手动埋点。可是它有以下缺点:

  • 一、不够优雅。
  • 二、代码有侵入性。

二、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,固然,不一样手机的配置不同,可是,这足够让咱们发现哪些界面布局的加载比较慢

三、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;
        }
    });

    // 二、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布局优化(下)》打下了良好的基础。下面,总结一下本篇文章涉及的五大主题:

  • 一、绘制原理:CPU\GPU、Android图形系统的总体架构、绘制线程、刷新机制。
  • 二、屏幕适配:OLED 屏幕和 LCD 屏幕的区别、屏幕适配方案。
  • 三、优化工具:使用Systrace来进行布局优化、利用Layout Inspector来查看视图层级结构、采用Choreographer来获取FPS以及自动化测量 UI 渲染性能的方式(gfxinfo、SurfaceFlinger等dumpsys命令)。
  • 四、布局加载原理:布局加载源码分析、LayoutInflater.Factory分析。
  • 五、获取界面布局耗时:使用AOP的方式去获取界面加载的耗时、利用LayoutInflaterCompat.setFactory2去监控每个控件加载的耗时。

下篇,咱们将进入布局优化的实战环节,敬请期待~

参考连接:

一、国内Top团队大牛带你玩转Android性能分析与优化 第五章 布局优化

二、极客时间之Android开发高手课 UI优化

三、手机屏幕的前世此生 可能比你想的还精彩

四、OLED 和 LCD 什么区别?

五、Android 目前稳定高效的UI适配方案

六、骚年你的屏幕适配方式该升级了!-smallestWidth 限定符适配方案

七、dimens_sw github

八、一种极低成本的Android屏幕适配方式

九、骚年你的屏幕适配方式该升级了!-今日头条适配方案

十、今日头条屏幕适配方案终极版正式发布!

十一、使用Systrace分析UI性能

十二、GAPID-Graphics API Debugger

1三、Android性能优化之渲染篇

1四、Android 屏幕绘制机制及硬件加速

1五、Android 图形处理官方教程

1六、Vulkan - 高性能渲染

1七、Android Vulkan Tutorial

1八、Test UI performance-gfxinfo

1九、使用dumpsys gfxinfo 测UI性能(适用于Android6.0之后)

20、TextureView API

2一、PrecomputedText API

2二、Litho Tutorial

2三、基本功 | Litho的使用及原理剖析

2四、Flutter官方文档中文版

2五、[Google Flutter 团队出品] 深刻了解 Flutter 的高性能图形渲染

2六、Flutter渲染机制—UI线程

2七、RenderThread:异步渲染动画

2八、RenderScript官方文档

2九、RenderScript :简单而快速的图像处理

30、RenderScript渲染利器

赞扬

若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!


Contanct Me

● 微信:

欢迎关注个人微信:bcce5360

● 微信群:

微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。