一、Perfetto基础
1、Perfetto介绍
Perfetto 是一个生产级的开源堆栈,用于提高性能 仪器和痕量分析。与 Systrace 不同,它提供数据源超集,可以用 protobuf 编码的二进制流形式记录任意长度的跟踪记录。可以将Perfetto理解为systrace的升级版,用在更新的平台、新图表展示更多的信息。它可帮助开发者收集 Android 关键子系统(如SurfaceFlinger/SystemServer/Input/Display 等 Framework 部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。
其基本功能包括以下几部分:
-
- 在安卓上记录痕迹
- 在 Linux 上记录跟踪
- 记录铬迹
- SQL 分析和指标
- 跟踪转换
- 堆分析
- 安卓上的调用堆栈采样
关于Perfetto的官方介绍和使用可以看这里:perfetto官网
2、Perfetto 使用流程
使用 Perfetto 前,要先了解一下 Perfetto 在各个平台上的使用方法,基本流程如下:
- 手机准备好你要进行抓取的界面
- 点击开始抓取(命令行的话就是开始执行命令)
- 手机上开始操作(不要太长时间,文件太大会很卡,而且不好定位问题)
- 设定好的时间到了之后,会将生成 xxxtrace.html 文件,使用如下网站打开Perfetto UI
一般抓到的 Perfetto 文件如下
3、抓取Trace的几种方式
3.1、命令抓取perfetto-traces方式
命令行形式比较灵活,速度也比较快,相关的TAG一次性配置好之后,以后再使用的时候就会很快就出结果,也可以把相关命令写成脚本,下次使用会比较方便。
命令相关的参数虽然比较多,但使用工具的时候不需考虑这么多,在对应的项目前打钩即可,命令行的时候才会去手动加参数
一般来说比较常用的是
- -o : 指示输出文件的路径和名字
- -t : 抓取时间(最新版本可以不用指定, 按 Enter 即可结束)
- -b : 指定 buffer 大小 (一般情况下,默认的 Buffer 是够用的,如果你要抓很长的 Trae , 那么建议调大 Buffer )
- -a : 指定 app 包名 (如果要 Debug 自定义的 Trace 点, 记得要加这个)
在低于Android R的版本上面默认是关闭的,需要先执行命令打开:
adb shell setprop persist.traced.enable 1 |
基本抓取并导出perfetto-traces命令如下,需要其他tag或者相关命令可以自行加入:
抓取perfetton: adb shell perfetto -o /data/misc/perfetto-traces/trace_file.perfetto-trace -t 10s sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory 导出trace文件: adb pull /data/misc/perfetto-traces/trace_file.perfetto-trace
3.2 使用atrace命令抓取systrace,具体方法如下:
抓取atrace命令 adb shell atrace -z -b 40000 am wm view res ss gfx view halbionic pm sched freq idle disk load sync binder_driver binder_lock memreclaim dalvik input -t 10 > /data/local/tmp/trace_output.atrace pull导出trace文件 adb pull /data/local/tmp/trace_output.atrace
3.3 使用systrace脚本抓取systrace,具体方法如下:
1、需要配置好python环境。
2、解压systrace.zip。
进入到解压后的systrace目录下,此时该目录下可以找到systrace.py文件,执行如下命令:
python systrace.py am wm view res ss gfx rs hal bionic pm sched freq idle disk binder_driver binder_lock memreclaim dalvik input database -t 10 -o tracelog\systrace.html
在systrace目录下的tracelog目录中查找生成的systrace.html文件。
3.4 通过手机里的System Tracing抓取
1、打开“开发者选项”,点击Settings->System->Developer options->System Tracing->ShowQuick Settings tile,添加system tracing的快捷图标。
在设置——系统——开发者选项——系统跟踪(不同软件可能路径不太一样)中设置需要录制的类别(Categories)、缓冲区大小(Buffer Size)
打开【录制跟踪记录】(Record trace)开始录制
弹出通知后即可开始复现问题或是进行特定操作
完成以后点击通知栏中的【点按即可停止跟踪】(Tap to stop tracing)即可停止并保存
2、测试前,点击下拉状态栏中Record trace图标开始抓取perfetto-trace,然后开始测试。
3、完成测试后,点击“下拉状态栏中的Record trace图标”或者“下拉状态栏中的正在抓取的通知”停止systrace的抓取。
4、弹出“trace saved”的通知后,可在手机目录/data/local/traces中获取抓取的perfetto-trace文件。
5、通过命令adb pull /data/local/traces将对应的systrace保存到本地。
4、查看线程状态
Perfetto 会用不同的颜色来标识不同的线程状态, 在每个方法上面都会有对应的线程状态来标识目前线程所处的状态,通过查看线程状态我们可以知道目前的瓶颈是什么, 是 cpu 执行慢还是因为 Binder 调用, 又或是进行 io 操作, 又或是拿不到 cpu 时间片
线程状态主要有下面几个
4.1 绿色 : 运行中(Running)
只有在该状态的线程才可能在 cpu 上运行。而同一时刻可能有多个线程处于可执行状态,这些线程的 task_struct 结构被放入对应 cpu 的可执行队列中(一个线程最多只能出现在一个 cpu 的可执行队列中)。调度器的任务就是从各个 cpu 的可执行队列中分别选择一个线程在该cpu 上运行
作用:我们经常会查看 Running 状态的线程,查看其运行的时间,与竞品做对比,分析快或者慢的原因:
- 是否频率不够?
- 是否跑在了小核上?
- 是否频繁在 Running 和 Runnable 之间切换?为什么?
- 是否频繁在 Running 和 Sleep 之间切换?为什么?
- 是否跑在了不该跑的核上面?比如不重要的线程占用了超大核
4.2 蓝色 : 可运行(Runnable)
线程可以运行但当前没有安排,在等待 cpu 调度
作用:Runnable 状态的线程状态持续时间越长,则表示 cpu 的调度越忙,没有及时处理到这个任务:
- 是否后台有太多的任务在跑?
- 没有及时处理是因为频率太低?
- 没有及时处理是因为被限制到某个 cpuset 里面,但是 cpu 很满?
- 此时 Running 的任务是什么?为什么?
4.3 白色 : 休眠中(Sleep)
线程没有工作要做,可能是因为线程在互斥锁上被阻塞,也可能等待某个线程返回,可以看是被谁唤醒,去查看是等待哪个线程。
作用 : 这里一般是在等事件驱动
4.4 橘色 : 不可中断的睡眠态 (Uninterruptible Sleep - IO Block)
线程在I / O上被阻塞或等待磁盘操作完成,一般底线都会标识出此时的 callsite :wait_on_page_locked_killable
作用:这个一般是标示 io 操作慢,如果有大量的橘色不可中断的睡眠态出现,那么一般是由于进入了低内存状态,申请内存的时候触发 pageFault, linux 系统的 page cache 链表中有时会出现一些还没准备好的 page(即还没把磁盘中的内容完全地读出来) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.
4.5 棕色 : 不可中断的睡眠态( Uninterruptible Sleep (non-IO))
线程在另一个内核操作(通常是内存管理)上被阻塞。
作用:一般是陷入了内核态,有些情况下是正常的,有些情况下是不正常的,需要按照具体的情况去分析
5、任务唤醒信息分析
Perfetto 会标识出一个非常有用的信息,可以帮助我们进行线程调用等待相关的分析。
一个线程被唤醒的信息往往比较重要,知道他被谁唤醒,那么我们也就知道了他们之间的调用等待关系,如果一个线程出现一段比较长的 sleep 情况,然后被唤醒,那么我们就可以去看是谁唤醒了这个线程,对应的就可以查看唤醒者的信息,看看为什么唤醒者这么晚才唤醒。
一个常见的情况是:应用主线程程使用 Binder 与 SystemServer 的 AMS 进行通信,但是恰好 AMS 的这个函数正在等待锁释放(或者这个函数本身执行时间很长),那么应用主线程就需要等待比较长的时间,那么就会出现性能问题,比如响应慢或者卡顿,这就是为什么后台有大量的进程在运行,或者跑完 Monkey 之后,整机性能会下降的一个主要原因
另外一个场景的情况是:应用主线程在等待此应用的其他线程执行的结果,这时候线程唤醒信息就可以用来分析主线程到底被哪个线程 Block 住了,如下场景:
我们可以看到这段slepping状态这段时间,我们可以点击后面段Runnig状态,查看它运行在哪个CPU上,点击后可以看到是哪个线程唤醒它的,就可以看是否有异常情况了。
6、信息区数据解析
6.1 CPU架构
简单来说目前的手机 CPU 按照核心数和架构来说,可以分为下面三类:
- 非大小核架构(正常所以核一样)
- 大小核架构(正常0-3小核,4-7大核)
- 大中小核架构(正常0-3小核,4-6中核,7超大核)
6.2 CPU Info信息
Pefetto 中CPU Info信息一般在最上面,里面经常会用到的信息包括:
- CPU 频率变化情况
- 任务执行情况
- 大小核的调度情况
- CPU Boost 调度情况
总的来说,Systrace 中的 Kernel CPU Info 这里一般是看任务调度信息,查看是否是频率或者调度导致当前任务出现性能问题,举例如下:
- 某个场景的任务执行比较慢,我们就可以查看是不是这个任务被调度到了小核?
- 某个场景的任务执行比较慢,当前执行这个任务的 CPU 频率是不是不够?
- 我的任务比较特殊,能不能把我这个任务放到大核去跑?
- 我这个场景对 CPU 要求很高,我能不能要求在我这个场景运行的时候,限制 CPU 最低频率?
6.3 Current Selection信息解析:
6.4 CPU by thread信息解析
7、快捷键使用
快捷键的使用可以加快查看 Perfetto 的速度,下面是一些常用的快捷键
W : 放大 Perfetto , 放大可以更好地看清局部细节
S : 缩小 Perfetto, 缩小以查看整体
A : 左移
D : 右移
M : 选中该时间段的范围,方便上下查看
用pefetto自带的搜索框需要4个字符以上,搜出来的数据会比较详细,可以配合Ctrl+F一起使用,有时候Ctrl+F搜索不到的数据,需要自带的Search搜索
8、何为刷新率
- 60 fps 的意思是说,画面每秒更新60次,是针对软件的
- 这60次更新,是要均匀更新的,不是说一会快,一会慢,那样视觉上也会觉得不流畅
- 每秒60次,也就是 1/60 ~= 16.67 ms 要更新一次
- 这里说的屏幕的刷新率,是针对硬件的,现在大部分手机屏幕的刷新率,都维持在60 HZ,移动设备上一般使用60HZ,是因为移动设备对于功耗的要求更高,提高手机屏幕的刷新率,对于手机来说,逻辑功耗会随着频率的增加而线性增大,同时更高的刷新率,意味着更短的TFT数据写入时间,对屏幕设计来说难度更大。
二、主线程与渲染线程
1、主线程的创建
Android App 的进程是基于 Linux 的,其管理也是基于 Linux 的进程管理机制,所以其创建也是调用了 fork 函数
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
1 | pid_t pid = fork(); |
Fork 出来的进程,我们这里可以把他看做主线程,但是这个线程还没有和 Android 进行连接,所以无法处理 Android App 的 Message ;由于 Android App 线程运行基于消息机制 ,那么这个 Fork 出来的主线程需要和 Android 的 Message 消息绑定,才能处理 Android App 的各种 Message
这里就引入了 ActivityThread ,确切的说,ActivityThread 应该起名叫 ProcessThread 更贴切一些。ActivityThread 连接了 Fork 出来的进程和 App 的 Message ,他们的通力配合组成了我们熟知的 Android App 主线程。所以说 ActivityThread 其实并不是一个 Thread,而是他初始化了 Message 机制所需要的 MessageQueue、Looper、Handler ,而且其 Handler 负责处理大部分 Message 消息,所以我们习惯上觉得 ActivityThread 是主线程,其实他只是主线程的一个逻辑处理单元。
1.1 ActivityThread 的创建
App 进程 fork 出来之后,回到 App 进程,查找 ActivityThread 的 Main函数
com/android/internal/os/ZygoteInit.java
1 | static final Runnable childZygoteInit( |
这里的 startClass 就是 ActivityThread,找到之后调用,逻辑就到了 ActivityThread的main函数
android/app/ActivityThread.java
1 | public static void main(String[] args) { |
main 函数处理完成之后,主线程就算是正式上线开始工作,其 Perfetto 流程如下:
1.2 ActivityThread 的功能
另外我们经常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了
1 | class H extends Handler { //摘抄了部分 |
可以看到,进程创建、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,然后进到具体的 handleXXX
2、渲染线程的创建和发展
主线程讲完了我们来讲渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工作都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分之前主线程的渲染工作,减轻主线程的负担
2.1 软件绘制
我们一般提到的硬件加速,指的就是 GPU 加速,这里可以理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个
1 | android:hardwareAccelerated="false" |
我们就可以关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,只有主线程,没有渲染线程,直接 cpu 调用 libSkia 来进行渲染。
主线程由于要进行渲染工作,所以执行的时间变长了,也更容易出现卡顿,同时帧与帧直接的空闲间隔也变短了,使得其他 Message 的执行时间被压缩
2.2 硬件加速绘制
正常情况下,硬件加速是开启的,主线程的 draw 函数并没有真正的执行 drawCall ,而是把要 draw 的内容记录到 DIsplayList 里面,同步到 RenderThread 中,一旦同步完成,主线程就可以被释放出来做其他的事情,RenderThread 则继续进行渲染工作
主线程和渲染线程一帧的工作流程(每一帧都会遵循这个流程,不过有的帧需要处理的事情多,有的帧需要处理的事情少) ,重点看 “UI Thread ” 和 RenderThread 这两行
这张图对应的工作流程如下
- 主线程处于 Sleep 状态,等待 Vsync 信号
- Vsync 信号到来,主线程被唤醒,Choreographer 回调 FrameDisplayEventReceiver.onVsync 开始一帧的绘制
- 处理 App 这一帧的 Input 事件(如果有的话)
- 处理 App 这一帧的 Animation 事件(如果有的话)
- 处理 App 这一帧的 Traversal 事件(如果有的话)
- 主线程与渲染线程同步渲染数据,同步结束后,主线程结束一帧的绘制,可以继续处理下一个 Message(如果有的话,IdleHandler 如果不为空,这时候也会触发处理),或者进入 Sleep 状态等待下一个 Vsync
- 渲染线程首先需要从 BufferQueue 里面取一个 Buffer(dequeueBuffer) , 进行数据处理之后,调用 OpenGL 相关的函数,真正地进行渲染操作,然后将这个渲染好的 Buffer 还给 BufferQueue (queueBuffer) , SurfaceFlinger 在 Vsync-SF 到了之后,将所有准备好的 Buffer 取出进行合成(这个流程在下面的SurfaceFlinger会讲解)
2.3 渲染线程初始化
渲染线程初始化在真正需要 draw 内容的时候,一般我们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,如果没有则去进行初始化
android/view/ViewRootImpl.java
1 | mAttachInfo.mThreadedRenderer.initializeIfNeeded( |
在Perfetto表现如下:
后续直接调用 draw
android/graphics/HardwareRenderer.java
1 | mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this); |
上面的 draw 只是更新 DIsplayList ,更新结束后,调用 syncAndDrawFrame ,通知渲染线程开始工作,主线程释放。渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui
frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
1 | int RenderProxy::syncAndDrawFrame() { |
其核心流程在 Perfetto 上的表现如下:
2.4 主线程和渲染线程的分工
主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DIsplayList ,但是不涉及 SurfaceFlinger 打交道;渲染线程负责渲染渲染相关的工作,一部分工作也是 CPU 来完成的,一部分操作是调用 OpenGL 函数来完成的
当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类,这样间接的进行绘制操作的优点如下
- DisplayList 可以按需多次绘制而无须同业务逻辑交互
- 特定的绘制操作(如 translation, scale 等)可以作用于整个 DisplayList 而无须重新分发绘制操作
- 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次
- 可以将对 DisplayList 的处理转移至另一个线程(也就是 RenderThread)
- 主线程在 sync 结束后可以处理其他的 Message,而不用等待 RenderThread 结束
3、游戏的主线程与渲染线程
游戏大多使用单独的渲染线程 ,直接跟 SurfaceFlinger 进行交互,其主线程的存在感比较低,绝大部分的逻辑都是自己在自己的渲染线程里面实现的。
大家可以看一下王者荣耀对应的 Perfetto ,一帧整体流程如下:
王者荣耀主线程的主要工作,就是把 Input 事件传给 Unity 的渲染线程,渲染线程收到 Input 事件之后,进行逻辑处理,画面更新等。
三、SurfaceFlinger讲解
SurfaceFlinger 的主要功能是接收缓冲区,创建缓冲区,并将缓冲区发送到显示器。WindowManager 为 SurfaceFlinger 提供了缓冲区和窗口元数据,SurfaceFlinger 使用这些缓冲区和窗口元数据来组合具有显示的表面。
详细定义可以看官网: SurfaceFlinger 的定义
在Perfetto中,我们关注的重点就是上面这幅图对应的部分
- App 部分
- BufferQueue 部分
- SurfaceFlinger 部分
- HWComposer 部分
这四部分,在 Perfetto 中都有可以对应的地方,以时间发生的顺序排序就是 1、2、3、4,下面我们从 Perfetto 的这四部分来看整个渲染的流程
1、App 部分
关于 App 部分,在上面的主线程渲染线程部分已经讲解过了,其主要的流程如下图:
从 SurfaceFlinger 的角度来看,App 部分主要负责生产 SurfaceFlinger 合成所需要的 Surface。
App 与 SurfaceFlinger 的交互主要集中在三点
- Vsync 信号的接收和处理
- RenderThread 的 dequeueBuffer
- RenderThread 的 queueBuffer
1.1 Vsync 信号的接收和处理
App 和 SurfaceFlinger 的第一个交互点就是 Vsync 信号的请求和接收,Vsync-App 信号到达,应用收到这个信号后,开始一帧的渲染准备
2、Triple Buffer
2.1 BufferQueue 部分
如下图,每个有显示界面的进程对应一个 BufferQueue,使用方创建并拥有 BufferQueue 数据结构,并且可存在于与其生产方不同的进程中,BufferQueue 工作流程如下:
上图主要是 dequeue、queue、acquire、release ,在这个例子里面,App 是生产者,负责填充显示缓冲区(Buffer);SurfaceFlinger 是消费者,将各个进程的显示缓冲区做合成操作
- dequeue(生产者发起) : 当生产者需要缓冲区时,它会通过调用 dequeueBuffer() 从 BufferQueue 请求一个可用的缓冲区,并指定缓冲区的宽度、高度、像素格式和使用标记。
- queue(生产者发起):生产者填充缓冲区并通过调用 queueBuffer() 将缓冲区返回到队列。
- acquire(消费者发起) :消费者通过 acquireBuffer() 获取该缓冲区并使用该缓冲区的内容
- release(消费者发起) :当消费者操作完成后,它会通过调用 releaseBuffer() 将该缓冲区返回到队列
2.2 Single Buffer
单 Buffer 的情况下,因为只有一个 Buffer 可用,那么这个 Buffer 既要用来做合成显示,又要被应用拿去做渲染
理想情况下,单 Buffer 是可以完成任务的(有 Vsync-Offset 存在的情况下)
- App 收到 Vsync 信号,获取 Buffer 开始渲染
- 间隔 Vsync-Offset 时间后,SurfaceFlinger 收到 Vsync 信号,开始合成
- 屏幕刷新,我们看到合成后的画面
但是很不幸,理想情况我们也就想一想,这期间如果 App 渲染或者 SurfaceFlinger 合成在屏幕显示刷新之前还没有完成,那么屏幕刷新的时候,拿到的 Buffer 就是不完整的,在用户看来,就有种撕裂的感觉
当然 Single Buffer 已经没有在使用,上面只是一个例子
2.3 Double Buffer
Double Buffer 相当于 BufferQueue 中有两个 Buffer 可供轮转,消费者在消费 Buffer的同时,生产者也可以拿到备用的 Buffer 进行生产操作
下面我们来看理想情况下,Double Buffer 的工作流程
但是 Double Buffer 也会存在性能上的问题,比如下面的情况,App 连续两帧生产都超过 Vsync 周期(准确的说是错过 SurfaceFlinger 的合成时机) ,就会出现掉帧情况
2.4 Triple Buffer
Triple Buffer 中,我们又加入了一个 BackBuffer ,这样的话 BufferQueue 里面就有三个 Buffer 可以轮转了,当 FrontBuffer 在被使用的时候,App 有两个空闲的 Buffer 可以拿去生产,就算生产过程中有 GPU 超时,CPU 任然可以拿到新的 Buffer 进行生产(即 SurfaceFling 消费 FrontBuffer,GPU 使用一个 BackBuffer,CPU使用一个 BackBuffer)
下面就是引入 Triple Buffer 之后,解决了 Double Buffer 中遇到的由于 Buffer 不足引起的掉帧问题
这里把两个图放到一起来看,方便大家做对比(一个是 Double Buffer 掉帧两次,一个是使用 Triple Buffer 只掉了一帧)
3、Triple Buffer 的作用
3.1 缓解掉帧
从上一节 Double Buffer 和 Triple Buffer 的对比图可以看到,在这种情况下(出现连续主线程超时),三个 Buffer 的轮转有助于缓解掉帧出现的次数(从掉帧两次 -> 只掉帧一次)
所以从第一节如何定义掉帧这里我们就知道,App 主线程超时不一定会导致掉帧,由于 Triple Buffer 的存在,部分 App 端的掉帧(主要是由于 GPU 导致),到 SurfaceFlinger 这里未必是掉帧,这是看 Perfetto 的时候需要注意的一个点
3.2 减少主线程和渲染线程等待时间
双 Buffer 的轮转, App 主线程有时候必须要等待 SurfaceFlinger(消费者)释放 Buffer 后,才能获取 Buffer 进行生产,这时候就有个问题,现在大部分手机 SurfaceFlinger 和 App 同时收到 Vsync 信号,如果出现App 主线程等待 SurfaceFlinger(消费者)释放 Buffer ,那么势必会让 App 主线程的执行时间延后,比如下面这张图,可以明显看到:Buffer B 并不是在 Vsync 信号来的时候开始被消费(因为还在使用),而是等 Buffer A 被消费后,Buffer B 被释放,App 才能拿到 Buffer B 进行生产,这期间就有一定的延迟,会让主线程可用的时间变短
3.3 降低 GPU 和 SurfaceFlinger 瓶颈
双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现SurfaceFlinger 无法及时合成而导致掉帧在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间。
SurfaceFlinger 端的 Perfetto 如下所示
4、SurfaceFlinger工作流程
SurfaceFlinger 的主要工作就是合成:
当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。
其 Perfetto 主线程可用看到其主要是在收到 Vsync 信号后开始工作
SurfaceFlinger相关代码在下面,后续会专门写一篇SurfaceFlinger实现流程:
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
4.1 SurfaceFlinger启动流程图解
4.2 SurfaceFlinger 端判断掉帧
通常我们通过 Perfetto 判断应用是否掉帧的时候,一般是直接看 SurfaceFlinger 部分,主要是下面几个步骤
- SurfaceFlinger 的主线程在每个 Vsync-SF 的时候是否没有合成?
- 如果没有合成操作,那么需要看没有合成的原因:
-
- 因为 SurfaceFlinger 检查发现没有可用的 Buffer 而没有合成操作?
- 因为 SurfaceFlinger 被其他的工作占用(比如截图、HWC 等)?
- 如果有合成操作,那么需要看对应的 App 的 可用 Buffer 个数是否正常:如果 App 此时可用 Buffer 为 0,那么看 App 端为何没有及时 queueBuffer(这就一般是应用自身的问题了),因为 SurfaceFlinger 合成操作触发可能是其他的进程有可用的 Buffer
下图为APP耗时导致的掉帧:
SurfaceFlinger 端可以看到 SurfaceFlinger 主线程和合成情况和应用对应的 BufferQueue 中 Buffer 的情况。如上图,就是一个掉帧的例子。App 没有及时渲染完成,且此时 BufferQueue 中也没有前几帧的 Buffer,所以这一帧 SurfaceFlinger 没有合成对应 App 的 Layer,在用户看来这里就掉了一帧
从 App 端无法看出是否掉帧,对应的 SurfaceFlinger 的 Trace 如下, 可以看到由于有 Triple Buffer 的存在, SF 这里有之前 App 的 Buffer,所以尽管 App 超过了一帧的时间, 但是 SF 这里依然有可用来合成的 Buffer, 所以没有掉帧
4.3 逻辑掉帧
上面的掉帧我们是从渲染这边来看的,这种掉帧在 Perfetto 中可以很容易就发现;还存在一种掉帧情况叫逻辑掉帧
逻辑掉帧指的是由于应用自己的代码逻辑问题,导致画面更新的时候,不是以均匀或者物理曲线的方式,而是出现跳跃更新的情况,这种掉帧一般在 Perfetto 上没法看出来,但是用户在使用的时候可以明显感觉到
举一个简单的例子,比如说列表滑动的时候,如果我们滑动松手后列表的每一帧前进步长是一个均匀变化的曲线,最后趋近于 0,这样就是完美的;但是如果出现这一帧相比上一帧走了 20,下一帧相比这一帧走了 10,下下一帧相比下一帧走了 30,这种就是跳跃更新,在 Perfetto 上每一帧都是及时渲染且 SurfaceFlinger 都及时合成的,但是用户用起来就是觉得会卡. 不过我列举的这个例子中,Android 已经针对这种情况做了优化,感兴趣的可以去看一下 android/view/animation/AnimationUtils.java 这个类,重点看下面三个方法的使用
1 | public static void lockAnimationClock(long vsyncMillis) |
Android 系统的动画一般不会有这个问题,但是应用开发者就保不齐会写这种代码,比如做动画的时候根据当前的时间(而不是 Vsync 到来的时间)来计算动画属性变化的情况,这种情况下,一旦出现掉帧,动画的变化就会变得不均匀。
5、HWComposer 部分
- Hardware Composer HAL (HWC) 用于确定通过可用硬件来合成缓冲区的最有效方法。作为 HAL,其实现是特定于设备的,而且通常由显示设备硬件原始设备制造商 (OEM) 完成。
- 当您考虑使用叠加平面时,很容易发现这种方法的好处,它会在显示硬件(而不是 GPU)中合成多个缓冲区。例如,假设有一部普通 Android 手机,其屏幕方向为纵向,状态栏在顶部,导航栏在底部,其他区域显示应用内容。每个层的内容都在单独的缓冲区中。您可以使用以下任一方法处理合成(后一种方法可以显著提高效率):
-
- 将应用内容渲染到暂存缓冲区中,然后在其上渲染状态栏,再在其上渲染导航栏,最后将暂存缓冲区传送到显示硬件。
- 将三个缓冲区全部传送到显示硬件,并指示它从不同的缓冲区读取屏幕不同部分的数据。
- 显示处理器功能差异很大。叠加层的数量(无论层是否可以旋转或混合)以及对定位和叠加的限制很难通过 API 表达。为了适应这些选项,HWC 会执行以下计算(由于硬件供应商可以定制决策代码,因此可以在每台设备上实现最佳性能):
-
- SurfaceFlinger 向 HWC 提供一个完整的层列表,并询问“您希望如何处理这些层?”
- HWC 的响应方式是将每个层标记为叠加层或 GLES 合成。
- SurfaceFlinger 会处理所有 GLES 合成,将输出缓冲区传送到 HWC,并让 HWC 处理其余部分。
- 当屏幕上的内容没有变化时,叠加平面的效率可能会低于 GL 合成。当叠加层内容具有透明像素且叠加层混合在一起时,尤其如此。在此类情况下,HWC 可以选择为部分或全部层请求 GLES 合成,并保留合成的缓冲区。如果 SurfaceFlinger 返回来要求合成同一组缓冲区,HWC 可以继续显示先前合成的暂存缓冲区。这可以延长闲置设备的电池续航时间。
- 运行 Android 4.4 或更高版本的设备通常支持 4 个叠加平面。尝试合成的层数多于叠加层数会导致系统对其中一些层使用 GLES 合成,这意味着应用使用的层数会对能耗和性能产生重大影响。
四、Choreographer讲解
Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用. 了解 Choreographer 还可以帮助 App 开发者知道程序每一帧运行的基本原理,也可以加深对 Message、Handler、Looper、MessageQueue、Measure、Layout、Draw 的理解
1、主线程运行机制的本质
在讲 Choreographer 之前,我们先理一下 Android 主线程运行的本质,其实就是 Message 的处理过程,我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息,MethodTrace 如下图所示
1.1 引入Choreographer前
引入 Vsync 之前的 Android 版本,渲染一帧相关的 Message ,中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,不稳定,MethodTrace 如下图所示
对于用户来说,稳定的帧率才是好的体验,比如你玩王者荣耀,相比 fps 在 60 和 40 之间频繁变化,用户感觉更好的是稳定在 50 fps 的情况.
所以 Android 的演进中,引入了 Vsync + TripleBuffer + Choreographer 的机制,其主要目的就是提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。
1.2 引入 Choreographer后
Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机 ,Vsync 信号到来唤醒 Choreographer 来做 App 的绘制操作 ,如果每个 Vsync 周期应用都能渲染完成,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用.
上面主要是以60HZ为例,当然目前使用 90Hz 刷新率屏幕的手机越来越多,Vsync 周期从 16.6ms 到了 11.1ms,上图中的操作要在更短的时间内完成,对性能的要求也越来越高。
2、Choreographer 作用
Choreographer 扮演 Android 渲染链路中承上启下的角色
- 承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等
- 启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync)
从上面可以看出来, Choreographer 担任的是一个工具人的角色,他之所以重要,是因为通过 Choreographer + SurfaceFlinger + Vsync + TripleBuffer 这一套从上到下的机制,保证了 Android App 可以以一个稳定的帧率运行(20fps、90fps 或者 60fps),减少帧率波动带来的不适感。
了解 Choreographer 还可以帮助 App 开发者知道程序每一帧运行的基本原理,也可以加深对 Message、Handler、Looper、MessageQueue、Input、Animation、Measure、Layout、Draw 的理解 , 很多 APM 工具也用到了 Choreographer( 利用 FrameCallback + FrameInfo ) + MessageQueue ( 利用 IdleHandler ) + Looper ( 设置自定义 MessageLogging) 这些组合拳,深入了解了这些之后,再去做优化,脑子里的思路会更清晰。
另外虽然画图是一个比较好的解释流程的好路子,但是我个人不是很喜欢画图,因为平时 Perfetto 和 MethodTrace 用的比较多,Perfetto 是按从左到右展示整个系统的运行情况的一个工具(包括 cpu、SurfaceFlinger、SystemServer、App 等关键进程),使用 Perfetto 和 MethodTrace 也可以很方便地展示关键流程。当你对系统代码比较熟悉的时候,看 Perfetto 就可以和手机运行的实际情况对应起来。所以下面的文章除了一些网图之外,其他的我会多以 Perfetto 来展示。
2.1 从 Perfetto 的角度来看 Choreogrepher 的工作流程
下图以滑动推特APP为例子,我们先看一下从左到右滑动桌面的一个完整的预览图(App 进程),可以看到 Perfetto 中从左到右,每一个绿色的帧都表示一帧,表示最终我们可以手机上看到的画面
- 图中每一个紫色的条宽度是一个 Vsync 的时间,这个是120HZ也就是 8.3ms
- 每一帧处理的流程:接收到 Vsync 信号回调-> UI Thread –> RenderThread –> SurfaceFlinger(图中未显示)
- UI Thread 和 RenderThread 就可以完成 App 一帧的渲染,渲染完的 Buffer 抛给 SurfaceFlinger 去合成,然后我们就可以在屏幕上看到这一帧了
- 可以看到推特滑动的每一帧耗时都很短(Ui Thread 耗时 + RenderThread 耗时),但是由于 Vsync 的存在,每一帧都会等到 Vsync 才会去做处理
有了上面这个整体的概念,我们将 UI Thread 的每一帧放大来看,看看 Choreogrepher 的位置以及 Choreogrepher 是怎么组织每一帧的
2.2 Choreographer 的工作流程
- Choreographer 初始化
-
- 初始化 FrameHandler ,绑定 Looper
- 初始化 FrameDisplayEventReceiver ,与 SurfaceFlinger 建立通信用于接收和请求 Vsync
- 初始化 CallBackQueues
- SurfaceFlinger 的 appEventThread 唤醒发送 Vsync ,Choreographer 回调 FrameDisplayEventReceiver.onVsync , 进入 Choreographer 的主处理函数 doFrame
- Choreographer.doFrame 计算掉帧逻辑
- Choreographer.doFrame 处理 Choreographer 的第一个 callback : input
- Choreographer.doFrame 处理 Choreographer 的第二个 callback : animation
- Choreographer.doFrame 处理 Choreographer 的第三个 callback : insets animation
- Choreographer.doFrame 处理 Choreographer 的第四个 callback : traversal
-
- traversal-draw 中 UIThread 与 RenderThread 同步数据
- Choreographer.doFrame 处理 Choreographer 的第五个 callback : commit ?
- RenderThread 处理绘制命令,将处理好的绘制命令发给 GPU 处理
- 调用 swapBuffer 提交给 SurfaceFlinger 进行合成(此时 Buffer 并没有真正完成,需要等 CPU 完成后 SurfaceFlinger 才能真正使用,新版本的 Perfetto 中有 gpu 的 fence 来标识这个时间)
第一步初始化完成后,后续就会在步骤 2-9 之间循环
同时也附上这一帧所对应的 MethodTrace(这里预览一下即可,下面会有详细的大图)
下面我们就从源码的角度,来看一下具体的实现
3、源码解析
下面从源码的角度来简单看一下,源码只摘抄了部分重要的逻辑,其他的逻辑则被剔除,另外 Native 部分与 SurfaceFlinger 交互的部分也没有列入,不是本文的重点,有兴趣的可以自己去跟一下。
3.1 Choreographer 的初始化
Choreographer 的单例初始化
1 | // Thread local storage for the choreographer. |
Choreographer 的构造函数
1 | private Choreographer(Looper looper, int vsyncSource) { |
FrameHandler
1 | private final class FrameHandler extends Handler { |
Choreographer 初始化链
在 Activity 启动过程,执行完 onResume 后,会调用 Activity.makeVisible(),然后再调用到 addView(), 层层调用会进入如下方法
1 | ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) |
3.2 FrameDisplayEventReceiver
Vsync 的注册、申请、接收都是通过 FrameDisplayEventReceiver 这个类,所以可以先简单介绍一下。 FrameDisplayEventReceiver 继承 DisplayEventReceiver , 有三个比较重要的方法
- onVsync – Vsync 信号回调
- run – 执行 doFrame
- scheduleVsync – 请求 Vsync 信号
1 | private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { |
3.3 Choreographer 中 Vsync 的注册
从下面的函数调用栈可以看到,Choreographer 的内部类 FrameDisplayEventReceiver.onVsync 负责接收 Vsync 回调,通知 UIThread 进行数据处理。
那么 FrameDisplayEventReceiver 是通过什么方式在 Vsync 信号到来的时候回调 onVsync 呢?答案是 FrameDisplayEventReceiver 的初始化的时候,最终通过监听文件句柄的形式,其对应的初始化流程如下
android/view/Choreographer.java
1 | private Choreographer(Looper looper, int vsyncSource) { |
android/view/Choreographer.java
1 | public FrameDisplayEventReceiver(Looper looper, int vsyncSource) { |
android/view/DisplayEventReceiver.java
1 | public DisplayEventReceiver(Looper looper, int vsyncSource) { |
简单来说,FrameDisplayEventReceiver 的初始化过程中,通过 BitTube(本质是一个 socket pair),来传递和请求 Vsync 事件,当 SurfaceFlinger 收到 Vsync 事件之后,通过 appEventThread 将这个事件通过 BitTube 传给 DisplayEventDispatcher ,DisplayEventDispatcher 通过 BitTube 的接收端监听到 Vsync 事件之后,回调 Choreographer.FrameDisplayEventReceiver.onVsync ,触发开始一帧的绘制,如下图
3.4 Choreographer 处理一帧的逻辑
Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理
android/view/Choreographer.java
1 | public void onVsync(long timestampNanos, long physicalDisplayId, int frame) { |
doFrame 函数主要做下面几件事
- 计算掉帧逻辑
- 记录帧绘制信息
- 执行 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_INSETS_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT
计算掉帧逻辑
1 | void doFrame(long frameTimeNanos, int frame) { |
Choreographer.doFrame 的掉帧检测比较简单,从下图可以看到,Vsync 信号到来的时候会标记一个 start_time ,执行 doFrame 的时候标记一个 end_time ,这两个时间差就是 Vsync 处理时延,也就是掉帧
记录帧绘制信息
Choreographer 中 FrameInfo 来负责记录帧的绘制信息,doFrame 执行的时候,会把每一个关键节点的绘制时间记录下来,我们使用 dumpsys gfxinfo 就可以看到。当然 Choreographer 只是记录了一部分,剩余的部分在 hwui 那边来记录。
从 FrameInfo 这些标志就可以看出记录的内容,后面我们看 dumpsys gfxinfo 的时候数据就是按照这个来排列的
1 | // Various flags set to provide extra metadata about the current frame |
doFrame 函数记录从 Vsync time 到 markPerformTraversalsStart 的时间
1 | void doFrame(long frameTimeNanos, int frame) { |
执行 Callbacks
1 | void doFrame(long frameTimeNanos, int frame) { |
Input 回调调用栈
input callback 一般是执行 ViewRootImpl.ConsumeBatchedInputRunnable
android/view/ViewRootImpl.java
1 | final class ConsumeBatchedInputRunnable implements Runnable { |
Input 时间经过处理,最终会传给 DecorView 的 dispatchTouchEvent,这就到了我们熟悉的 Input 事件分发
Animation 回调调用栈
一般我们接触的多的是调用 View.postOnAnimation 的时候,会使用到 CALLBACK_ANIMATION
1 | public void postOnAnimation(Runnable action) { |
那么一般是什么时候回调用到 View.postOnAnimation 呢,我截取了一张图,大家可以自己去看一下,接触最多的应该是 startScroll,Fling 这种操作
其调用栈根据其 post 的内容,下面是桌面滑动松手之后的 fling 动画。
另外我们的 Choreographer 的 FrameCallback 也是用的 CALLBACK_ANIMATION
1 | public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { |
Traversal 调用栈
1 | void scheduleTraversals() { |
doTraversal 的 TraceView 示例
3.5 下一帧的 Vsync 请求
由于动画、滑动、Fling 这些操作的存在,我们需要一个连续的、稳定的帧率输出机制。这就涉及到了 Vsync 的请求逻辑,在连续的操作,比如动画、滑动、Fling 这些情况下,每一帧的 doFrame 的时候,都会根据情况触发下一个 Vsync 的申请,这样我们就可以获得连续的 Vsync 信号。
看下面的 scheduleTraversals 调用栈(scheduleTraversals 中会触发 Vsync 请求)
我们比较熟悉的 invalidate 和 requestLayout 都会触发 Vsync 信号请求
我们下面以 Animation 为例,看看 Animation 是如何驱动下一个 Vsync ,来持续更新画面的
ObjectAnimator 动画驱动逻辑
android/animation/ObjectAnimator.java
1 | public void start() { |
android/animation/ValueAnimator.java
1 | private void start(boolean playBackwards) { |
android/animation/AnimationHandler.java
1 | public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { |
调用 postFrameCallback 会走到 mChoreographer.postFrameCallback ,这里就会触发 Choreographer 的 Vsync 请求逻辑
android/animation/AnimationHandler.java
1 | public void postFrameCallback(Choreographer.FrameCallback callback) { |
android/view/Choreographer.java
1 | private void postCallbackDelayedInternal(int callbackType, |
通过上面的 Animation.start 设置,利用了 Choreographer.FrameCallback 接口,每一帧都去请求下一个 Vsync
动画过程中一帧的 TraceView 示例
3.6 源码小结
- Choreographer 是线程单例的,而且必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和 Looper 绑定,一般是 App 主线程的 Looper 绑定
- DisplayEventReceiver 是一个 abstract class,其 JNI 的代码部分会创建一个IDisplayEventConnection 的 Vsync 监听者对象。这样,来自 AppEventThread 的 VSYNC 中断信号就可以传递给 Choreographer 对象了。当 Vsync 信号到来时,DisplayEventReceiver 的 onVsync 函数将被调用。
- DisplayEventReceiver 还有一个 scheduleVsync 函数。当应用需要绘制UI时,将首先申请一次 Vsync 中断,然后再在中断处理的 onVsync 函数去进行绘制。
- Choreographer 定义了一个 FrameCallback interface,每当 Vsync 到来时,其 doFrame 函数将被调用。这个接口对 Android Animation 的实现起了很大的帮助作用。以前都是自己控制时间,现在终于有了固定的时间中断。
- Choreographer 的主要功能是,当收到 Vsync 信号时,去调用使用者通过 postCallback 设置的回调函数。目前一共定义了五种类型的回调,它们分别是:
-
- CALLBACK_INPUT : 处理输入事件处理有关
- CALLBACK_ANIMATION : 处理 Animation 的处理有关
- CALLBACK_INSETS_ANIMATION : 处理 Insets Animation 的相关回调
- CALLBACK_TRAVERSAL : 处理和 UI 等控件绘制有关
- CALLBACK_COMMIT : 处理 Commit 相关回调,主要是是用于执行组件 Application/Activity/Service 的 onTrimMemory,在 ApplicationThread 的 scheduleTrimMemory 方法中向 Choreographer 插入的;另外这个 Callback 也提供了一个监测一帧耗时的时机
- ListView 的 Item 初始化(obtain\setup) 会在 input 里面也会在 animation 里面,这取决于
- CALLBACK_INPUT 、CALLBACK_ANIMATION 会修改 view 的属性,所以要先与 CALLBACK_TRAVERSAL 执行
4、APM 与 Choreographer
由于 Choreographer 的位置,许多性能监控的手段都是利用 Choreographer 来做的,除了自带的掉帧计算,Choreographer 提供的 FrameCallback 和 FrameInfo 都给 App 暴露了接口,让 App 开发者可以通过这些方法监控自身 App 的性能,其中常用的方法如下:
- 利用 FrameCallback 的 doFrame 回调
- 利用 FrameInfo 进行监控
-
- 使用 :adb shell dumpsys gfxinfo framestats
- 示例 :adb shell dumpsys gfxinfo com.meizu.flyme.launcher framestats
- 利用 SurfaceFlinger 进行监控
-
- 使用 :adb shell dumpsys SurfaceFlinger –latency
- 示例 :adb shell dumpsys SurfaceFlinger –latency com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher#0
- 利用 SurfaceFlinger PageFlip 机制进行监控
-
- 使用 : adb service call SurfaceFlinger 1013
- 备注:需要系统权限
- Choreographer 自身的掉帧计算逻辑
- BlockCanary 基于 Looper 的性能监控
4.1 利用 FrameCallback 的 doFrame 回调
FrameCallback 接口
1 | public interface FrameCallback { |
接口使用
1 | Choreographer.getInstance().postFrameCallback(youOwnFrameCallback ); |
接口处理
1 | public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { |
4.2 利用 FrameInfo 进行监控
adb shell dumpsys gfxinfo framestats
1 | Window: StatusBar |
4.3 利用 SurfaceFlinger 进行监控
命令解释:
- 数据的单位是纳秒,时间是以开机时间为起始点
- 每一次的命令都会得到128行的帧相关的数据
数据:
- 第一行数据,表示刷新的时间间隔refresh_period
- 第1列:这一部分的数据表示应用程序绘制图像的时间点
- 第2列:在SF(软件)将帧提交给H/W(硬件)绘制之前的垂直同步时间,也就是每帧绘制完提交到硬件的时间戳,该列就是垂直同步的时间戳
- 第3列:在SF将帧提交给H/W的时间点,算是H/W接受完SF发来数据的时间点,绘制完成的时间点。
掉帧 jank 计算
每一行都可以通过下面的公式得到一个值,该值是一个标准,我们称为jankflag,如果当前行的jankflag与上一行的jankflag发生改变,那么就叫掉帧
ceil((C - A) / refresh-period)
4.4 利用 SurfaceFlinger PageFlip 机制进行监控
1 | Parcel data = Parcel.obtain(); |
4.5 Choreographer 自身的掉帧计算逻辑
SKIPPED_FRAME_WARNING_LIMIT 默认为30 , 由 debug.choreographer.skipwarning 这个属性控制
1 | if (jitterNanos >= mFrameIntervalNanos) { |
4.6 BlockCanary
Blockcanary 做性能监控使用的是 Looper 的消息机制,通过对 MessageQueue 中每一个 Message 的前后进行记录,打到监控性能的目的
android/os/Looper.java
1 | public static void loop() { |
5、MessageQueue 与 Choreographer
所谓的异步消息其实就是这样的,我们可以通过 enqueueBarrier 往消息队列中插入一个 Barrier,那么队列中执行时间在这个 Barrier 以后的同步消息都会被这个 Barrier 拦截住无法执行,直到我们调用 removeBarrier 移除了这个 Barrier,而异步消息则没有影响,消息默认就是同步消息,除非我们调用了 Message 的 setAsynchronous,这个方法是隐藏的。只有在初始化 Handler 时通过参数指定往这个 Handler 发送的消息都是异步的,这样在 Handler 的 enqueueMessage 中就会调用 Message 的 setAsynchronous 设置消息是异步的,从上面 Handler.enqueueMessage 的代码中可以看到。
所谓异步消息,其实只有一个作用,就是在设置 Barrier 时仍可以不受 Barrier 的影响被正常处理,如果没有设置 Barrier,异步消息就与同步消息没有区别,可以通过 removeSyncBarrier 移除 Barrier
5.1 SyncBarrier 在 Choreographer 中使用的一个示例
scheduleTraversals 的时候 postSyncBarrier
1 | void scheduleTraversals() { |
doTraversal 的时候 removeSyncBarrier
1 | void doTraversal() { |
Choreographer post Message 的时候,会把这些消息设为 Asynchronous ,这样 Choreographer 中的这些 Message 的优先级就会比较高,
1 | Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); |
五、SystemServer
1、窗口动画
Systrace 中的 SystemServer 一个比较重要的地方就是窗口动画,由于窗口归 SystemServer 来管,那么窗口动画也就是由 SystemServer 来进行统一的处理,其中涉及到两个比较重要的线程,Android.Anim 和 Android.Anim.if 这两个线程。
2、ActivityManagerService
AMS 和 WMS 算是 SystemServer 中最繁忙的两个 Service 了,与 AMS 相关的 Trace 一般会用 TRACE_TAG_ACTIVITY_MANAGER 这个 TAG,在 Systrace 中的名字是 ActivityManager
下面是启动一个新的进程的时候,AMS 的输出
在进程和四大组件的各种场景一般都会有对应的 Trace 点来记录,比如大家熟悉的 ActivityStart、ActivityResume、activityStop 等,这些 Trace 点有一些在应用进程,有一些在 SystemServer 进程,所以大家在看 Activity 相关的代码逻辑的时候,需要不断在这两个进程之间进行切换,这样才能从一个整体的角度来看应用的状态变化和 SystemServer 在其中起到的作用。
3、WindowManagerService
与 WMS 相关的 Trace 一般会用 TRACE_TAG_WINDOW_MANAGER 这个 TAG,在 Systrace 中 WindowManagerService 在 SystemServer 中多在对应的 Binder 中出现,比如下面应用启动的时候,relayoutWindow 的 Trace 输出
在 Window 的各种场景一般都会有对应的 Trace 点来记录,比如大家熟悉的 relayoutWIndow、performLayout、prepareToDisplay 等
4、Input
4.1 input在Perfetto的实现流程
下面这张图是一个概览图,以滑动推特app为例 (滑动应用包括一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件,这些事件和事件流都会在 Perfetto 上有所体现,这也是我们分析 Perfetto 的一个重要的切入点),主要牵扯到的模块是 SystemServer 和 App 模块,下图是他们各个区域在Perfetto上表现
放大后Input在Perfetto上的表现
InputReader 和 InputDispatcher 是跑在 SystemServer 里面的两个 Native 线程,负责读取和分发 Input 事件,我们分析 Perfetto 的 Input 事件流,首先是找到这里。下面针对上图中标号进行简单说明
1.触摸屏每隔几毫秒扫描一次,如果有触摸事件,那么把事件上报到对应的驱动
2.InputReader 负责从 EventHub 里面把 Input 事件读取出来,放到 InboundQueue 中然后交给 InputDispatcher 进行事件分发
3.InputDispatcher 从 InboundQueue 中取出 Input 事件进行包装然后派发到各个 App(连接) 的 OutBoundQueue ,同时将事件记录到各个 App(连接) 的 WaitQueue。
4.App 接收到 Input 事件,同时记录到 PendingInputEventQueue ,然后对事件进行分发处理
5.App 处理完成后,回调 InputManagerService 将负责监听的 WaitQueue 中对应的 Input 移除
注:
OutboundQueue 里面放的是即将要被派发给对应 AppConnection 的事件
WaitQueue 里面记录的是已经派发给 AppConnection 但是 App 还在处理没有返回处理成功的事件
deliverInputEvent 标识 App UI Thread 被 Input 事件唤醒
PendingInputEventQueue 里面记录的是 App 需要处理的 Input 事件,这里可以看到已经到了应用进程 。
通过上面的流程,一次 Input 事件就被消耗掉了(当然这只是正常情况,还有很多异常情况、细节处理,这里就不细说了,自己看相关流程的时候可以深挖一下)
① InputReader
InputReader 是一个 Native 线程,跑在 SystemServer 进程里面,其核心功能是从 EventHub 读取事件、进行加工、将加工好的事件发送到 InputDispatcher
InputReader Loop 流程如下
- getEvents:通过 EventHub (监听目录 /dev/input )读取事件放入 mEventBuffer ,而mEventBuffer 是一个大小为256的数组, 再将事件 input_event 转换为 RawEvent
- processEventsLocked: 对事件进行加工, 转换 RawEvent -> NotifyKeyArgs(NotifyArgs)
- QueuedListener->flush:将事件发送到 InputDispatcher 线程, 转换 NotifyKeyArgs -> KeyEntry(EventEntry)
核心代码 loopOnce 处理流程如下:
InputReader 核心 Loop 函数 loopOnce 逻辑如下
1 | void InputReader::loopOnce() { |
② InputDispatcher
上面的 InputReader 调用 mQueuedListener->flush 之后 ,将 Input 事件加入到InputDispatcher 的 mInboundQueue ,然后唤醒 InputDispatcher
InputDispatcher 的核心逻辑如下:
- dispatchOnceInnerLocked(): 从 InputDispatcher 的 mInboundQueue 队列,取出事件 EventEntry。另外该方法开始执行的时间点 (currentTime) 便是后续事件 dispatchEntry 的分发时间 (deliveryTime)
- dispatchKeyLocked():满足一定条件时会添加命令 doInterceptKeyBeforeDispatchingLockedInterruptible;
- enqueueDispatchEntryLocked():生成事件 DispatchEntry 并加入 connection 的 outbound 队列
- startDispatchCycleLocked():从 outboundQueue 中取出事件 DispatchEntry, 重新放入 connection 的 waitQueue 队列;
- InputChannel.sendMessage 通过 socket 方式将消息发送给远程进程;
- runCommandsLockedInterruptible():通过循环遍历的方式,依次处理 mCommandQueue 队列中的所有命令。而 mCommandQueue 队列中的命令是通过 postCommandLocked() 方式向该队列添加的。
其核心处理逻辑在 dispatchOnceInnerLocked 这里
1 | void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) { |
③ InboundQueue
InputDispatcher 执行 notifyKey 的时候,会将 Input 事件封装后放到 InboundQueue 中,后续 InputDispatcher 循环处理 Input 事件的时候,就是从 InboundQueue 取出事件然后做处理
④ OutboundQueue
Outbound 意思是出站,这里的 OutboundQueue 指的是要被 App 拿去处理的事件队列,每一个 App(Connection) 都对应有一个 OutboundQueue ,从 InboundQueue 那一节的图来看,事件会先进入 InboundQueue ,然后被 InputDIspatcher 派发到各个 App 的 OutboundQueue
⑤ waitQueue
当 InputDispatcher 将 Input 事件分发出去之后,将 DispatchEntry 从 outboundQueue 中取出来放到 WaitQueue 中,当 publish 出去的事件被处理完成(finished),InputManagerService 就会从应用中得到一个回复,此时就会取出 WaitQueue 中的事件,从 Perfetto 中看就是对应 App 的 WaitQueue 减少
如果主线程发生卡顿,那么 Input 事件没有及时被消耗,也会在 WaitQueue 这里体现出来,如下图:
4.2 Input 刷新与 Vsync
Input 的刷新取决于触摸屏的采样,目前比较多的屏幕采样率是 120Hz,对应就是 8.3ms 采样一次,我们来看一下其在 Perfetto 上的展示
可以看到上图中, InputReader 每隔 8.3ms 就可以读上来一个数据,交给 InputDispatcher 去分发给 App ,那么是不是屏幕采样率越高越好呢?也不一定,比如上面那张图,虽然 InputReader 每隔 8.3ms 就可以读上来一个数据给 InputDispatcher 去分发给 App ,但是从 WaitQueue 的表现来看,应用并没有消耗这个 Input 事件,这是为什么呢?
原因在于应用消耗 Input 事件的时机是 Vsync 信号来了之后,刷新率为 60Hz 的屏幕,一般系统也是 60 fps ,也就是说两个 Vsync 的间隔在 16.6ms ,这期间如果有两个或者三个 Input 事件,那么必然有一个或者两个要被抛弃掉,只拿最新的那个。也就是说:
- 在屏幕刷新率和系统 FPS 都是 60 的时候,盲目提高触摸屏的采样率,是没有太大的效果的,反而有可能出现上面图中那样,有的 Vsync 周期中有两个 Input 事件,而有的 Vsync 周期中有三个 Input 事件,这样造成事件不均匀,可能会使 UI 产生抖动
- 在屏幕刷新率和系统 FPS 都是 60 的时候,使用 120Hz 采样率的触摸屏就可以了
- 如果在屏幕刷新率和系统 FPS 都是 90 的时候 ,那么 120Hz 采样率的触摸屏显然不够用了,这时候应该采用 180Hz 采样率的屏幕
4.3 Input 调试信息
Dumpsys Input 主要是 Debug 用,我们也可以来看一下其中的一些关键信息,到时候遇到了问题也可以从这里面找 , 其命令如下:
1 | adb shell dumpsys input |
其中的输出比较多,我们终点截取 Device 信息、InputReader、InputDispatcher 三段来看就可以了
① Device 信息
主要是目前连接上的 Device 信息,下面摘取的是 touch 相关的
1 | 3: main_touch |
② Input Reader 状态
InputReader 这里就是当前 Input 事件的一些展示
1 | Device 3: main_touch |
③ InputDispatcher 状态
InputDispatch 这里的重要信息主要包括
- FocusedApplication :当前获取焦点的应用
- FocusedWindow : 当前获取焦点的窗口
- TouchStatesByDisplay
- Windows :所有的 Window
- MonitoringChannels :Window 对应的 Channel
- Connections :所有的连接
- AppSwitch: not pending
- Configuration
1 | Input Dispatcher State: |
5、Binder
很多卡顿问题和响应速度的问题,是因为跨进程 binder 通信的时候,锁竞争导致 binder 通信事件变长,影响了调用端。最常见的就是应用渲染线程 dequeueBuffer 的时候 SurfaceFlinger 主线程阻塞导致 dequeueBuffer 耗时,从而导致应用渲染出现卡顿; 或者 SystemServer 中的 AMS 或者 WMS 持锁方法等待太多, 导致应用调用的时候等待时间比较长导致主线程卡顿
5.1 Binder 架构图
5.2 Binder调用图解
Binder 主要是用来跨进程进行通信,可以看下面这张图,简单显示了在 Perfetto 中 ,Binder 通信是如何显示的
点击 Binder 可以查看其详细信息,其中有的信息在分析问题的时候可以用到,这里不做过多的描述
5.3 Binder 持锁图解
Monitor 指的是当前锁对象的池,在 Java 中,每个对象都有两个池,锁(monitor)池和等待池:
锁池(同步队列 SynchronizedQueue ):假设线程 A 已经拥有了某个对象(注意:不是类 )的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中。
这里用了争夺(contention)这个词,意思是这里由于在和目前对象的锁正被其他对象(Owner)所持有,所以没法得到该对象的锁的拥有权,所以进入该对象的锁池
Owner : 指的是当前拥有这个对象的锁的对象。这里是 Binder:1605_B,4667 是其线程 ID。
6、HandlerThread
6.1 BackgroundThread
com/android/internal/os/BackgroundThread.java
1 | private BackgroundThread() { |
Systrace 中的 BackgroundThread
BackgroundThread 在系统中使用比较多,许多对性能没有要求的任务,一般都会放到 BackgroundThread 中去执行
7、ServiceThread
ServiceThread 继承自 HandlerThread ,下面介绍的几个工作线程都是继承自 ServiceThread ,分别实现不同的功能,根据线程功能不同,其线程优先级也不同:UIThread、IoThread、DisplayThread、AnimationThread、FgThread、SurfaceAnimationThread
每个 Thread 都有自己的 Looper 、Thread 和 MessageQueue,互相不会影响。Android 系统根据功能,会使用不同的 Thread 来完成。
7.1 UiThread
com/android/server/UiThread.java
1 | private UiThread() { |
Systrace 中的 UiThread,一般分析手写笔问题的时候会看
UiThread 被使用的地方如下,具体的功能可以自己去源码里面查看,关键字是 UiThread.get()
7.2 IoThread
com/android/server/IoThread.java
1 | private IoThread() { |
IoThread 被使用的地方如下,具体的功能可以自己去源码里面查看,关键字是 IoThread.get()
7.3 DisplayThread
com/android/server/DisplayThread.java
1 | private DisplayThread() { |
Systrace 中的 DisplayThread
7.4 AnimationThread
com/android/server/AnimationThread.java
1 | private AnimationThread() { |
Systrace 中的 AnimationThread
AnimationThread 在源码中的使用,可以看到 WindowAnimator 的动画执行也是在 AnimationThread 线程中的,Android P 增加了一个 SurfaceAnimationThread 来分担 AnimationThread 的部分工作,来提高 WindowAnimation 的动画性能
7.5 FgThread
com/android/server/FgThread.java
1 | private FgThread() { |
Systrace 中的 FgThread
FgThread 在源码中的使用,可以自己搜一下,下面是具体的使用的一个例子
1 | FgThread.getHandler().post(() -> { |
7.6 SurfaceAnimationThread
1 | com/android/server/wm/SurfaceAnimationThread.java |
Systrace 中的 SurfaceAnimationThread
SurfaceAnimationThread 的名字叫 android.anim.lf , 与 android.anim 有区别,
这个 Thread 主要是执行窗口动画,用于分担 android.anim 线程的一部分动画工作,减少由于锁导致的窗口动画卡顿问题
1 | SurfaceAnimationRunner(@Nullable AnimationFrameCallbackProvider callbackProvider, |
六、参考文档:
- VSYNC
- CPU 电源状态
- Android 开发高手课
- SW-VSYNC 的生成与传递
- 绑定CPU逻辑核心的利器——taskset
- Android Performance
- Input系统—InputReader线程 - Gityuan博客 | 袁辉辉的技术博客
- Input系统—启动篇 - Gityuan博客 | 袁辉辉的技术博客
- Input系统—InputDispatcher线程 - Gityuan博客 | 袁辉辉的技术博客
- 你似乎来到了没有知识存在的荒原 - 知乎
- 理解Android Binder机制1/3:驱动篇
- 理解Android Binder机制2/3:C++层
- 理解Android Binder机制3/3:Java层
- 图形 | Android 开源项目 | Android Open Source Project
- [译]理解 RenderThread - 掘金
- website upgrading…
- Flutter渲染流程简析 - 掘金
- Flutter渲染机制—UI线程 - Gityuan博客 | 袁辉辉的技术博客
- 进程和线程概览 | App quality | Android Developers
- Android应用与SurfaceFlinger建立连接的过程 - 简书
- Choreographer原理 - Gityuan博客 | 袁辉辉的技术博客
- https://developer.android.com/reference/android/view/Choreographer
- https://www.jishuwen.com/d/2Vcc
- Android性能测评与优化-流畅度测评 - 掘金
- Android硬件加速(二)-RenderThread与OpenGL GPU渲染 - 掘金
- Android图形系统概述 - Gityuan博客 | 袁辉辉的技术博客
- http://echuang54.blogspot.com/2015/01/dispsync.html