屏幕
在不同分辨率下,dpi将会不同,比如:
根据上面的表格,我们可以发现,720P,和1080P的手机,dpi是不同的,这也就意味着,不同的分辨率中,1dp对应不同数量的px(720P中,1dp=2px,1080P中1dp=3px),这就实现了,当我们使用dp来定义一个控件大小的时候,他在不同的手机里表现出相应大小的像素值。
我们可以说,通过dp加上自适应布局和weight比例布局可以基本解决不同手机上适配的问题,这基本是最原始的Android适配方案。
糗事百科适配方案:
字节跳动适配方案:
CPU 与 GPU
UI 渲染还依赖两个核心的硬件:CPU 和 GPU。UI 组件在绘制到屏幕之前,都需要经过 Rasterization(栅格化)操作,这是一个耗时操作,而 GPU 可以加快栅格化。
软件绘制使用的是 Skia 库,它是一款能在低端设备如手机上呈现高质量的 2D 跨平台图形框架,类似 Chrome、Flutter 内部使用的都是 Skia库。
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:简单而快速的图像处理 Android RenderScript 简单实现图片的高斯模糊效果
布局优化
在编写Android的布局时总会遇到这样或者那样的痛点,比如:
- 有些布局的在很多页面都用到了,而且样式都一样,每次用到都要复制粘贴一大段,有没有办法可以复用呢?
- 解决了1中的问题之后,发现复用的布局外面总要额外套上一层布局,要知道布局嵌套是会影响性能的呐;
- 有些布局只有用到时才会显示,但是必须提前写好,设置虽然为了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="1、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="3、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中的布局中再初始化控件:
//1、初始化被inflate的布局后再初始化其中的控件,
FrameLayout frameLayout = findViewById(R.id.view_inflate);//android:inflatedId设置的id
Switch sw = frameLayout.findViewById(R.id.sw);
sw.toggle();
如果主布局中控件的ID没有冲突,可以直接初始化控件使用:
//2、直接初始化控件
Switch sw = findViewById(R.id.sw);
sw.toggle();