App绘制优化

屏幕

在不一样分辨率下,dpi将会不一样,好比:html

根据上面的表格,咱们能够发现,720P,和1080P的手机,dpi是不一样的,这也就意味着,不一样的分辨率中,1dp对应不一样数量的px(720P中,1dp=2px,1080P中1dp=3px),这就实现了,当咱们使用dp来定义一个控件大小的时候,他在不一样的手机里表现出相应大小的像素值。java

咱们能够说,经过dp加上自适应布局和weight比例布局能够基本解决不一样手机上适配的问题,这基本是最原始的Android适配方案。android

糗事百科适配方案:shell

www.jianshu.com/p/a4b8e4c5d…数组

字节跳动适配方案:缓存

mp.weixin.qq.com/s/d9QCoBP6k…bash

CPU 与 GPU

UI 渲染还依赖两个核心的硬件:CPU 和 GPU。UI 组件在绘制到屏幕以前,都须要通过 Rasterization(栅格化)操做,这是一个耗时操做,而 GPU 能够加快栅格化。架构

软件绘制使用的是 Skia 库,它是一款能在低端设备如手机上呈现高质量的 2D 跨平台图形框架,相似 Chrome、Flutter 内部使用的都是 Skia库。app

OpenGL 与 Vulkan

Android 7.0 把 OpenGL ES 升级到最新的3.2 版本同时,还添加了对Vulkan的支持。Vulkan 是用于高性能 3D图形的低开销、跨平台 API。框架

Android 渲染的演进

Image Stream Producers(图像生产者)

任何能够产生图形信息的组件都统称为图像的生产者,好比OpenGL ES, Canvas 2D, 和 媒体解码器等。

Image Stream Consumers(图像消费者)

SurfaceFlinger是最多见的图像消费者,Window Manager将图形信息收集起来提供给SurfaceFlinger,SurfaceFlinger接受后通过合成再把图形信息传递给显示器。同时,SurfaceFlinger也是惟一一个可以改变显示器内容的服务。SurfaceFlinger使用OpenGL和Hardware Composer来生成surface. 某些OpenGL ES 应用一样也可以充当图像消费者,好比相机能够直接使用相机的预览界面图像流,一些非GL应用也能够是消费者,好比ImageReader 类。

Window Manager

Window Manager是一个用于控制window的系统服务,包含一系列的View。每一个Window都会有一个surface,Window Manager会监视window的许多信息,好比生命周期、输入和焦点事件、屏幕方向、转换、动画、位置、转换、z-order等,而后将这些信息(统称window metadata)发送给SurfaceFlinger,这样,SurfaceFlinger就能将window metadata合成为显示器上的surface。

Hardware Composer

为硬件抽象层(HAL)的子系统。SurfaceFlinger能够将某些合成工做委托给Hardware Composer,从而减轻OpenGL和GPU的工做。此时,SurfaceFlinger扮演的是另外一个OpenGL ES客户端,当SurfaceFlinger将一个缓冲区或两个缓冲区合成到第三个缓冲区时,它使用的是OpenGL ES。这种方式会比GPU更为高效。

通常应用开发都要将UI数据使用Activity这个载体去展现,典型的Activity显示流程为:

startActivity启动Activity; 为Activity建立一个window(PhoneWindow),并在WindowManagerService中注册这个window; 切换到前台显示时,WindowManagerService会要求SurfaceFlinger为这个window建立一个surface用来绘图。SurfaceFlinger建立一个”layer”(surface)。(以想象一下C/S架构,SF对应Server,对应Layer;App对应Client,对应Surface),这个layer的核心便是一个BufferQueue,这时候app就能够在这个layer上render了; 将全部的layer进行合成,显示到屏幕上。

通常app而言,在任何屏幕上起码有三个layer:

屏幕顶端的status bar 屏幕下面的navigation bar 还有就是app的UI部分。 一些特殊状况下,app的layer可能多余或者少于3个,例如对全屏显示的app就没有status bar,而对launcher,还有个为了wallpaper显示的layer。status bar和navigation bar是由系统进行去render,由于不是普通app的组成部分嘛。而app的UI部分对应的layer固然是本身去render,因此就有了第4条中的全部layer进行“合成”。

GUI框架

Hardware Composer 那么android是如何使用这两种合成机制的呢?这里就是Hardware Composer的功劳。处理流程为:

SurfaceFlinger给HWC提供layer list,询问如何处理这些layer; HWC将每一个layer标记为overlay或者GLES composition,而后回馈给SurfaceFlinger; SurfaceFlinger须要去处理那些GLES的合成,而不用去管overlay的合成,最后将overlay的layer和GLES合成后的buffer发送给HWC处理。

在绘制过程当中,Android 各个图形组件的做用:

  • 画笔:Skia 或者 OpenGL。
  • 画纸:Surface。
  • 画板:Graphic Buffer,缓冲用于应用程序图形的绘制。Android 4.1 以前的双缓冲和以后的三缓冲机制。
  • 显示:SurfaceFlinger。将 WindowManager 提供的全部 Furface,经过硬件合成器 Hardware Composer 合成并输出到显示屏。

Android 的硬件加速的历史

  • Android 3.0 以前,没有硬件加速。
  • Android 3.0 开始,Android 支持 硬件加速。
  • Android 4.0 默认开始硬件加速。

没有开启硬件加速时的绘制方式:

  • Surface。每一个 View 都由某个窗口管理,而每一个窗口都关联一个 Surface。
  • Canvas。经过 Surface 的 lock() 方法得到一个 Canvas,Canvas 能够简单理解为 Skia 底层接口的封装。
  • Graphic Buffer。SurfaceFlinger 会托管一个 BufferQueue,从 BufferQueue 中拿到 Graphic Buffer,而后经过 Canvas 和 Skia 将绘制内容栅格化到上面。
  • SurfaceFlinger,经过 Swap Buffer 把 Front Graphic Buffer 的内容交给 SurfaceFinger,最后硬件合成器 Hardware Composer 合成并输出到显示屏。

硬件加速后的绘制方式:

最核心的差异是,经过 GPU 完成 Graphic Buffer 的内容绘制。此外还映入了 DisplayList 的概念,每一个 View 内部都有一个 DisplayList,当某个 View 须要重绘时,将它标记为 Dirty。

须要重绘时,也是局部重绘,只会绘制一个 View 的 DisplayList,而不是像软件绘制那也须要向上递归。

Android 4.1:Project Butter

单层缓冲引起“画面撕裂”问题

单层缓冲引起“画面撕裂”问题

如上图,CPU/GPU 向 Buffer 中生成图像,屏幕从 Buffer 中取图像、刷新后显示。这是一个典型的生产者——消费者模型。理想的状况是帧率和刷新频率相等,每绘制一帧,屏幕显示一帧。而实际状况是,两者之间没有必然的大小关系,若是没有锁来控制同步,很容易出现问题。 所谓”撕裂”就是一种画面分离的现象,这样获得的画像虽然类似可是上半部和下半部确实明显的不一样。这种状况是因为帧绘制的频率和屏幕显示频率不一样步致使的,好比显示器的刷新率是75Hz,而某个游戏的FPS是100. 这就意味着显示器每秒更新75次画面,而显示卡每秒更新100次,比你的显示器快33%。

双缓冲

两个缓存区分别为 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中写数据,屏幕从 Frame Buffer 中读数据。VSync 信号负责调度从 Back Buffer 到 Frame Buffer 的复制操做,可认为该复制操做在瞬间完成。

双缓冲的模型下,工做流程这样的:

在某个时间点,一个屏幕刷新周期完成,进入短暂的刷新空白期。此时,VSync 信号产生,先完成复制操做,而后通知 CPU/GPU 绘制下一帧图像。复制操做完成后屏幕开始下一个刷新周期,即将刚复制到 Frame Buffer 的数据显示到屏幕上。 在这种模型下,只有当 VSync 信号产生时,CPU/GPU 才会开始绘制。这样,当帧率大于刷新频率时,帧率就会被迫跟刷新频率保持同步,从而避免“tearing”现象。

VSYNC 偏移

应用和SurfaceFlinger的渲染回路必须同步到硬件的VSYNC,在一个VSYNC事件中,显示器将显示第N帧,SurfaceFlinger合成第N+1帧,app合成第N+2帧。 使用VSYNC同步能够保证延迟的一致性,减小了app和SurfaceFlinger的错误,以及显示在各个阶段之间的偏移。然而,前提是app和SurfaceFlinger每帧时间的变化并不大。所以,从输入到显示的延迟至少有两帧。 为了解决这个问题,您可使用VSYNC偏移量来减小输入到显示的延迟,其方法为将app和SurfaceFlinger的合成信号与硬件的VSYNC关联起来。由于一般app的合成耗时是小于两帧的(33ms左右)。 VSYNC偏移信号细分为如下3种,它们都保持相同的周期和偏移向量:

HW_VSYNC_0:显示器开始显示下一帧。 VSYNC:app读取输入并生成下一帧。 SF VSYNC:SurfaceFlinger合成下一帧的。 收到VSYNC偏移信号以后, SurfaceFlinger 才开始接收缓冲区的数据进行帧的合成,而app才处理输入并渲染帧,这些操做都将在16.7ms完成。

Jank 掉帧

注意,当 VSync 信号发出时,若是 GPU/CPU 正在生产帧数据,此时不会发生复制操做。屏幕进入下一个刷新周期时,从 Frame Buffer 中取出的是“老”数据,而非正在产生的帧数据,即两个刷新周期显示的是同一帧数据。这是咱们称发生了“掉帧”(Dropped Frame,Skipped Frame,Jank)现象。

流畅性解决方案思路

从dumpsys SurfaceFlinger --latency中获取127帧的数据 上面的命令返回的第一行为设备自己固有的帧耗时,单位为ns,一般在16.7ms左右 从第二行开始,分为3列,一共有127行,表明每一帧的几个关键时刻,单位也为ns

第一列t1: when the app started to draw (开始绘制图像的瞬时时间) 第二列t2: the vsync immediately preceding SF submitting the frame to the h/w (VSYNC信令将软件SF帧传递给硬件HW以前的垂直同步时间),也就是对应上面所说的软件Vsync 第三列t3: timestamp immediately after SF submitted that frame to the h/w (SF将帧传递给HW的瞬时时间,及完成绘制的瞬时时间)

将第i行和第i-1行t2相减,便可获得第i帧的绘制耗时,提取出每一帧不断地dump出帧信息,计算出

一些计算规则

计算fps: 每dumpsys SurfaceFlinger一次计算汇总出一个fps,计算规则为: frame的总数N:127行中的非0行数 绘制的时间T:设t=当前行t2 - 上一行的t2,求出全部行的和∑t fps=N/T (要注意时间转化为秒) 计算中一些细节问题 一次dumpsys SurfaceFlinger会输出127帧的信息,可是这127帧多是这个样子:

...
0               0               0
0               0               0
0               0               0
575271438588    575276081296    575275172129
575305169681    575309795514    575309142441
580245208898    580250445565    580249372231
580279290043    580284176346    580284812908
580330468482    580334851815    580333739054 
0               0               0
0               0               0
...
575271438588    575276081296    575275172129
575305169681    575309795514    575309142441
复制代码

出现0的地方是因为buffer中没有数据,而非0的地方为绘制帧的时刻,所以仅计算非0的部分数据 观察127行数据,会发现偶尔会出现9223372036854775808这种数字,这是因为字符溢出致使的,所以这一行数据也不能加入计算 不能单纯的dump一次计算出一个fps,举个例子,若是A时刻操做了手机,停留3s后,B时刻再次操做手机,按照上面的计算方式,则t>3s,而且也会参与到fps的计算去,从而形成了fps不许确,所以,须要加入一个阀值判断,当t大于某个值时,就计算一次fps,而且把相关数据从新初始化,这个值通常取500ms 若是t<16.7ms,则直接按16.7ms算,一样的总耗时T加上的也是16.7

计算jank的次数: 若是t3-t1>16.7ms,则认为发生一次卡顿 流畅度得分计算公式 设目标fps为target_fps,目标每帧耗时为target_ftime=1000/target_fps 从如下几个维度衡量流畅度:

fps: 越接近target_fps越好,权重分配为40% 掉帧数:越少越好,权重分配为40% 超时帧:拆分红如下两个维度

超时帧的个数,越少越好,权重分配为5% 最大超时帧的耗时,越接近target_ftime越好,权重分配为15%

end_time = round(last_frame_time / 1000000000, 2)
T = utils.get_current_time()
fps = round(frame_counts * 1000 / total_time, 2)

# 计算得分
g = fps / target
if g > 1:
  g = 1
if max_frame_time - kpi <= 1:
       max_frame_time = kpi
h = kpi / max_frame_time
 score = round((g * 50 + h * 10 + (1 - over_kpi_counts / frame_counts) * 40), 2)
复制代码

2012 年 I/O 大会上宣布 Project Butter 黄油计划,在 4.1 中正式开启这个机制。

Project Butter 主要包含:VSYNC 和 Triple Buffering(三缓存机制)。

VSYNC 相似时钟中断。每次收到 VSYNC 中断,CPU 会当即准备 Buffer 数据,业内标准刷新频率是 60Hz(每秒刷新 60次),也就是一帧数据的准备时间要在 16ms 内完成。

Android 4.1 以前,Android 使用双缓冲机制,不一样的 View 或者 Activity 它们都会共用一个 Window,也就是共用同一个 Surface。

每一个 Surface 都会有一个 BufferQueue 缓冲队列,这个队列会由 SurfaceFlinger 管理,经过匿名共享内存机制与 App 应用层交互。

安卓系统中有 2 种 VSync 信号:

屏幕产生的硬件 VSync: 硬件 VSync 是一个脉冲信号,起到开关或触发某种操做的做用。 由 SurfaceFlinger 将其转成的软件 Vsync 信号:经由 Binder 传递给 Choreographer。

如何理解 Triple Buffering(三缓存机制)?

双缓冲只有 A 和 B 两个缓冲区,若是 CPU/GPU 绘制时间较长,超过一个 VSYNC 信号周期,由于缓冲区 B 中的数据没有准备好,只能继续展现 A 缓冲区的内容,这样缓冲区 A 和 B 都分别被显示设备和 GPU 占用,CPU 没法准备下一帧的数据。

增长一个缓冲区,CPU、GPU 和显示设备都有各自的缓冲区,互不影响。

简单来讲,三缓存机制就是在双缓冲机制的基础上,增长一个 Graphic Buffer 缓冲区,这样能够最大限度的利用空闲时间,带来的坏处是多私用了一个 Graphic Buffer 所占用的内存。

检测工具:

Systrace,Android 4.1 新增的新能数据采样和分析工具。 Tracer for OpenGL ES,Android 4.1 新增的工具,能够逐帧、逐函数的记录 App 用 OpenGL ES 的绘制过程。 过分绘制工具,Android 4.2 新增,参考《检查 GPU 渲染速度和绘制过分》

60 fps

手机屏幕是由许多的像素点组成的,每一个像素点经过显示不一样的颜色最终屏幕呈现各类各样的图像。手机系统的类型和手机硬件的不一样致使UI的流畅性体验个不一致。

屏幕展现的颜色数据

  • 在GPU中有一块缓冲区叫作 Frame Buffer ,这个帧缓冲区能够认为是存储像素值的二位数组。
  • 数组中的每个值就对应了手机屏幕的像素点须要显示的颜色。
  • 因为这个帧缓冲区的数值是在不断变化的,因此只要完成对屏幕的刷新就能够显示不一样的图像了。
  • 至于刷新工做的逻辑电路会按期的刷新 Frame Buffer的。 目前主流的刷新频率为60次/秒 折算出来就是16ms刷新一次。
  • GPU 除了帧缓冲区用以交给手机屏幕进行绘制外. 还有一个缓冲区 Back Buffer 这个用以交给应用的,让CPU往里面填充数据。
  • GPU会按期交换 Back Buffer 和 Frame Buffer ,也就是对Back Buffer中的数据进行栅格化后将其转到 Frame Buffer 而后交给屏幕进行显示绘制,同时让原先的Frame Buffer 变成 Back Buffer 让程序处理。

Android的16ms

在Choreographer类中咱们有一个方法获取屏幕刷新速率:

public final class Choreographer {
	private static float getRefreshRate() {
        DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
                Display.DEFAULT_DISPLAY);
        return di.refreshRate;
    }
}

public final class DisplayInfo implements Parcelable {
    public float refreshRate;
}

final class VirtualDisplayAdapter extends DisplayAdapter {
	private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient {
		@Override
        public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
            if (mInfo == null) {
                mInfo = new DisplayDeviceInfo();
                mInfo.refreshRate = 60;
            }
            return mInfo;
        }
	}
}
复制代码

VSYNC

VSYNC是Vertical Synchronization(垂直同步)的缩写,是一种在PC上已经很早就普遍使用的技术。 可简单的把它认为是一种定时中断。

由上图可知

1.时间从0开始,进入第一个16ms:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者互不干扰,一切正常。 2.时间进入第二个16ms:由于早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。故Display能够直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU 却并未及时去绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。 3.时间进入第3个16ms,此时Display应该显示第2帧数据,但因为CPU和GPU尚未处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1 帧多画了一次(对应时间段上标注了一个Jank)。 4.经过上述分析可知,此处发生Jank的关键问题在于,为什么第1个16ms段内,CPU/GPU没有及时处理第2帧数据?缘由很简单,CPU多是在忙别的事情(好比某个应用经过sleep 固定时间来实现动画的逐帧显示),不知道该处处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了!

NSYNC的出现

由图可知,每收到VSYNC中断,CPU就开始处理各帧数据。整个过程很是完美。 不过,仔细琢磨图2却会发现一个新问题:图2中,CPU和GPU处理数据的速度彷佛都能在16ms内完成,并且还有时间空余,也就是说,CPU/GPU的FPS(帧率,Frames Per Second)要高于Display的FPS。确实如此。因为CPU/GPU只在收到VSYNC时才开始数据处理,故它们的FPS被拉低到与Display的FPS相同。但这种处理并无什么问题,由于Android设备的Display FPS通常是60,其对应的显示效果很是平滑。 若是CPU/GPU的FPS小于Display的FPS,会是什么状况呢?请看下图:

由图可知: 1.在第二个16ms时间段,Display本应显示B帧,但却由于GPU还在处理B帧,致使A帧被重复显示。 2.同理,在第二个16ms时间段内,CPU无所事事,由于A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦过了VSYNC时间点, CPU就不能被触发以处理绘制工做了。

Triple Buffer

为何CPU不能在第二个16ms处开始绘制工做呢?缘由就是只有两个Buffer。若是有第三个Buffer的存在,CPU就能直接使用它, 而不至于空闲。出于这一思路就引出了Triple Buffer。结果如图所示:

由图可知: 第二个16ms时间段,CPU使用C Buffer绘图。虽然仍是会多显示A帧一次,但后续显示就比较顺畅了。 是否是Buffer越多越好呢?回答是否认的。由图4可知,在第二个时间段内,CPU绘制的第C帧数据要到第四个16ms才能显示, 这比双Buffer状况多了16ms延迟。因此,Buffer最好仍是两个,三个足矣。

以上对VSYNC进行了理论分析,其实也引出了Project Buffer的三个关键点: 核心关键:须要VSYNC定时中断。 Triple Buffer:当双Buffer不够使用时,该系统可分配第三块Buffer。 另外,还有一个很是隐秘的关键点:即将绘制工做都统一到VSYNC时间点上。这就是Choreographer的做用。在它的统一指挥下,应用的绘制工做都将变得层次分明。

Android 5.0:RenderThread

5.0 以前,GPU 的高性能运算,都是在 UI 线程完成的,5.0 以后引入了两个重大改变,一个是引入 RenderNode 的概念,它对 DisplayList 及一些 View 显示属性作了进一步封装;另外一个是引入 RenderThread,全部 GL 命令执行都放在这个单独的线程上,渲染线程在 RenderNode 中存有渲染帧的全部信息,能够作一些属性动画。

此处还能够开启 Profile GPU Rendering 检查,6.0 以后,会输出下面的计算和绘制每一个阶段的耗时。

UI 渲染测量的两种工具:

测试工具:Profile GPU Rendering 和 Show GPU Overdraw。参考:《检查 GPU 渲染速度和绘制过分》

Layout Inspector

用于分析手机上正在运行的呃App的视图布局结构。

GPU呈现模式分析工具简介

Profile GPU Rendering工具的使用很简单,就是直观上看一帧的耗时有多长,绿线是16ms的阈值,超过了,可能会致使掉帧,这个跟VSYNC垂直同步信号有关系,固然,这个图表并非绝对严谨的(后文会说缘由)。每一个颜色的方块表明不一样的处理阶段,先看下官方文档给的映射表:

想要彻底理解各个阶段,要对硬件加速及GPU渲染有必定的了解,不过,有一点,必须先记内心:虽名为 Profile GPU Rendering,但图标中全部阶段都发生在CPU中,不是GPU 。最终CPU将命令提交到 GPU 后触发GPU异步渲染屏幕,以后CPU会处理下一帧,而GPU并行处理渲染,二者硬件上算是并行。 不过,有些时候,GPU可能过于繁忙,不能跟上CPU的步伐,这个时候,CPU必须等待,也就是最终的swapbuffer部分,主要是最后的红色及黄色部分(同步上传的部分不会有问题,我的认为是由于在Android GPU与CPU是共享内存区域的),在等待时,将看到橙色条和红色条中出现峰值,且命令提交将被阻止,直到 GPU 命令队列腾出更多空间。

稳定定位工具:Systrace 和 Tracer for OpenGL ES,参考《Slow rendering》,Android 3.1 以后,推荐使用 Graphics API Debugger(GAPID)来替代 Tracer for OpenGL ES 工具。

有哪些自动化测量 UI 渲染性能的工具?

使用dumpsys gfxinfo 测UI性能

dumpsys是一款运行在设备上的Android工具,将 gfxinfo命令传递给dumpsys可在logcat中提供输出,其中包含各阶段发生的动画以及帧相关的性能信息。

adb shell dumpsys gfxinfo < PACKAGE_NAME >
复制代码

该命令可用于搜集帧的耗时数据。运行该命令后,能够等到以下的 结果:

Applications Graphics Acceleration Info:
Uptime: 102809662 Realtime: 196891968

** Graphics info for pid 31148 [com.android.settings] **

Stats since: 102794621664587ns
Total frames rendered: 105
Janky frames: 2 (1.90%)
50th percentile: 5ms
90th percentile: 7ms
95th percentile: 9ms
99th percentile: 19ms
Number Missed Vsync: 0
Number High input latency: 0
Number Slow UI thread: 2
Number Slow bitmap uploads: 0
Number Slow issue draw commands: 1
HISTOGRAM: 5ms=78 6ms=16 7ms=4 8ms=1 9ms=2 10ms=0 11ms=0 12ms=0 13ms=2 14ms=0 15ms=0 16ms=0 17ms=0 18ms=0 19ms=1 20ms=0 21ms=0 22ms=0 23ms=0 24ms=0 25ms=0 26ms=0 27ms=0 
...
...
复制代码

Graphics info for pid 31148 [com.android.settings]: 代表当前dump的为设置界面的帧信息,pid为31148 Total frames rendered: 105 本次dump搜集了105帧的信息 Janky frames: 2 (1.90%) 105帧中有2帧的耗时超过了16ms,卡顿几率为1.9% Number Missed Vsync: 0 垂直同步失败的帧 Number High input latency: 0 处理input时间超时的帧数 Number Slow UI thread: 2 因UI线程上的工做致使超时的帧数 Number Slow bitmap uploads: 0 因bitmap的加载耗时的帧数 Number Slow issue draw commands: 1 因绘制致使耗时的帧数 HISTOGRAM: 5ms=78 6ms=16 7ms=4 ... 直方图数据,表面耗时为0-5ms的帧数为78,耗时为5-6ms的帧数为16,同理类推。

在Android 6.0之后为gfxinfo 提供了一个新的参数framestats,其做用能够从最近的帧中提供很是详细的帧信息,以便您能够更准确地跟踪和调试问题。

> adb shell dumpsys gfxinfo < PACKAGE_NAME > framestats
复制代码

此命令将应用程序生成的最后120帧信息打印出,其中包含纳秒时间戳。如下是来自adb dumpsys gfxinfo <PACKAGE_NAME>的示例原始输出framestats:

0 ,27965466202353 ,27965466202353 ,27965449758000 ,27965461202353 ,27965467153286 ,27965471442505 ,27965471925682 ,27965474025318 ,27965474588547 ,27965474860786 ,27965475078599 ,27965479796151 ,27965480589068 ,0 ,27965482993342 ,27965482993342 ,27965465835000 ,27965477993342 ,27965483807401 ,27965486875630 ,
27965487288443 ,27965489520682 ,27965490184380 ,27965490568703 ,27965491408078 ,27965496119641 ,27965496619641 ,0 ,27965499784331 ,27965499784331 ,27965481404000 ,27965494784331 ,27965500785318 ,27965503736099 ,27965504201151 ,27965506776568 ,27965507298443 ,27965507515005 ,27965508405474 ,27965513495318 ,27965514061984 ,

0,27965516575320,27965516575320,27965497155000,27965511575320,27965517697349,27965521276151,27965521734797,27965524350474,27965524884536,27965525160578,27965526020891,27965531371203,27965532114484,
复制代码

此输出的每一行表明应用程序生成的一帧。每一行的列数都相同,每列对应描述帧在不一样的时间段的耗时状况。

Framestats数据格式

因为数据块以CSV格式输出,所以将其粘贴电子表格工具中很是简单,或者经过脚本进行收集和分析。下表说明了输出数据列的格式。全部的时间戳都是纳秒。

  • FLAGS

FLAGS列为'0'的行能够经过从FRAME_COMPLETED列中减去INTENDED_VSYNC列计算其总帧时间。

若是非零,则该行应该被忽略,由于该帧的预期布局和绘制时间超过16ms,为异常帧。

  • INTENDED_VSYNC

帧的的预期起点。若是此值与VSYNC不一样,是因为 UI 线程中的工做使其没法及时响应垂直同步信号所形成的。

  • VSYNC

花费在vsync监听器和帧绘制的时间(Choreographer frame回调,动画,View.getDrawingTime()等)

  • OLDEST_INPUT_EVENT

输入队列中最旧输入事件的时间戳,若是没有输入事件,则输入Long.MAX_VALUE。

此值主要用于平台工做,对应用程序开发人员的用处有限。

  • NEWEST_INPUT_EVENT

输入队列中最新输入事件的时间戳,若是帧没有输入事件,则为0。

此值主要用于平台工做,对应用程序开发人员的用处有限。

然而,经过查看(FRAME_COMPLETED - NEWEST_INPUT_EVENT),能够大体了解应用程序添加的延迟时间。

  • HANDLE_INPUT_START

将输入事件分派给应用程序的时间戳。

经过查看这段时间和ANIMATION_START之间的时间,能够测量应用程序处理输入事件的时间。

若是这个数字很高(> 2ms),这代表程序花费了很是长的时间来处理输入事件,例如View.onTouchEvent(),也就是说此工做须要优化,或者分发到不一样的线程。请注意,某些状况下这是能够接受的,例如发起新活动或相似活动的点击事件,而且此数字很大。

  • ANIMATION_START

运行Choreographer注册动画的时间戳。

经过查看这段时间和PERFORM_TRANVERSALS_START之间的时间,能够肯定评估运行的全部动画器(ObjectAnimator,ViewPropertyAnimator和经常使用转换器)须要多长时间。

若是此数字很高(> 2ms),请检查您的应用是否编写了自定义动画以确保它们适用于动画。

  • PERFORM_TRAVERSALS_START

PERFORM_TRAVERSALS_STAR-DRAW_START,则能够提取布局和度量阶段完成的时间。(注意,在滚动或动画期间,你会但愿这应该接近于零..)

  • DRAW_START

performTraversals的绘制阶段开始的时间。这是录制任何无效视图的显示列表的起点。

这和SYNC_START之间的时间是在树中全部无效视图上调用View.draw()所花费的时间。

  • SYNC_QUEUED

同步请求发送到RenderThread的时间。

这标志着开始同步阶段的消息被发送到RenderThread的时刻。若是此时间和SYNC_START之间的时间很长(> 0.1ms左右),则意味着RenderThread忙于处理不一样的帧。在内部,这被用来区分帧作了太多的工做,超过了16ms的预算,因为前一帧超过了16ms的预算,帧被中止了。

  • SYNC_START

绘图的同步阶段开始的时间。

若是此时间与ISSUE_DRAW_COMMANDS_START之间的时间很长(> 0.4ms左右),则一般表示有许多新的位图必须上传到GPU。

  • ISSUE_DRAW_COMMANDS_START

硬件渲染器开始向GPU发出绘图命令的时间。

这段时间和FRAME_COMPLETED之间的时间间隔显示了应用程序正在生产多少GPU。像这样出现太多透支或低效率渲染效果的问题。

  • SWAP_BUFFERS

eglSwapBuffers被调用的时间。

  • FRAME_COMPLETED

帧的完整时间。花在这个帧上的总时间能够经过FRAME_COMPLETED - INTENDED_VSYNC来计算。

你能够用不一样的方式使用这些数据。例以下面的直方图,显示不一样帧时间的分布(FRAME_COMPLETED - INTENDED_VSYNC),以下图所示。

这张图一目了然地告诉咱们,大多数的帧耗时都远低于16ms(用红色表示),但几帧明显超过了16ms。随着时间的推移,咱们能够查看此直方图中的变化,以查看批量变化或新建立的异常值。您还能够根据数据中的许多时间戳来绘制出输入延迟,布局花费的时间或其余相似的感兴趣度量。

若是在开发者选项中的CPU呈现模式分析中选择adb shell dumpsys gfxinfo,则adb shell dumpsys gfxinfo命令将输出最近120帧的时间信息,并将其分红几个不一样的类别,能够直观的显示各部分的快慢。

与上面的framestats相似,将它粘贴到您选择的电子表格工具中很是简单,或者经过脚本进行收集和解析。下图显示了应用程序生成的帧每一个阶段的详细耗时。

运行gfxinfo,复制输出,将其粘贴到电子表格应用程序中,并将数据绘制为直方图的结果。

每一个垂直条表明一帧动画; 其高度表示计算该动画帧所用的毫秒数。条形图中的每一个彩色段表示渲染管道的不一样阶段,以便您能够看到应用程序的哪些部分可能会形成瓶颈。

framestats信息和frame耗时信息一般为2s收集一次(一次120帧,一帧16ms,耗时约2s)。为了精确控制时间窗口,例如,将数据限制为特定的动画 ,能够重置全部计数器,并从新收集的数据。

> adb shell dumpsys gfxinfo < PACKAGE_NAME > reset
复制代码

一样 也适用于须要捕获小于2s的数据。

dumpsys是能发现问题或者判断问题的严重性,但没法定位真正的缘由。若是要定位缘由,应当配合systrace工具使用。

SurfaceFlinger。三缓存机制,在 4.1 以后,每一个 Surface 都会有三个 Graphic Buffer,这部分能够才看到到当前渲染所占用的内存信息。对于这部份内存,当应用退到后台的时候,系统会将这些内存回收,不会记入应用的内存占用中。

UI 优化有哪些经常使用手段?

  • 尽可能使用硬件加速。

有些 Convas API 不能完美支持硬件加速,参考 drawing-support 文档。SVG 对不少指令硬件加速也不支持。

  • Create View 优化。

View 的建立在 UI 线程,复杂界面会引发耗时,耗时分析能够分解成:XML 的随机读 的 I/O 时间、解析 XML 时间、生成对象的时间等。可使用的优化方式有:使用代码建立,例如使用 XML 转换为 Java 代码的工具,例如 X2C;

异步建立,在子线程建立 View 会出现异常,能够先把子线程的 Looper 的 MessageQueue 替换成 UI 线程 Looper 的 Queue;

View 重用。

  • measure/layout 优化。渲染流程中 measure 和 layout 也须要 CPU 在主线程执行。优化的方法有:减小 UI 布局层次,尽可能扁平化,<ViewStub>,<merge>、优化 layout 开销,避免使用 RelativeLayout 或者 基于 weighted 的 LinearLayout。使用ConstraintLayout;背景优化,不要重复设置背景。

TextView 是系统空间中,很是复杂的一个控件,强大的背后就表明了不少计算,2018 年 Google I/O 发布了 PercomputedText 并已经集成在 Jetpack 中,它提供了接口,能够异步进行 measure 和 layout,没必要在主线程中执行。

UI 优化的进阶手段有哪些?

  • Litho:异步布局。这是 Facebook 开源的声明式 Android UI 渲染框架,基于另一个 Facebook 开源的布局引擎 Yoga 开发的。Litho 的缺点很明显,过重了。
  • Flutter:本身的布局 + 渲染引擎。Flutter 使用 Skia 引擎渲染 UI,直接使用 Dart 虚拟机,跳出了 Android 原有的方案。参考:《Flutter 原理和实践
  • RenderThread 和 RenderScript。5.0 开始,系统增长了 RenderThread,当主线程阻塞时,普通动画会出现丢帧卡顿,而使用 RenderThread 渲染的动画即便阻塞了主线程,仍然不受影响。参考《RenderThread实现动画异步渲染》

RenderScript 参考:

RenderScript 渲染利器

RenderScript:简单而快速的图像处理 Android RenderScript 简单实现图片的高斯模糊效果

布局优化

在编写Android的布局时总会遇到这样或者那样的痛点,好比:

  1. 有些布局的在不少页面都用到了,并且样式都同样,每次用到都要复制粘贴一大段,有没有办法能够复用呢?
  2. 解决了1中的问题以后,发现复用的布局外面总要额外套上一层布局,要知道布局嵌套是会影响性能的呐;
  3. 有些布局只有用到时才会显示,可是必须提早写好,设置虽然为了invisible或gone,仍是多多少少会占用内存的。

include

include的中文意思是“包含”,“包括”,你当一个在主页面里使用include标签时,就表示当前的主布局包含标签中的布局,这样一来,就能很好地起到复用布局的效果了在那些经常使用的布局好比标题栏和分割线等上面用上它能够极大地减小代码量的它有两个主要的属性。:

  • layout:必填属性,为你须要插入当前主布局的布局名称,经过R.layout.xx的方式引用;
  • id:当你想给经过包括添加进来的布局设置一个ID的时候就可使用这个属性,它能够重写插入主布局的布局ID。

常规使用

咱们先建立一个ViewOptimizationActivity,而后再建立一个layout_include.xml布局文件,它的内容很是简单,就一个TextView的:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:gravity="center_vertical"
    android:textSize="14sp"
    android:background="@android:color/holo_red_light"
    android:layout_height="40dp">
</TextView>
复制代码

如今咱们就用include标签,将其添加到ViewOptimizationActivity的布局中:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ViewOptimizationActivity">

    <!--include标签的使用-->
    <TextView
        android:textSize="18sp"
        android:text="一、include标签的使用"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <include
        android:id="@+id/tv_include1"
        layout="@layout/layout_include"/>

</LinearLayout>
复制代码

为了验证它就是layout_include.xml的根布局的TextView的ID,咱们在ViewOptimizationActivity中初始化的TextView,并给它设置文字:

TextView tvInclude1 = findViewById(R.id.tv_include1);
tvInclude1.setText("1.1 常规下的include布局");
复制代码

说明咱们设置的布局和标识都是成功的不过你可能会对ID这个属性有疑问:?ID我能够直接在的TextView中设置啊,为何重写它呢别忘了咱们的目的是复用,当在你主一个布局中使用include标签添加两个以上的相同布局时,ID相同就会冲突了,因此重写它可让咱们更好地调用它和它里面的控件。还有一种状况,假如你的主布局是RelateLayout,这时为了设置相对位置,你也须要给它们设置不一样的ID。

重写根布局的布局属性

除了id以外,咱们还能够重写宽高,边距和可见性(visibility)这些布局属性。可是必定要注意,单单重写android:layout_height或者android:layout_width是不行,必须两个同时重写才起做用。包括边距也是这样,若是咱们想给一个包括进来的布局添加右边距的话的完整写法是这样的:

<include
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginEnd="40dp"
        android:id="@+id/tv_include2"
        layout="@layout/layout_include"/>
复制代码

控件ID相同时的处理

在1.1中咱们知道了ID能够属性重写include布局的根布局ID,但对于根布局里面的布局和控件是无能为力的,若是这时一个布局在主布局中包括了屡次,那怎么区别里面的控件呢?

咱们先建立一个layout_include2.xml的布局,它的根布局是FrameLayout,里面有一个TextView,它的ID是tv_same:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/holo_orange_light"
    android:layout_height="wrap_content">

    <TextView
        android:gravity="center_vertical"
        android:id="@+id/tv_same"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

</FrameLayout>
复制代码

在主布局中添加进去:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ViewOptimizationActivity">

    <!--include标签的使用-->
    ……

    <include layout="@layout/layout_include2"/>

    <include
        android:id="@+id/view_same"
        layout="@layout/layout_include2"/>

</LinearLayout>
复制代码

为了区分,这里给第二个layout_include2设置了ID也许你已经反应过来了,没错,咱们就是要建立根布局的对象,而后再去初始化里面的控件:

TextView tvSame = findViewById(R.id.tv_same);
tvSame.setText("1.3 这里的TextView的ID是tv_same");
FrameLayout viewSame = findViewById(R.id.view_same);
TextView tvSame2 = viewSame.findViewById(R.id.tv_same);
tvSame2.setText("1.3 这里的TextView的ID也是tv_same");
复制代码

merge

include标签虽然解决了布局重用的问题,却也带来了另一个问题:布局嵌套由于把须要重用的布局放到一个子布局以后就必须加一个根布局,若是你的主布局的根布局和你须要包括的根布局都是同样的(好比都是LinearLayout),那么就至关于在中间多加了一层多余的布局了。有那么没有办法能够在使用include时不增长布局层级呢?答案固然是有的,就是那使用merge标签。

使用merge标签要注意一点一:必须是一个布局文件中的根节点,看起来跟其余布局没什么区别,但它的特别之处在于页面加载时它的不会绘制,它就像是布局或者控件的搬运工,把“货物”搬到主布局以后就会功成身退,不会占用任何空间,所以也就不会增长布局层级了。这正如它的名字同样,只起“合并”做用。

常规使用

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_merge1"
        android:text="我是merge中的TextView1"
        android:background="@android:color/holo_green_light"
        android:gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="40dp" />

    <TextView
        android:layout_toEndOf="@+id/tv_merge1"
        android:id="@+id/tv_merge2"
        android:text="我是merge中的TextView2"
        android:background="@android:color/holo_blue_light"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp" />
</merge>
复制代码

这里我使用了一些相对布局的属性,缘由后面你就知道了咱们接着在ViewOptimizationActivity的布局添加RelativeLayout的,而后使用包括标签将layout_merge.xml添加进去:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/view_merge"
        layout="@layout/layout_merge"/>
</RelativeLayout>
复制代码

对布局层级的影响

在layout_merge.xml中,使用咱们相对布局的属性android:layout_toEndOf将蓝色的TextView设置到了绿色的TextView的右边,而layout_merge.xml的父布局是RelativeLayout,因此这个属性是起了做用了,merge标签不会影响里面的控件,也不会增长布局层级。

看到能够RelativeLayout下面直接就是两个TextView的了,merge标签并无增长布局层级。能够看出merge的局限性,即须要你明确将merge里面的布局控件状语从句:include到什么类型的布局中,提早设置merge里面的布局和控件的位置。

合并的ID

学习在include标签时咱们知道,android:id属性能够重写被包括的根布局ID,但若是根节点merge呢?说前面了merge并不会做为一个布局绘制出来,因此这里给它设置ID是不起做用的。咱们在它的父布局RelativeLayout中再加一个TextView的,使用android:layout_below属性把设置到layout_merge下面:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/view_merge"
        layout="@layout/layout_merge"/>

    <TextView
        android:text="我不是merge中的布局"
        android:layout_below="@+id/view_merge"
        android:background="@android:color/holo_purple"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>
</RelativeLayout>
复制代码

运行以后你会发现新加的TextView中会把合并布局盖住,没有像预期那样在其下方。把若是android:layout_below中的ID改成layout_merge.xml中任一的TextView的ID(好比tv_merge1),运行以后就能够看到以下效果:

即布局父RelativeLayout下级布局就是包括进去的TextView的了。

ViewStub

你必定遇到这样的状况:页面中有些布局在初始化时不必显示,可是又不得不事先在布局文件中写好,设置虽然成了invisible或gone,可是在初始化时仍是会加载,这无疑会影响页面加载速度。针对这一状况,Android为咱们提供了一个利器---- ViewStub。这是一个不可见的,大小为0的视图,具备懒加载的功能,它存在于视图层级中,但只会在setVisibility()状语从句:inflate()方法调用只会才会填充视图,因此不会影响初始化加载速度它有如下三个重要属性:

  • android:layout:ViewStub须要填充的视图名称,为“R.layout.xx”的形式;
  • android:inflateId:重写被填充的视图的父布局ID。

与include标签不一样,ViewStub的android:id属性的英文设置ViewStub自己ID的,而不是重写布局ID,这一点可不要搞错了。另外,ViewStub还提供了OnInflateListener接口,用于监听布局是否已经加载了。

填充布局的正确方式

咱们先建立一个layout_view_stub.xml,放置里面一个Switch开关:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="@android:color/holo_blue_dark"
    android:layout_height="100dp">
    <Switch
        android:id="@+id/sw"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</FrameLayout>
复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ViewOptimizationActivity">

    <!--ViewStub标签的使用-->
    <TextView
        android:textSize="18sp"
        android:text="三、ViewStub标签的使用"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ViewStub
        android:id="@+id/view_stub"
        android:inflatedId="@+id/view_inflate"
        android:layout="@layout/layout_view_stub"
        android:layout_width="match_parent"
        android:layout_height="100dp" />
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:text="显示"
            android:id="@+id/btn_show"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

        <Button
            android:text="隐藏"
            android:id="@+id/btn_hide"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="wrap_content" />

        <Button
            android:text="操做父布局控件"
            android:id="@+id/btn_control"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>
复制代码

在ViewOptimizationActivity中监听ViewStub的填充事件:

viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub viewStub, View view) {
                Toast.makeText(ViewOptimizationActivity.this, "ViewStub加载了", Toast.LENGTH_SHORT).show();
            }
        });
复制代码

而后经过按钮事件来填充和显示layout_view_stub:

@Override
public void onClick(View view) {
    switch (view.getId()) {
        case R.id.btn_show:
            viewStub.inflate();
            break;
        case R.id.btn_hide:
            viewStub.setVisibility(View.GONE);
            break;
        default:
            break;
    }
}
复制代码

运行以后,点击“显示”按钮,layout_view_stub显示了,并弹出 “ViewStub加载了” 的吐司;点击“隐藏”按钮,布局又隐藏掉了,可是再点击一下“显示”按钮,页面竟然却闪退了,查看日志,发现抛出了一个异常:

java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
复制代码

咱们打开ViewStub的源码

public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }
复制代码
private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }
复制代码

果真,ViewStub在这里调用了removeViewInLayout()方法把本身从布局移除了。到这里咱们就明白了,ViewStub在填充布局成功以后就会自我销毁,再次调用inflate()方法就会抛出IllegalStateException异常异常了。此时若是想要再次显示布局,能够调用setVisibility()方法。

为了不inflate()方法屡次调用,咱们能够采用以下三种方式:

try {
    viewStub.inflate();
} catch (IllegalStateException e) {
    Log.e("Tag",e.toString());
    view.setVisibility(View.VISIBLE);
}
复制代码
if (isViewStubShow){
    viewStub.setVisibility(View.VISIBLE);
}else {
    viewStub.inflate();
}
复制代码
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}
复制代码

viewStub.getVisibility()为什么老是等于0?

在显示ViewStub中的布局时,你可能会采起以下的写法:

if (viewStub.getVisibility() == View.GONE){
    viewStub.setVisibility(View.VISIBLE);
}else {
    viewStub.setVisibility(View.GONE);
}
复制代码

若是你将viewStub.getVisibility()的值打印出来,就会看到它始终为0,这偏偏是View.VISIBLE的值。奇怪,咱们明明写了viewStub.setVisibility(View.GONE),layout_view_stub也隐藏了,为何ViewStub的状态仍是可见呢?

从新回到3.1.3,看看ViewStub中的setVisibility()源码,首先判断弱引用对象mInflatedViewRef是否为空,不为空则取出存放进去的对象,也就是咱们ViewStub中的视图中,而后调用了视图的setVisibility()方法,mInflatedViewRef为空时,则判断能见度为VISIBLE或无形时调用充气()方法填充布局,若是为GONE的话则不予处理。这样一来,在mInflatedViewRef不为空,也就是已经填充了布局的状况下,ViewStub中的setVisibility()方法其实是在设置内部视图的可见性,而不是ViewStub自己。这样的设计其实也符合ViewStub的特性,即填充布局以后就自我销毁了,给其设置可见性是没有意义的。

仔细比较一下,其实ViewStub就像是一个懒惰的包含,咱们须要它加载时才加载。要操做布局里面的控件也跟包同样,你能够先初始化ViewStub中的布局中再初始化控件:

//一、初始化被inflate的布局后再初始化其中的控件,
FrameLayout frameLayout = findViewById(R.id.view_inflate);//android:inflatedId设置的id
Switch sw = frameLayout.findViewById(R.id.sw);
sw.toggle();
复制代码

若是主布局中控件的ID没有冲突,能够直接初始化控件使用:

//二、直接初始化控件
Switch sw = findViewById(R.id.sw);
sw.toggle();
复制代码
相关文章
相关标签/搜索