Android布局优化(转)

转自

0前言

Android的绘制优化其实可以分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题,所以它可以认为是卡顿优化的一个子集。

对于Android开发来说,写布局可以说是一个比较简单的工作,但是如果想将写的每一个布局的渲染性能提升到比较好的程度,要付出的努力是要远远超过写布局所付出的。由于布局优化这一主题包含的内容太多,因此,笔者将它分为了上、下两篇,本篇,即为深入探索Android布局优化的上篇。本篇包含的主要内容如下所示:

  1. 绘制原理

  2. 屏幕适配

  3. 优化工具

  4. 布局加载原理

  5. 获取界面布局耗时

说到Android的布局绘制,那么我们就不得不先从布局的绘制原理开始说起。

1绘制原理

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

1、CPU与GPU

  • CPU负责计算显示内容,包括Measure、Layout、Record、Execute等操作。在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。

  • GPU负责栅格化(用于将UI元素绘制到屏幕上,即将UI组件拆分到不同的像素上显示)。

这里举两个栗子来讲解一些CPU和GPU的作用:

  • 文字的显示首先经过CPU换算成纹理,然后再传给GPU进行渲染。

  • 而图片的显示首先是经过CPU的计算,然后加载到内存当中,最后再传给GPU进行渲染。

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

image

这里软件绘制使用的是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官方的架构图如下:

image

为了比较好的描述它们之间的作用,我们可以把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用分别如下:

  • 画笔: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系统显示原理一节。

https://jsonchao.github.io/2019/07/28/Android%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E7%BB%98%E5%88%B6%E4%BC%98%E5%8C%96/

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

1、其中的OpenGl API调用和Graphic Buffer缓冲区至少会占用几MB以上的内存,内存消耗较大。

2、有些OpenGl的绘制API还没有支持,特别是比较低的Android系统版本,并且由于Android每一个版本都会对渲染模块进行一些重构,导致了在硬件加速绘制过程中会出现一些不可预知的Bug。

如在Android 5.0~7.0机型上出现的libhwui.so崩溃问题,需要使用inline Hook、GOT Hook等native调试手段去进行分析定位,可能的原因是ReaderThread与UI线程的sync同步过程出现了差错,而这种情况一般都是有多个相同的视图绘制而导致的,比如View的复用、多个动画同时播放。

4、刷新机制

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

2优化工具

1、Systrace

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

1、关注Frames

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

image.gif

图中每一个小圆圈代表着当前帧的状态,大致的对应关系如下:

  • 正常:绿色。

  • 丢帧:黄色。

  • 严重丢帧:红色。

并且,选中其中某一帧,我们还可以在视图最下方的详情框看到该帧对应的相关的Alerts报警信息,以帮助我们去排查问题;

此外,如果是大于等于Android 5.0的设备(即API Level21),创建帧的工作工作分为UI线程和render线程。而在Android 5.0之前的版本中,创建帧的所有工作都是在UI线程上完成的。

接下来,我们看看该帧对应的详情图,如下所示:

image

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

2、关注Alerts栏

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

image

在这里,我们可以将Alert框看做是一个是待修复的Bug列表,通常一个区域的改进可以消除应用程序中的所有类中该类型的警报,所以,不要为这里的警报数量所担忧。

2、Layout Inspector

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

具体的操作路径为:

<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; color: rgb(51, 51, 51); font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; text-align: left;">点击Tools工具栏 ->第三栏的Layout Inspector -> 选中当前的进程 </pre>

下面为操作之后打开的Awesome-WanAndroid首页图,如下所示:

https://github.com/JsonChao/Awesome-WanAndroid

image

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

image.gif

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 <PackageName>

这里我以Awesome-WanAndroid项目为例,输出其对应的gfxinfo信息如下所示:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroidApplications Graphics Acceleration Info:Uptime: 549887348 Realtime: 549887348** Graphics info for pid 1722     [json.chao.com.wanandroid] **Stats since: 549356564232951nsTotal frames rendered: 5210Janky frames: 193 (3.70%)50th percentile: 5ms90th percentile: 9ms95th percentile: 13ms99th percentile: 34msNumber Missed Vsync: 31Number High input latency: 0Number Slow UI thread: 153Number Slow bitmap uploads: 6Number Slow issue draw commands: 51HISTOGRAM: 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=0Caches:Current memory usage / total memory usage (bytes):TextureCache          5087048 / 59097600Layers total          0 (numLayers = 0)RenderBufferCache           0 /  4924800GradientCache           20480 /  1048576PathCache                   0 /  9849600TessellationCache           0 /  1048576TextDropShadowCache         0 /  4924800PatchCache                  0 /   131072FontRenderer A8        184219 /  1478656    A8   texture 0       184219 /  1478656FontRenderer RGBA           0 /        0FontRenderer total     184219 /  1478656Other:FboCache                    0 /        0Total memory usage:6586184 bytes, 6.28 MBPipeline=FrameBuilderProfile 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@4a2142e151 views, 154.02 kB of display listsjson.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.ArticleDetailActivity/android.view.ViewRootImpl@4bccbcf19 views, 18.70 kB of display listsTotal ViewRootImpl: 2Total Views:        170Total 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... -> 直方图数据列表,说明了耗时05ms的帧数为4254,耗时56ms的帧数为131,后续的数据依次类推即可。

后续的log数据表明了不同组件的缓存占用信息,帧的建立路径信息以及总览信息等等,参考意义不大。

可以看到,上述的数据只能让我们总体感受到绘制性能的好坏,并不能去定位具体帧的问题,那么,还有更好的方式去获取具体帧的信息吗?

添加framestats去获取最后120帧的详细信息

该命令的格式如下:

adb shell dumpsys gfxinfo <PackageName> framestats

这里还是以Awesome-WanAndroid项目为例,输出项目标签页的帧详细信息:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys gfxinfo json.chao.com.wanandroid framestatsApplications Graphics Acceleration Info:Uptime: 603118462 Realtime: 603118462...Window: json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivityStats since: 603011709157414nsTotal frames rendered: 3295Janky frames: 117 (3.55%)50th percentile: 5ms90th percentile: 9ms95th percentile: 14ms99th percentile: 32msNumber Missed Vsync: 17Number High input latency: 3Number Slow UI thread: 97Number Slow bitmap uploads: 13Number Slow issue draw commands: 20HISTOGRAM: 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帧。为了精确控制收集数据的时间窗口,如将数据限制为特定的动画,可以重置计数器,重新聚合统计的信息,对应命令如下:

<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; color: rgb(51, 51, 51); font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; text-align: left;">

adb shell dumpsys gfxinfo <PackageName> reset

</pre>

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#00x7f5b1ca580: 9690.00 KiB | 1080 (1088) x 2280 |    1 |        1 | 0x10000900 | json.chao.com.wanandroid/json.chao.com.wanandroid.ui.main.activity.MainActivity#00x7f5b1ca880: 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的布局加载原理有比较深入的了解。

image

3布局加载原理

1、为什么要了解Android布局加载原理?

知其然知其所以然,不仅要明白在平时开发过程中是怎样对布局API进行调用,还要知道它内部的实现原理是什么。

明白具体的实现原理与流程之后,我们可能会发现更多可优化的点。

2、布局加载源码分析

我们都知道,Android的布局都是通过setContentView()这个方法进行设置的,那么它的内部肯定实现了布局的加载,接下来,我们就详细分析下它内部的实现原理与流程。

以Awesome-WanAndroid项目为例,我们在通用Activity基类的onCreate方法中进行了布局的设置:

setContentView(getLayoutId());

点进去,发现是调用了AppCompatActivity的setContentView方法:

@Overridepublic 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,实现方法如下所示:

 @Overridepublic 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方法:

@NonNullXmlResourceParser 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 */@NonNullXmlResourceParser 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中的布局加载流程:

  1. 在setContentView方法中,会通过LayoutInflater的inflate方法去加载对应的布局。

  2. inflate方法中首先会调用Resources的getLayout方法去通过IO的方式去加载对应的Xml布局解析器到内存中。

  3. 接着,会通过createViewFromTag根据每一个tag创建具体的View对象。

  4. 它内部主要是按优先顺序为Factory2和Factory的onCreatView、createView方法进行View的创建,而createView方法内部采用了构造器反射的方式实现。

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

  1. 布局文件解析中的IO过程。

  2. 创建View对象时的反射过程。

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; <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 {    public View onCreateView(String name, Context context, AttributeSet attrs);}

onCreateView方法中的第一个参数就是指的tag名字,比如TextView等等,我们还注意到Factory2比Factory的onCreateView方法多一个parent的参数,这是当前创建的View的父View。看来,Factory2比Factory功能要更强大一些。

最后,我们总结下Factory与Factory2的区别:

  • 1、Factory2继承与Factory。

  • 2、Factory2比Factory的onCreateView方法多一个parent的参数,即当前创建View的父View。

4获取界面布局耗时

1、常规方式

如果要获取每个界面的加载耗时,我们就必需在setContentView方法前后进行手动埋点。但是它有如下缺点:

  • 1、不够优雅。

  • 2、代码有侵入性。

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界面,就可以看到如下的界面布局加载耗时日志:

WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 174WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 13WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 44WanAndroid-PEGASILOG: │ [PerformanceAop.java | 36 | getSetContentViewTime] AppCompatActivity.setContentView(..) cost 61WanAndroid-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方法进行重写,代码如下所示:

@Overrideprotected 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();}

鸿洋注:这里去捕获控件创建确实是个思路,但是并不能捕获到所有的控件,如果大家有这方面需求,可以在 github 上看一些换肤框架的处理。

这样我们就实现了利用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方法,代码如下所示:

@Overridepublic 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方法如下所示:

@Overrideprotected 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()方法如下所示:

@Overridepublic 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}. */@Overridepublic 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方法的实现上面我们已经分析过了,此处就不再赘述了。

5总结

在本篇文章中,我们主要对Android的布局绘制以及加载原理、优化工具、全局监控布局和控件的加载耗时进行了全面的讲解,这为大家学习《深入探索Android布局优化(下)》打下了良好的基础。

下面,总结一下本篇文章涉及的五大主题:

  1. 绘制原理:CPU\GPU、Android图形系统的整体架构、绘制线程、刷新机制。

  2. 屏幕适配:OLED 屏幕和 LCD 屏幕的区别、屏幕适配方案。(本文略过了,可以去原文查看)

  3. 优化工具:使用Systrace来进行布局优化、利用Layout Inspector来查看视图层级结构、采用Choreographer来获取FPS以及自动化测量 UI 渲染性能的方式(gfxinfo、SurfaceFlinger等dumpsys命令)。

  4. 布局加载原理:布局加载源码分析、LayoutInflater.Factory分析。

  5. 获取界面布局耗时:使用AOP的方式去获取界面加载的耗时、利用LayoutInflaterCompat.setFactory2去监控每一个控件加载的耗时。

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

参考链接:

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

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

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

4、OLED 和 LCD 什么区别?

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

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

7、dimens_sw github

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

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

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

11、使用Systrace分析UI性能

12、GAPID-Graphics API Debugger

13、Android性能优化之渲染篇

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

15、Android 图形处理官方教程

16、Vulkan - 高性能渲染

17、Android Vulkan Tutorial

18、Test UI performance-gfxinfo

19、使用dumpsys gfxinfo 测UI性能(适用于Android6.0以后)

20、TextureView API

21、PrecomputedText API

22、Litho Tutorial

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

24、Flutter官方文档中文版

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

26、Flutter渲染机制—UI线程

27、RenderThread:异步渲染动画

28、RenderScript官方文档

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

30、RenderScript渲染利器

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容