普通视图

发现新文章,点击刷新页面。
昨天以前首页

Android Perfetto 系列 3:熟悉 Perfetto View

作者 Gracker
2024年5月21日 07:30

本篇是 Perfetto 系列文章的第三篇,前两篇介绍了 Perfetto 是什么以及 Perfetto Trace 怎么抓,本篇主要是在网页端打开 Perfetto Trace 之后,面对复杂的 Perfetto 信息该怎么看。

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

本系列旨在通过 Perfetto 这个工具,从一个新的视角审视 Android 系统的整体运作方式。此外,它还旨在提供一个不同的角度来学习 App 、 Framework、Linux 等关键模块。尽管你可能已经阅读过许多关于 Android Framework、App 、性能优化的文章,但或许因为难以记住代码或不明白其运行流程,你仍感到困惑。通过 Perfetto 这个图形化工具,你可能会获得更深入的理解。

Perfetto 系列目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

Perfetto View 界面

抓到 Perfetto Trace 之后,一般是在 ui.perfetto.dev 中打开(如果用官方提供的脚本,则会在抓去结束后自动在这个网站上打开,想看看怎么实现的话可以去看看脚本的源码)。打开后界面如下:

Perfetto View 界面

可以通过 Open trace file 或者直接把 Perfetto Trace 拖到白色区域来打开 Trace。

Perfetto Trace 界面

打开 Perfetto Trace 之后界面如下:

Trace 操作区

大致上 Perfetto Trace 界面可以分为四个区域:

  1. 最右边的操作区:这里最主要的是 Current Trace 这一栏下面的那几个会经常用到。
    1. Show timeline :显示当前 Trace,切到了别的界面之后,再点这个就会回到 Trace View 界面
    2. Query:写 SQL 查询语句和查看执行结果的地方
    3. Metrics:官方默认帮你写好的一些分析结果,可以选择直接打开
    4. Info and stats :当前 Trace 和手机 App 的一些信息
  2. 上方的信息和操作区域:最主要就是看时间。
  3. 中间的 Trace 内容区:操作最多的区域,Trace 内容都在这部分,最上面的几部分是从功能的角度来划成一个区域的,比如 CPU 区(可以查看当前 Task 跑在哪个核心上,频率是多少,跑了多长时间、被谁唤醒)、Ftrace event 区等;下面的就是以 App Process 为单位展示的(包括 App 的各种线程、Input 事件、Binder Call、Memory、Buffer 等信息)。
  4. 最下方的信息区:这个区域主要是展示各种信息、我们选中了某个 Task 段之后,这里就会展示这个 Task 相关的信息(如果你加了 Log,这里也会显示 Log;ftrace event 同理)。

Perfetto 界面最初看的时候会觉得很乱,花里胡哨的,但是用习惯了之后,真香~

基本操作

Perfetto Trace 界面的操作是非常顺滑的,这是相比 Systrace 的一个巨大的优势,Systrace 打开稍大的 Trace 就会卡卡的,但是 Perfetto Trace 打开 500Mb 的 Trace 依然操作很顺滑。

操作看 Trace 的快捷键跟 Systrace 很像,w/s 是放大/缩小,a/d 是左右移动,鼠标点击是选择。官方左下角的文档有详细的操作说明,忘记了的话可以随时去看看,熟能生巧:

基本操作

SQL 相关的操作

其他快捷键

其他快捷键里面用的比较多的:

  1. f 是放大选中

  2. m 是临时 Mark 一段区域(与 Systrace 一样), 用来上下看时间、看其他进程信息等。临时的意思就是你如果按 m 去 mark 另外一个区域,那么上一个用 m mark 出来的 Mark 区域就会消失。退出临时选中:esc ,或者选择其他的 Slice 按 m,当前这个 Slice 的选中效果就会消失

  3. shift + m 是持续 Mark 一段区域(如果你不点,他就不会消失),主要是用来长时间 Mark 住一段信息,比如你把一份 Trace 中所有的掉帧点都 Mark 出来,就可以用 shift + m,这样就不会丢失。

    点击小旗子,就可以看到这段区间内的执行信息

  1. 删除持续 Mark

    1. 点击你选中的那个 Slice 的最上面那个三角
    2. 下面选择 Tab:Current Selection
    3. 点击最后边的 Remove ,就可以把他 Remove 掉了
  2. q :隐藏和显示信息 Tab,由于 Perfetto 非常占屏幕,熟练使用 q 键很重要,看的时候快速打开,看完后快速关闭。

  3. 插旗子:Perfetto 还可以通过插旗子的方法来在 Trace 上做标记,Perfetto 支持你把鼠标放到 Trace 最上面,就会出现一个旗子,点击左键即可插一个旗子在上面,方便我们标记某个事件发生,或者某个时间点

  1. 取消插的旗子:与退出持续选中一样,点击旗子,右下角有个 Remove ,点击就可以把这个旗子干掉了,就不插图了

Perfetto 使用技巧

查看唤醒源

我们可以通过查看某一个 Task 的唤醒源,来了解 App 和 Framework 的运转流程,Systrace 和 Perfetto 都可以查看唤醒源,不过 Perfetto 在这方面做的更丝滑一些。

Android Systrace 响应速度实战 3 :响应速度延伸知识 这篇文章中,有讲 Systrace 是如何查看唤醒源的,其实略微还是有些麻烦的。 Perfetto 中查看唤醒源则非常方便且操作很顺滑:

比如我们想看下图中, RenderThread 是被谁唤醒的,我们可以有好几种方法:

点击 Runnable 状态

与 Systrace 操作一样,直接点击 Running 前面的 Runnable,就可以在下面的信息区看到 Related thread states:

  1. Waker:唤醒源
  2. Previous state:这个 Task 的前一个状态
  3. Next state:这个 Task 的后一个状态

点击他上方的 Running 状态,查看连续唤醒信息:

或者我们可以点击 Running 状态,点击小箭头直接跳到对应的 CPU Info 区域,这里可以看到更详细的信息,也可以连续点击 Task,来追踪唤醒源,并可以通过信息区的小箭头来回在 CPU Info 区域和 Task 区域跳转

点击 RenderThread 上方的 Running 状态,通过小箭头跳转到 CPU Info 区域

RenderThread 是被 MainThread 唤醒的

再点击 MainThread 可以看到他是被 SurfaceFlinger 唤醒的,下方信息区还有对应的唤醒延迟分析

查看 Critical Path(Task)

Critical Task 指的是与当前我们选中的 Task 有依赖关系的 Task,比如我们的 Task 是 e,e 要等 d 执行结束后才能执行,d 要等 c,c 要等 b,b 要等 a,那么 e 的 Critical Task 就是 a、b、c、d。

Perfetto 上就可以查看某一个 Task 的 Critical Task,鉴于 Critical path lite 是 Critical path 的子集,我们这里只介绍 Critical path:

点击 Running 状态,然后点击在下面的信息区点击 Critical path

稍等片刻就可以看到我们选择的 MainThread 对应的 Critical path:

放大来看,可以看到我们选择的 MainThread 的边缘,第一个 Critical Task 是唤醒他的 sf 的 app 线程

再往左看 sf 的 app 线程是被 sf 的 TimerDispatch 线程唤醒的,这里就不贴了。

其实可以看到,Perfetto 提供的 Critical Path 其实就是把连续唤醒的 Task 都聚集到一起了,方便我们来看各个 Task 之间的关系。

Pin (固定到最上面)

在每个 Thread 的最左边,有一个图钉一样的按钮,点击之后,这个 Thread 就会被固定到最上面,方便我们把自己最关注的 Thread 放到一起。

比如下面是我 Pin 的从 App 到 SF 的流程图,放到一起的话就会清晰很多,看掉帧的话也会更方便。

CPU Info 区域 Task 高亮

在 CPU Info 区域,鼠标放到某一个 Task 上,就会这个 Task 对应的 Thread 的其他 Task 都会高亮。

我们经常会用这个方法来初步看某些 Thread 的摆核信息

查看 Buffer 消耗情况

App 的 Buffer 消费者是 SurfaceFlinger,通过 App Process 这边的 Actual Timeline 这行,我们可以看到 Buffer 具体是被 SurfaceFlinger 的哪一框消费了。

快速查看 App 执行超时

由于 Android 多 Buffer 机制的存在,App 执行超时不一定会卡顿,但是超时是需要我们去关注的。

通过 Perfetto 提供给的 Expected Timeline 和 Actual Timeline 这两行,可以清楚看到执行超时的地方。

点击 Actial Timeline 红色那一段,底下的信息栏会显示掉帧原因:

在 Perfetto 上查看 Log

在信息栏上切换到 Android Logs 这个 Tab,鼠标放倒某一行上,Perfetto 就会把对应的 Timeline 拉一条直线,可以看到这个 Log 所对应的时间

同样切换到 Ftrace events tab 也可以查看对应的 ftrace 的 event 和对应的时间线

分析 Thread 的 Running 信息

可以通过鼠标左键按住滑动,选中一段区域来进行分析,比如选中 CPU State 这一栏的话,就可以看到这一段时间对应的 Running、Runnable、Sleep、Uninterruptible Sleep 的占比。

这在分析 App 启动的时候经常会用到。

总结

上面分享了 Perfetto 基本的界面和操作,以及分享了一些比较常用的 Perfetto 的技巧。Google 目前在积极推广和维护 Perfetto,很多新功能指不定哪天就蹦出来了,到时候觉得有用我也会更新上来。

至此 Perfetto 基础篇就结束了,后续就是通过 Perfetto 这个工具,来了解 Android 系统运行的基本流程,以及使用 Perfetto 以及 Perfetto SQL 来分析遇到的性能、功耗等问题。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

Android Perfetto 系列 2:Perfetto Trace 抓取

作者 Gracker
2024年5月21日 07:29

上一篇文章 Android Perfetto 系列 1:Perfetto 工具简介 介绍了 Perfetto 是什么,这篇简单介绍一下 Perfetto 的抓取。

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

本系列旨在通过 Perfetto 这个工具,从一个新的视角审视 Android 系统的整体运作方式。此外,它还旨在提供一个不同的角度来学习 App 、 Framework、Linux 等关键模块。尽管你可能已经阅读过许多关于 Android Framework、App 、性能优化的文章,但或许因为难以记住代码或不明白其运行流程,你仍感到困惑。通过 Perfetto 这个图形化工具,你可能会获得更深入的理解。

Perfetto 系列目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

正文

使用 Perfetto 分析问题跟使用 Systrace 分析问题的步骤是一样的:

  1. 首先你需要抓取 Perfetto 文件
  2. ui.perfetto.dev 中打开 Trace 文件进行分析或者使用命令行来进行分析

这篇文章就简单介绍一下使用 Perfetto 抓取 Trace 文件的方法,个人比较推荐使用命令行来抓取,不管是自己配置的命令行还是官方的命令行抓取工具,都非常实用。

1. 使用命令行来抓取 Perfetto(推荐)

基本命令 - adb shell perfetto

对于之前一直用 Systrace 工具的小伙伴来说,命令行抓取 Trace 非常方便。同样,Perfetto 也提供了简单的命令行来抓取,最简单的使用方法与 Systrace 基本一致。你可以直接连到你的 Android 设备上使用/system/bin/perfetto命令来启动跟踪。例如:

1
2
3
4
5
6
7
8
//1. 首先执行命令
adb shell perfetto -o /data/misc/perfetto-traces/trace_file.perfetto-trace -t 20s \
sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

// 2. 操作手机,复现场景,比如滑动或者启动等

// 3. 将 trace 文件 pull 到本地
adb pull /data/misc/perfetto-traces/trace_file.perfetto-trace

这个命令会启动一个 20 秒钟的跟踪,收集指定的数据源信息,并将跟踪文件保存到/data/misc/perfetto-traces/trace_file.perfetto-trace

执行 adb pull 命令把 trace pull 出来,就可以直接在ui.perfetto.dev 上打开了。

进阶命令 adb shell perfetto with config file

这里就是 Perfetto 与 Systrace 不同的地方,Perfetto 可以抓取的信息非常多,其数据来源也非常多,每次都用命令行加一大堆配置的话会很不方便。这时候我们就可以使用一个单独的**配置文件(Config)**,来存储这些信息,每次抓取的时候,指定这个配置文件即可。

对于在 Android 12 之前和之后版本上使用 Perfetto 的配置文件传递,以下是详细的指南和对应的命令行示例。

在 Android 12 及之后的设备上

从 Android 12 开始,可以直接使用/data/misc/perfetto-configs目录来存储配置文件,这样就不需要通过 stdin 来传递配置文件了。具体命令如下:

1
2
adb push config.pbtx /data/misc/perfetto-configs/config.pbtx
adb shell perfetto --txt -c /data/misc/perfetto-configs/config.pbtx -o /data/misc/perfetto-traces/trace.perfetto-trace

在这个例子中,首先将配置文件config.pbtx推送到/data/misc/perfetto-configs目录中。然后,直接在 Perfetto 命令中通过-c选项指定配置文件的路径来启动跟踪。

在 Android 12 之前的设备上

由于 SELinux 的严格规则,直接通过文件路径传递配置文件在非 root 设备上会失败。因此,需要使用标准输入(stdin)来传递配置文件。这可以通过将配置文件的内容cat到 Perfetto 命令中实现。具体命令如下:

1
2
adb push config.pbtx /data/local/tmp/config.pbtx
adb shell 'cat /data/local/tmp/config.pbtx | perfetto -c - -o /data/misc/perfetto-traces/trace.perfetto-trace'

这里,config.pbtx是你的 Perfetto 配置文件,首先使用adb push命令将其推送到设备的临时目录中。然后,使用cat命令将配置文件的内容传递给 Perfetto 命令。

Config 的来源

Config 我建议使用 ui.perfetto.devRecord new trace 这里进行选择定制,然后再保存到本地的文件里面,不同的场景就加载不同的 Config 即可,文章最后一部分有详细讲到这部分,感兴趣的可以看一下。

官方也提供了 share 按钮,你可以把你自己的 config share 给其他人,非常方便。同时我也会建了一个 Github 的库,方便大家在分享(进行中)。

官方代码库也有一些已经配置好的,各位可以下下来自己使用:https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/test/configs/

注意事项

  • 确保 adb 正常:在使用这些命令之前,请确保你的设备已经启用了 USB 调试,并且已经通过adb devices命令确认设备已经正确连接。
  • Ctrl+C 中断: 当使用adb shell perfetto命令时,如果你尝试使用 Ctrl+C 来提前结束跟踪,这个信号不会通过 ADB 传播。如果你需要提前结束跟踪,建议使用一个交互式的 PTY-based session 来运行adb shell
  • SELinux 限制: 在 Android 12 之前的非 root 设备上,由于 SELinux 的严格规则,配置文件只能通过cat config | adb shell perfetto -c -的方式传递(其中-c -表示从标准输入读取配置)。从 Android 12 开始,可以使用/data/misc/perfetto-configs路径来存储配置文件。
  • 在 Android 10 之前的版本, adb 没法直接把 /data/misc/perfetto-traces pull 出来. 你可以使用 adb shell cat /data/misc/perfetto-traces/trace > trace 来替代

2. 使用 Perfetto 提供的官方脚本抓取(强烈推荐)

Perfetto 团队还提供了一个便捷的脚本tools/record_android_trace,它简化了从命令行记录跟踪的流程。这个脚本会自动处理路径问题,完成跟踪后自动拉取跟踪文件,并在浏览器中打开它。本质上这个脚本还是使用的 adb shell perfetto 命令,不过官方帮你封装好了,使用示例:

On Linux and Mac:

1
2
3
4
curl -O https://raw.githubusercontent.com/google/perfetto/master/tools/record_android_trace
chmod u+x record_android_trace
./record_android_trace -o trace_file.perfetto-trace -t 10s -b 64mb \
sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

On Windows:

1
2
3
curl -O https://raw.githubusercontent.com/google/perfetto/master/tools/record_android_trace
python3 record_android_trace -o trace_file.perfetto-trace -t 10s -b 64mb \
sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

同样的,这里也可以通过 -c 来指定配置文件,比如

1
2
3
4
curl -O https://raw.githubusercontent.com/google/perfetto/master/tools/record_android_trace
chmod u+x record_android_trace
./record_android_trace -c config.pbtx -o trace_file.perfetto-trace -t 10s -b 64mb \
sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

这将会记录一个 10 秒的跟踪,并将输出文件保存为trace_file.perfetto-trace

执行后会自动抓取 Trace, 自动在浏览器自动打开,非常方便

脚本内容可以直接访问:https://raw.githubusercontent.com/google/perfetto/master/tools/record_android_trace 来查看,

3. 使用手机上的开发者工具来抓取

当然有时候会没有办法连接到电脑上,或者测试内容不能插 usb,这时候就可以使用 Android 上的自带的系统跟踪应用(System Tracing App)来抓取 Trace。这个应用内置于开发者选项中,可以让你通过几个简单的步骤来配置和启动性能跟踪。

启动系统跟踪应用

  1. 启用开发者选项:首先,确保你的设备已经启用了开发者选项。如果你的设置里面没有开发者选项,你需要在关于手机那里,找到编译编号,然后连续点击 7 次,就可以打开开发者选项。

  2. 打开开发者选项:在设置菜单中找到并打开开发者选项。

  3. 启动系统跟踪:在开发者选项中向下滚动直到找到“系统跟踪(System Trace)”或类似的选项。点击它,将打开系统跟踪应用。

大概长下面这样(每个手机可能界面或者文字会有差异,但是功能是一样的)

系统跟踪应用提供了一系列的配置选项,包括但不限于:

  • 跟踪时长:你可以指定跟踪的持续时间,例如 10 秒或更长时间。
  • 数据源:选择你想要收集数据的来源。这可能包括 CPU、内存、网络等多种不同的数据源。
  • 输出文件位置:指定跟踪文件保存的位置。

启动和停止跟踪

配置好所有需要的参数后,你可以通过点击“录制跟踪记录”按钮来启动跟踪。再次“录制跟踪记录”按钮就可以结束抓取,完成抓取后,通常会有一个提示告诉你抓取已经完成,并提供查看或分享跟踪文件的选项。就可以将跟踪文件导出到电脑上,使用 Perfetto 网页 UI 进行更深入的分析。

4. 使用网页端来抓取

网页端抓取的功能比较迷,很多时候你都会抓取失败,比如连不上 adb、连上之后说你需要执行 kill。所以我更推荐大家使用配置好的命令行来抓取,网页端更适合进行 Config 的配置。

Perfetto 还提供了一个强大的 网页端工具(ui.perfetto.dev),允许开发者通过浏览器配置和启动跟踪。你只需要访问 网站,点击“Record new trace”,然后根据需要选择数据源和配置参数。确保你的设备通过 ADB 连接到电脑,并且在网页端选择“Add ADB device”。之后,点击“Start Recording”即可开始收集跟踪数据。

这里选好你想抓取的信息源之后,可以点击 Recording command 来查看,这里可以看到你选好的 Config 的具体内容,你可以分享或者保存到本地的文件里面,用命令行抓取的时候使用。

选取 Config 的时候,Android apps 那一栏里面的 Atrace userspace annotations、Event log (logcat)、Frame timeline 建议都选上(command + a)

另外如果想看调用栈,可以把 Stack Samples 这里的 Callstack sampling 勾选上(注意需要最新版本的 Android 才可以,而且所 debug 的进程得是 debugable 的)

至于其他的有啥用,可以自己探索,后续的 Perfetto 也会介绍到每个部分和他在 Trace 上的呈现,帮助大家更快入手 Perfetto。

从网页端提取参数

前面提到网页端的可以图形化选择 Config 这个很方便,选好之后,点击 Recording command 这里,就可以看到已经选好的 Config,你在保存的时候记得把下面这几行去掉就可以了

参考文档

  1. https://perfetto.dev/docs/quickstart/android-tracing
  2. https://perfetto.dev/docs/concepts/config
  3. https://developer.android.com/tools/releases/platform-tools?hl=zh-cn
  4. https://mp.weixin.qq.com/s/nsqc51L5T4mrTUVsPgkj6A
  5. https://juejin.cn/post/7344983784549400613
  6. https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/test/configs/

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

Android Perfetto 系列 1:Perfetto 工具简介

作者 Gracker
2024年5月21日 07:29

本篇是 Perfetto 系列文章的第一篇,主要是简单介绍 Perfetto 工具,包括 Perfetto 的历史、发展,以及 Perfetto 能做什么。

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

本系列旨在通过 Perfetto 这个工具,从一个新的视角审视 Android 系统的整体运作方式。此外,它还旨在提供一个不同的角度来学习 App 、 Framework、Linux 等关键模块。尽管你可能已经阅读过许多关于 Android Framework、App 、性能优化的文章,但或许因为难以记住代码或不明白其运行流程,你仍感到困惑。通过 Perfetto 这个图形化工具,你可能会获得更深入的理解。

Perfetto 系列目录

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View

如果大家还没看过 Systrace 系列,下面是传送门:

  1. Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
  2. 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

正文

2019 年开始写 Systrace 系列,陆陆续续写了 20 多篇,从基本使用到各个模块在 Systrace 上的呈现,再到启动速度、流畅性等实战,基本上可以满足初级系统开发者和 App 开发者对于 Systrace 工具的需求。通过博客也加了不少志同道合的小伙伴,光交流群就建了有 6 个。这里非常感谢大家的支持。

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

所以就有了这个系列,我也有在星球里面写了几条为什么要更新 Perfetto 系列的原因(之前一直觉得 Systrace 系列就够了):

  1. 目前 Oppo、Vivo 这些手机厂商内部,都已经切换成 Perfetto 了,不管是抓 Trace 还是看 Trace,都在使用 Perfetto ,很多新人接触的都是 Perfetto 而不是 Systrace ,守着之前的老 Systrace 系列会流失这部分读者
  2. 之前的 Systrace 系列,对应的 Code 已经比较老了,全新的 Perfetto 系列可以使用 Android 14 的 Code 来进行更新
  3. 个人对 Perfetto 的使用也没有很深入,有些高阶功能目前还只是浅尝辄止。可以通过重写 Perfetto 系列来进行这部分内容的强化
  4. Perfetto 是个很强大的工具,他的背后是整个 Android + Linux 系统,所以在写这个系列的时候,应该是以他背后的这个 Android + Linux 为主,而不是仅仅局限于 Perfetto 这个工具。工具只是我们观测 Android + Linux 的方式,理解整个 Android 系统运行的规律,思考其运行的原理,通过工具挖掘问题,通过问题思考本质,这才是对开发者来说有意义的
  5. 很多 Android 系统运行相关的内容,Perfetto 的官方文档还是没有讲,这部分我这边可以作为补足;另外官方文档是英文版本的,中文博客可以补充这方面。
  6. Perfetto 可以拿到 Google Dev Fest 上作为演讲内容~。

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

Perfetto

性能分析为什么需要上帝视角

在介绍 Perfetto 之前,我们需要了解为什么性能分析需要 Systrace 和 Perfetto 这样的工具:以 Android 系统为例,影响性能的因素是非常多的:App 自身质量、系统各个模块的性能、Linux 的性能、硬件性能,再加上各个厂商的策略、厂商定制的功能、系统自身的负载、低内存、发热、Android 各个版本的差异、用户的使用习惯等。这都不是通过分析某一个 App 或者某一个模块就能知道原因的,我们需要一个上帝视角,从更高的纬度来看 Android 系统的运行情况。

而 Perfetto 工具就提供了这样的一个上帝视角,通过上帝视角我们可以看到 Android 系统在运行时的各个细节,比如

  1. Input 事件是怎么流转的
  2. 你正在使用的 App 的每一帧是怎么从 App 产生到上屏的
  3. CPU 的实时频率、负载、摆核、唤醒等
  4. 系统中的各个 App 是怎么运行的
  5. ….

App 开发者和 Android 系统开发者也都会在重要的代码逻辑处加上 Trace 点,打开部分 Debug 选项之后,更是可以得到非常详细的信息,甚至一个 Task 为什么摆在某个 cpu 上,都会有详细的记载。通过这些在 Perfetto 上所展示的信息,我们能初步分析到性能问题的原因,接下来继续分析就会有针对性。

同样为了说明性能优化的复杂性,可以看看 <性能之巅**> 这本书中对于性能的描述,具体来说就是方法论,非常贴合本文的主题,也强烈推荐各位搞性能优化的同学,把这本书作为手头常读的方法论书籍:系统性能工程是一个充满挑战的领域,具体原因有很多,其中包括以下事实,系统性能是主观的、复杂的,而且常常是多问题并存的**

性能是主观的

  1. 技术学科往往是客观的,太多的业界人士审视问题非黑即白。在进行软件故障查找的时候,判断 bug 是否存在或 bug 是否修复就是这样。bug 的出现总是伴随着错误信息,错误信息通常容易解读,进而你就明白错误为什么会出现了
  2. 与此不同,性能常常是主观性的。开始着手性能问题的时候,对问题是否存在的判断都有可能是模糊的,在问题被修复的时候也同样,被一个用户认为是“不好”的性能,另一个用户可能认为是“好”的

系统是复杂的

  1. 除了主观性之外,性能工程作为一门充满了挑战的学科,除了因为系统的复杂性,还因为对于性能,我们常常缺少一个明确的分析起点。有时我们只是从猜测开始,比如,责怪网络,而性能分析必须对这是不是一个正确的方向做出判断
  2. 性能问题可能出在子系统之间复杂的互联上,即便这些子系统隔离时表现得都很好。也可能由于连锁故障(cascading failure)出现性能问题,这指的是一个出现故障的组件会导致其他组件产生性能问题。要理解这些产生的问题,你必须理清组件之间的关系,还要了解它们是怎样协作的
  3. 瓶颈往往是复杂的,还会以意想不到的方式互相联系。修复了一个问题可能只是把瓶颈推向了系统里的其他地方,导致系统的整体性能并没有得到期望的提升。
  4. 除了系统的复杂性之外,生产环境负载的复杂特性也可能会导致性能问题。在实验室环境很难重现这类情况,或者只能间歇式地重现
  5. 解决复杂的性能问题常常需要全局性的方法。整个系统——包括自身内部和外部的交互——都可能需要被调查研究。这项工作要求有非常广泛的技能,一般不太可能集中在一人身上,这促使性能工程成为一门多变的并且充满智力挑战的工作

可能有多个问题并存

  1. 找到一个性能问题点往往并不是问题本身,在复杂的软件中通常会有多个问题
  2. 性能分析的又一个难点:真正的任务不是寻找问题,而是辨别问题或者说是辨别哪些问题是最重要的
  3. 要做到这一点,性能分析必须量化(quantify)问题的重要程度。某些性能问题可能并不适用于你的工作负载或者只在非常小的程度上适用。理想情况下,你不仅要量化问题,还要估计每个问题修复后能带来的增速。当管理层审查工程或运维资源的开销缘由时,这类信息尤其有用。
  4. 有一个指标非常适合用来量化性能,那就是 延时(latency)

Perfetto 介绍

Perfetto 是一个高级的开源工具,专为性能监测和分析而设计。它配备了一整套服务和库,能够捕获和记录系统层面以及应用程序层面的活动数据。此外,Perfetto 还提供了内存分析工具,既适用于本地应用也适用于 Java 环境。它的一个强大功能是,可以通过 SQL 查询库来分析跟踪数据,让你能够深入理解性能数据背后的细节。为了更好地处理和理解大规模数据集,Perfetto 还提供了一个基于 Web 的用户界面,使你能够直观地可视化和探索多 GB 大小的跟踪文件。简而言之,Perfetto 是一个全面的解决方案,旨在帮助开发者和性能工程师以前所未有的深度和清晰度来分析和优化软件性能。

谷歌在 2017 年开始了第一笔提交,随后的 6 年(截止到 2024)内总共有 100 多位开发者提交了近 3.7W 笔提交,几乎每天都有 PR 与 Merge 操作,是一个相当活跃的项目。 除了功能强大之外其野心也非常大,官网上号称它是下一代面向可跨平台的 Trace/Metric 数据抓取与分析工具。应用也比较广泛,除了 Perfetto 网站,Windows Performance Tool 与 Android Studio,以及华为的 GraphicProfiler 也支持 Perfetto 数据的可视化与分析。 我们相信谷歌还会持续投入资源到 Perfetto 项目,可以说它应该就是下一代性能分析工具了,会完全取代 Systrace。

如果你已经习惯使用 Systrace,那么切换到 Perfetto 会非常顺滑,因为 Perfetto 是完全兼容 Systrace 的。你之前抓的 Systrace 文件,可以直接扔到 Perfetto Viewer 网站里面直接打开。如果你还没有适应 Perfetto ,你也可以从 Perfetto Viewer 一键打开 Systrace。

下图是 Perfetto 的架构图,可以看到 Perfetto 包含了三大块:

  1. Record traces :即数据抓取模块,可以看到抓取的内容和来源非常丰富,Java、 Native 、Linux 都有涉及到,相比 Systrace 要丰富很多。
  2. Analyze traces :主要是 trace 分析模块,包括 Trace 解析、SQL 查询、Metrics 分析等,这部分有专门的命令行工具提供,方便大家直接调用或者在工具链里面去调用。
  3. Visualize Traces:Trace 的呈现、抓取等

这几个模块在后续的系列文章中都会详细介绍

Perfetto 的核心优势和功能亮点:

通过长时间的使用和对比,以及看各种分享,总结了一下 Perfetto 的核心优势和功能两点

  1. 支持长时间数据抓取

    • Perfetto 通过后台服务支持长时间数据抓取,利用 Protobuf 编码存储数据。
  2. 数据来源与兼容性

    • 基于 Linux 内核的 Ftrace 机制,记录用户空间与内核空间的关键事件。
    • 兼容 Systrace 的功能,并有望最终取代它。
  3. 全面的数据支持

    • 支持 Trace、Metric 和 Log 类型的数据。
    • 提供多种数据抓取方式,包括网页、命令行工具、开发者选项以及 Perfetto Trigger API。
  4. 高效的数据分析

    • 提供数据可视化网页,支持大文件渲染,优于 Systrace。
    • Trace 文件可转换为 SQLite 数据库文件,支持 SQL 查询和脚本执行。
    • 提供 Python API,允许将数据导出为 DataFrame 格式,为深入分析提供便利。
    • 支持函数调用堆栈展示。
    • 支持内存堆栈展示。
    • 支持 pin 住你感兴趣的行到最上面,不用一直上下翻(通过脚本可以一打开就自动 pin)
    • 支持可视化 Binder 调用和跳转
    • 支持很方便的查询唤醒源
    • 支持 Critical Task 的可视化查询
  5. Google 的持续更新

    • Google 的工具团队在持续更新 Perfetto,版本 Release 和 Bugfix 都非常及时,可以在 Github 上观察。

这里专门提一下 SQL,Perfetto 可以使用 SQL 这是一个巨大的改进,他在解析 Trace 文件的时候,会内建许多 SQL 表和图,方便使用 SQL 语句进行查询,比如下面这几个查询,就是非常实用的(图来自内核工匠)。

另外他的官方文档里面,介绍到对应的部分,也会把对应的 SQL 以及查询结果示例贴出来。

有了这个,就再也不怕老板说你没有数据了,分分钟 SQL 查出来,表格转图标,一份高质量的 Report 就出来了:优化前后,xxx 指标下降了 xx%,属实是非常方便的。

相比 Systrace 没那么好用的地方

Vsync-App 没那么直观

Vsync-App 在 Perfetto 相对来说没那么直观,比如你看习惯了 Systrace 中 Vsync-App 那条贯穿整个 Trace 的竖线,你再看 Perfetto 就没有这个,就会觉得怪怪的:

Perfetto 中,你可以把 Vsync-App Pin 到最上面来看 Vsync 信息

Systrace 中,Vsync 以竖线的方式贯穿整个 Trace,很容易辨别:

当然 Perfetto 取消这个也是有理由的:Vsync-App 其实并不能说明 App 有性能问题,Perfetto 使用了另外一个方式来展示,如果你用 Perfetto 命令抓的 Trace,就会有下面这个信息,记录了 App 一帧的 Expected Timeli 和 Actual Timeline 。相比 Vsync-App,这两指标更能说明问题原文档

  1. 预期时间线每个切片代表应用程序用于渲染帧的时间。为了避免系统出现卡顿,应用程序需要在这个时间范围内完成。开始时间是调度 Choreographer 回调的时间。
  2. 实际时间线这些切片代表了应用程序完成帧的实际时间(包括 GPU 工作)并将其发送到 SurfaceFlinger 进行合成的时间。开始时间是应用程序开始运行的时间。这里的切片的结束时间代表的是。后处理时间是应用程序的帧被发布到 SurfaceFlinger 的时间。

通过看 Expected Timeli 和 Actual Timeline 的差异,我们可以很快速定位到卡顿的点(红色标识的 Actual Timeline 那一帧就是卡顿)

其计算方式如下,看了图你就知道为什么这两个是更准确的(包含了 GPU 执行时间)

相对应的,SurfaceFlinger 也有这两个指标

折叠功能比较烂,比较废屏幕

如果你是普通的宽屏,打开 Perfetto 随便 Pin 几个关键线程到最上面,你下面的可操作空间就很小了,如果碰到某个关键线程堆栈比较长,那就更是顶级折磨了,而且他这个堆栈还不能折叠(Systrace 可以)

解决办法:

  1. 少 Pin 几个关键线程 (那还有啥乐趣)
  2. 把显示器竖起来(宽度就打折了)

最终我们找到了完美的方案:换成 LG 那个魔方屏幕,16:18 ,看 Perfetto 简直是绝配(办公室已经被我安利有三台了)

28MQ780

  1. 不差钱:LG 28MQ780 - 3599
  2. 平替:[联合创新 28C1Q - 2999](https://item.jd.com/100058985199.html)

参考文档

  1. Perfetto Github 库
  2. Perfetto 官方文档
  3. 内核工匠 - Perfetto 进阶

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

Android Perfetto 系列目录

作者 Gracker
2024年3月27日 22:29

随着 Google 宣布 Systrace 工具停更,推出 Perfetto 工具,Perfetto 在我的日常工作中已经基本能取代 Systrace 工具。同时 Oppo、Vivo 等大厂也已经把 Systrace 切换成了 Perfetto,许多新接触 Android 性能优化的小伙伴对于 Perfetto 那眼花缭乱的界面和复杂的功能感觉头疼,希望我能把之前的那些 Systrace 文章使用 Perfetto 来呈现。

所以就有了这个系列,我也有在星球里面写了几条为什么要更新 Perfetto 系列的原因(之前一直觉得 Systrace 系列就够了):

  1. 目前 Oppo、Vivo 这些手机厂商内部,都已经切换成 Perfetto 了,不管是抓 Trace 还是看 Trace,都在使用 Perfetto ,很多新人接触的都是 Perfetto 而不是 Systrace ,守着之前的老 Systrace 系列会流失这部分读者
  2. 之前的 Systrace 系列,对应的 Code 已经比较老了,全新的 Perfetto 系列可以使用 Android 14 的 Code 来进行更新
  3. 个人对 Perfetto 的使用也没有很深入,有些高阶功能目前还只是浅尝辄止。可以通过重写 Perfetto 系列来进行这部分内容的强化
  4. Perfetto 是个很强大的工具,他的背后是整个 Android + Linux 系统,所以在写这个系列的时候,应该是以他背后的这个 Android + Linux 为主,而不是仅仅局限于 Perfetto 这个工具。工具只是我们观测 Android + Linux 的方式,理解整个 Android 系统运行的规律,思考其运行的原理,通过工具挖掘问题,通过问题思考本质,这才是对开发者来说有意义的
  5. 很多 Android 系统运行相关的内容,Perfetto 的官方文档还是没有讲,这部分我这边可以作为补足;另外官方文档是英文版本的,中文博客可以补充这方面。
  6. Perfetto 系列写好了,可以拿到 Google Dev Fest 上作为演讲内容~。

Paul Graham 说:要么给大部分人提供有点想要的东西,要么给小部分人提供非常想要的东西。Perfetto 其实就是小部分人非常想要的东西,那就开始写吧,欢迎大家多多交流和沟通,发现错误和描述不准确的地方请及时告知我,我会及时修改,以免误人子弟。

本系列旨在通过 Perfetto 这个工具,从一个新的视角审视 Android 系统的整体运作方式。此外,它还旨在提供一个不同的角度来学习 App 、 Framework、Linux 等关键模块。尽管你可能已经阅读过许多关于 Android Framework、App 、性能优化的文章,但或许因为难以记住代码或不明白其运行流程,你仍感到困惑。通过 Perfetto 这个图形化工具,你可能会获得更深入的理解。

Perfetto 系列目录

  1. Android Perfetto 系列 1:Perfetto 工具简介
  2. Android Perfetto 系列 2:Perfetto Trace 抓取
  3. Android Perfetto 系列 3:熟悉 Perfetto View
  4. 待更新

欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容

Systrace 系列

另外 Systrace 工具尽管已经不更新了,但是之前的 Systrace 系列文章,内容依然没有过时,还是有很多公司在使用 Systrace 来分析各种系统问题,Systrace 工具是分析 Android 性能问题的利器,它可以从一个图形的角度,来展现整机的运行情况。Systrace 工具不仅可以分析性能问题,用它来进行 Framework 的学习也是很好的。

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - Input 解读
  6. Systrace 基础知识 - Vsync 产生与工作机制解读
  7. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  8. Systrace 基础知识 - MainThread 和 RenderThread 解读
  9. Systrace 基础知识 - Binder 和锁竞争解读
  10. Systrace 基础知识 - Triple Buffer 解读
  11. Systrace 基础知识 - CPU Info 解读
  12. Systrace 基础知识 - SystemServer 解读
  13. Systrace 基础知识 - SurfaceFlinger 解读
  14. Systrace 流畅性实战 1 :了解卡顿原理
  15. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  16. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  17. Systrace 响应速度实战 1 :了解响应速度原理
  18. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  19. Systrace 响应速度实战 3 :响应速度延伸知识
  20. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  22. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

当 App 有了系统权限,真的可以为所欲为?

作者 Gracker
2023年5月14日 20:15

前一段时间有个 App 很火,是 Android App 利用了 Android 系统漏洞,获得了系统权限,做了很多事情。想看看这些个 App 在利用系统漏洞获取系统权限之后,都干了什么事,于是就有了这篇文章。由于准备仓促,有些 Code 没有仔细看,感兴趣的同学可以自己去研究研究,多多讨论,对应的文章和 Code 链接都在下面:

  1. 深蓝洞察:2022 年度最 “不可赦” 漏洞
  2. XXX apk 内嵌提权代码,及动态下发 dex 分析
  3. Android 反序列化漏洞攻防史话

关于这个 App 是如何获取这个系统权限的,Android 反序列化漏洞攻防史话,这篇文章讲的很清楚,就不再赘述了,我也不是安全方面的专家,但是建议大家多读几遍这篇文章

序列化和反序列化是指将内存数据结构转换为字节流,通过网络传输或者保存到磁盘,然后再将字节流恢复为内存对象的过程。在 Web 安全领域,出现过很多反序列化漏洞,比如 PHP 反序列化、Java 反序列化等。由于在反序列化的过程中触发了非预期的程序逻辑,从而被攻击者用精心构造的字节流触发并利用漏洞从而最终实现任意代码执行等目的。

这篇文章主要来看看 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个库里面提供的 Dex ,看看 App 到底想知道用户的什么信息?总的来说,App 获取系统权限之后,主要做了下面几件事(正常 App 无法或者很难做到的事情),各种不把用户当人了。

  1. 自启动、关联启动相关的修改,偷偷打开或者默认打开:与手机厂商斗智斗勇。
  2. 开启通知权限。
  3. 监听通知内容。
  4. 获取用户的使用手机的信息,包括安装的 App、使用时长、用户 ID、用户名等。
  5. 修改系统设置。
  6. 整一些系统权限的工具方便自己使用。

另外也可以看到,这个 App 对于各个手机厂商的研究还是比较深入的,针对华为、Oppo、Vivo、Xiaomi 等终端厂商都有专门的处理,这个也是值得手机厂商去反向研究和防御的。

最好我还加上了这篇文章在微信公众号发出去之后的用户评论,以及知乎回答的评论区(问题已经被删了,但是我可以看到:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317,目前为止是 2471 个赞)可以说是脑洞大开(关于 App 如何作恶)。

0. Dex 文件信息

本文所研究的 dex 文件是从 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个仓库获取的,Dex 文件总共有 37 个,不多,也不大,慢慢看。这些文件会通过后台服务器动态下发,然后在 App 启动的时候进行动态加载,可以说是隐蔽的很,然而 Android 毕竟是开源软件,要抓你个 App 的行为还是很简单的,这些 Dex 就是被抓包抓出来的,可以说是人脏货俱全了。

由于是 dex 文件,所以直接使用 https://github.com/tp7309/TTDeDroid 这个库的反编译工具打开看即可,比如我配置好之后,直接使用 showjar 这个命令就可以

showjar 95cd95ab4d694ad8bdf49f07e3599fb3.dex

默认是用 jadx 打开,就可以看到反编译之后的内容,我们重点看 Executor 里面的代码逻辑即可

打开后可以看到具体的功能逻辑,可以看到一个 dex 一般只干一件事,那我们重点看这件事的核心实现部分即可

1. 通知监听和通知权限相关

1.1 获取 Xiaomi 手机通知内容

  1. 文件 : 95cd95ab4d694ad8bdf49f07e3599fb3.dex
  2. 功能 :获取用户的 Active 通知
  3. 类名 :com.google.android.sd.biz_dynamic_dex.xm_ntf_info.XMGetNtfInfoExecutor

1. 反射拿到 ServiceManager

一般我们会通过 ServiceManager 的 getService 方法获取系统的 Service,然后进行远程调用

2. 通过 NotificationManagerService 获取通知的详细内容

通过 getService 传入 NotificationManagerService 获取 NotificationManager 之后,就可以调用 getActiveNotifications 这个方法了,然后具体拿到 Notification 的下面几个字段

  1. 通知的 Title
  2. 发生通知的 App 的包名
  3. 通知发送时间
  4. key
  5. channelID :the id of the channel this notification posts to.

可能有人不知道这玩意是啥,下面这个图里面就是一个典型的通知

其代码如下

可以看到 getActiveNotifications 这个方法,是 System-only 的,普通的 App 是不能随便读取 Notification 的,但是这个 App 由于有权限,就可以获取

当然微信的防撤回插件使用的一般是另外一种方法,比如辅助服务,这玩意是合规的,但是还是推荐大家能不用就不用,它能帮你防撤回,他就能获取通知的内容,包括你知道的和不知道的

1.2. 打开 Xiaomi 手机上的通知权限(Push)

  1. 文件 :0fc0e98ac2e54bc29401efaddfc8ad7f.dex
  2. 功能 :可能有的时候小米用户会把 App 的通知给关掉,App 想知道这个用户是不是把通知关了,如果关了就偷偷打开
  3. 类名 :com.google.android.sd.biz_dynamic_dex.xm_permission.XMPermissionExecutor

这么看来这个应该还是蛮实用的,你个调皮的用户,我发通知都是为了你好,你怎么忍心把我关掉呢?让我帮你偷偷打开吧

App 调用 NotificationManagerService 的 setNotificationsEnabledForPackage 来设置通知,可以强制打开通知
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

然后查看 NotificationManagerService 的 setNotificationsEnabledForPackage 这个方法,就是查看用户是不是打开成功了
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

还有针对 leb 的单独处理~ 细 !

1.3. 打开 Vivo 机器上的通知权限(Push)

  1. 文件 :2eb20dc580aaa5186ee4a4ceb2374669.dex
  2. 功能 :Vivo 用户会把 App 的通知给关掉,这样在 Vivo 手机上 App 就收不到通知了,那不行,得偷偷打开
  3. 类名 :com.google.android.sd.biz_dynamic_dex.vivo_open_push.VivoOpenPushExecutor

核心和上面那个是一样的,只不过这个是专门针对 vivo 手机的

1.4 打开 Oppo 手机的通知权限

  1. 文件 :67c9e686004f45158e94002e8e781192.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.oppo_notification_ut.OppoNotificationUTExecutor

没有反编译出来,看大概的逻辑应该是打开 App 在 oppo 手机上的通知权限

1.5 Notification 监听

  1. 文件 :ab8ed4c3482c42a1b8baef558ee79deb.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.ud_notification_listener.UdNotificationListenerExecutor

这个就有点厉害了,在监听 App 的 Notification 的发送,然后进行统计

监听的核心代码

这个咱也不是很懂,是时候跟做了多年 SystemUI 和 Launcher 的老婆求助了....@史工

1.6 App Notification 监听

  1. 文件 :4f260398-e9d1-4390-bbb9-eeb49c07bf3c.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.notification_listener.NotificationListenerExecutor

上面那个是 UdNotificationListenerExecutor , 这个是 NotificationListenerExecutor,UD 是啥?

这个反射调用的 setNotificationListenerAccessGranted 是个 SystemAPI,获得通知的使用权,果然有权限就可以为所欲为

1.7 打开华为手机的通知监听权限

  1. 文件 :a3937709-b9cc-48fd-8918-163c9cb7c2df.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_notification_listener.HWNotificationListenerExecutor

华为也无法幸免,哈哈哈

1.8 打开华为手机通知权限

  1. 文件 :257682c986ab449ab9e7c8ae7682fa61.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_permission.HwPermissionExecutor

2. Backup 状态

2.1. 鸿蒙 OS 上 App Backup 状态相关,保活用?

  1. 文件 :6932a923-9f13-4624-bfea-1249ddfd5505.dex
  2. 功能 :Backup 相关

这个看了半天,应该是专门针对华为手机的,收到 IBackupSessionCallback 回调后,执行 PackageManagerEx.startBackupSession 方法

查了下这个方法的作用,启动备份或恢复会话

2.2. Vivo 手机 Backup 状态相关

  1. 文件 :8c34f5dc-f04c-40ba-98d4-7aa7c364b65c.dex
  2. 功能 :Backup 相关

3. 文件相关

3.1 获取华为手机 SLog 和 SharedPreferences 内容

  1. 文件 : da03be2689cc463f901806b5b417c9f5.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_get_input.HwGetInputExecutor

拿这个干嘛呢?拿去做数据分析?

获取 SharedPreferences

获取 slog

4. 用户数据

4.1 获取用户使用手机的数据

  1. 文件 : 35604479f8854b5d90bc800e912034fc.dex
  2. 功能 :看名字就知道是获取用户的使用手机的数据
  3. 类名 :com.google.android.sd.biz_dynamic_dex.usage_event_all.UsageEventAllExecutor

看核心逻辑是同 usagestates 服务,来获取用户使用手机的数据,难怪我手机安装了什么 App、用了多久这些,其他 App 了如指掌

那么他可以拿到哪些数据呢?应有尽有~,包括但不限于 App 启动、退出、挂起、Service 变化、Configuration 变化、亮灭屏、开关机等,感兴趣的可以看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
frameworks/base/core/java/android/app/usage/UsageEvents.java
private static String eventToString(int eventType) {
switch (eventType) {
case Event.NONE:
return "NONE";
case Event.ACTIVITY_PAUSED:
return "ACTIVITY_PAUSED";
case Event.ACTIVITY_RESUMED:
return "ACTIVITY_RESUMED";
case Event.FOREGROUND_SERVICE_START:
return "FOREGROUND_SERVICE_START";
case Event.FOREGROUND_SERVICE_STOP:
return "FOREGROUND_SERVICE_STOP";
case Event.ACTIVITY_STOPPED:
return "ACTIVITY_STOPPED";
case Event.END_OF_DAY:
return "END_OF_DAY";
case Event.ROLLOVER_FOREGROUND_SERVICE:
return "ROLLOVER_FOREGROUND_SERVICE";
case Event.CONTINUE_PREVIOUS_DAY:
return "CONTINUE_PREVIOUS_DAY";
case Event.CONTINUING_FOREGROUND_SERVICE:
return "CONTINUING_FOREGROUND_SERVICE";
case Event.CONFIGURATION_CHANGE:
return "CONFIGURATION_CHANGE";
case Event.SYSTEM_INTERACTION:
return "SYSTEM_INTERACTION";
case Event.USER_INTERACTION:
return "USER_INTERACTION";
case Event.SHORTCUT_INVOCATION:
return "SHORTCUT_INVOCATION";
case Event.CHOOSER_ACTION:
return "CHOOSER_ACTION";
case Event.NOTIFICATION_SEEN:
return "NOTIFICATION_SEEN";
case Event.STANDBY_BUCKET_CHANGED:
return "STANDBY_BUCKET_CHANGED";
case Event.NOTIFICATION_INTERRUPTION:
return "NOTIFICATION_INTERRUPTION";
case Event.SLICE_PINNED:
return "SLICE_PINNED";
case Event.SLICE_PINNED_PRIV:
return "SLICE_PINNED_PRIV";
case Event.SCREEN_INTERACTIVE:
return "SCREEN_INTERACTIVE";
case Event.SCREEN_NON_INTERACTIVE:
return "SCREEN_NON_INTERACTIVE";
case Event.KEYGUARD_SHOWN:
return "KEYGUARD_SHOWN";
case Event.KEYGUARD_HIDDEN:
return "KEYGUARD_HIDDEN";
case Event.DEVICE_SHUTDOWN:
return "DEVICE_SHUTDOWN";
case Event.DEVICE_STARTUP:
return "DEVICE_STARTUP";
case Event.USER_UNLOCKED:
return "USER_UNLOCKED";
case Event.USER_STOPPED:
return "USER_STOPPED";
case Event.LOCUS_ID_SET:
return "LOCUS_ID_SET";
case Event.APP_COMPONENT_USED:
return "APP_COMPONENT_USED";
default:
return "UNKNOWN_TYPE_" + eventType;
}
}

4.2 获取用户使用数据

  1. 文件:b50477f70bd14479a50e6fa34e18b2a0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.usage_event.UsageEventExecutor

上面那个是 UsageEventAllExecutor,这个是 UsageEventExecutor,主要拿用户使用 App 相关的数据,比如什么时候打开某个 App、什么时候关闭某个 App,6 得很,真毒瘤

4.3 获取用户使用数据

  1. 文件:1a68d982e02fc22b464693a06f528fac.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.app_usage_observer.AppUsageObserver

看样子是注册了 App Usage 的权限,具体 Code 没有出来,不好分析

5. Widget 和 icon 相关

经吃瓜群众提醒,App 可以通过 Widget 伪造一个 icon,用户在长按图标卸载这个 App 的时候,你以为卸载了,其实是把他伪造的这个 Widget 给删除了,真正的 App 还在 (不过我没有遇到过,这么搞真的是脑洞大开,且不把 Android 用户当人)

5.1. Vivo 手机添加 Widget

  1. 文件:f9b6b139-4516-4ac2-896d-8bc3eb1f2d03.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_widget.VivoAddWidgetExecutor

这个比较好理解,在 Vivo 手机上加个 Widget

5.2 获取 icon 相关的信息

  1. 文件:da60112a4b2848adba2ac11f412cccc7.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.get_icon_info.GetIconInfoExecutor

这个好理解,获取 icon 相关的信息,比如在 Launcher 的哪一行,哪一列,是否在文件夹里面。问题是获取这玩意干嘛???迷

5.3 Oppo 手机添加 Widget

  1. 文件:75dcc8ea-d0f9-4222-b8dd-2a83444f9cd6.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppoaddwidget.OppoAddWidgetExecutor

5.4 Xiaomi 手机更新图标?

  1. 文件:5d372522-b6a4-4c1b-a0b4-8114d342e6c0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.xm_akasha.XmAkashaExecutor

小米手机上的桌面 icon 、shorcut 相关的操作,小米的同学来认领

6. 自启动、关联启动、保活相关

6.1 打开 Oppo 手机自启动

  1. 文件:e723d560-c2ee-461e-b2a1-96f85b614f2b.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_boot_perm.OppoBootPermExecutor

看下面这一堆就知道是和自启动相关的,看来自启动权限是每个 App 都蛋疼的东西啊

6.2 打开 Vivo 关联启动权限

  1. 文件:8b56d820-cac2-4ca0-8a3a-1083c5cca7ae.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_association_start.VivoAssociationStartExecutor

看名字就是和关联启动相关的权限,vivo 的同学来领了

直接写了个节点进去

6.3 关闭华为耗电精灵

  1. 文件:7c6e6702-e461-4315-8631-eee246aeba95.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.hw_hide_power_window.HidePowerWindowExecutor

看名字和实现,应该是和华为的耗电精灵有关系,华为的同学可以来看看

6.4 Vivo 机型保活相关

  1. 文件:7877ec6850344e7aad5fdd57f6abf238.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_get_loc.VivoGetLocExecutor

猜测和保活相关,Vivo 的同学可以来认领一下

7. 安装卸载相关

7.1 Vivo 手机回滚卸载

  1. 文件:d643e0f9a68342bc8403a69e7ee877a7.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_rollback_uninstall.VivoRollbackUninstallExecutor

这个看上去像是用户卸载 App 之后,回滚到预置的版本,好吧,这个是常规操作

7.2 Vivo 手机 App 卸载

  1. 文件:be7a2b643d7e8543f49994ffeb0ee0b6.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_official_uninstall.OfficialUntiUninstallV3

看名字和实现,也是和卸载回滚相关的

7.3 Vivo 手机 App 卸载相关

  1. 文件:183bb87aa7d744a195741ce524577dd0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_official_uninstall.VivoOfficialUninstallExecutor

同上

其他

SyncExecutor

  1. 文件:f4247da0-6274-44eb-859a-b4c35ec0dd71.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.sync.SyncExecutor

没看懂是干嘛的,核心应该是 Utils.updateSid ,但是没看到实现的地方

UdParseNotifyMessageExecutor

  1. 文件:f35735a5cbf445c785237797138d246a.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.ud_parse_nmessage.UdParseNotifyMessageExecutor

看名字应该是解析从远端传来的 Notify Message,具体功能未知

6.3 TDLogcatExecutor

  1. 文件
    1. 8aeb045fad9343acbbd1a26998b6485a.dex
    2. 2aa151e2cfa04acb8fb96e523807ca6b.dex
  2. 类名
    1. com.google.android.sd.biz_dynamic_dex.td.logcat.TDLogcatExecutor
    2. com.google.android.sd.biz_dynamic_dex.td.logcat.TDLogcatExecutor

没太看懂这个是干嘛的,像是保活又不像,后面有时间了再慢慢分析

6.4 QueryLBSInfoExecutor

  1. 文件:74168acd-14b4-4ff8-842e-f92b794d7abf.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.query_lbs_info.QueryLBSInfoExecutor

获取 LBS Info

6.5 WriteSettingsExecutor

  1. 文件:6afc90e406bf46e4a29956aabcdfe004.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.write_settings.WriteSettingsExecutor

看名字应该是个工具类,写 Settings 字段的,至于些什么应该是动态下发的

6.6 OppoSettingExecutor

  1. 文件:61517b68-7c09-4021-9aaa-cdebeb9549f2.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.opposettingproxy.OppoSettingExecutor

Setting 代理??没看懂干嘛的,Oppo 的同学来认领,难道是另外一种形式的保活?

6.7 CheckAsterExecutor

  1. 文件:561341f5f7976e13efce7491887f1306.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.check_aster.CheckAsterExecutor

Check aster ?不是很懂

6.8 OppoCommunityIdExecutor

  1. 文件:538278f3-9f68-4fce-be10-12635b9640b2.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_community_id.OppoCommunityIdExecutor

获取 Oppo 用户的 ID?要这玩意干么?

6.9 GetSettingsUsernameExecutor

  1. 文件:4569a29c-b5a8-4dcf-a3a6-0a2f0bfdd493.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_get_settings_username.GetSettingsUsernameExecutor

获取 Oppo 手机用户的 username,话说你要这个啥用咧?

6.10 LogcatExecutor

  1. 文件:218a37ea-710d-49cb-b872-2a47a1115c69.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.logcat.LogcatExecutor

配置 Log 的参数

6.11 VivoBrowserSettingsExecutor

  1. 文件:136d4651-df47-41b4-bb80-2ec0ab1bc775.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_browser_settings.VivoBrowserSettingsExecutor

Vivo 浏览器相关的设置,不太懂要干嘛

评论区比文章更精彩

微信公众号评论区

image-20230514203931411

image-20230514203940833

image-20230514203951666

image-20230514204055973

image-20230514204002395

image-20230514204022808

image-20230514204042836

image-20230514204123412

image-20230514204200492

知乎评论区

知乎回答已经被删了,我通过主页可以看到,但是点进去是已经被删了:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317

image-20230514205638861

image-20230514205909534

image-20230514205857945

image-20230514205937705

image-20230514205947268

image-20230514210010062

image-20230514210020926

image-20230514210040479

image-20230514210107839

image-20230514210122906

image-20230514210141653

image-20230514210152755

image-20230514210226176

image-20230514210235233

image-20230514210255912

image-20230514210344475

iOS 和 Android 哪个更安全?

这里就贴一下安全大佬 sunwear 的评论

img

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

作者 Gracker
2022年3月13日 08:38

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第三篇,本文主要讲了使用 Systrace 分析 CPU 状态时遇到的 SleepUninterruptible Sleep 状态的原因排查方法与优化方法,这两个状态导致性能变差概率非常高,而且排查起来也比较费劲,网上也没有系统化的文档。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Linux 中的 Sleep 状态是什么

TASK_INTERUPTIBLE vs TASK_UNINTERRUPTIBLE

一个线程的状态不属于 Running 或者 Runnable 的时候,那就是 Sleep 状态了(严谨来说,还有其他状态,不过对性能分析来说不常见,比如 STOP、Trace 等)。

在 Linux 中的Sleep 状态可以细分为 3 个状态:

  • TASK_INTERUPTIBLE → 可中断
  • TASK_UNINTERRUPTIBLE → 不可中断
  • TASK_KILLABLE → 等同于 TASK_WAKEKILL | TASK_UNINTERRUPTIBLE

 "图 1: 性能之巅 2 CPU 优化"

在 Systrace/Perfetto 中,Sleep 状态指的是 Linux 中的TASK_INTERUPTIBLE,trace 中的颜色为白色。Uninterruptible Sleep 指的是 Linux 中的 TASK_UNINTERRUPTIBLE,trace 中的颜色为橙色。

本质上他们都是处于睡眠状态,拿不到 CPU的时间片,只有满足某些条件时才会拿到时间片,即变为 Runnable,随后是 Running。

TASK_INTERRUPTIBLE 与 TASK_UNINTERRUPTIBLE 本质上都是 Sleep,区别在于前者是可以处理 Signal 而后者不能,即使是 Kill 类型的Signal。因此,除非是拿到自己等待的资源之外,没有其他方法可以唤醒它们。 TASK_WAKEKILL 是指可以接受 Kill 类型的Signal 的TASK_UNINTERRUPTIBLE。

Android 中的 Looper、Java/Native 锁等待都属于 TAKS_INTERRUPTIBLE,因为他们可以被其他进程唤醒,应该说绝大部分的程序都处于 TAKS_INTERRUPTIBLE 状态,即 Sleep 状态。 看看 Systrace 中的一大片进程的白色状态就知道了(trace 中表现为白色块),它们绝大部分时间都是在 Runnning 跟 Sleep 状态之间转换,零星会看到几个 Runnable 或者 UninterruptibleSleep,即蓝色跟橙色。

TASK_UNINTERRUPTIBLE 作用

似乎看来 TASK_INTERUPTIBLE 就可以了,那为什么还要有 TASK_UNINTERRUPTIBLE 状态呢?

中断来源有两个,一个是硬件,另一个就是软件。硬件中断是外围控制芯片直接向 CPU 发送了中断信号,被 CPU 捕获并调用了对应的硬件处理函数。软件中断,前面说的 Signal、驱动程序里的 softirq 机制,主要用来在软件层面触发执行中断处理程序,也可以用作进程间通讯机制。

一个进程可以随时处理软中断或者硬件中断,他们的执行是在当前进程的上下文上,意味着共享进程的堆栈。但是在某种情况下,程序不希望有任何打扰,它就想等待自己所等待的事情执行完成。比如与硬件驱动打交道的流程,如 IO 等待、网络操作。 这是为了保护这段逻辑不会被其他事情所干扰,避免它进入不可控的状态

Linux 处理硬件调度的时候也会临时关闭中断控制器、调度的时候会临时关闭抢占功能,本质上为了 防止程序流程进入不可控的状态。这类状态本身执行时间非常短,但系统出异常、运行压力较大的时候可能会影响到性能。

https://elixir.bootlin.com/linux/latest/ident/TASK_UNINTERRUPTIBLE

可以看到内核中使用此状态的情况,典型的有 Swap 读数据、信号量机制、mutex 锁、内存慢路径回收等场景。

分析时候的注意点

首先要认识到 TASK_INTERUPTIBLE、TASK_UNINTERRUPTIBLE 状态的出现是正常的,但是如果这些这些状态的累计占比达到了一定程度,就要引起注意了。特别是在关键操作路径上这类状态的占比较多的时候,需要排查原因之后做相应的优化。 分析问题以及做优化的时候需要牢牢把握两个关键点,它类似于内功心法一样:

  1. 原因的排查方法
  2. 优化方法论

你需要知道是什么原因导致了这次睡眠,是主动的还是被动的?如果是主动的,通过走读代码调查是否是正常的逻辑。如果是被动的,故事的源头是什么? 这需要你对系统有足够多的认识,以及分析问题的经验,你需要经常看案例以增强自己的知识。

以下把 TASK_INTERUPTIBLE 称之为 Sleep,TASK_UNINTERRUPTIBLE称之为 UninterruptibleSleep,目的是与 Systrac 中的用词保持一致。

初期分析 Sleep 与 UninterruptibleSleep 状态的经验不足时你会感到困惑,这种困惑主要是来自于对系统的不了解。你需要读大量的框架层、内核层的代码才能从 Trace 中找出蛛丝马迹。目前并没有一种 Trace 工具能把整个逻辑链路描述的很清楚,而且他们有时候还有不准的时候,比如 Systrace 中的 wakeup_from 信息。只有广泛的系统运行原理做为支持储备,再结合 Trace 工具分析问题,才能做到准确定位问题根因。否则就是我经常说的「性能优化流氓」,你说什么是什么,别人也没法证伪。反复折磨测试同学复测,没测出来之后,这个问题也就不了了之了。

本文没办法列举完所有状态的原因,因此只能列举最为常见的类型,以及典型的实际案例。更重要的是,你需要掌握诊断方法,并结合源代码来定位问题。

Trace 中的可视化效果

Pefetto 中支持显示的状态

Systrace 支持显示的状态

Sleep 状态分析

图 1: UIThread 等待 RenderThread

图 2: Binder 调用等待

诊断方法

通过 wakeup from tid: ***查看唤醒线程

Sleep 最常见的有图 1(UIThread 与 RenderThread 同步)的情况与图 2(Binder 调用)的情况。 Sleep 状态一般是由程序主动等待某个事件的发生而造成的,比如锁等待,因此它有个比较明确的唤醒源。比如图 1,UIThread 等待的是 RenderThread,你可以通过阅读代码来了解这种多线程之间的交互关系。虽然最直接,但是对开发者的要求非常高,因为这需要你熟读图形栈的代码。这可不是一般的难度,是追求的目标,但不具备普适性。

更简单的方法是通过所谓的 wakeup from tid: *** 来调查线程之间的交互关系。从前面的 Runnable 文章 中讲过,任何线程进入 Running 之前会先进入到 Runnable 状态,由此再转换成 Running。从 Sleep 状态切换到 Running,必然也要经过 Runnable。

进入到 Runnable 有两种方式,一种是 Running 中的程序被抢占了,暂时进入到 Runnable。还有一种是由另外一个线程将此线程(处于 Sleep 的线程)变成了 Runnable。

我们在调查Sleep 唤醒线程关系的时候,应用到的原理是第二种情况。在 Systrace 中这种是被 wakeup from tid: *** 信息所呈现。线程被抢占之后变成 Runnable,在 Systrace 中是被 Running Instead 呈现。

需要特别注意的是 wakeupfrom 这个有时候不准,原因是跟具体的 tracepoint 类型有关。分析的时候要注意甄别,不要一味地相信这个数据是对的。

其他方法

  1. Simpleperf 还原代码执行流
  2. 在 Systrace 寻找时间点对齐的事件

方法 1 适合用来看程序到底在执行什么操作进入到这种状态,是 IO 还是锁等待?球里连载 Simpleperf 工具的使用方法,其中「Simpleperf 分析篇 (1): 使用 Firefox Profiler 可视分析 Simpleperf 数据」介绍了可以按时间顺序看函数调用的可视化方法。其他使用也会陆续更新,直接搜关键字即可。

方法 2 是个比较笨的方法,但有时候也可以通过它找到蛛丝马迹,不过缺点是错误率比较高。

耗时过长的常见原因

  • Binder 操作 → 通过打开 Binder 对应的 trace,可方便地观察到调用到远端的 Binder 执行线程。如果 Binder 耗时长,要分析远端的 Binder 执行情况,是否是锁竞争?得不到CPU 时间片?要具体问题具体分析
  • Java\futex锁竞争等待 → 最常见也是最容易引起性能问题,当负载较高时候特别容易出现,特别是在 SystemServer 进程中。这是 Binder 多线程并行化或抢占公共资源导致的弊端。
  • 主动等待 → 线程主动进入 Sleep 状态,等待其它线程的唤醒,比如等待信号量的释放。优化建议:需要看代码逻辑分析等待是否合理,不合理就要优化掉。
  • 等待 GPU 执行完毕 → 等 GPU 任务执行完毕,Trace 中可以看到等 GPU fence 时间。常见的原因有渲染任务过重、 GPU 能力弱、GPU 频率低等。优化建议:提升 GPU 频率、降低渲染任务复杂度,比如精简 Shader、降低渲染分辨率、降低Texture 画质等。

UninterruptibleSleep 状态分析

诊断方法

本质上UninterruptibleSleep 也是一种 Sleep,因此分析 Sleep 状态时用到的方法也是通用的。不过此状态有两个特殊点与 Sleep 不同,因此在此特别说明。

  1. UninterruptibleSleep 分为 IOWait 与 Non-IOWait
  2. UninterruptibleSleep 有 Block reason

UninterruptibleSleep 分为 IOWait 与 Non-IOWait

IO 等待好理解,就是程序执行了 IO 操作。最简单的,程序如果没法从 PageCache 缓存里快速拿到数据,那就要与设备进行 IO 操作。CPU 内部缓存的访问速度是最快的,其次是内存,最后是磁盘。它们之间的延迟差异是数量级差异,因此系统越是从磁盘中读取数据,对整体性能的影响就越大。

非 IO 等待主要是指内核级别的锁等待,或者驱动程序中人为设置的等待。Linux 内核中某些路径是热点区域,因此不得不拿锁来进行保护。比如Binder 驱动,当负载大到一定程度,Binder 的内部的锁竞争导致的性能瓶颈就会呈现出来。

Block Reason

谷歌的 Riley Andrews(riandrews@google.com) 15年左右往内核里提交了一个 tracepoint 补丁,用于记录当发生 UninterruptibleSleep 的时候是否是 IO 等待、调用函数等信息。Systrace 中的展示的 IOWait 与 BlockReason,就是通过解析这条 tracepoint 而来的。这条代码提交的介绍如下(由于这笔提交未合入到 Linux 上游主线,因此要注意你用的内核是否单独带了此补丁):

1
2
3
4
5
6
7
sched: add sched blocked tracepoint which dumps out context of sleep.
Decare war on uninterruptible sleep. Add a tracepoint which
walks the kernel stack and dumps the first non-scheduler function
called before the scheduler is invoked.

Change-Id: [I19e965d5206329360a92cbfe2afcc8c30f65c229](https://android-review.googlesource.com/#/q/I19e965d5206329360a92cbfe2afcc8c30f65c229)
Signed-off-by: Riley Andrews [riandrews@google.com](mailto:riandrews@google.com)

在 ftrace(Systrace 使用的数据抓取机制) 中的被记录为

1
sched_blocked_reason: pid=30235 iowait=0 caller=get_user_pages_fast+0x34/0x70 

这句话被 Systrace 可视化的效果为:

主线程中有一段 Uninterruptible Sleep 状态,它的 BlockReason 是 get_user_pages_fast。它是一个 Linux 内核中函数的名字,代表着是线程是被它切换到了 UninterruptibleSleep 状态。为了查看具体的原因,需要查看这个函数的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* get_user_pages_fast() - pin user pages in memory
* @start: starting user address
* @nr_pages: number of pages from start to pin
* @gup_flags: flags modifying pin behaviour
* @pages: array that receives pointers to the pages pinned.
* Should be at least nr_pages long.
*
* Attempt to pin user pages in memory without taking mm->mmap_lock.
* If not successful, it will fall back to taking the lock and
* calling get_user_pages().
*
* Returns number of pages pinned. This may be fewer than the number requested.
* If nr_pages is 0 or negative, returns 0. If no pages were pinned, returns
* -errno.
*/
int get_user_pages_fast(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages)
{
if (!is_valid_gup_flags(gup_flags))
return -EINVAL;

/*
* The caller may or may not have explicitly set FOLL_GET; either way is
* OK. However, internally (within mm/gup.c), gup fast variants must set
* FOLL_GET, because gup fast is always a "pin with a +1 page refcount"
* request.
*/
gup_flags |= FOLL_GET;
return internal_get_user_pages_fast(start, nr_pages, gup_flags, pages);
}
EXPORT_SYMBOL_GPL(get_user_pages_fast);

从函数解释上可以看到,函数首先是通过无锁的方式pin 应用侧的 pages,如果失败的时候不得不尝试持锁后走慢速执行路径。此时,无法持锁的时候那就要等待了,直到先前持锁的人释放锁。那之前被谁持有了呢?这时候可以利用之前介绍的Sleep 诊断方法,如下图。

UninterruptibleSleep 状态相比 Sleep 有点复杂,因为它涉及到 Linux 内部的实现。可能是内核本身的机制有问题,也有可能是应用层使用不对,因此要联合上层的行为综合诊断才行。毕竟内核也不是万能的,它也有自己的能力边界,当应用层的使用超过其边界的时候,就会出现影响性能的现象。

IOWait 常见原因与优化方法

1. 主动IO 操作

  • 程序进行频繁、大量的读或者写 IO 操作,这是最常见的情况。
  • 多个应用同时下发 IO 操作,导致器件的压力较大。同时执行的程序多的时候 IO 负载高的可能性也大。
  • 器件本身的 IO 性能较差,可通过 IO Benchmark 来进行排查。 常见的原因有磁盘碎片化、器件老化、剩余空间较少(越是低端机越明显)、读放大、写放大等等。
  • 文件系统特性,比如有些文件系统的内部操作会表现为 IO 等待。
  • 开启 Swap 机制的内核下,数据从 Swap 中读取。

优化方法

  • 调优 Readahead 机制
  • 指定文件到 PageCache,即 PinFile 机制
  • 调整 PageCache 回收策略
  • 调优清理垃圾文件策略

2. 低内存导致的 IO 变多

内存是个非常有意思的东西,由于它的速度比磁盘快,因此 OS 设计者们把内存当做磁盘的缓存,通过它来避免了部分IO操作的请求,非常有效的提升了整体 IO 性能。有两个极端情况,当系统内存特别大的时候,绝大部分操作都可以在内存中执行,此时整体 IO 性能会非常好。当系统内存特别低,以至于没办法缓存 IO 数据的时候,几乎所有的 IO 操作都直接与器件打交道,这时候整体性能相比内存多的时候而言是非常差的。

所以系统中的内存较少的时候 IO 等待的概率也会变高。所以,这个问题就变成了如何让系统中有足够多的内存?如何调节磁盘缓存的淘汰算法?

优化方法

  • 关键路径上减少 IO 操作
  • 通过Readahead 机制读数据
  • 将热点数据尽量聚集在一起,使被 Readahead 机制命中的概率高
  • 最后一个老生常谈的,减少大量的内存分配、内存浪费等操作

系统中的内存是被各个进程所共用。当app 只考虑自己,肆无忌惮的使用计算资源,必然会影响到其他程序。这时候系统还是会回来压制你,到头来亏损的还是自己。 不过能想到这一步的开发者比较少,也不现实。明文化的执行系统约定,可能是个终极解决方案。

Non-IOWait 常见原因

  • 低内存导致等待 → 低内存的时候要回收其他程序或者缓存上的内存。
  • Binder 等待 → 有大量 Binder 操作的时候出现概率较高。
  • 各种各样的内核锁,不胜枚举。结合「诊断方法」来分析。

系统调度与 UninterruptibleSleep 耦合的问题

当线程处于 UninterruptibleSleep 非 IO等待状态(即内核锁),而持有该锁的其他线程因 CPU 调度原因,较长时间处于 Runnable 状态。这时候就出现了有意思的现象,即使被等待的线程处于高优先级,它的依赖方没有被调度器及时的识别到,即使是非常短的锁持有,也会出现较长时间的等待。

规避或者彻底解决这类问题都是件比较难的事情,不同厂家实现了不同的解决方案,也是比较考虑厂家技术能力的一个问题。

附录

Linux 线程状态释义

线程状态描述
SSLEEPING
R、R+RUNNABLE
DUNINTR_SLEEP
TSTOPPED
tDEBUG
ZZOMBIE
XEXIT_DEAD
xTASK_DEAD
KWAKE_KILL
WWAKING
DK
DW

案例: 从 Swap 读取数据时的等待

案例: 同进程的多个线程进行 mmap

共享同一个 mm_struct 的线程同时执行 mmap() 系统调用进行 vma 分配时发生锁竞争。

mmap_write_lock_killable() 与 mmap_write_unlock() 包起来的区域就是由锁受保护的区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long pgoff)
{
unsigned long ret;
struct mm_struct *mm = current->mm;
unsigned long populate;
LIST_HEAD(uf);

ret = security_mmap_file(file, prot, flag);
if (!ret) {
if (mmap_write_lock_killable(mm))
return -EINTR;
ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,
&uf);
mmap_write_unlock(mm);
userfaultfd_unmap_complete(mm, &uf);
if (populate)
mm_populate(ret, populate);
}
return ret;
}

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Systrace 线程 CPU 运行状态分析技巧 - Running 篇

作者 Gracker
2022年3月13日 08:38

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第二篇,主要分析了 Systrace 中 cpu 的 Running 状态出现的原因和 Running 过长时的一些优化思路。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Running 时间长

显示方式

Trace 中显示绿色,表示线程处于运行态

原因 1: 代码本身复杂度高,执行耗时久

这是最常见的一种方式,当然不排除平台有bug,有时候厂商在libc、syscal等高频核心函数,加了一些逻辑导致了代码运行时间长。

优化建议: 优化逻辑、算法,降低复杂度。为了进一步判断具体是哪个函数耗时,可使用 AS CPU Profiler、simpleperf,或者自己通过 Trace.begin/end() API 添加更多 tracepoint 点

当然不排除有的时候平台有bug,在关键的libc或内核函数加了一些逻辑

原因 2: 代码以解释方式执行

Trace 中看到 「Compiling」字眼时可能意味着它是解释执行方式进行。刚安装的应用(未做 odex)的程序经常会出现这种情况

优化建议: 使用 dex2oat 之后的版本试试,解释执行方式下的低性能暂无改善方法,除非执行 dex2oat 或者提高代码效率本身

除此之外,使用了编程语言的某种特性,如频繁的调用 JNI,反复性反射调用。除了通过积攒经验方式之外,通过工具解决的方法就是通过 CPU Profiler、simpleperf 等工具进行诊断

原因 3: 线程跑小核,导致执行时间长

对 CPU Bound 的操作来说跑在小核可能没法满足性能需求,因为小核的定位是处理非UX 强相关的线程。不过 Android 没办法保证这一点,有时候任务就是会安排在小核上执行。

优化建议:线程绑核、SchedBoost 等操作,让线程跑尽量跑更高算力的核上,比如大核。有时候即使迁核了也不见效,这时候要看看频率是否拉得足够高,见“原因 4”

原因 4: 线程所跑的大核运行频率太低

优化建议:

  1. 优化代码逻辑,主动降低运行负载,CPU 频率低也能流畅运行
  2. 修改调度器升频相关的参数,让 CPU 根据负载提频更激进
  3. 用平台提供的接口锁定 CPU 频率(俗称的「锁频」)

原因 5: 温升导致 CPU 关核、限频

优化建议:

手机因结构原因导致散热能力差或温升参数过于激进时,为了保护体验跟不烫伤人,几乎所有手机厂家的系统会限制 CPU 频率或者直接关核。排查思路是首先需要找到触发温升的原因。

温升的排查的第一步,首先要看是外因导致还是内因导致。外因是指是否由外部高温导致,如太阳底下,火炉边;往往夏天的时候导致手机发热的情况越严重

内因主要由 CPU、Modem、相机模组或者其他发热比较厉害的器件导致的。以 CPU 为例,如果后台某个线程吃满 CPU,那就首先要解决它。如果是前台应用负载高导致大电流消耗,同样道理,那就降低前台本身的负载。其他器件也是同样道理,首先要看是否是无意义的运行,其次是优化业务逻辑本身

除此之外,温升参数过于激进的话导致触发限频关核的概率也会提高,因此通过与竞品对比等方式调优温升参数本身来达到优化目的

原因 6: CPU 算力弱

优化建议:

ARM 处理器在相同频率下不同微架构的配置导致的性能差异是非常明显的,不同运行频率、L1/L2 Cache 的容量均能影响 CPU 的 MIPS(Million Instructions Per Second) 执行结果。

优化思路有两条:

  1. 编译器参数
  2. 优化代码逻辑

第一条比较难,大部分应用开发者来说也不太现实,系统厂商如华为,方舟编译器优化 JNI 的思路本质是不改应用代码情况下提高代码执行效率来达到性能上的提升

第二条可以通过 simpleperf 等工具,找到热点代码或者观察 CPU 行为后做进一步的改善,如:

  • Cache miss 率过高导致执行耗时,就要优化内存访问相关逻辑
  • 代码复杂指令过多导致耗时,就要优化代码逻辑,降低代码复杂度
  • 设计好业务缓存,尽量提高缓存命中率,避免抖动(反复地申请与释放)

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇

作者 Gracker
2022年1月21日 10:40

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第一篇,主要分析了 Systrace 中 cpu 的 runnable 状态出现的原因和 Runnable 过长时的一些优化思路。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Runnable 状态说明

Runnable 状态在 Trace 中的显示方式

Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长 中讲解了导致 CPU 的 Running 状态耗时久的原因与优化方法,这一节介绍 Runnable 状态切换原理与对应的排查与优化思路。在 Systrace 中显示为蓝色,表示线程处于 Runnable,等待被 CPU 调度执行。

图 1: Systrace 中 Runnable 的可视化效果展示

图 2: 性能之巅 2 CPU 优化

从图 2 可知,一个 CPU 核在某个时刻只能执行一个线程,因此所有待执行的任务都在一个「可执行队列」里排队,一个 CPU 核就有一个队列。能插入到这个队列里排队的,代表着这个线程除了 CPU 资源,其他资源均已获取,如 IO、锁、信号量等。处于「可执行队列」的时候,线程的状态就会被置为 RUNNABLE,也就是 Systrace 里看到的 Runnable 状态。

Linux 内核是通过赋予不同线程执行时间片并通过轮转的方式来达到同时执行多个线程的效果,因此当一个 Running 中的线程的时间片用完时(通常是 ms 级别)将此线程置为 Runnable,等待下一次被调度。也有比较特殊的情况,那就是抢占。有些高优先级的线程可以抢占当前执行的线程,而不必等到此线程的时间片到期。

当一个 CPU 有多个核的时候显然可以多个核同时工作,这时候不必都在一个 CPU 核上排队,根据负载情况(也就是排队情况),将线程迁移到其他核执行是必要的操作。掌管这些调度策略的,是通过 Linux 的调度器来实现的,它具体通过多个调度类(Schedule Class)来管理不同线程的优先级,常见的有:

  1. SCHED_RR、SCHED_FIFO: 实时调度类,整体优先级上高于 NORMAL。
  2. SCHED_NORMAL: 普通调度类,目前常用的是 CFS(Complete Fair Scheduler)调度器。
    实时类的优先级高于普通调度类,高优先级的能抢占低优先级,并且要等待高优先级的执行完才能执行低优先级的。一般情况下 Runnable 的时间都很短,但出异常的的时候它会影响关键线程的关键任务在指定时间内完成。

图 3: AOSP 渲染架构

这个可能不止是一个线程,甚至是多个。特别是涉及到 UI 相关的任务,这种情况就更为复杂了。AOSP 体系下典型的一帧绘制是经过 UI Thread → Render Thread → SurfaceFlinger → HWC(参考 图 3),其中任何一个线程被 Runnable 阻塞导致没有在规定时间内完成渲染任务,都将会导致界面的卡顿(也就是掉帧)。

Runnable 过长的原因和优化思路

我们从实践中总结出以下 5 大门类,系统层面出异常的原因较多,但也见过应用自身逻辑导致 Runnable 过长情况。

原因 1: 优先级设置错误

  • 应用设置了过高的优先级:至于抢占了其他线程的任务,对后者来说显得自己优先级太低了。
  • 应用设置了过低的优先级:当此线程处于「关键链路」时,以 Runnable 执行的概率就越高,导致卡顿概率也高。
  • 系统出 Bug 时把线程优先级设为过高或者过低。

优化思路:

  1. 应用视情况调整线程优先级,可从 Trace 中可以看到是被哪个线程抢占了。
  2. 系统将关键线程调度策略设置成 FIFO。

我们在实践中见到过不少应用因为设置错了优先级反而导致更卡。原因比较复杂,可能开发者所使用的机器用当时的优先级策略没问题,但是在别的厂商的调度器(头部大厂基本都有自己改动调度器)下就会出现水土不兼容的情况。一般情况下,三方应用开发者不建议直接调用这类 API,弄巧成拙,屡见不鲜。

长远看来更靠谱的方式是合理安排自己的任务模型,不要把对实时性要求很高的任务放到 worker 线程上。

原因 2: 绑核不合理

有时候为了让线程运行得更快,会把线程绑定到大核,在前面解决 Running 时间长时也有建议绑大核,但是绑核一定要谨慎,因为一旦把线程绑定在某个核,表示线程只能运行在这个核上即使其它核很空闲。如果多个线程都绑定在某个核,当这个核很繁忙调度不过来时,这些线程就会出现 Runnable 时间很长的情况。所以绑核一定要谨慎!下面是绑核需要注意的一些事项:

  1. 线程绑核不要绑定在单个核上,这样容错率会特别低,因为一旦这个核被其它线程抢占绑定这个核的线程就要等着,所以尽量以 CPU 簇为单位进行绑核,比如线程要绑定大核,可以指定 4-7 大核而不是指定某个一大核。
  2. 2 个大核平台尽可能减少绑定大的核线程数目,不然会使得大核很容易繁忙,把绑核会变成「负优化」。
  3. 要正确区分大小核,比如 8 个核的平台,4-7 不一定就是大核,有的平台可能 0-3 才是大核。
  4. 只能在 CPUSET 允许范围内绑核,如果 CPUSET 只允许进程跑 0-3,如果进程试图绑定在 4-7 会绑核失败,甚至会有一些意料之外的致命错误。

原因 3: 软件架构设计不合理

重申下,Runnable 是指在 CPU 核上的排队耗时,按常识可可知道排队长、频繁排队时出问题概率也就越高。一个绘制任务所依赖的线程数量越多,出问题的概率也越高,因为排队次数变多了嘛。

软件架构不止要满足业务需求,也要在性能、扩展性方面上做思考,从上面推导可知,如果你程序编程模型需要大量线程协同运行来完成关键操作,如绘制,那出问题的概率就越高。

最常见的有,两个线程之间有频繁的有通讯与等待(线程 A 把任务转移到线程 B 执行,A 等待 B 任务执行完后被唤醒), CPU 繁忙时很容易打出 Runnable 等待状态,CPU 越忙概率越高。

优化思路:

  1. 应用调整线程优先级,见「原因 1」。
  2. 优化代码架构/逻辑,免频繁等待其他线程的唤醒,在 Trace 中可以看到线程的依赖关系。可借助 CPU Profiler 探查代码执行逻辑,提高分析唤醒关系的效率。
  3. 平台通过修改调度器来识别有关系链的线程组,优先调度这个组里的线程。

原因 4: 应用自己或系统整体负载高导致排队的任务非常多

从上述的调度原理可知,如果大量任务挤在一个核的「可执行队列」上,显然越是后面,优先级越低的任务排队时间就越长。

排查的时候你可以在 Perfetto/Systrace 的 CPU 核维度任务上,即使在放大后的界面看到排满了密密麻麻的任务,这基本上就意味着系统整体负载较高了。通过计算,可算出 CPU 每时刻的使用量,基本上都会在 90%以上。 你可以通过选择一个区间,以时间来排序,看看都在执行什么任务,以此来逐个排查同时执行大量程序的原因是什么。

简单总结就是,同时执行的任务太多了,主要原因来自两方面:

1.应用自身高占用

应用自身就把 CPU 资源都给占满了,狂开十来个线程来做事情,即使是头部大厂也会做这种事。

优化建议:

  1. 找出应用所有占用高的线程,看看各线程此刻跑起来的行为是否异常,如果异常则要优化它。
  2. 优化线程负载本身,可使用 simpleperf 等工具进行函数级别的定位。
  3. 调整优先级,使用比 CFS 更高优先级的调度器,如设置为 RT。不过它带来的隐患也较多,需要慎重。
  4. 优化软件架构,区分关键与非关键线程,通过合理设置「绑核 & 优先级」来为关键线程让出资源。 如,不重要线程绑到小核运行或设置低优先级、渲染相关线程设置高优先级等,让渲染线程相关的线程能占用到更多的 CPU 资源。设计架构的时候一定要考虑运行环境恶劣的情况,因为安卓从设计上就不敢保证所有资源都优先供给你,肯定有别人跟你抢资源。

2.系统服务高占用

有的厂商 ROM 自己本身就有很多任务,设计不合理的话自己家程序就吃满了大量资源,导致留给应用运行的资源较少。还有些是管控措施设计的一般,以至于留给了大量流氓应用可乘之机,各路神仙利用自己的「黑科技」在后台保活后进行各种拉活同步操作。

3.平台厂家的黑科技

厂家除了要优化自身服务,以做到「点到为止」外,可以实现如下功能来尽可能把资源分配合理化,让出更多资源给前台应用。

  1. 通过 CGROUP 的 CPUSET 子系统,让不同优先级的线程运行在不同的 CPU 核心。AOSP 自带了 CPUSET 分组功能,不过有些缺陷如:
    1. 分组不够精细,很多后台都可以跑满所有核
    2. 没有考虑进程的工作状态,如 音乐、导航、录音、视频、通话、下载
    3. 对 Java 进程 fork 的子进程放任不管
  2. 通过 CGROUP 的 CPUCTL 子系统,进行资源配额,如限制异常进程、普通后台进程的不同量级的 CPU 最高使用量。
  3. 通过线程&进程级别的冻结技术,在应用退出后台之后冻结进程让其拿不到 CPU 资源,类似 iOS 的做法。难点在于:
    1. 切断和恢复各跨进程通信
    2. 进程关系的梳理
    3. 兼容性问题,需要有大量的测试验证
  4. 按需启动系统进程与管控好后台进程自启动。

每一个优化说简单也简单,说难也难,依赖厂家的技术积累。

原因 5: CPU 算力限制、锁频、锁核、状态异常

排队做核酸检测一样,检测窗口多的队列排队时间少。CPU 算力差、关核、限频,导致 Runnable 的概率也更高。通常的原因有:

  1. 场景控制
    • 不同场景模式下的不同频率、核心策略
    • 高温下的锁频锁核
  2. CPU 省电模式:如高通的 Low Power Mode。
  3. CPU 状态切换:如 C2/C1 切换到 C0 耗时久。
  4. CPU 损坏,概率小但也有可能会出现。
  5. 低端机 :安卓上的低端机。

其中:

  1. 原因 1 场景控制, 考验厂家的能力与各自的标准,应用程序能做的还是那句名言 → 降低自己负载,少惹平台。 厂家为了设计好「场景控制」,需要有精细化的场景识别与合理的控制能力,将功耗与性能的平衡做到全局最优化,不同场景下应突出不同的业务能力,而不是一杆子拍死。
  2. 高温下的优化建议请参考「Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长」中的「原因 5: 温升导致 CPU 关核、限频」。
  3. 原因 3 CPU 状态切换 是芯片固有的特性,出现的概率小,但也不是不可能,每个芯片架构升级换代的时候就时不时遇到「妥协」版的 CPU 产品。厂家对芯片的评估是个比较隐性的能力,很少会被大众提及,但是非常重要的一个能力。电子消费品历史中,也总是重演关键器件选错了,导致厂家走入万劫不复境地的真实案例。
  4. 原因 5,安卓上的低端机,真的就指配备里低算力的 CPU,这与苹果的做法不一样,它的 CPU 至少跟当期旗舰是一样的。同样参考 「Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长」中的「原因 6: 算力弱」。

原因 6: 调度器异常

几乎所有的厂家都做了调度器优化方面的工作,虽然概率小,但也有可能会出异常。场景锁频锁核机制有问题、内核各种 governor 的出问题的时候,会出现明明 CPU 的其他核都很闲,但任务都挤在某几个核上。

系统开发者能做的就是把基础「可观测性技术」建好,出问题时可以快速诊断,因为这类问题一是不好复现,二是现象出现时机较短,可能立马就恢复了。

原因 7: 处理器区分执行 32 位与 64 位进程

有些过渡期的芯片,如最近推出的骁龙 8Gen1 与 天玑 9000,会有非常奇葩的运行限制。32 位的程序只能运行某个特定微架构上,64 位的则畅通无阻。且先不说这种「脑残设计」是处于什么所谓「平衡」,他带来的问题是,当你用的应用大量还是 32 位的时候,很多任务(以进程为单位)都挤在某个核心上运行,结合前面的理论,都挤在一起,出现 Runnable 的概率就更高。

  1. 对应用开发者,建议尽快升级至 64 位程序。如果你用的是第三方方案,尽早通知改进或者改用其他方案。
  2. 对系统开发者,一是根据问题联系应用厂商做更新,二是特殊加强后台管理功能,进一步降低 32 位程序的运行负载。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android Systrace 响应速度实战 3 :响应速度延伸知识

作者 Gracker
2021年9月13日 10:46

在讨论 Android 性能问题的时候,卡顿、响应速度、ANR 这三个性能相关的知识点通常会放到一起来讲,因为引起卡顿、响应慢、ANR 的原因类似,只不过根据重要程度,被人为分成了卡顿、响应慢、ANR 三种,所以我们可以定义广义上的卡顿,包含了卡顿、响应慢和 ANR 三种,所以如果用户反馈说手机卡顿或者 App 卡顿,大部分情况下都是广义上的卡顿,需要搞清楚,到底出现了哪一种问题

如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲

另外在 App 或者厂商内部,卡顿、响应速度、ANR 这几个性能指标都是有单独的标准的,比如 掉帧率、启动速度、ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了

本文是响应速度系列的第三篇,主要是讲在使用 Systrace 分析应用响应速度问题的时候,其中的一些延伸知识,包括启动速度测试、Log 输出解读、Systrace 状态解读、三方启动库等内容

Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了

Systrace 系列文章如下

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

1. Systrace 中进程三种状态解读

Systrace 中,进程的任务最常见的有三种状态:Sleep、Running、Runnable。在优化的过程中,这几个状态也需要我们关注。进程任务状态在最上面,以颜色来做区分:

  1. 绿色:Running
  2. 蓝色:Runnable
  3. 白色:Sleep

1.1 如何分析 Sleep 状态的 Task

一般白色的 Sleep 有两种,即应用主动 Sleep 和被动 Sleep

  1. nativePoll 这种,一般属于主动 Sleep,因为没有消息处理了,所以进入 Sleep 状态等待 Message,这种一般是正常的,我们不需要去关注。比如两帧之间的那段,就是主动 sleep 的
  2. 被动 Sleep 一般是由用户主动调用 sleep,或者用 Binder 与其他进程进行通信,这个是我们最常见的,也是分析性能问题的时候经常会遇到的,需要重点关注

如下图,这种在启动过程中,有较长时间的 sleep 情况,一般下面就可以看到是否在进行 Binder 通信,如果在启动过程中有频繁的 Binder 通信,那么应用等待的时间就会变长,导致响应时间变慢

QBslSMaNhoheCWfY

这种一般可以点击这个 Task 最下面的 binder transaction 来查看 Binder 调用信息,比如

FqqYKpVnNGUuivrq

有时候没有 Binder 信息,是被其他的等待的线程唤醒,那么可以查看唤醒信息,也可以找到应用是在等待什么

zPFoHiQJvTWMmUuF

放大上图中我们点击的 Runnable 的地方

YJH0UWMlVDuJjSro

1.2 如何分析 Running 状态的 Task

Running 状态的任务就是目前在 CPU 某一个核心上运行的任务,如果某一段任务是 Running 状态,且耗时变长,那么需要分析:

  1. 是否应用的本身逻辑耗时,比如新增了某些代码逻辑
  2. 是否跑在了对应的核心上

NDvF1X4GFiUpC6vN

K49yBsgPUrHkYyw6

在某些 Android 机器上,大家一般会对 App 的主线程和渲染线程进行调度方面的优化:一般前台应用的 UI Thread 和 RenderThread 都是跑在大核上的

1.3 如何分析 Runnable 状态的 Task

一个 Task 要从 Sleep 状态转到 Running 状态,必须先变成 Runnable 状态,其状态转换图如下

3gilFNQA8lh1A7l0

在 Systrace 上的表现如下

GR7crsOwDOy4X8qL

正常情况下,应用进入 Runnable 状态之后,会马上被调度器调度,进入 Running 状态,开始干活;但是在系统繁忙的时候,应用就会有大量的时间在 Runnable 状态,因为 cpu 已经跑满,各种任务都需要排队等待调度

如果应用启动的时候出现大量的 Runnable 任务,那么需要查看系统的状态

2. TraceView 工具在响应速度方面的使用

TraceView 指的是我们在 AS Profiler 里面抓取 CPU 信息的时候出现的那个,大家看下面的截图就知道了

image-20211028011509615

2.1 如何抓取应用启动时候的 TraceView

使用下面的命令可以抓取应用的冷启动,这些命令也可以分开执行,需要把里面的包名和 Activity 名切换成自己应用的包名

1
adb shell am start -n com.aboback.wanandroidjetpack/.splash.SplashActivity --start-profiler /data/local/tmp/traceview.trace --sampling 1 && sleep 10 && adb shell am profile stop com.aboback.wanandroidjetpack && adb pull /data/local/tmp/traceview.trace .

或者分开执行上面的命令

1
2
3
4
5
6
7
8
9
10
// 1. 冷启动 App,sampleing = 1 意思是 1ms 采样一次
adb shell am start -n com.aboback.wanandroidjetpack/.splash.SplashActivity --start-profiler /data/local/tmp/traceview.trace --sampling 1

// 2. 等待应用完全启动之后,结束 profile
adb shell am profile stop com.aboback.wanandroidjetpack

// 3. 将 Trace 文件从手机里面 pull 出来
adb pull /data/local/tmp/traceview.trace .

// 4. 使用 Android Studio 打开 traceview.trace 文件

2.2 TraceView 工具怎么看

抓出来的 TraceView 可以直接在 Android Studio 中打开

其中图里面用绿色标记的函数,就是应用自己的函数,黄色标注的是系统的函数

Application.onCreate

vRpdMHl8CTzCGcxS

Activity.onCreate

Jl9s7vZbMQTTrWIT

doFrame

X4f8vX27JS5x4TyF

WebView 初始化

jxmFl0K9M8cpNCWy

2.3 TraceView 工具的弊端

由于采样比较细,所以会性能损耗比较大,所以抓出来的 TraceView,其中每个方法的执行时间是不准的,所以不可用作为真实的时间参考,但是可以用来定位具体的函数调用栈。

需要跟 Systrace 来进行互补

3. SimplePerf 工具在启动速度分析的使用

使用 SimplePerf 工具也可以抓取启动时候的堆栈信息,既包括 Java 也包括 Native

比如我们要抓取 com.aboback.wanandroidjetpack 这个应用的冷启动,可以执行下面的命令(SimplePerf 的环境初始化参考 https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md 这篇文章 ,其中 app_profiler.py 就是 SimplePerf 的工具)

1
python app_profiler.py -p com.aboback.wanandroidjetpack

执行上面的命令之后,需要手动在手机上启动 App,然后主动结束

1
2
3
4
5
$ python app_profiler.py -p com.aboback.wanandroidjetpack  
INFO:root:prepare profiling
INFO:root:start profiling1
INFO:root:run adb cmd: ['adb', 'shell', '/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data', '-e task-clock:u -f 1000 -g --duration 10', '--log', 'info', '--app', 'com.aboback.wanandroidjetpack'] simpleperf I environment.cpp:601] Waiting for process of app com.aboback.wanandroidjetpack
simpleperf I environment.cpp:593] Got process 32112 for package com.aboback.wanandroidjetpack

抓取结束之后,调用解析脚本来生成 html 报告

1
python report_html.py

就会得到下面这个

itcLtktb5xxv5gp0

不仅可以看到 Java 层的堆栈,也可以看到 Native 的堆栈,这里只是简单的使用,更详细的方法可以参考下面几个文档

SimplePerf 初步试探 https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/README.md

  1. Android application profiling https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md
  2. Android platform profiling https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_platform_profiling.md
  3. Executable commands reference https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/executable_commands_reference.md
  4. Scripts reference https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/scripts_reference.md

4. 其他组件启动时在 Systrace 中的位置

4.1 Service 的启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final void scheduleCreateService(IBinder token,
ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
updateProcessState(processState, false);
CreateServiceData s = new CreateServiceData();
s.token = token;
s.info = info;
s.compatInfo = compatInfo;
sendMessage(H.CREATE_SERVICE, s);
}

public final void scheduleBindService(IBinder token, Intent intent,
boolean rebind, int processState) {
updateProcessState(processState, false);
BindServiceData s = new BindServiceData();
s.token = token;
s.intent = intent;
s.rebind = rebind;
sendMessage(H.BIND_SERVICE, s);
}

可以看到,代码执行都是往 H 这个 Handler 中发送 Message,所以如果我们在代码里面启动 Service,并不是马上就执行的,而是由 MessageQueue 里面的 Message 顺序决定的

NRY3YuU1Bsgk6wvv

放大真正执行的部分可以看到,其执行的时机是在 MessageQueue 按照 Message 的顺序执行(这里是在应用第一帧执行结束后),后面的 Message 就是应用自己的 Message、启动 Service、执行广播接收器

r463HjnnO0KgdOFl

4.2 执行自己的 Message

执行自定义的 Message 在 Systrace 中的显示

iUZhyqUStVIyZHtf

4.3 启动 Service

Service 启动在 Systrace 中的显示

8RzVxF9ai3r420W0

4.4 启动 BroadcastReceiver

执行 Receiver 在 Systrace 中的显示

kY3rkAhlqk7klnOL

Broadcast 的注册:一般是在 Activity 生命周期函数中注册,在哪里注册就在哪里执行

UR245WLGUvPEcvTz

4.5 ContentProvider 的启动时机

6PdzLLkZ7hdA61Tk

019y0cn5kNNGFVnm

5. AppStartup 是否能优化启动速度?

三方库的初始化

很多三方库都需要在 Application 中进行初始化,并顺便获取到 Application 的上下文

但是也有的库不需要我们自己去初始化,它偷偷摸摸就给初始化了,用到的方法就是使用 ContentProvider 进行初始化,定义一个 ContentProvider,然后在 onCreate 拿到上下文,就可以进行三方库自己的初始化工作了。而在 APP 的启动流程中,有一步就是要执行到程序中所有注册过的 ContentProvider 的 onCreate 方法,所以这个库的初始化就默默完成了。

这种做法确实给集成库的开发者们带来了很大的便利,现在很多库都用到了这种方法,比如 Facebook,Firebase,WorkManager

ContentProvider 的初始化时机如下:

VhpUlBTz1UNteVQz

但是当大部分三方库使用这种方法初始化的时候,就会有下面几个问题

  1. 启动过程中的 ContentProvider 过多
  2. 应用开发者无法控制使用这种方式初始化的库的初始化时机
  3. 无法处理这些三方库的依赖

AppStartup 库

针对上面的情况,Google 推出了 AppStartup 库,AppStartup 库的优点

  • 可以共享单个 Contentprovider
  • 可以明确地设置初始化顺序
  • 通过这个库可以移除三方库的 ContentProvider 启动时候自动初始化的步骤,手动通过 LazyLoad 的方式启动,这样可以起到优化启动速度的作用

根据测算结果来看,使用 AppStartup 库并不能显著加快应用启动速度,除非你有非常多 (50+)的 ContentProvider 在应用启动的时候初始,那么 AppStartup 才会有比较明显的效果

如果三方的 SDK 使用 ContentProvider 初始化耗时,那么可以考虑针对这个 ContentProvider 进行延迟初始化,比如

1
2
3
4
5
6
7
8
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
</provider>

ExampleLoggerInitializer 的 meta-data 当中加入了一个 tools:node=”remove”的标记

总结

  1. App Startup 的设计是为了解决一个问题:即不同的库使用不同的 ContentProvider 进行初始化,导致 ContentProvider 太多,管理杂乱,影响耗时的问题
  2. App Startup 具体能减少多少耗时时间:根据测试,如果二三十个三方库都集成了 App Startup,减少的耗时大概在 20ms 以内
  3. App Startup 的使用场景应该
    1. APK 有很多的 ContentProvider 在启动时候初始化
    2. APK 中有的三方库 ContentProvider 初始化很耗时,但是又不是必须要在启动的时候初始,可以按需初始化
    3. 应用开发者想自己控制各个库的初始化时机或者初始化顺序

需要 App 开发同学验证

  1. 检查打包出来的 apk 的配置文件里面看一下,有多少个三方库是利用 ContentProvider 初始化的(或者在 AS 的 src\main\AndroidManifest.xml 文件最下面打开 Merged Manifest 标签查看)
  2. 确认这些 ContentProvider 在启动时候的耗时
  3. 确认哪些 ContentProvider 可以延迟加载或者用时加载
  4. 如果需要的话,接入 AppStartup 库

6. IdleHandler 在 App 启动场景下的使用

在启动优化的过程中,idleHandler 可以在 MessageQueue 空闲的时候执行任务,如下图,可以很清晰地查看 idleHandler 的执行时机

XaSQsq3pfgDgCNVe

其使用场景如下:

  1. 在启动的过程中,可以借助 idleHandler 来做一些延迟加载的事情。比如在启动过程中 Activity 的 onCreate 里面 addIdleHandler,这样在 Message 空闲的时候,可以执行这个任务

    7fLroBfAyXPaUu8W

  2. 进行启动时间统计:比如在页面完全加载之后,调用 activity.reportFullyDrawn 来告知系统这个 Activity 已经完全加载,用户可以使用了,比如下面的例子,在主页的 List 加载完成后,调用 activity.reportFullyDrawn

    BN97DrcXfHiqA5DY

    其对应的 Systrace 如下

    1xrN3YMzAY71ZAWi

    这时候得到的应用的冷启动时间才是正常的

    A5mH2AW4U06l43jk

另外系统有些功能,也会依赖于 FullyDrawn,所以建议主动上报(即主动在 App 完全启动后调用 activity.reportFullyDrawn)

系列文章

  1. Systrace 响应速度实战 1 :了解响应速度原理
  2. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  3. Systrace 响应速度实战 3 :响应速度延伸知识
  4. Systrace 基础知识系列-放个链接在这里方便大家直接点过去

参考文章

  1. Android 应用启动全流程分析
  2. 探究 | App Startup 真的能减少启动耗时吗
  3. Jetpack 新成员,App Startup 一篇就懂
  4. App Startup
  5. Android App 启动优化全记录
  6. Android application profiling

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例

作者 Gracker
2021年9月13日 10:46

在讨论 Android 性能问题的时候,卡顿响应速度ANR 这三个性能相关的知识点通常会放到一起来讲,因为引起卡顿、响应慢、ANR 的原因类似,只不过根据重要程度,被人为分成了卡顿、响应慢、ANR 三种,所以我们可以定义广义上的卡顿,包含了卡顿、响应慢和 ANR 三种,所以如果用户反馈说手机卡顿或者 App 卡顿,大部分情况下都是广义上的卡顿,需要搞清楚,到底出现了哪一种问题

如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢 ,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲

另外在 App 或者厂商内部,卡顿响应速度ANR 这几个性能指标都是有单独的标准的,比如 掉帧率启动速度ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了

本文是响应速度系列的第二篇,主要是以 Android App 冷启动为例,讲解如何使用 Systrace 来分析 App 冷启动

Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了

Systrace 系列文章如下

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

1. 准备工作

这个案例和对应的 Systrace 偏工程化一些,省略了很多细节,因为应用的启动流程涉及的知识非常广,如果每个都细化的话,会有很大的篇幅。推荐大家看这篇文章,非常详细:Android 应用启动全流程分析

所以这里以 Systrace 为主线,讲解应用启动的时候各个关键模块的大概工作流程。了解大概流程之后,就可以分段去深入自己感兴趣或者自己负责的部分,这里首先放一张 Systrace 和手机截图所对应的图,大家可以先看看这个图,然后再往下看(博客里面 Perfetto 和 Systrace 混合使用)

应用启动完整图

为了更方便分析应用冷启动,我们需要做下面的准备工作

  1. 打开 Binder 调试,方便在 Trace 中显示 Binder 信息(即可以在 Systrace 中看到 Binder 调用的函数)- 需要 Root
    1. 开启 ipc debug: adb shell am trace-ipc start
    2. 抓取结束后,可以执行下面的命令关闭adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
  2. Trace 命令加入 irq tag,默认的命令不包含 irq,需要自己加 irq 的 TAG,这样打开 Trace 之后,就可以看到 irq 相关的内容,最后的抓 trace 命令如下:
     python /mnt/d/Android/platform-tools/systrace/systrace.py gfx input view webview wm am sm rs bionic power pm ss database network adb idle pdx sched irq freq idle disk workq binder_driver binder_lock -a com.xxx.xxx ,注意这里的 com.xxx.xxx 换成自己的包名,如果不是调试特定的包名,可以去掉 -a com.xxx.xxx
  3. 推荐 :如果要 Debug 的 App 可以进行编译(即可以使用 Gradle 编译,一般自己开发的项目都可以),可以在分析响应速度问题的时候,引入 TraceFix 库(接入方法参考 https://github.com/Gracker/TraceFix)。接入之后,编译的时候就会进行代码插桩,在 App 代码的每一个函数中都插入 Trace 点,这样在分析的时候可以看到更详细的 App 的信息
    1. 使用插件前,只能看到 Framework 里面的 Trace 点
    2. 使用插件后,可以看到 Trace 中显示的信息多了很多(App 自身的代码逻辑,Framework 的代码没法插桩)


2. Android App 冷启动流程分析

本文以 在桌面上冷启动一个 Android App 为例,应用冷启动的整个流程包含了从用户触摸屏幕到应用完全显示的整个流程,其中涉及到

  1. 触摸屏中断处理阶段
  2. InputReader 和 InputDispatcher 处理 input 事件阶段
  3. Launcher 处理 input 事件阶段
  4. SystemServer 处理启动事件
  5. 启动动画
  6. 应用启动和自身逻辑阶段

上一篇文章有讲到响应速度问题,需要搞清楚 起点终点,对于应用冷启动来说,起点就是 input 事件,终点就是应用完全展示给用户(用户可操作)

下面将从上面几个关键流程,通过 Systrace 的来介绍整个流程

2.1 触摸屏中断处理阶段

由于我们的案例是在桌面冷启动一个 App,那么在手指触摸手机屏幕的时候,触摸屏会触发中断,这个中断我们最早能在 Systrace 中看到的地方如下:

对应的 cpu ss 区域和 中断区域(加了 irq 的 tag 才可以看到)

一般来说,点击屏幕会触发若干个中断,这些信号经过处理之后,触摸屏驱动会把这些点更新到 EventHub 中,让 InputReader 和 InputDIspatcher 进行进一步的处理。这一步一般不会出现什么问题,厂商这边对触摸屏的调教可能会关注这里

2.2 InputReader 和 InputDispatcher 处理 Input 事件阶段

InputReader 和 InputDispatcher 这两个线程跑在 SystemServer 里面,专门负责处理 Input 事件,具体的流程可以参考Android Systrace 基础知识 - Input 解读 这篇文章

InputReader 和 InputDispatcher

这里由于我们是点击桌面上的一个 App 的图标,可以看到底层上报上来的事件包括一个 Input_Down 事件 + 若干个 Input Move 事件 + 一个 Input Up 事件,组成了一个完整的点击事件

由于 Launcher 在进程创建的时候就注册了 Input 监听,且此时 Launcher 在前台且可见,所以 Launcher 进程可以收到这些 Input 事件,并根据 Input 事件的类型进行处理,input 事件在 SystemServer 和 App 的流转在 Systrace 中的具体表现可以参考 Android Systrace 基础知识 - Input 解读 ,这里把核心的两张图放上来

2.2.1 Input 事件在 SystemServer 中流转

看下图即可,如果要看更详细的,可以查看 Android Systrace 基础知识 - Input 解读

Input 事件在 SystemServer 中流转

2.2.2 Input 事件在 Launcher 进程流转

看下图即可,如果要看更详细的,可以查看 Android Systrace 基础知识 - Input 解读

2.3 Launcher 进程处理 Input 事件阶段

Launcher 处理 Input 事件也是响应时间的一个重要阶段,主要包括两个响应速度指标

  1. 点击桌面到桌面第一帧响应(一般 Launcher 会在接收到 Down 事件的时候,将 App 图标置灰,以表示接收到了事件;有的定制桌面 App 图标会有一个缩小的动画,表示被按压)
  2. 桌面第一帧响应到启动 App(这段时间指的是桌面在收到 Down 对 App 图标做处理后,到收到 Up 事件判断需要启动 App 的时间)

另外提一下,滑动桌面到桌面第一帧响应时间(这个指的是滑动桌面的场景,左右滑动桌面的时候,用高速相机拍摄,从手指动开始,到桌面动的第一帧的时间)也是一个很重要的响应速度指标,部分厂商也会在这方面做优化,感兴趣的可以自己试试主流厂商的桌面滑动场景(跟原生的机器对比 Systrace 即可)

在冷启动的场景里面,Launcher 在收到 up 事件后,会进行逻辑判断,然后启动对应的 App(这里主要是交给 AMS 来处理,又回到了 SystemServer 进程)

A9Y3FkDNSFlPuho5

这个阶段通常也是做系统优化的会比较关注,做 App 的同学还不需要关注到这里(Launcher App 的除外);另外在最新的版本,应用启动的动画是由 Launcher 和 SystemServer 共同完成的,目的就是可以做一些复杂的动画而没有割裂感,大家可以用慢镜头拍一下启动时候和退出应用的动画,可以看到有的应用图标是分层的,甚至会动,这是之前纯粹由 SystemServer 这边来做动画所办不到的

2.4 SystemServer 处理 StartActivity 阶段

SystemServer 处理主要是有2部分

  1. 处理启动命令
  2. 通知 Launcher 进入 Pause 状态
  3. fork 新的进程

处理启动命令

这个 SystemServer 进程中的 Binder 调用就是 Launcher 通过 ActivityTaskManager.getService().startActivity 调用过来的

fork 新的进程,则是在判断启动的 Activity 的 App 进程没有启动后,需要首先启动进程,然后再启动 Activity,这里是冷启动和其他启动不一样的地方。fork 主要是 fork Zygote64 这个进程(部分 App 是 fork 的 Zygote32 )

fork 新进程

fork 新进程对应的代码

Zygote 64 位进程执行 Fork 操作

对应的 App 进程出现

对应的代码如下,这里就正式进入了 App 自己的进程逻辑了

应用启动后,SystemServer 会记录从 startActivity 被调用到应用第一帧显示的时长,在 Systrace 中的显示如下(注意结尾是应用第一帧,如果应用启动的时候是 SplashActivity -> MainActivity,那么这里的结尾只是 SplashActivity,MainActivity 的完全启动需要自己查看

2.5 应用进程启动阶段

通常的大型应用,App 冷启动通常包括下面三个部分,每一个部分耗时都会导致应用的整体启动速度变慢,所以在优化启动速度的时候,需要明确知道应用启动结束的点(需要跟测试沟通清楚,一般是界面保持稳定的那个点)

  1. 应用进程启动到 SplashActivity 第一帧显示(部分 App 没有 SplashActivity,所以可以省略这一步,直接到进程启动到 主 Activit 第一帧显示 )
  2. SplashActivity 第一帧显示到主 Activity 第一帧显示
  3. 主 Activity 第一帧显示到界面完全显示

下面针对这三个阶段来具体分析(当然你的 App 如果简单的话,可能没有 SplashActivity ,直接进的就是主 Activity,那么忽略第二步就可以了)

应用进程启动到 SplashActivity 第一帧显示

由于是冷启动,所以 App 进程在 Fork 之后,需要首先执行 bindApplication ,这个也是区分冷热启动的一个重要的点。Application 的环境创建好之后,就开始组件的启动(这里是 Activity 组件,通过 Service、Broadcast、ContentProvider 组件启动的进程则会在 bindApplication 之后先启动这些组件)

Activity 的生命周期函数会在 Activity 组件创建的时候执行,包括 onStart、onCreate、onResume 等,然后还要经过一次 Choreographer#doFrame 的执行(包括 measure、layout、draw)以及 RenderThread 的初始化和第一帧任务的绘制,再加上 SurfaceFlinger 一个 Vsync 周期的合成,应用第一帧才会真正显示(也就是下图中 finishDrawing 的位置),这部分详细的流程可以查看 Android Systrace 基础知识 - MainThread 和 RenderThread 解读

SplashActivity 第一帧显示到主 Activity 第一帧显示

大部分的 App 都有 SplashActivity 来播放广告,播放完成之后才是真正的主 Activity 的启动,同样包括 Activity 组件的创建,包括 onStart、onCreate、onResume 、自有启动逻辑的执行、WebView 的初始化等等等等,直到主 Activity 的第一帧显示

主 Activity 第一帧显示到界面完全加载并显示

一般来说,主 Activity 需要多帧才能显示完全,因为有很多资源(最常见的是图片)是异步加载的,第一帧可能只加载了一个显示框架、而其中的内容在准备好之后才会显示出来。这里也可以看到,通过 Systrace 不是很方便来判断应用冷启动的终点(除非你跟测试约定好,在某个 View 显示之后就算启动完成,然后你在这个 View 里面打个 Systrace 的 Tag,通过跟踪这个 Tag 就可以粗略判断具体 Systrace 里面哪一帧是启动完成的点)

我制作了一个 Systrace + 截图的方式,来进行演示,方便你了解 App 启动各个阶段都对应在 Systrace 的哪里(使用的是一个开源的 WanAndroid 客户端)

OIG57AudX193jD3u

End

本文重点放在了如何在 Systrace 中展示 App 的完整冷启动流程,方便大家在做 App 的启动优化的时候,可以通过 Systrace 来快速定位到启动瓶颈,也方便进行竞品的对比和分析,

  1. 至于如何分析,可以查看 Systrace 响应速度实战 1 :了解响应速度原理 的分析套路部分
  2. 至于如何优化,可以查看 Android App 启动优化全记录 这篇文章,这里就不再重复了。不过随着技术的发展,有些优化手段会消失,而会有新的优化手段冒出来,我也会对这篇文章进行维护,如果大家发现新的优化技术,麻烦博客留言或者加微信(553000664)通知我,我来进行调研和更新

系列文章

  1. Systrace 响应速度实战 1 :了解响应速度原理
  2. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  3. Systrace 响应速度实战 3 :响应速度延伸知识
  4. Systrace 基础知识系列-放个链接在这里方便大家直接点过去

参考文章

  1. Android 应用启动全流程分析
  2. 探究 | App Startup 真的能减少启动耗时吗
  3. Jetpack 新成员,App Startup 一篇就懂
  4. App Startup
  5. Android App 启动优化全记录
  6. Android application profiling

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android Systrace 响应速度实战 1 :了解响应速度原理

作者 Gracker
2021年9月13日 10:46

在讨论 Android 性能问题的时候,卡顿响应速度ANR 这三个性能相关的知识点通常会放到一起来讲,因为引起卡顿、响应慢、ANR 的原因类似,只不过根据重要程度,被人为分成了卡顿、响应慢、ANR 三种,所以我们可以定义广义上的卡顿,包含了卡顿、响应慢和 ANR 三种,所以如果用户反馈说手机卡顿或者 App 卡顿,大部分情况下都是广义上的卡顿,需要搞清楚,到底出现了哪一种问题

如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢 ,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲

另外在 App 或者厂商内部,卡顿响应速度ANR 这几个性能指标都是有单独的标准的,比如 掉帧率启动速度ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了

本文是响应速度系列的第一篇,主要是讲响应速度相关的理论知识,包括性能工程概述、响应速度涉及到的知识点、响应速度的分析方法和套路等

关于卡顿的文章可以参考这一篇 Systrace 流畅性实战 1 :了解卡顿原理 ,ANR 的文章后续会介绍,本文主要是讲响应速度相关的基本原理

Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了

性能工程

在介绍响应速度的原理之前,这里先放一段 <**性能之巅**> 这本书中对于性能的描述,具体来说就是方法论,非常贴合本文的主题,也强烈推荐各位搞性能优化的同学,把这本书作为手头常读的方法论书籍:

性能是充满挑战的

系统性能工程是一个充满挑战的领域,具体原因有很多,其中包括以下事实,系统性能是主观的、复杂的,而且常常是多问题并存的

性能是主观的

  1. 技术学科往往是客观的,太多的业界人士审视问题非黑即白。在进行软件故障查找的时候,判断 bug 是否存在或 bug 是否修复就是这样。bug 的出现总是伴随着错误信息,错误信息通常容易解读,进而你就明白错误为什么会出现了
  2. 与此不同,性能常常是主观性的。开始着手性能问题的时候,对问题是否存在的判断都有可能是模糊的,在问题被修复的时候也同样,被一个用户认为是“不好”的性能,另一个用户可能认为是“好”的

系统是复杂的

  1. 除了主观性之外,性能工程作为一门充满了挑战的学科,除了因为系统的复杂性,还因为对于性能,我们常常缺少一个明确的分析起点。有时我们只是从猜测开始,比如,责怪网络,而性能分析必须对这是不是一个正确的方向做出判断
  2. 性能问题可能出在子系统之间复杂的互联上,即便这些子系统隔离时表现得都很好。也可能由于连锁故障(cascading failure)出现性能问题,这指的是一个出现故障的组件会导致其他组件产生性能问题。要理解这些产生的问题,你必须理清组件之间的关系,还要了解它们是怎样协作的
  3. 瓶颈往往是复杂的,还会以意想不到的方式互相联系。修复了一个问题可能只是把瓶颈推向了系统里的其他地方,导致系统的整体性能并没有得到期望的提升。
  4. 除了系统的复杂性之外,生产环境负载的复杂特性也可能会导致性能问题。在实验室环境很难重现这类情况,或者只能间歇式地重现
  5. 解决复杂的性能问题常常需要全局性的方法。整个系统——包括自身内部和外部的交互——都可能需要被调查研究。这项工作要求有非常广泛的技能,一般不太可能集中在一人身上,这促使性能工程成为一门多变的并且充满智力挑战的工作

可能有多个问题并存

  1. 找到一个性能问题点往往并不是问题本身,在复杂的软件中通常会有多个问题
  2. 性能分析的又一个难点:真正的任务不是寻找问题,而是辨别问题或者说是辨别哪些问题是最重要的
  3. 要做到这一点,性能分析必须量化(quantify)问题的重要程度。某些性能问题可能并不适用于你的工作负载或者只在非常小的程度上适用。理想情况下,你不仅要量化问题,还要估计每个问题修复后能带来的增速。当管理层审查工程或运维资源的开销缘由时,这类信息尤其有用。
  4. 有一个指标非常适合用来量化性能,那就是 延时(latency

– 以上几段内容摘录自 <**性能之巅**>

Systrace 系列文章如下

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识

响应速度概述

响应速度是应用 App 性能的重要指标之一。响应慢通常表现为点击效果延迟操作等待白屏时间长等,主要场景包括:

  • 应用启动场景,包括冷启动、热启动、温启动等
  • 界面跳转场景,包括应用内页面跳转、App 之间跳转
  • 其他非跳转的点击场景(开关、弹窗、长按、控件选择、单击、双击等)
  • 亮灭屏、开关机、解锁、人脸识别、拍照、视频加载等场景

从原理上来说,响应速度场景往往是由一个 input 事件(以 Message 的形式给到需要处理的应用主线程)触发(比如点击、长按、电源键、指纹等),由一个或者多个 Message 的执行结束为结尾,而这些 Message 中一般都有关键的界面绘制相关的 Message 。衡量一个场景的响应速度,我们通常从事件触发开始计时,到应用处理完成计时结束,这一段时间就称为响应时间。

如下图所示,响应速度的问题,通常就是这些 Message 的某个执行超过预期(主观),导致最终完成的时间长于用户期待的时间

XyGmCIpqfZxRFCs7

由于响应速度是一个比较主观的性能指标(而流畅度就是一个很精确的指标,掉一帧就是掉一帧),而且根据角色的不同,对这个性能指标的判定也不同,比如 Android 系统开发者和应用开发者以及测试同学,对 应用冷启动 的起点和终点就有不同的判定:

  1. 系统开发者 往往从 input 中断开始看,部分以应用第一帧为结束点(因为比较好计算),部分以应用加载完成为结束点(比较主观,除非结束点比较容易通过工具去判断),主要是以优化应用的整体性能为主,涉及到的方面就比较广,包括 input 事件传递、SystemServer、SurfaceFlinger、Kernel 、Launcher 等
  2. App 开发者 一般从 Application 的 onCreate 或者 attachContext 开始看,大部分以界面完全加载或者用户可操作为结束点,因为是自己的应用,结束点在代码里面可以主动加,主要还是以优化应用自身的启动速度为主,市面上讲启动速度优化的,大部分是讲这部分
  3. 测试同学 则更多从用户的真实体验角度来看,以桌面点击应用图标且应用图标变色为第一帧,内容完全加载为结束点。测试过程一般使用 高速相机 + 自动化,通过机械手图形识别技术,可以自动进行响应速度测试并抓取相关的测试数据

响应速度问题分析思路

分清起点和终点

分析响应速度,最重要的是要找到起点终点,上一节讲到,不同角色的开发者,对这个性能指标的判定起点和终点都不一样;而且这个指标有很主观的成分,所以在开始的时候,就要跟各方来确定好起点和终点,具体的数值标准,下面一些手段可以帮助大家来确定

  1. 竞品分析。一般来说,响应速度这个指标都会有一个对标的竞品,竞品手机或者竞品 App,相同的条件下,竞品手机或者竞品 App 从点击到响应花费了多少时间,可以作为一个标准
  2. 对比前一个版本。有时候系统进行大版本升级或者 App 进行版本迭代,那么上一个版本的数据就可以拿来作为标准进行对比

一般来说,起点都比较好确定,无非是一个点击事件或者一个自定义的触发事件;而终点的确定就比较麻烦,比如如何确定一个复杂的 App (比如淘宝)启动完成的时间点,用 Systrace 的第一帧或者 Log 输出的 Displayed 时间或者 onWindowFocusChange 回调的时间显然是不准确的。目前市面上使用高速相机 + 图像识别来做是一个比较主流的做法

响应速度常见问题

Android 系统自身原因导致响应慢

下面这些列举的是 Android 系统自身的原因,与 Android 机器的性能有比较大的关系,性能越差,越容易出现响应速度问题。下面就列出了 Android 系统原因导致的 App 响应速度出现问题的原因,以及这个时候 App 端在 Systrace 中的表现

  1. CPU 频率不足
    • App 端的表现:主线程处于 Running 状态,但是执行耗时变长
  2. CPU 大小核调度:关键任务跑到了小核
    • App 端的表现:Systrace 看主线程处于 Running 状态,但是执行耗时变长
  3. SystemServer 繁忙,主要影响
    1. 响应 App 主线程 Binder 调用处理耗时
      • App 端的表现:Systrace 看主线程处于 Sleep 状态,在等待 Binder 调用返回
    2. 应用启动过程逻辑处理耗时
      • App 端的表现:Systrace 看主线程处于 Sleep 状态,在等待 Binder 调用返回
  4. SurfaceFlinger 繁忙,主要影响应用的渲染线程的 dequeueBuffer、queueBuffer
    • App 端的表现:Systrace 看应用渲染线程的 dequeueBuffer、queueBuffer 处于 Binder 等待状态
  5. 系统低内存,低内存的时候,很大概率出现下面几种情况,都会对 SystemServer 和应用有影响
    1. 低内存的时候,有些应用会频繁被杀和启动,而应用启动时一个重操作,会占用 CPU 资源,导致前台 App 启动变慢
      • App 端的表现:Systrace 看应用主线程 Runnable 状态变多,Running 状态变少,整体函数执行耗时增加
    2. 低内存的时候,, 很容易触发各个进程的 GC , , 用于内存回收的 HeapTaskDeamon、kswapd0 出现非常频繁
      • App 端的表现:Systrace 看应用主线程 Runnable 状态变多,Running 状态变少,整体函数执行耗时增加
    3. 低内存会导致磁盘 IO 变多, 如果频繁进行磁盘 IO , 由于磁盘 IO 很慢, 那么主线程会有很多进程处于等 IO 的状态, 也就是我们经常看到的 Uninterruptible Sleep
      • App 端的表现:Systrace 看应用主线程 Uninterruptible Sleep 和 Uninterruptible Sleep - IO 状态变多,Running 状态变少,整体函数执行耗时增加
  6. 系统触发温控频率被限制:由于温度过高,CPU 最高频率被限制
    • App 端的表现:主线程处于 Running 状态,但是执行耗时变长
  7. 整机 CPU 繁忙:可能有多个高负载进程同时在运行,或者有单个进程负载过高跑满了 CPU
    • App 端的表现:从 Systrace 来看,CPU 区域的任务非常满,所有的核心上都有任务在执行,App 的主线程和渲染线程多处于 Runnable 状态,或者频繁在 Runnable 和 Running 之间切换

应用自身原因

应用自身原因主要是应用启动时候的组件初始化、View 初始化、数据初始化耗时等,具体包括:

  1. Application.onCreate:应用自身的逻辑 + 三方 SDK 初始化耗时
  2. Activity 的生命周期函数:onStart、onCreate、onResume 耗时
  3. Services 的生命周期函数耗时
  4. Broadcast 的 onReceive 耗时
  5. ContentProvider 初始化耗时(注意已经被滥用)
  6. 界面布局初始化:measure、layout、draw 等耗时
  7. 渲染线程初始化:setSurface、queueBuffer、dequeueBuffer、Textureupload 等耗时
  8. Activity 跳转:从 SplashActivity 到 MainActivity 耗时
  9. 应用向主线程 post 的耗时 Message 耗时
  10. 主线程或者渲染线程等待子线程数据更新耗时
  11. 主线程或者渲染线程等待子进程程数据更新耗时
  12. 主线程或者渲染线程等待网络数据更新耗时
  13. 主线程或者渲染线程 binder 调用耗时
  14. WebView 初始化耗时
  15. 初次运行 JIT 耗时

响应速度问题分析套路(以 Systrace 为主)

  1. 确认前提条件(老化,数据量、下载等)、操作步骤、问题现象,本地复现
  2. 需要明确测试标准
    1. 启动时间的起点是哪里
    2. 启动时机的终点是哪里
  3. 抓取所需的日志信息(Systrace、常规 log 等)
  4. 首先分析 Systrace,大概找出差异的点
    1. 首先查看应用耗时点,分析对比机差异,这里可以把应用启动阶段分成好几段来看,来对比分析是哪部分时间增加
      1. Application 创建
      2. Activity 创建
      3. 第一个 doFrame
      4. 后续内容加载
      5. 应用自己的 Message
    2. 分析应用耗时的点
      1. 是否某一段方法自身执行耗时比较久(Running 状态) –> 应用自身问题
      2. 主线程是否有大段 Running 状态,但是底下没有任何堆栈 –> 应用自身问题,加 TraceTag 或者使用 TraceView 来找到对应的代码逻辑
      3. 是否在等 Binder 耗时比较久(Sleep 状态) –> 检测 Binder 服务端,一般是 SystemServer
      4. 是否在等待子线程返回数据(Sleep 状态) –> 应用自身问题,通过查看 wakeup 信息,来找到依赖的子线程
      5. 是否在等待子进程返回数据(Sleep 状态) –> 应用自身问题,通过查看 wakeup 信息,来找到依赖的子进程或者其他进程(一般是 ContentProvider 所在的进程)
      6. 是否有大量的 Runnable –> 系统问题,查看 CPU 部分,看看是否已经跑满
      7. 是否有大量的 IO 等待(Uninterruptible Sleep | WakeKill - Block I/O) –> 检查系统是否已经低内存
      8. RenderThread 是否执行 dequeueBuffer 和 queueBuffer 耗时 –> 查看 SurfaceFlinger
    3. 如果分析是系统的问题,则根据上面耗时的点,查看系统对应的部分,一般情况要优先查看系统是否异常,参考上面列出的的系统原因,主要看下面四个区域(Systrace)
      1. Kernel 区域
        1. 查看关键任务是否跑在了小核 –> 一般小核是 0-3(也有特例),如果启动时候的关键任务跑到了小核,执行速度也会变慢
        2. 查看频率是否没有跑满 –> 表现是核心频率没有达到最大值,比如最大值是 2.8Ghz,但是只跑到了 1.8Ghz,那么可能是有问题的
        3. 查看 CPU 使用率,是否已经跑满了 –> 表现是 CPU 区域八个核心上,任务和任务之间没有空隙
        4. 查看是否低内存
          1. 应用进程状态有大量的 Uninterruptible Sleep | WakeKill - Block I/O
          2. HeapTaskDeamon 任务执行频繁
          3. kswapd0 任务执行频繁
      2. SystemServer 进程区域
        1. input 事件读取和分发是否有异常 –> 表现是 input 事件传递耗时,比较少见
        2. binder 执行是否耗时 –> 表现是 SystemServer 对应的 Binder 执行代码逻辑耗时
        3. binder 等 am、wm 锁是否耗时–> 表现是 SystemServer 对应的 Binder 都在等待锁,可以通过 wakeup 信息跟踪等锁情况,分析等锁是不是由于应用导致的
        4. 是否有应用频繁启动或者被杀 –> 在 Systrace 中查看 startProcess,或者查看 Event Log
      3. SurfaceFlinger 进程区域
        1. dequeueBuffer 和 queueBuffer 是否执行耗时 –> 表现是 SurfaceFlinger 的对应的 Binder 执行 dequeueBuffer 和 queueBuffer 耗时
        2. 主线程是否执行耗时 –> 表现是 SurfaceFlinger 主线程耗时,可能是在执行其他的任务
      4. Launcher 进程区域(冷热启动场景)
        1. Launcher 进程处理点击事件是否耗时 –> 表现在处理 input 事件耗时
        2. Launcher 自身 pause 是否耗时 –> 表现在执行 onPause 耗时
        3. Launcher 应用启动动画是否耗时或者卡顿 –> 表现在动画耗时或者卡顿
  5. 初步分析有怀疑的点之后
    1. 如果是系统的原因,首先需要看应用自身是否能规避,如果不能规避,则转给系统来处理
    2. 如果是应用自身的原因,可以使用 TraceView(AS 自带的 CPU Profiler)、Simple Perf 等继续查看更加详细的函数调用信息,也可以使用 TraceFix 插件,插入更多的 TraceTag 之后,重新抓取 Systrace 来对比分析
  6. 问题可能有很多个原因
    1. 首先要把影响最大的因素找出来优化,影响比较小的因素可以先忽略
    2. 有些问题需要系统的配合才能解决,这时候需要跟系统一起进行调优(比如各大 App 厂商就会有专门跟手机厂商打交道的,手机厂商会以 SDK 的形式,暴露部分系统接口给 App 来使用,比如 Oppo 、华为、Vivo 等)
    3. 有些问题影响很小或者无解,这时候需要跟测试同学沟通清楚
    4. 有些问题是重复问题或不同平台的相同,可以在 Bug 库中搜索是否有案例

本篇文章主要是一个响应速度基础知识方面的一个普及,其中涉及到大量的系统知识,不熟悉的同学可以跟着 Systrace 基础知识系列 过一下

系列文章

  1. Systrace 响应速度实战 1 :了解响应速度原理
  2. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  3. Systrace 响应速度实战 3 :响应速度延伸知识
  4. Systrace 基础知识系列-放个链接在这里方便大家直接点过去

参考文章

  1. Android 应用启动全流程分析
  2. 探究 | App Startup 真的能减少启动耗时吗
  3. Jetpack 新成员,App Startup 一篇就懂
  4. App Startup
  5. Android App 启动优化全记录
  6. Android application profiling

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android Systrace 基础知识 - SurfaceFlinger 解读

作者 Gracker
2020年2月14日 10:25

本文是 Systrace 系列文章的第五篇,主要是对 SurfaceFlinger 的工作流程进行简单介绍,介绍了 SurfaceFlinger 中几个比较重要的线程,包括 Vsync 信号的解读、应用的 Buffer 展示、卡顿判定等,由于 Vsync 这一块在 Systrace 基础知识 - Vsync 解读Android 基于 Choreographer 的渲染机制详解 这两篇文章里面已经介绍过,这里就不再做详细的讲解了。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

这里直接上官方对于 SurfaceFlinger 的定义

  1. 大多数应用在屏幕上一次显示三个层:屏幕顶部的状态栏、底部或侧面的导航栏以及应用界面。有些应用会拥有更多或更少的层(例如,默认主屏幕应用有一个单独的壁纸层,而全屏游戏可能会隐藏状态栏)。每个层都可以单独更新。状态栏和导航栏由系统进程渲染,而应用层由应用渲染,两者之间不进行协调。
  2. 设备显示会按一定速率刷新,在手机和平板电脑上通常为 60 fps。如果显示内容在刷新期间更新,则会出现撕裂现象;因此,请务必只在周期之间更新内容。在可以安全更新内容时,系统便会收到来自显示设备的信号。由于历史原因,我们将该信号称为 VSYNC 信号。
  3. 刷新率可能会随时间而变化,例如,一些移动设备的帧率范围在 58 fps 到 62 fps 之间,具体要视当前条件而定。对于连接了 HDMI 的电视,刷新率在理论上可以下降到 24 Hz 或 48 Hz,以便与视频相匹配。由于每个刷新周期只能更新屏幕一次,因此以 200 fps 的帧率为显示设备提交缓冲区就是一种资源浪费,因为大多数帧会被舍弃掉。SurfaceFlinger 不会在应用每次提交缓冲区时都执行操作,而是在显示设备准备好接收新的缓冲区时才会唤醒。
  4. 当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。
  5. SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。」

—- 引用自SurfaceFlinger 和 Hardware Composer

下面是上述流程所对应的流程图, 简单地说, SurfaceFlinger 最主要的功能:SurfaceFlinger 接受来自多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。

那么 Systrace 中,我们关注的重点就是上面这幅图对应的部分

  1. App 部分
  2. BufferQueue 部分
  3. SurfaceFlinger 部分
  4. HWComposer 部分

这四部分,在 Systrace 中都有可以对应的地方,以时间发生的顺序排序就是 1、2、3、4,下面我们从 Systrace 的这四部分来看整个渲染的流程

App 部分

关于 App 部分,其实在Systrace 基础知识 - MainThread 和 RenderThread 解读这篇文章里面已经说得比较清楚了,不清楚的可以去这篇文章里面看,其主要的流程如下图:

从 SurfaceFlinger 的角度来看,App 部分主要负责生产 SurfaceFlinger 合成所需要的 Surface。

App 与 SurfaceFlinger 的交互主要集中在三点

  1. Vsync 信号的接收和处理
  2. RenderThread 的 dequeueBuffer
  3. RenderThread 的 queueBuffer

Vsync 信号的接收和处理

关于这部分内容可以查看 Android 基于 Choreographer 的渲染机制详解 这篇文章,App 和 SurfaceFlinger 的第一个交互点就是 Vsync 信号的请求和接收,如上图中第一条标识,Vsync-App 信号到达,就是指的是 SurfaceFlinger 的 Vsync-App 信号。应用收到这个信号后,开始一帧的渲染准备

RenderThread 的 dequeueBuffer

dequeue 有出队的意思,dequeueBuffer 顾名思义,就是从队列中拿出一个 Buffer,这个队列就是 SurfaceFlinger 中的 BufferQueue。如下图,应用开始渲染前,首先需要通过 Binder 调用从 SurfaceFlinger 的 BufferQueue 中获取一个 Buffer,其流程如下:

App 端的 Systrace 如下所示
-w1249

SurfaceFlinger 端的 Systrace 如下所示
-w826

RenderThread 的 queueBuffer

queue 有入队的意思,queueBuffer 顾名思义就是讲 Buffer 放回到 BufferQueue,App 处理完 Buffer 后(写入具体的 drawcall),会把这个 Buffer 通过 eglSwapBuffersWithDamageKHR -> queueBuffer 这个流程,将 Buffer 放回 BufferQueue,其流程如下

App 端的 Systrace 如下所示
-w1165

SurfaceFlinger 端的 Systrace 如下所示
-w1295

通过上面三部分,大家应该对下图中的流程会有一个比较直观的了解了
-w410

BufferQueue 部分

BufferQueue 部分其实在Systrace 基础知识 - Triple Buffer 解读 这里有讲,如下图,结合上面那张图,每个有显示界面的进程对应一个 BufferQueue,使用方创建并拥有 BufferQueue 数据结构,并且可存在于与其生产方不同的进程中,BufferQueue 工作流程如下:

上图主要是 dequeue、queue、acquire、release ,在这个例子里面,App 是生产者,负责填充显示缓冲区(Buffer);SurfaceFlinger 是消费者,将各个进程的显示缓冲区做合成操作

  1. dequeue(生产者发起) : 当生产者需要缓冲区时,它会通过调用 dequeueBuffer() 从 BufferQueue 请求一个可用的缓冲区,并指定缓冲区的宽度、高度、像素格式和使用标记。
  2. queue(生产者发起):生产者填充缓冲区并通过调用 queueBuffer() 将缓冲区返回到队列。
  3. acquire(消费者发起) :消费者通过 acquireBuffer() 获取该缓冲区并使用该缓冲区的内容
  4. release(消费者发起) :当消费者操作完成后,它会通过调用 releaseBuffer() 将该缓冲区返回到队列

SurfaceFlinger 部分

工作流程

从最前面我们知道 SurfaceFlinger 的主要工作就是合成:

当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。

其 Systrace 主线程可用看到其主要是在收到 Vsync 信号后开始工作
-w1296

其对应的代码如下,主要是处理两个 Message

  1. MessageQueue::INVALIDATE — 主要是执行 handleMessageTransaction 和 handleMessageInvalidate 这两个方法
  2. MessageQueue::REFRESH — 主要是执行 handleMessageRefresh 方法

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
void SurfaceFlinger::onMessageReceived(int32_t what) NO_THREAD_SAFETY_ANALYSIS {
ATRACE_CALL();
switch (what) {
case MessageQueue::INVALIDATE: {
......
bool refreshNeeded = handleMessageTransaction();
refreshNeeded |= handleMessageInvalidate();
......
break;
}
case MessageQueue::REFRESH: {
handleMessageRefresh();
break;
}
}
}

//handleMessageInvalidate 实现如下
bool SurfaceFlinger::handleMessageInvalidate() {
ATRACE_CALL();
bool refreshNeeded = handlePageFlip();

if (mVisibleRegionsDirty) {
computeLayerBounds();
if (mTracingEnabled) {
mTracing.notify("visibleRegionsDirty");
}
}

for (auto& layer : mLayersPendingRefresh) {
Region visibleReg;
visibleReg.set(layer->getScreenBounds());
invalidateLayerStack(layer, visibleReg);
}
mLayersPendingRefresh.clear();
return refreshNeeded;
}

//handleMessageRefresh 实现如下, SurfaceFlinger 的大部分工作都是在handleMessageRefresh 中发起的
void SurfaceFlinger::handleMessageRefresh() {
ATRACE_CALL();

mRefreshPending = false;

const bool repaintEverything = mRepaintEverything.exchange(false);
preComposition();
rebuildLayerStacks();
calculateWorkingSet();
for (const auto& [token, display] : mDisplays) {
beginFrame(display);
prepareFrame(display);
doDebugFlashRegions(display, repaintEverything);
doComposition(display, repaintEverything);
}

logLayerStats();

postFrame();
postComposition();

mHadClientComposition = false;
mHadDeviceComposition = false;
for (const auto& [token, displayDevice] : mDisplays) {
auto display = displayDevice->getCompositionDisplay();
const auto displayId = display->getId();
mHadClientComposition =
mHadClientComposition || getHwComposer().hasClientComposition(displayId);
mHadDeviceComposition =
mHadDeviceComposition || getHwComposer().hasDeviceComposition(displayId);
}

mVsyncModulator.onRefreshed(mHadClientComposition);

mLayersWithQueuedFrames.clear();
}

handleMessageRefresh 中按照重要性主要有下面几个功能

  1. 准备工作
    1. preComposition();
    2. rebuildLayerStacks();
    3. calculateWorkingSet();
  2. 合成工作
    1. begiFrame(display);
    2. prepareFrame(display);
    3. doDebugFlashRegions(display, repaintEverything);
    4. doComposition(display, repaintEverything);
  3. 收尾工作
    1. logLayerStats();
    2. postFrame();
    3. postComposition();

由于显示系统有非常庞大的细节,这里就不一一进行讲解了,如果你的工作在这一部分,那么所有的流程都需要熟悉并掌握,如果只是想熟悉流程,那么不需要太深入,知道 SurfaceFlinger 的主要工作逻辑即可

掉帧

通常我们通过 Systrace 判断应用是否掉帧的时候,一般是直接看 SurfaceFlinger 部分,主要是下面几个步骤

  1. SurfaceFlinger 的主线程在每个 Vsync-SF 的时候是否没有合成?
  2. 如果没有合成操作,那么需要看没有合成的原因:
    1. 因为 SurfaceFlinger 检查发现没有可用的 Buffer 而没有合成操作?
    2. 因为 SurfaceFlinger 被其他的工作占用(比如截图、HWC 等)?
    3. 因为 SurfaceFlinger 在等 presentFence ?
    4. 因为 SurfaceFlinger 在等 GPU fence?
  3. 如果有合成操作,那么需要看 你关心的 App 的 可用 Buffer 个数是否正常:如果 App 此时可用 Buffer 为 0,那么看 App 端为何没有及时 queueBuffer(这就一般是应用自身的问题了),因为 SurfaceFlinger 合成操作触发可能是其他的进程有可用的 Buffer

关于这一部分的 Systrace 怎么看,在 Systrace 基础知识 - Triple Buffer 解读-掉帧检测 部分已经有比较详细的解读,大家可以过去看这一段

HWComposer 部分

关于 HWComposer 的功能部分我们就直接看 官方的介绍 即可

  1. Hardware Composer HAL (HWC) 用于确定通过可用硬件来合成缓冲区的最有效方法。作为 HAL,其实现是特定于设备的,而且通常由显示设备硬件原始设备制造商 (OEM) 完成。
  2. 当您考虑使用叠加平面时,很容易发现这种方法的好处,它会在显示硬件(而不是 GPU)中合成多个缓冲区。例如,假设有一部普通 Android 手机,其屏幕方向为纵向,状态栏在顶部,导航栏在底部,其他区域显示应用内容。每个层的内容都在单独的缓冲区中。您可以使用以下任一方法处理合成(后一种方法可以显著提高效率):
    1. 将应用内容渲染到暂存缓冲区中,然后在其上渲染状态栏,再在其上渲染导航栏,最后将暂存缓冲区传送到显示硬件。
    2. 将三个缓冲区全部传送到显示硬件,并指示它从不同的缓冲区读取屏幕不同部分的数据。
  3. 显示处理器功能差异很大。叠加层的数量(无论层是否可以旋转或混合)以及对定位和叠加的限制很难通过 API 表达。为了适应这些选项,HWC 会执行以下计算(由于硬件供应商可以定制决策代码,因此可以在每台设备上实现最佳性能):
    1. SurfaceFlinger 向 HWC 提供一个完整的层列表,并询问“您希望如何处理这些层?”
    2. HWC 的响应方式是将每个层标记为叠加层或 GLES 合成。
    3. SurfaceFlinger 会处理所有 GLES 合成,将输出缓冲区传送到 HWC,并让 HWC 处理其余部分。
  4. 当屏幕上的内容没有变化时,叠加平面的效率可能会低于 GL 合成。当叠加层内容具有透明像素且叠加层混合在一起时,尤其如此。在此类情况下,HWC 可以选择为部分或全部层请求 GLES 合成,并保留合成的缓冲区。如果 SurfaceFlinger 返回来要求合成同一组缓冲区,HWC 可以继续显示先前合成的暂存缓冲区。这可以延长闲置设备的电池续航时间。
  5. 运行 Android 4.4 或更高版本的设备通常支持 4 个叠加平面。尝试合成的层数多于叠加层数会导致系统对其中一些层使用 GLES 合成,这意味着应用使用的层数会对能耗和性能产生重大影响。

——– 引用自SurfaceFlinger 和 Hardware Composer

我们继续接着看 SurfaceFlinger 主线程的部分,对应上面步骤中的第三步,下图可以看到 SurfaceFlinger 与 HWC 的通信部分
-w1149

这也对应了最上面那张图的后面部分
-w563

不过这其中的细节非常多,这里就不详细说了。至于为什么要提 HWC,因为 HWC 不仅是渲染链路上重要的一环,其性能也会影响整机的性能,Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章里面就有列有 HWC 导致的卡顿问题(性能不足,中断信号慢等问题)

想了解更多 HWC 的知识,可以参考这篇文章Android P 图形显示系统(一)硬件合成HWC2,当然,作者的Android P 图形显示系这个系列大家可以仔细看一下

参考文章

  1. Android P 图形显示系统(一)硬件合成HWC2
  2. Android P 图形显示系统
  3. SurfaceFlinger 的定义
  4. surfacefliner

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - CPU Info 解读

作者 Gracker
2019年12月21日 15:16

本文是 Systrace 系列文章的第十二篇,主要是对 Systrace 中的 CPU 信息区域(Kernel)进行简单介绍,简单介绍了如何在 Systrace 中查看 Kernel 模块输出的 CPU 相关的信息,了解 CPU 频率、调度、锁频、锁核相关的信息

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

CPU 区域图例

下面是高通骁龙 845 手机 Systrace 对应的 Kernel 中的 CPU Info 区域(底下的一些这里不讲,主要是讲 Kernel CPU 信息)

CPU 区域图例

Systrace 中 CPU Info 一般在最上面,里面经常会用到的信息包括:

  1. CPU 频率变化情况
  2. 任务执行情况
  3. 大小核的调度情况
  4. CPU Boost 调度情况

总的来说,Systrace 中的 Kernel CPU Info 这里一般是看任务调度信息,查看是否是频率或者调度导致当前任务出现性能问题,举例如下:

  1. 某个场景的任务执行比较慢,我们就可以查看是不是这个任务被调度到了小核?
  2. 某个场景的任务执行比较慢,当前执行这个任务的 CPU 频率是不是不够?
  3. 我的任务比较特殊,比如指纹解锁,能不能把我这个任务放到大核去跑?
  4. 我这个场景对 CPU 要求很高,我能不能要求在我这个场景运行的时候,限制 CPU 最低频率?

与 CPU 运行信息相关的内容在 Systrace 基础知识 – 分析 Systrace 预备知识 这篇文章里面有详细的讲解,不熟悉的同学可以配合这篇文章一起食用

核心架构

简单来说目前的手机 CPU 按照核心数和架构来说,可以分为下面三类:

  1. 非大小核架构
  2. 大小核架构
  3. 大中小核架构

目前的大部分 CPU 都是大小核架构,当然也有一些 CPU 是大中小核架构,比如高通骁龙 855\865,也有少部分 CPU 是非大小核架构

下面就来说说各种架构的区别,方便大家后续查看 Systrace

大小核架构

非大小核架构

很早的机器 CPU 只有双核心或者四核心的时候,一般只有一种核心架构,也就是说这四个核心或者两个核心是同构的,相同的频率,相同的功耗,一起开启或者关闭;有些高通的中低端处理器也会使用同构的八核心处理器,比如高通骁龙 636

现在的大部分机器已经不使用非大小核的架构了

大小核架构

现在的 CPU 一般采用 8 核心,八个核心中,CPU 0-3 一般是小核心,CPU 4-7,如下图中 Systrace 中就是按照这个排列的

小核心一般来说主频低,功耗也低,使用的一般是 arm A5X 系列,比如高通骁龙 845,小核心是由四个 A55 (最高主频 1.8GHz ) 组成

大核心一般来说最高主频比较高,功耗相对来说也会比较高,使用的一般是 arm A7X 系列,比如高通骁龙 845,大核心就是由四个 A75(最高主频 2.8GHz)组成

下图就是 845 的 CPU

845

当然大小核架构中还有一些变种,比如高通骁龙 636 (4 小核 + 2 大核)或者高通骁龙 710 (6 小核 + 2 大核),宗旨还是不变,大核心用来支持高负载场景,小核心用来日常使用,至于够不够用,就看你舍不舍得花银子,毕竟一分价钱一分货,高通爸爸也不是做福利的

下面这些高通的主流大小核处理器的参数如下

大中小核架构

部分 CPU 比较另辟蹊径,选择了大中小核的架构,比如高通骁龙 855 8 核 (1 个 A76 的大核+3 个 A76 的中核 + 4 个 A55 的小核)和之前的的 MTK X30 10 核 (2 个 A73 的大核 + 4 个 A53 的中核 + 4 个 A35 的小核)以及麒麟 980 8 核 (2 个 A76 的大核 + 2 个 A76 的中核 + 4 个 A55 的小核)

相比大小核架构,大中小核架构中的大核可以理解为超大核(高通称之为 Gold +) ,这个超大核的个数一般比较少(1-2 个),主频一般会比较高,功耗相对也会高很多,这个是用来处理一些比较繁重的任务

下图是 855、845 和麒麟 980 的对比

顺带提一嘴,今年的高通骁龙 865 依然是大中小核的架构,大核和中核用的是 A77 架构,小核用的是 A55,大核和中核最高频率不一样,大核只有一个,主频到 2.8GHz,不知道 865 Plus 会不会搞到 3GHz

绑核

绑核,顾名思义就是把某个任务绑定到某个或者某些核心上,来满足这个任务的性能需求

  1. 任务本身负载比较高,需要在大核心上面才能满足时间要求
  2. 任务本身不想被频繁切换,需要绑定在某一个核心上面
  3. 任务本身不重要,对时间要求不高,可以绑定或者限制在小核心上面运行

上面是一些绑核的例子,目前 Android 中绑核操作一般是由系统来实现的,常用的有三种方法

配置 CPUset

使用 CPUset 子系统可以限制某一类的任务跑在特定的 CPU 或者 CPU 组里面,比如下面,Android 中会划分一些默认的 CPU 组,厂商可以针对不同的 CPU 架构进行定制,目前默认划分

  1. system-background 一些低优先级的任务会被划分到这里,只能跑到小核心里面
  2. foreground 前台进程
  3. top-app 目前正在前台和用户交互的进程
  4. background 后台进程
  5. foreground/boost 前台 boost 进程,通常是用来联动的,现在已经没有用到了,之前的时候是应用启动的时候,会把所有 foreground 里面的进程都迁移到这个进程组里面

每个 CPU 架构对应的 CPUset 的配置都不一样,每个厂商也会有不同的策略在里面,比如下面就是一个 Google 官方默认的配置,各位也可以查看对应的节点来查看自己的 CPUset 组的配置

1
2
3
4
5
6
7
8
9
//官方默认配置
write /dev/CPUset/top-app/CPUs 0-7
write /dev/CPUset/foreground/CPUs 0-7
write /dev/CPUset/foreground/boost/CPUs 4-7
write /dev/CPUset/background/CPUs 0-7
write /dev/CPUset/system-background/CPUs 0-3
// 自己查看
adb shell cat /dev/CPUset/top-app/CPUs
0-7

对应的,可以在每个 CPUset 组的 tasks 节点下面看有哪些进程和线程是跑在这个组里面的

1
2
3
4
5
$ adb shell cat /dev/CPUset/top-app/tasks
1687
1689
1690
3559

需要注意每个任务跑在哪个组里面,是动态的,并不是一成不变的,有权限的进程就可以改

部分进程也可以在启动的时候就配置好跑到哪个进程里面,下面是 lmkd 的启动配置,writepid /dev/CPUset/system-background/tasks 这一句把自己安排到了 system-background 这个组里面

1
2
3
4
5
6
7
8
service lmkd /system/bin/lmkd
class core
user lmkd
group lmkd system readproc
capabilities DAC_OVERRIDE KILL IPC_LOCK SYS_NICE SYS_RESOURCE BLOCK_SUSPEND
critical
socket lmkd seqpacket 0660 system system
writepid /dev/CPUset/system-background/tasks

大部分 App 进程是根据状态动态去变化的,在 Process 这个类中有详细的定义

android/os/Process.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* Default thread group -
* has meaning with setProcessGroup() only, cannot be used with setThreadGroup().
* When used with setProcessGroup(), the group of each thread in the process
* is conditionally changed based on that thread's current priority, as follows:
* threads with priority numerically less than THREAD_PRIORITY_BACKGROUND
* are moved to foreground thread group. All other threads are left unchanged.
* @hide
*/
public static final int THREAD_GROUP_DEFAULT = -1;

/**
* Background thread group - All threads in
* this group are scheduled with a reduced share of the CPU.
* Value is same as constant SP_BACKGROUND of enum SchedPolicy.
* FIXME rename to THREAD_GROUP_BACKGROUND.
* @hide
*/
public static final int THREAD_GROUP_BG_NONINTERACTIVE = 0;

/**
* Foreground thread group - All threads in
* this group are scheduled with a normal share of the CPU.
* Value is same as constant SP_FOREGROUND of enum SchedPolicy.
* Not used at this level.
* @hide
**/
private static final int THREAD_GROUP_FOREGROUND = 1;

/**
* System thread group.
* @hide
**/
public static final int THREAD_GROUP_SYSTEM = 2;

/**
* Application audio thread group.
* @hide
**/
public static final int THREAD_GROUP_AUDIO_APP = 3;

/**
* System audio thread group.
* @hide
**/
public static final int THREAD_GROUP_AUDIO_SYS = 4;

/**
* Thread group for top foreground app.
* @hide
**/
public static final int THREAD_GROUP_TOP_APP = 5;

/**
* Thread group for RT app.
* @hide
**/
public static final int THREAD_GROUP_RT_APP = 6;

/**
* Thread group for bound foreground services that should
* have additional CPU restrictions during screen off
* @hide
**/
public static final int THREAD_GROUP_RESTRICTED = 7;

在 OomAdjuster 中会动态根据进程的状态修改其对应的 CPUset 组, 详细可以自行查看 OomAdjuster 中 computeOomAdjLocked、updateOomAdjLocked、applyOomAdjLocked 的执行逻辑(Android 10)

配置 affinity

使用 affinity 也可以设置任务跑在哪个核心上,其系统调用的 taskset, taskset 用来查看和设定“CPU 亲和力”,其实就是查看或者配置进程和 CPU 的绑定关系,让某进程在指定的 CPU 核上运行,即是“绑核”。

taskset 的用法

显示进程运行的CPU

1
taskset -p pid

注意,此命令返回的是十六进制的,转换成二进制后,每一位对应一个逻辑 CPU,低位是 0 号CPU,依次类推。如果每个位置上是1,表示该进程绑定了该 CPU。例如,0101 就表示进程绑定在了 0 号和 3 号逻辑 CPU 上了

绑核设定

1
2
taskset -pc 3  pid    表示将进程pid绑定到第3个核上
taskset -c 3 command   表示执行 command 命令,并将 command 启动的进程绑定到第3个核上。

Android 中也可以使用这个系统调用,把任务绑定到某个核心上运行。部分较老的内核里面不支持 CPUset,就会用 taskset 来设置

调度算法

在 Linux 的调度算法中修改调度逻辑,也可以让指定的 task 跑在指定的核上面,部分厂家的核调度优化就是使用的这种方法,这里就不具体来讲了

锁频

正常情况下,CPU 的调度算法都可以满足日常的使用,但是在 Android 中的部分场景里面,单纯依靠调度器,可能会无法满足这个场景对性能的要求。比如说应用启动场景,如果让调度器去拉频率迁核,可能就会有一定的延迟,比如任务先在小核跑,发现小核频率不够,那就把小核频率往上拉,拉上去之后发现可能还是不够,经过几次一直拉到最高发现还是不够,然后把这个任务迁移到中核,频率也是一次一次拉,拉到最高发现还是不够,最好迁移到大核去做。这样一套下来,时间过去不少不说,启动速度也不是最快的

基于这种情况的考虑,系统中一般都会在这种特殊场景直接暴力拉核,将硬件资源直接拉到最高去运行,比如 CPU、GPU、IO、BUS 等;另外也会在某些场景把某些资源限制使用,比如发热太严重的时候,需要限制 CPU 的最高频率,来达到降温的目的;有时候基于功耗的考虑,也会限制一些资源在某些场景的使用

目前 Android 系统一般会在下面几个场景直接进行锁频(不同厂家也会自己定制)

  1. 应用启动
  2. 应用安装
  3. 转屏
  4. 窗口动画
  5. List Fling
  6. Game

以 高通平台为例,在 CPU Info 中我们也可以看到锁频的情况

CPU 状态

CPU info 中还有标识 CPU 状态的标记,如下图所示,CPU 状态有 0 ,1,2,3 这四种

之前的 CPU 支持热插拔,即不用的时候可以直接关闭,不过目前的 CPU 都不支持热插拔,而是使用 C-State

下面是摘抄的其他平台的支持 C0-C4 的处理器的状态和功耗状态,Android 中不同的平台表现不一致,大家可以做一下参考

  1. C0 状态(激活)
  2. 这是 CPU 最大工作状态,在此状态下可以接收指令和处理数据
  3. 所有现代处理器必须支持这一功耗状态
  4. C1 状态(挂起)
  5. 可以通过执行汇编指令“ HLT (挂起)”进入这一状态
  6. 唤醒时间超快!(快到只需 10 纳秒!)
  7. 可以节省 70% 的 CPU 功耗
  8. 所有现代处理器都必须支持这一功耗状态
  9. C2 状态(停止允许)
  10. 处理器时钟频率和 I/O 缓冲被停止
  11. 换言之,处理器执行引擎和 I/0 缓冲已经没有时钟频率
  12. 在 C2 状态下也可以节约 70% 的 CPU 和平台能耗
  13. 从 C2 切换到 C0 状态需要 100 纳秒以上
  14. C3 状态(深度睡眠)
  15. 总线频率和 PLL 均被锁定
  16. 在多核心系统下,缓存无效
  17. 在单核心系统下,内存被关闭,但缓存仍有效可以节省 70% 的 CPU 功耗,但平台功耗比 C2 状态下大一些
  18. 唤醒时间需要 50 微妙

Systrace 中的详细信息

Systrace 我们一般用 Chrome 打开,转换成图形化信息之后更加方便从整体去看,但其实 Systrace 也可以以文本的方式打开,也可以看到一些详细的信息。

比如下面就是一条标识 CPU 调度的 Message,解析的时候,里面的信息会被解析到各个模块

1
appEventThread-8193  [001] d..2 1638545.400415: sched_switch: prev_comm=appEventThread prev_pid=8193 prev_prio=97 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120

详细来看

1
2
3
4
5
appEventThread-8193    -- 标识 TASK-PID
[001] -- 标识是哪个 CPU ,这里是 cpu0
d..2 -- 这是四个位,每个位分别对应 irqs-off、need-resched、hardirq/softirq、preempt-depth
1638545.400415 -- 标识 delay TIMESTAMP
sched_switch ...到最后 -- 标识信息区,里面包含前一个任务描述,前一个任务的 pid,前一个任务的优先级 ,当前任务,当前任务 pid,当前任务优先级

另外里面仔细看也可以看到许多有趣的输出,可以加深对调度的理解

  1. sched_waking: comm=kworker/u16:4 pid=17373 prio=120 target_cpu=003
  2. sched_blocked_reason: pid=17373 iowait=0 caller=rpmh_write_batch+0x638/0x7d0
  3. cpu_idle: state=0 cpu_id=3
  4. softirq_raise: vec=6 [action=TASKLET]
  5. cpu_frequency_limits: min=1555200 max=1785600 cpu_id=0
  6. cpu_frequency_limits: min=710400 max=2419200 cpu_id=4
  7. cpu_frequency_limits: min=825600 max=2841600 cpu_id=7

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

参考

  1. 绑定CPU逻辑核心的利器——taskset
  2. CPU 电源状态

春笋

拍了张照片,觉得还不错,分享给大家

Android Systrace 基础知识 - Triple Buffer 解读

作者 Gracker
2019年12月15日 23:31

本文是 Systrace 系列文章的第十一篇,主要是对 Systrace 中的 Triple Buffer 进行简单介绍,简单介绍了如何在 Systrace 中判断卡顿情况的发生,进行初步的定位和分析,以及介绍 Triple Buffer 的引入对性能的影响

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

怎么定义掉帧?

Systrace 中可以看到应用的掉帧情况,我们经常看到说主线程超过 16.6 ms 就会掉帧,其实不然,这和我们这一篇文章讲到的 Triple Buffer 和一定的关系,一般来说,Systrace 中我们从 App 端和 SurfaceFlinger 端一起来判断掉帧情况

App 端判断掉帧

如果之前没有看过 Systrace 的话,仅仅从理论上来说,下面这个 Trace 中的应用是掉帧了,其主线程的绘制时间超过了 16.6ms ,但其实不一定,因为 BufferQueue 和 TripleBuffer 的存在,此时 BufferQueue 中可能还有上一帧或者上上一帧准备好的 Buffer,可以直接被 SurfaceFlinger 拿去做合成,当然也可能没有

所以从 Systrace 的 App 端我们是无法直接判断是否掉帧的,需要从 Systrace 里面的 SurfaceFlinger 端去看

SurfaceFlinger 端判断掉帧

SurfaceFlinger 端可以看到 SurfaceFlinger 主线程和合成情况和应用对应的 BufferQueue 中 Buffer 的情况。如上图,就是一个掉帧的例子。App 没有及时渲染完成,且此时 BufferQueue 中也没有前几帧的 Buffer,所以这一帧 SurfaceFlinger 没有合成对应 App 的 Layer,在用户看来这里就掉了一帧

而在第一张图中我们说从 App 端无法看出是否掉帧,那张图对应的 SurfaceFlinger 的 Trace 如下, 可以看到由于有 Triple Buffer 的存在, SF 这里有之前 App 的 Buffer,所以尽管 App 测一帧超过了 16.6 ms, 但是 SF 这里依然有可用来合成的 Buffer, 所以没有掉帧

SurfaceFlinger

逻辑掉帧

上面的掉帧我们是从渲染这边来看的,这种掉帧在 Systrace 中可以很容易就发现;还存在一种掉帧情况叫逻辑掉帧

逻辑掉帧指的是由于应用自己的代码逻辑问题,导致画面更新的时候,不是以均匀或者物理曲线的方式,而是出现跳跃更新的情况,这种掉帧一般在 Systrace 上没法看出来,但是用户在使用的时候可以明显感觉到

举一个简单的例子,比如说列表滑动的时候,如果我们滑动松手后列表的每一帧前进步长是一个均匀变化的曲线,最后趋近于 0,这样就是完美的;但是如果出现这一帧相比上一帧走了 20,下一帧相比这一帧走了 10,下下一帧相比下一帧走了 30,这种就是跳跃更新,在 Systrace 上每一帧都是及时渲染且 SurfaceFlinger 都及时合成的,但是用户用起来就是觉得会卡. 不过我列举的这个例子中,Android 已经针对这种情况做了优化,感兴趣的可以去看一下 android/view/animation/AnimationUtils.java 这个类,重点看下面三个方法的使用

1
2
3
public static void lockAnimationClock(long vsyncMillis)
public static void unlockAnimationClock()
public static long currentAnimationTimeMillis()

Android 系统的动画一般不会有这个问题,但是应用开发者就保不齐会写这种代码,比如做动画的时候根据**当前的时间(而不是 Vsync 到来的时间)**来计算动画属性变化的情况,这种情况下,一旦出现掉帧,动画的变化就会变得不均匀,感兴趣的可以自己思考一下这一块

另外 Android 出现掉帧情况的原因非常多,各位可以参考下面三篇文章食用:

  1. Android 中的卡顿丢帧原因概述 - 方法论
  2. Android 中的卡顿丢帧原因概述 - 系统篇
  3. Android 中的卡顿丢帧原因概述 - 应用篇

BufferQueue 和 Triple Buffer

BufferQueue

首先看一下 BufferQueue,BufferQueue 是一个生产者(Producer)-消费者(Consumer)模型中的数据结构,一般来说,消费者(Consumer) 创建 BufferQueue,而生产者(Producer) 一般不和 BufferQueue 在同一个进程里面

其运行逻辑如下

  1. 当生产者(Producer) 需要 Buffer 时,它通过调用 dequeueBuffer()并指定 Buffer 的宽度,高度,像素格式和使用标志,从 BufferQueue 请求释放 Buffer
  2. 生产者(Producer) 将填充缓冲区,并通过调用 queueBuffer()将缓冲区返回到队列。
  3. 消费者(Consumer) 使用 acquireBuffer()获取 Buffer 并消费 Buffer 的内容
  4. 使用完成后,消费者(Consumer)将通过调用 releaseBuffer()将 Buffer 返回到队列

Android 通过 Vsync 机制来控制 Buffer 在 BufferQueue 中的流动时机,如果对 Vsync 机制不了解,可以参考下面这两篇文章,看完后你会有个大概的了解

  1. Systrace 基础知识 - Vsync 解读
  2. Android 基于 Choreographer 的渲染机制详解

上面的流程比较抽象,这里举一个具体的例子,方便大家理解上面那张图,对后续了解 Systrace 中的 BufferQueue 也会有帮助。

在 Android App 的渲染流程里面,App 就是个生产者(Producer) ,而 SurfaceFlinger 是一个消费者(Consumer),所以上面的流程就可以翻译为

  1. App 需要 Buffer 时,它通过调用 dequeueBuffer()并指定 Buffer 的宽度,高度,像素格式和使用标志,从 BufferQueue 请求释放 Buffer
  2. App 可以用 cpu 进行渲染也可以调用用 gpu 来进行渲染,渲染完成后,通过调用 queueBuffer()将缓冲区返回到 App 对应的 BufferQueue(如果是 gpu 渲染的话,这里还有个 gpu 处理的过程)
  3. SurfaceFlinger 在收到 Vsync 信号之后,开始准备合成,使用 acquireBuffer()获取 App 对应的 BufferQueue 中的 Buffer 并进行合成操作
  4. 合成结束后,SurfaceFlinger 将通过调用 releaseBuffer()将 Buffer 返回到 App 对应的 BufferQueue

理解了 BufferQueue 的作用后,接下来来讲解一下 BufferQueue 中的 Buffer

从上面的图可以看到,BufferQueue 中的生产者和消费者通过 dequeueBuffer、queueBuffer、acquireBuffer、releaseBuffer 来申请或者释放 Buffer,那么 BufferQueue 中需要几个 Buffer 来进行运转呢?下面从单 Buffer,双 Buffer 和 Triple Buffer 的角度分析(注意这里只是从 Buffer 的角度来做分析的, 比如 App 测涉及到 Buffer 的是 RenderThread , 不过由于 RenderThread 与 MainThread 有一定的联系, 比如 unBlockUiThread 执行的时机, MainThread 也会因为 RenderThread 执行慢而被 Block 住)

Single Buffer

单 Buffer 的情况下,因为只有一个 Buffer 可用,那么这个 Buffer 既要用来做合成显示,又要被应用拿去做渲染

Single Buffer

理想情况下,单 Buffer 是可以完成任务的(有 Vsync-Offset 存在的情况下)

  1. App 收到 Vsync 信号,获取 Buffer 开始渲染
  2. 间隔 Vsync-Offset 时间后,SurfaceFlinger 收到 Vsync 信号,开始合成
  3. 屏幕刷新,我们看到合成后的画面

Single Buffer

但是很不幸,理想情况我们也就想一想,这期间如果 App 渲染或者 SurfaceFlinger 合成在屏幕显示刷新之前还没有完成,那么屏幕刷新的时候,拿到的 Buffer 就是不完整的,在用户看来,就有种撕裂的感觉

Single Buffer

当然 Single Buffer 已经没有在使用,上面只是一个例子

Double Buffer

Double Buffer 相当于 BufferQueue 中有两个 Buffer 可供轮转,消费者在消费 Buffer的同时,生产者也可以拿到备用的 Buffer 进行生产操作

Double Buffer

下面我们来看理想情况下,Double Buffer 的工作流程

DoubleBufferPipline_NoJank

但是 Double Buffer 也会存在性能上的问题,比如下面的情况,App 连续两帧生产都超过 Vsync 周期(准确的说是错过 SurfaceFlinger 的合成时机) ,就会出现掉帧情况

Double Buffer

Triple Buffer

Triple Buffer 中,我们又加入了一个 BackBuffer ,这样的话 BufferQueue 里面就有三个 Buffer 可以轮转了,当 FrontBuffer 在被使用的时候,App 有两个空闲的 Buffer 可以拿去生产,就算生产过程中有 GPU 超时,CPU 任然可以拿到新的 Buffer 进行生产(即 SurfaceFling 消费 FrontBuffer,GPU 使用一个 BackBuffer,CPU使用一个 BackBuffer)

Triple Buffer

下面就是引入 Triple Buffer 之后,解决了 Double Buffer 中遇到的由于 Buffer 不足引起的掉帧问题

TripleBufferPipline_NoJank

这里把两个图放到一起来看,方便大家做对比(一个是 Double Buffer 掉帧两次,一个是使用 Triple Buffer 只掉了一帧)

TripleBuffer_VS_DoubleBuffer

Triple Buffer 的作用

缓解掉帧

从上一节 Double Buffer 和 Triple Buffer 的对比图可以看到,在这种情况下(出现连续主线程超时),三个 Buffer 的轮转有助于缓解掉帧出现的次数(从掉帧两次 -> 只掉帧一次)

所以从第一节如何定义掉帧这里我们就知道,App 主线程超时不一定会导致掉帧,由于 Triple Buffer 的存在,部分 App 端的掉帧(主要是由于 GPU 导致),到 SurfaceFlinger 这里未必是掉帧,这是看 Systrace 的时候需要注意的一个点

缓解掉帧

减少主线程和渲染线程等待时间

双 Buffer 的轮转, App 主线程有时候必须要等待 SurfaceFlinger(消费者)释放 Buffer 后,才能获取 Buffer 进行生产,这时候就有个问题,现在大部分手机 SurfaceFlinger 和 App 同时收到 Vsync 信号,如果出现App 主线程等待 SurfaceFlinger(消费者)释放 Buffer ,那么势必会让 App 主线程的执行时间延后,比如下面这张图,可以明显看到:Buffer B 并不是在 Vsync 信号来的时候开始被消费(因为还在使用),而是等 Buffer A 被消费后,Buffer B 被释放,App 才能拿到 Buffer B 进行生产,这期间就有一定的延迟,会让主线程可用的时间变短

减少主线程和渲染线程等待时间

我们来看一下在 Systrace 中的上面这种情况发生的时候的表现

减少主线程和渲染线程等待时间

而 三个 Buffer 轮转的情况下,则基本不会有这种情况的发生,渲染线程一般在 dequeueBuffer 的时候,都可以顺利拿到可用的 Buffer (当然如果 dequeueBuffer 本身耗时那就不是这里的讨论范围了)

降低 GPU 和 SurfaceFlinger 瓶颈

这个比较好理解,双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现 SurfaceFlinger 无法及时合成而导致掉帧

在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间

比如下面两张图,就是对应的 SurfaceFlinger 和 App 的双 Buffer 掉帧情况,由于 SurfaceFlinger 本身就比较耗时(特定场景),而 App 的 dequeueBuffer 得不到及时的响应,导致发生了比较严重的掉帧情况。在换成 Triple Buffer 之后,这种情况就基本上没有了

Debug Triple Buffer

Dumpsys SurfaceFlinger

dumpsys SurfaceFlinger 可以查看 SurfaceFlinger 输出的众多当前的状态,比如一些性能指标、Buffer 状态、图层信息等,后续有篇幅的话可以单独拿出来讲,下面是截取的 Double Buffer 情况下和 Triple Buffer 情况下的各个 App 的 Buffer 使用情况,可以看到不同的 App,在负载不一样的情况下,对 Triple Buffer 的使用率是不一样的;Double Buffer 则完全使用的是双 Buffer

关闭 Triple Buffer

不同 Android 版本属性设置不一样(这是 Google 的一个逻辑 Bug,Android 10 上面已经修复了)

Android 版本 <= Android P

1
2
3
4
//控制代码
property_get("ro.sf.disable_triple_buffer", value, "1");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

修改对应的属性值,然后重启 Framework

1
2
3
4
//按顺序执行下面的语句(需要 Root 权限)
adb root
adb shell setprop ro.sf.disable_triple_buffer 0
adb shell stop && adb shell start

Android 版本 > Android P

1
2
3
4
//控制代码
property_get("ro.sf.disable_triple_buffer", value, "0");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

修改对应的属性值,然后重启 Framework

1
2
3
4
//按顺序执行下面的语句(需要 Root 权限)
adb root
adb shell setprop ro.sf.disable_triple_buffer 1
adb shell stop && adb shell start

参考

  1. https://source.android.google.cn/devices/graphics

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - Binder 和锁竞争解读

作者 Gracker
2019年12月6日 19:12

本文是 Systrace 系列文章的第十篇,主要是对 Systrace 中的 Binder 和锁信息进行简单介绍,简单介绍了 Binder 的情况,介绍了 Systrace 中 Binder 通信的表现形式,以及 Binder 信息查看,SystemServer 锁竞争分析等

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Binder 概述

Android 的大部分进程间通信都使用 Binder,这里对 Binder 不做过多的解释,想对 Binder 的实现有一个比较深入的了解的话,推荐你阅读下面三篇文章

  1. 理解Android Binder机制1/3:驱动篇
  2. 理解Android Binder机制2/3:C++层
  3. 理解Android Binder机制3/3:Java层

之所以要单独讲 Systrace 中的 Binder 和锁,是因为很多卡顿问题和响应速度的问题,是因为跨进程 binder 通信的时候,锁竞争导致 binder 通信事件变长,影响了调用端。最常见的就是应用渲染线程 dequeueBuffer 的时候 SurfaceFlinger 主线程阻塞导致 dequeueBuffer 耗时,从而导致应用渲染出现卡顿; 或者 SystemServer 中的 AMS 或者 WMS 持锁方法等待太多, 导致应用调用的时候等待时间比较长导致主线程卡顿

这里放一张文章里面的 Binder 架构图 , 本文主要是以 Systrace 为主,所以会讲 Systrace 中的 Binder 表现,不涉及 Binder 的实现

Binder 调用图例

Binder 主要是用来跨进程进行通信,可以看下面这张图,简单显示了在 Systrace 中 ,Binder 通信是如何显示的

图中主要是 SystemServer 进程和 高通的 perf 进程通信,Systrace 中右上角 ViewOption 里面勾选 Flow Events 就可以看到 Binder 的信息

点击 Binder 可以查看其详细信息,其中有的信息在分析问题的时候可以用到,这里不做过多的描述

对于 Binder,这里主要介绍如何在 Systrace 中查看 Binder 锁信息锁等待这两个部分,很多卡顿和响应问题的分析,都离不开这两部分信息的解读,不过最后还是要回归代码,找到问题后,要读源码来理顺其代码逻辑,以方便做相应的优化工作

Systrace 显示的锁的信息

monitor contention with owner Binder:1605_B (4667) at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733) waiters=2 blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)

上面的话分两段来看,以 blocking 为分界线 

第一段信息解读

monitor contention with owner Binder:1605_B (4667) at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733) waiters=2

Monitor 指的是当前锁对象的池,在 Java 中,每个对象都有两个池,锁(monitor)池和等待池:

锁池(同步队列 SynchronizedQueue ):假设线程 A 已经拥有了某个对象(注意:不是类 )的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中。

这里用了争夺(contention)这个词,意思是这里由于在和目前对象的锁正被其他对象(Owner)所持有,所以没法得到该对象的锁的拥有权,所以进入该对象的锁池

Owner : 指的是当前拥有这个对象的锁的对象。这里是 Binder:1605_B,4667 是其线程 ID。

at 后面跟的是拥有这个对象的锁的对象正在做什么。这里是在执行 void com.android.server.wm.ActivityTaskManagerService.activityPaused 这个方法,其代码位置是 :ActivityTaskManagerService.java:1733 其对应的代码如下:

com/android/server/wm/ActivityTaskManagerService.java

1
2
3
4
5
6
7
8
9
10
11
@Override
public final void activityPaused(IBinder token) {
final long origId = Binder.clearCallingIdentity();
synchronized (mGlobalLock) { // 1733 是这一行
ActivityStack stack = ActivityRecord.getStackLocked(token);
if (stack != null) {
stack.activityPausedLocked(token, false);
}
}
Binder.restoreCallingIdentity(origId);
}

可以看到这里 synchronized (mGlobalLock) ,获取了 mGlobalLock 锁的拥有权,在他释放这个对象的锁之前,任何其他的调用 synchronized (mGlobalLock) 的地方都得在锁池中等待

waiters 值得是锁池里面正在等待锁的操作的个数;这里 waiters=2 表示目前锁池里面已经有一个操作在等待这个对象的锁释放了,加上这个的话就是 3 个了

第二段信息解读

blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)

第二段信息相对来说简单一些,就是标识了当前被阻塞等锁的方法 , 这里是 ActivityManager 的 getFocusedStackInfo 被阻塞,其对应的代码

com/android/server/wm/ActivityTaskManagerService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public ActivityManager.StackInfo getFocusedStackInfo() throws RemoteException {
enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "getStackInfo()");
long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) { // 2064 是这一行
ActivityStack focusedStack = getTopDisplayFocusedStack();
if (focusedStack != null) {
return mRootActivityContainer.getStackInfo(focusedStack.mStackId);
}
return null;
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}

可以看到这里也是调用了 synchronized (ActivityManagerService.this) ,从而需要等待获取 ams 对象的锁拥有权

总结

上面这段话翻译过来就是

ActivityTaskManagerService 的 getFocusedStackInfo 方法在执行过程中被阻塞,原因是因为执行同步方法块的时候,没有拿到同步对象的锁的拥有权;需要等待拥有同步对象的锁拥有权的另外一个方法 ActivityTaskManagerService.activityPaused 执行完成后,才能拿到同步对象的锁的拥有权,然后继续执行

可以对照原文看上面的翻译

monitor contention with owner Binder:1605_B (4667)
at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733)
waiters=2
blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)

等锁分析

还是上面那个 Systrace,Binder 信息里面显示 waiters=2,意味着前面还有两个操作在等锁释放,也就是说总共有三个操作都在等待 Binder:1605_B (4667) 释放锁,我们来看一下 Binder:1605_B 的执行情况

从上图可以看到,Binder:1605_B 正在执行 activityPaused,中间也有一些其他的 Binder 操作,最终 activityPaused 执行完成后,释放锁

下面我们就把这个逻辑里面的执行顺序理顺,包括两个 waiters

锁等待

上图中可以看到 mGlobalLock 这个对象锁的争夺情况

  1. Binder_1605_B 首先开始执行 activityPaused,这个方法中是要获取 mGlobalLock 对象锁的,由于此时 mGlobalLock 没有竞争,所以 activityPaused 获取对象锁之后开始执行
  2. android.display 线程开始执行 checkVisibility 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态
  3. android.anim 线程开始执行 relayoutWindow 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态
  4. android.bg 线程开始执行 getFocusedStackInfo 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态

经过上面四步,就形成了 Binder_1605_B 线程在运行,其他三个争夺 mGlobalLock 对象锁失败的线程分别进入 sleep 状态,等待 Binder_1605_B 执行结束后释放 mGlobalLock 对象锁

锁释放

上图可以看到 mGlobalLock 锁的释放和后续的流程

  1. Binder_1605_B 线程的 activityPaused 执行结束,mGlobalLock 对象锁释放
  2. 第一个进入等待的 android.display 线程开始执行 checkVisibility 方法 ,这里从 android.display 线程的唤醒信息可以看到,是被 Binder_1605_B(4667) 唤醒的
  3. android.display 线程的 checkVisibility 执行结束,mGlobalLock 对象锁释放
  4. 第二个进入等待的 android.anim 线程开始执行 relayoutWindow 方法 ,这里从 android.anim 线程的唤醒信息可以看到,是被 android.display(1683) 唤醒的
  5. android.anim 线程的 relayoutWindow 执行结束,mGlobalLock 对象锁释放
  6. 第三个进入等待的 android.bg 线程开始执行 getFocusedStackInfo 方法 ,这里从 android.bg 线程的唤醒信息可以看到,是被 android.anim(1684) 唤醒的

经过上面 6 步,这一轮由于 mGlobalLock 对象锁引起的等锁现象结束。这里只是一个简单的例子,在实际情况下,SystemServer 中的 BInder 等锁情况会非常严重,经常 waiter 会到达 7 - 10 个,非常恐怖,比如下面这种:

这也就可以解释为什么 Android 手机 App 安装多了、用的久了之后,系统就会卡的一个原因;另外重启后也会有短暂的时候出现这种情况

如果不知道怎么查看唤醒信息,可以查看: Systrace中查看进程信息唤醒 这篇文章

相关代码

Monitor 信息

art/runtime/monitor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string Monitor::PrettyContentionInfo(const std::string& owner_name,
pid_t owner_tid,
ArtMethod* owners_method,
uint32_t owners_dex_pc,
size_t num_waiters) {
Locks::mutator_lock_->AssertSharedHeld(Thread::Current());
const char* owners_filename;
int32_t owners_line_number = 0;
if (owners_method != nullptr) {
TranslateLocation(owners_method, owners_dex_pc, &owners_filename, &owners_line_number);
}
std::ostringstream oss;
oss << "monitor contention with owner " << owner_name << " (" << owner_tid << ")";
if (owners_method != nullptr) {
oss << " at " << owners_method->PrettyMethod();
oss << "(" << owners_filename << ":" << owners_line_number << ")";
}
oss << " waiters=" << num_waiters;
return oss.str();
}

Block 信息

art/runtime/monitor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (ATRACE_ENABLED()) {
if (owner_ != nullptr) { // Did the owner_ give the lock up?
std::ostringstream oss;
std::string name;
owner_->GetThreadName(name);
oss << PrettyContentionInfo(name,
owner_->GetTid(),
owners_method,
owners_dex_pc,
num_waiters);
// Add info for contending thread.
uint32_t pc;
ArtMethod* m = self->GetCurrentMethod(&pc);
const char* filename;
int32_t line_number;
TranslateLocation(m, pc, &filename, &line_number);
oss << " blocking from "
<< ArtMethod::PrettyMethod(m) << "(" << (filename != nullptr ? filename : "null")
<< ":" << line_number << ")";
ATRACE_BEGIN(oss.str().c_str());
started_trace = true;
}
}

参考

  1. 理解Android Binder机制1/3:驱动篇
  2. 理解Android Binder机制2/3:C++层
  3. 理解Android Binder机制3/3:Java层

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - Vsync 解读

作者 Gracker
2019年12月1日 06:38

本文是 Systrace 系列文章的第七篇,主要是是介绍 Android 中的 Vsync 机制。文章会从 Systrace 的角度来看 Android 系统如何基于 Vsync 每一帧的展示。Vsync 是 Systrace 中一个非常关键的机制,虽然我们在操作手机的时候看不见,摸不着,但是在 Systrace 中我们可以看到,Android 系统在 Vsync 信号的指引下,有条不紊地进行者每一帧的渲染、合成操作,使我们可以享受稳定帧率的画面。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC,HWC 可生成 VSYNC 事件并通过回调将事件发送到 SurfaceFlinge , DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号

Android 基于 Choreographer 的渲染机制详解 这篇文章里面,我们有提到 :Choreographer 的引入,主要是配合 Vsync,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms,Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用

渲染层(App)与 Vsync 打交道的是 Choreographer,而合成层与 Vsync 打交道的,则是 SurfaceFlinger。SurfaceFlinger 也会在 Vsync 到来的时候,将所有已经准备好的 Surface 进行合成操作

下图显示在 Systrace 中,SurfaceFlinger 进程中的 VSYNC_APP 和 VSYNC_SF 的情况

Android 图形数据流向

首先我们要大概了解 Android 中的图形数据流的方向,从下面这张图,结合 Android 的图像流,我们大概把从 App 绘制到屏幕显示,分为下面几个阶段:

  1. 第一阶段:App 在收到 Vsync-App 的时候,在主线程进行 measure、layout、draw(构建 DisplayList , 里面包含 OpenGL 渲染需要的命令及数据) 。这里对应的 Systrace 中的主线程 doFrame 操作
  2. 第二阶段:CPU 将数据上传(共享或者拷贝)给 GPU, 这里 ARM 设备 内存一般是 GPU 和 CPU 共享内存。这里对应的 Systrace 中的渲染线程的 flush drawing commands 操作
  3. 第三阶段:通知 GPU 渲染,真机一般不会阻塞等待 GPU 渲染结束,CPU 通知结束后就返回继续执行其他任务,使用 Fence 机制辅助 GPU CPU 进行同步操作
  4. 第四 阶段:swapBuffers,并通知 SurfaceFlinger 图层合成。这里对应的 Systrace 中的渲染线程的 eglSwapBuffersWithDamageKHR 操作
  5. 第五阶段:SurfaceFlinger 开始合成图层,如果之前提交的 GPU 渲染任务没结束,则等待 GPU 渲染完成,再合成(Fence 机制),合成依然是依赖 GPU,不过这就是下一个任务了.这里对应的 Systrace 中的 SurfaceFlinger 主线程的 onMessageReceived 操作(包括 handleTransaction、handleMessageInvalidate、handleMessageRefresh)SurfaceFlinger 在合成的时候,会将一些合成工作委托给 Hardware Composer,从而降低来自 OpenGL 和 GPU 的负载,只有 Hardware Composer 无法处理的图层,或者指定用 OpenGL 处理的图层,其他的 图层偶会使用 Hardware Composer 进行合成
  6. 第六阶段 :最终合成好的数据放到屏幕对应的 Frame Buffer 中,固定刷新的时候就可以看到了

下面这张图也是官方的一张图,结合上面的阶段,从左到右看,可以看到一帧的数据是如何在各个进程之间流动的

Systrace 中的图像数据流

了解了 Android 中的图形数据流的方向,我们就可以把上面这个比较抽象的数据流图,在 Systrace 上进行映射展示

上图中主要包含 SurfaceFlinger、App 和 hwc 三个进程,下面就来结合图中的标号,来进一步说明数据的流向

  1. 第一个 Vsync 信号到来, SurfaceFlinger 和 App 同时收到 Vsync 信号
  2. SurfaceFlinger 收到 Vsync-sf 信号,开始进行 App 上一帧的 Buffer 的合成
  3. App 收到 Vsycn-app 信号,开始进行这一帧的 Buffer 的渲染(对应上面的第一、二、三、四阶段)
  4. 第二个 Vsync 信号到来 ,SurfaceFlinger 和 App 同时收到 Vsync 信号,SurfaceFlinger 获取 App 在第二步里面渲染的 Buffer,开始合成(对应上面的第五阶段),App 收到 Vsycn-app 信号,开始新一帧的 Buffer 的渲染(对应上面的第一、二、三、四阶段)

Vsync Offset

文章最开始有提到,Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC,HWC 可生成 VSYNC 事件并通过回调将事件发送到 SurfaceFlinge , DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号.

disp_sync_arch

其中 app 和 sf 相对 hw_vsync_0 都有一个偏移,即 phase-app 和 phase-sf,如下图

Vsync Offset 我们指的是 VSYNC_APP 和 VSYNC_SF 之间有一个 Offset,即上图中 phase-sf - phase-app 的值,这个 Offset 是厂商可以配置的。如果 Offset 不为 0,那么意味着 App 和 SurfaceFlinger 主进程不是同时收到 Vsync 信号,而是间隔 Offset (通常在 0 - 16.6ms 之间)

目前大部分厂商都没有配置这个 Offset,所以 App 和 SurfaceFlinger 是同时收到 Vsync 信号的.

可以通过 Dumpsys SurfaceFlinger 来查看对应的值

Offset 为 0:(sf phase - app phase = 0)

1
2
3
4
5
6
Sync configuration: [using: EGL_ANDROID_native_fence_sync EGL_KHR_wait_sync]
DispSync configuration:
app phase 1000000 ns, sf phase 1000000 ns
early app phase 1000000 ns, early sf phase 1000000 ns
early app gl phase 1000000 ns, early sf gl phase 1000000 ns
present offset 0 ns refresh 16666666 ns

Offset 不为 0 (SF phase - app phase = 4 ms)

1
2
3
4
5
6
7
Sync configuration: [using: EGL_ANDROID_native_fence_sync EGL_KHR_wait_sync]

VSYNC configuration:
         app phase:   2000000 ns         SF phase:   6000000 ns
   early app phase:   2000000 ns   early SF phase:   6000000 ns
GL early app phase:   2000000 nsGL early SF phase:   6000000 ns
    present offset:         0 ns     VSYNC period:  16666666 ns

下面以 Systrace 为例,来看 Offset 在 Systrace 中的表现

Offset 为 0

首先说 Offset 为 0 的情况, 此时 App 和 SurfaceFlinger 是同时收到 Vsync 信号 , 其对应的 Systrace 图如下:

这个图上面也有讲解,这里就不再详细说明,大家只需要看到,App 渲染好的 Buffer,要等到下一个 Vsync-SF 来的时候才会被 SurfaceFlinger 拿去做合成,这个时间大概在 16.6 ms。这时候大家可能会想,如果 App 的 Buffer 渲染结束,Swap 到 BufferQueue 中 ,就触发 SurfaceFlinger 去做合成,那岂不是省了一些时间(0-16.6ms )?

答案是可行的,这也就引入了 Offset 机制,在这种情况下,App 先收到 Vsync 信号,进行一帧的渲染工作,然后过了 Offset 时间后,SurfaceFlinger 才收到 Vsync 信号开始合成,这时候如果 App 的 Buffer 已经 Ready 了,那么 SurfaceFlinger 这一次合成就可以包含 App 这一帧,用户也会早一点看到。

Offset 不为 0

下图中,就是一个 Offset 为 4ms 的案例,App 收到 Vsync 4 ms 之后,SurfaceFlinger 才收到 Vsync 信号

Offset 的优缺点

Offset 的一个比较难以确定的点就在于 Offset 的时间该如何设置,这也是众多厂商默认都不进行配置 Offset 的一个原因,其优缺点是动态的,与机型的性能和使用场景有很大的关系

  1. 如果 Offset 配置过短,那么可能 App 收到 Vsync-App 后还没有渲染完成,SurfaceFlinger 就收到 Vsync-SF 开始合成,那么此时如果 App 的 BufferQueue 中没有之前累积的 Buffer,那么 SurfaceFlinger 这次合成就不会有 App 的东西在里面,需要等到下一个 Vsync-SF 才能合成这次 App 的内容,时间相当于变成了 Vsync 周期+Offset,而不是我们期待的 Offset
  2. 如果 Offset 配置过长,就起不到作用了

HW_Vsync

这里需要说明的是,不是每次申请 Vsync 都会由硬件产生 Vsync,只有此次请求 vsync 的时间距离上次合成时间大于 500ms,才会通知 hwc,请求 HW_VSYNC

以桌面滑动为例,看 SurfaceFlinger 的进程 Trace 可以看到 HW_VSYNC 的状态

后续 App 申请 Vsync 时候,会有两种情况,一种是有 HW_VSYNC 的情况,一种是没有有 HW_VSYNC 的情况

不使用HW_VSYNC

使用 HW_VSYNC

HW_VSYNC 主要是利用最近的硬件 VSYNC 来做预测,最少要 3 个,最多是 32 个,实际上要用几个则不一定, DispSync 拿到 6 个 VSYNC 后就会计算出 SW_VSYNC,只要收到的 Present Fence 没有超过误差,硬件 VSYNC 就会关掉,不然会继续接收硬件 VSYNC 计算 SW_VSYNC 的值,直到误差小于 threshold.关于这一块的计算具体过程,可以参考这篇文章: S W-VS YN C 的生成与传递 ,关于这一块的流程大家也可以参考这篇文章,里面有更细节的内容,这里摘录了他的结论

SurfaceFlinger 通过实现了 HWC2::ComposerCallback 接口,当 HW-VSYNC 到来的时候,SurfaceFlinger 将会收到回调并且发给 DispSync。DispSync 将会把这些 HW-VSYNC 的时间戳记录下来,当累计了足够的 HW-VSYNC 以后(目前是大于等于 6 个),就开始计算 SW-VSYNC 的偏移 mPeriod。计算出来的 mPeriod 将会用于 DispSyncThread 用来模拟 HW-VSYNC 的周期性起来并且通知对 VSYNC 感兴趣的 Listener,这些 Listener 包括 SurfaceFlinger 和所有需要渲染画面的 app。这些 Listener 通过 EventThread 以 Connection 的抽象形式注册到 EventThread。DispSyncThread 与 EventThread 通过 DispSyncSource 作为中间人进行连接。EventThread 在收到 SW-VSYNC 以后将会把通知所有感兴趣的 Connection,然后 SurfaceFlinger 开始合成,app 开始画帧。在收到足够多的 HW-VSYNC 并且在误差允许的范围内,将会关闭通过 EventControlThread 关闭 HW-VSYNC。

本文其他地址

待更新

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

参考

  1. VSYNC
  2. https://juejin.im/post/5b6948086fb9a04fb87771fb
  3. http://gityuan.com/2017/02/05/graphic_arch/
  4. SW-VSYNC 的生成与传递
  5. http://echuang54.blogspot.com/2015/01/dispsync.html

Android Systrace 基础知识 - MainThread 和 RenderThread 解读

作者 Gracker
2019年11月6日 17:11

本文是 Systrace 系列文章的第九篇,主要是是介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程渲染线程。文章会从 Systrace 的角度来看 MainThread 和 RenderThread 的工作流程,以及涉及到的相关知识:卡顿、软件渲染、掉帧计算等

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识

正文

这里以滑动列表为例 ,我们截取主线程和渲染线程一帧的工作流程(每一帧都会遵循这个流程,不过有的帧需要处理的事情多,有的帧需要处理的事情少) ,重点看 “UI Thread ” 和 RenderThread 这两行

这张图对应的工作流程如下

  1. 主线程处于 Sleep 状态,等待 Vsync 信号
  2. Vsync 信号到来,主线程被唤醒,Choreographer 回调 FrameDisplayEventReceiver.onVsync 开始一帧的绘制
  3. 处理 App 这一帧的 Input 事件(如果有的话)
  4. 处理 App 这一帧的 Animation 事件(如果有的话)
  5. 处理 App 这一帧的 Traversal 事件(如果有的话)
  6. 主线程与渲染线程同步渲染数据,同步结束后,主线程结束一帧的绘制,可以继续处理下一个 Message(如果有的话,IdleHandler 如果不为空,这时候也会触发处理),或者进入 Sleep 状态等待下一个 Vsync
  7. 渲染线程首先需要从 BufferQueue 里面取一个 Buffer(dequeueBuffer) , 进行数据处理之后,调用 OpenGL 相关的函数,真正地进行渲染操作,然后将这个渲染好的 Buffer 还给 BufferQueue (queueBuffer) , SurfaceFlinger 在 Vsync-SF 到了之后,将所有准备好的 Buffer 取出进行合成(这个流程在讲 SurfaceFlinger 的时候会提到)

上面这个流程在 Android 基于 Choreographer 的渲染机制详解 这篇文章里面已经介绍的很详细了,包括每一帧的 doFrame 都在做什么、卡顿计算的原理、APM 相关. 没有看过这篇文章的同学,建议先去扫一眼

那么这篇文章我们主要从 Android 基于 Choreographer 的渲染机制详解 这篇文章没有讲到的几个点来入手,帮你更好地理解主线程和渲染线程

  1. 主线程的发展
  2. 主线程的创建
  3. 渲染线程的创建
  4. 主线程和渲染线程的分工
  5. 游戏的主线程与渲染线程
  6. Flutter 的主线程和渲染线程

主线程的创建

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 是主线程,其实他只是主线程的一个逻辑处理单元。

ActivityThread 的创建

App 进程 fork 出来之后,回到 App 进程,查找 ActivityThread 的 Main函数

com/android/internal/os/ZygoteInit.java

1
2
3
4
5
static final Runnable childZygoteInit(
int targetSdkVersion, String[] argv, ClassLoader classLoader) {
RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv);
return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader);
}

这里的 startClass 就是 ActivityThread,找到之后调用,逻辑就到了 ActivityThread的main函数

android/app/ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
//1. 初始化 Looper、MessageQueue
Looper.prepareMainLooper();
// 2. 初始化 ActivityThread
ActivityThread thread = new ActivityThread();
// 3. 主要是调用 AMS.attachApplicationLocked,同步进程信息,做一些初始化工作
thread.attach(false, startSeq);
// 4. 获取主线程的 Handler,这里是 H ,基本上 App 的 Message 都会在这个 Handler 里面进行处理
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
// 5. 初始化完成,Looper 开始工作
Looper.loop();
}

注释里面都很清楚,这里就不详细说了,main 函数处理完成之后,主线程就算是正式上线开始工作,其 Systrace 流程如下:

ActivityThread 的功能

另外我们经常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class H extends Handler { //摘抄了部分
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
public static final int RECEIVER = 113;
public static final int CREATE_SERVICE = 114;
public static final int STOP_SERVICE = 116;
public static final int BIND_SERVICE = 121;
public static final int UNBIND_SERVICE = 122;
public static final int DUMP_SERVICE = 123;
public static final int REMOVE_PROVIDER = 131;
public static final int DISPATCH_PACKAGE_BROADCAST = 133;
public static final int DUMP_PROVIDER = 141;
public static final int UNSTABLE_PROVIDER_DIED = 142;
public static final int INSTALL_PROVIDER = 145;
public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
}

可以看到,进程创建、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,然后进到具体的 handleXXX 

渲染线程的创建和发展

主线程讲完了我们来讲渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工作都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分之前主线程的渲染工作,减轻主线程的负担

软件绘制

我们一般提到的硬件加速,指的就是 GPU 加速,这里可以理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个

1
android:hardwareAccelerated="false"

我们就可以关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,直接 cpu 调用 libSkia 来进行渲染。其 Systrace 的表现如下

与这篇文章开头的开了硬件加速的那个图对比,可以看到主线程由于要进行渲染工作,所以执行的时间变长了,也更容易出现卡顿,同时帧与帧直接的空闲间隔也变短了,使得其他 Message 的执行时间被压缩

硬件加速绘制

正常情况下,硬件加速是开启的,主线程的 draw 函数并没有真正的执行 drawCall ,而是把要 draw 的内容记录到 DIsplayList 里面,同步到 RenderThread 中,一旦同步完成,主线程就可以被释放出来做其他的事情,RenderThread 则继续进行渲染工作

渲染线程初始化

渲染线程初始化在真正需要 draw 内容的时候,一般我们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,如果没有则去进行初始化

android/view/ViewRootImpl.java

1
2
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);

后续直接调用 draw

android/graphics/HardwareRenderer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();

updateRootDisplayList(view, callbacks);

if (attachInfo.mPendingAnimatingRenderNodes != null) {
final int count = attachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
registerAnimatingRenderNode(
attachInfo.mPendingAnimatingRenderNodes.get(i));
}
attachInfo.mPendingAnimatingRenderNodes.clear();
attachInfo.mPendingAnimatingRenderNodes = null;
}

int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
attachInfo.mViewRootImpl.mSurface.release();
attachInfo.mViewRootImpl.invalidate();
}
if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
attachInfo.mViewRootImpl.invalidate();
}
}

上面的 draw 只是更新 DIsplayList ,更新结束后,调用 syncAndDrawFrame ,通知渲染线程开始工作,主线程释放。渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui

frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

1
2
3
int RenderProxy::syncAndDrawFrame() {
return mDrawFrameTask.drawFrame();
}

关于 RenderThread 的工作流程这里就不细说了,后续会有专门的篇幅来讲解这个,目前 hwui 这一块的流程也有很多优秀的文章,大家可以对照文章和源码来看,其核心流程在 Systrace 上的表现如下:

主线程和渲染线程的分工

主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DIsplayList ,但是不涉及 SurfaceFlinger 打交道;渲染线程负责渲染渲染相关的工作,一部分工作也是 CPU 来完成的,一部分操作是调用 OpenGL 函数来完成的

当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类,这样间接的进行绘制操作的优点如下

  1. DisplayList 可以按需多次绘制而无须同业务逻辑交互
  2. 特定的绘制操作(如 translation, scale 等)可以作用于整个 DisplayList 而无须重新分发绘制操作
  3. 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次
  4. 可以将对 DisplayList 的处理转移至另一个线程(也就是 RenderThread)
  5. 主线程在 sync 结束后可以处理其他的 Message,而不用等待 RenderThread 结束

RenderThread 的具体流程大家可以看这篇文章 : http://www.cocoachina.com/articles/35302

游戏的主线程与渲染线程

游戏大多使用单独的渲染线程,有单独的 Surface ,直接跟 SurfaceFlinger 进行交互,其主线程的存在感比较低,绝大部分的逻辑都是自己在自己的渲染线程里面实现的。

大家可以看一下王者荣耀对应的 Systrace ,重点看应用进程和 SurfaceFlinger 进程(30fps)

可以看到王者荣耀主线程的主要工作,就是把 Input 事件传给 Unity 的渲染线程,渲染线程收到 Input 事件之后,进行逻辑处理,画面更新等。

Flutter 的主线程和渲染线程

这里提一下 Flutter App 在 Systrace 上的表现,由于 Flutter 的渲染是基于 libSkia 的,所以它也没有 RenderThread ,而是他自建的 RenderEngine , Flutter 比较重要的两个线程是 ui 线程和 gpu 线程,对应到下面提到的  Framework 和 Engine 两层

Flutter 中也会监听 Vsync 信号 ,其 VsyncView 中会以 postFrameCallback 的形式,监听 doFrame 回调,然后调用 nativeOnVsync ,将 Vsync 到来的信息传给 Flutter UI 线程,开始一帧的绘制。

可以看到 Flutter 的思路跟游戏开发的思路差不多,不依赖具体的平台,自建渲染管道,更新快,跨平台优势明显。

Flutter SDK 自带 Skia 库,不用等系统升级就可以用到最新的 Skia 库,而且 Google 团队在 Skia 上做了很多优化,所以官方号称性能可以媲美原生应用

Flutter 的框架分为 Framework 和 Engine 两层,应用是基于 Framework 层开发的,Framework 负责渲染中的 Build,Layout,Paint,生成 Layer 等环节。Engine 层是 C++实现的渲染引擎,负责把 Framework 生成的 Layer 组合,生成纹理,然后通过 Open GL 接口向 GPU 提交渲染数据。

当需要更新 UI 的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework,然后 Framework 会进行 animations, build,layout,compositing,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后通过 Open Gl 接口提交数据给 GPU,GPU 经过处理后在显示器上面显示。整个流程如下图:

性能

如果主线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。主线程执行超时通常会带来两个问题

  1. 卡顿:如果主线程 + 渲染线程每一帧的执行都超过 16.6ms(60fps 的情况下),那么就可能会出现掉帧。
  2. 卡死:如果界面线程被阻塞超过几秒钟时间(根据组件不同 , 这里的阈值也不同),用户会看到 “应用无响应” (ANR) 对话框(部分厂商屏蔽了这个弹框,会直接 Crash 到桌面)

对于用户来说,这两个情况都是用户不愿意看到的,所以对于 App 开发者来说,两个问题是发版本之前必须要解决的,ANR 这个由于有详细的调用栈,所以相对来说比较好定位;但是间歇性卡顿这个,可能就需要使用工具来进行分析了:Systrace + TraceView,所以理解主线程和渲染线程的关系和他们的工作原理是非常重要的,这也是本系列的一个初衷

另外关于卡顿,可以参考下面三篇文章,你的 App 卡顿不一定是你 App 的问题,也有可能是系统的问题,不过不管怎么说,首先要会分析卡顿问题。

  1. Android 中的卡顿丢帧原因概述 - 方法论
  2. Android 中的卡顿丢帧原因概述 - 系统篇
  3. Android 中的卡顿丢帧原因概述 - 应用篇

参考

  1. https://juejin.im/post/5a9e01c3f265da239d48ce32
  2. http://www.cocoachina.com/articles/35302
  3. https://juejin.im/post/5b7767fef265da43803bdc65
  4. http://gityuan.com/2019/06/15/flutter_ui_draw/
  5. https://developer.android.google.cn/guide/components/processes-and-threads

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可

点此链接下载文章所涉及到的 Systrace 附件

本文其他地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - MainThread 和 RenderThread 解读

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - Input 解读

作者 Gracker
2019年11月4日 09:38

本文是 Systrace 系列文章的第六篇,主要是对 Systrace 中的 Input 进行简单介绍,介绍其 Input 的流程; Systrace 中 Input 信息的体现 ,以及如何结合 Input 信息,分析与 Input 相关的问题

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

Android 基于 Choreographer 的渲染机制详解 这篇文章中,我有讲到,Android App 的主线程运行的本质是靠 Message 驱动的,这个 Message 可以是循环动画、可以是定时任务、可以是其他线程唤醒,不过我们最常见的还是 Input Message ,这里的 Input 是以 InputReader 这里的分类,不仅包含触摸事件(Down、Up、Move) , 可包含 Key 事件(Home Key 、 Back Key) . 这里我们着重讲的是触摸事件

由于 Android 系统在 Input 链上加了一些 Trace 点,且这些 Trace 点也比较完善,部分厂家可能会自己加一些,不过我们这里以标准的 Trace 点来讲解,这样不至于你换了个手机抓的 Trace 就不一样了

Input 在 Android 中的地位是很高的,我们在玩手机的时候,大部分应用的滑动、跳转这些都依靠 Input 事件来驱动,后续我会专门写一篇文章,来介绍 Android 中基于 Input 的运行机制。这里是从 Systrace 的角度来看 Input 。看下面的流程之前,脑子里先有个关于 Input 的大概处理流程,这样看的时候,就可以代入:

  1. 触摸屏每隔几毫秒扫描一次,如果有触摸事件,那么把事件上报到对应的驱动
  2. InputReader 读取触摸事件交给 InputDispatcher 进行事件派发
  3. InputDispatcher 将触摸事件发给注册了 Input 事件的 App
  4. App 拿到事件之后,进行 Input 事件分发,如果此事件分发的过程中,App 的 UI 发生了变化,那么会请求 Vsync,则进行一帧的绘制

另外在看 Systrace 的时候,要牢记 Systrace 中时间是从左到右流逝的,也就是说如果你在 Systrace 上画一条竖直线,那么竖直线左边的事件永远比右边的事件先发生,这也是我们分析源码流程的一个基石。我希望大家在看基于 Systrace 的源码流程分析之后,脑子里有一个图形化的、立体的流程图,你跟的代码走到哪一步了在图形你在脑中可以快速定位出来

Input in Systrace

下面这张图是一个概览图,以滑动桌面为例 (滑动桌面包括一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件,这些事件和事件流都会在 Systrace 上有所体现,这也是我们分析 Systrace 的一个重要的切入点),主要牵扯到的模块是 SystemServer 和 App 模块,其中用蓝色标识的是事件的流动信息,红色的是辅助信息。

InputReaderInputDispatcher 是跑在 SystemServer 里面的两个 Native 线程,负责读取和分发 Input 事件,我们分析 Systrace 的 Input 事件流,首先是找到这里。下面针对上图中标号进行简单说明

  1. InputReader 负责从 EventHub 里面把 Input 事件读取出来,然后交给 InputDispatcher 进行事件分发
  2. InputDispatcher 在拿到 InputReader 获取的事件之后,对事件进行包装和分发 (也就是发给对应的)
  3. OutboundQueue 里面放的是即将要被派发给对应 AppConnection 的事件
  4. WaitQueue 里面记录的是已经派发给 AppConnection 但是 App 还在处理没有返回处理成功的事件
  5. PendingInputEventQueue 里面记录的是 App 需要处理的 Input 事件,这里可以看到已经到了应用进程
  6. deliverInputEvent 标识 App UI Thread 被 Input 事件唤醒
  7. InputResponse 标识 Input 事件区域,这里可以看到一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件的处理阶段都被算到了这里
  8. App 响应 Input 事件 : 这里是滑动然后松手,也就是我们熟悉的桌面滑动的操作,桌面随着手指的滑动更新画面,松手后触发 Fling 继续滑动,从 Systrace 就可以看到整个事件的流程

下面以第一个 Input_Down 事件的处理流程来进行详细的工作流说明,其他的 Move 事件和 Up 事件的处理是一样的(部分不一样,不过影响不大)

InputDown 事件在 SystemServer 的工作流

放大 SystemServer 的部分,可以看到其工作流(蓝色),滑动桌面包括 Input_Down + 若干个 Input_Move + Input_Up ,我们这里看的是 Input_Down 这个事件

InputDown 事件在 App 的工作流

应用在收到 Input 事件后,有时候会马上去处理 (没有 Vsync 的情况下),有时候会等 Vsync 信号来了之后去处理,这里 Input_Down 事件就是直接去唤醒主线程做处理,其 Systrace 比较简单,最上面有个 Input 事件队列,主线程则是简单的处理

App 的 Pending 队列

主线程处理 Input 事件

主线程处理 Input 事件这个大家比较熟悉,从下面的调用栈可以看到,Input 事件传到了 ViewRootImpl,最终到了 DecorView ,然后就是大家熟悉的 Input 事件分发机制

关键知识点和流程

从上面的 Systrace 来看,Input 事件的基本流向如下:

  1. InputReader 读取 Input 事件
  2. InputReader 将读取的 Input 事件放到 InboundQueue 中
  3. InputDispatcher 从 InboundQueue 中取出 Input 事件派发到各个 App(连接) 的 OutBoundQueue
  4. 同时将事件记录到各个 App(连接) 的 WaitQueue
  5. App 接收到 Input 事件,同时记录到 PendingInputEventQueue ,然后对事件进行分发处理
  6. App 处理完成后,回调 InputManagerService 将负责监听的 WaitQueue 中对应的 Input 移除

通过上面的流程,一次 Input 事件就被消耗掉了(当然这只是正常情况,还有很多异常情况、细节处理,这里就不细说了,自己看相关流程的时候可以深挖一下) , 那么本节就从上面的关键流中取几个重要的知识点讲解(部分流程和图参考和拷贝了 Gityuan 的博客的图,链接在最下面参考那一节)

InputReader

InputReader 是一个 Native 线程,跑在 SystemServer 进程里面,其核心功能是从 EventHub 读取事件、进行加工、将加工好的事件发送到 InputDispatcher

InputReader Loop 流程如下

  1. getEvents:通过 EventHub (监听目录 /dev/input )读取事件放入 mEventBuffer ,而mEventBuffer 是一个大小为256的数组, 再将事件 input_event 转换为 RawEvent 
  2. processEventsLocked: 对事件进行加工, 转换 RawEvent -> NotifyKeyArgs(NotifyArgs) 
  3. QueuedListener->flush:将事件发送到 InputDispatcher 线程, 转换 NotifyKeyArgs -> KeyEntry(EventEntry)

核心代码 loopOnce 处理流程如下:

InputReader 核心 Loop 函数 loopOnce 逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    std::vector<InputDeviceInfo> inputDevices;
    { // acquire lock
......
//获取输入事件、设备增删事件,count 为事件数量
    size_t count = mEventHub ->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    {
......
        if (count) {//处理事件
            processEventsLocked(mEventBuffer, count);
        }

    }
......
    mQueuedListener->flush();//将事件传到 InputDispatcher,这里getListener 得到的就是 InputDispatcher
}

InputDispatcher

上面的 InputReader 调用 mQueuedListener->flush 之后 ,将 Input 事件加入到InputDispatcher 的 mInboundQueue ,然后唤醒 InputDispatcher , 从 Systrace 的唤醒信息那里也可以看到 InputDispatch 线程是被 InputReader 唤醒的

InputDispatcher 的核心逻辑如下:

  1. dispatchOnceInnerLocked(): 从 InputDispatcher 的 mInboundQueue 队列,取出事件 EventEntry。另外该方法开始执行的时间点 (currentTime) 便是后续事件 dispatchEntry 的分发时间 (deliveryTime)
  2. dispatchKeyLocked():满足一定条件时会添加命令 doInterceptKeyBeforeDispatchingLockedInterruptible;
  3. enqueueDispatchEntryLocked():生成事件 DispatchEntry 并加入 connection 的 outbound 队列
  4. startDispatchCycleLocked():从 outboundQueue 中取出事件 DispatchEntry, 重新放入 connection 的 waitQueue 队列;
  5. InputChannel.sendMessage 通过 socket 方式将消息发送给远程进程;
  6. runCommandsLockedInterruptible():通过循环遍历的方式,依次处理 mCommandQueue 队列中的所有命令。而 mCommandQueue 队列中的命令是通过 postCommandLocked() 方式向该队列添加的。

其核心处理逻辑在 dispatchOnceInnerLocked 这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    // Ready to start a new event.
    // If we don't already have a pending event, go grab one.
    if (! mPendingEvent) {
        if (mInboundQueue.isEmpty()) {
        } else {
            // Inbound queue has at least one entry.
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();
        }

        // Poke user activity for this event.
        if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
            pokeUserActivityLocked(mPendingEvent);
        }

        // Get ready to dispatch the event.
        resetANRTimeoutsLocked();
    }
    case EventEntry::TYPE_MOTION: {
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);
        break;
    }

    if (done) {
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;
        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

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 中的事件,从 Systrace 中看就是对应 App 的 WaitQueue 减少

如果主线程发生卡顿,那么 Input 事件没有及时被消耗,也会在 WaitQueue 这里体现出来,如下图:

整体逻辑

图来自 Gityuan 博客

Input 刷新与 Vsync

Input 的刷新取决于触摸屏的采样,目前比较多的屏幕采样率是 120Hz 和 160Hz ,对应就是 8ms 采样一次或者 6.25ms 采样一次,我们来看一下其在 Systrace 上的展示

可以看到上图中, InputReader 每隔 6.25ms 就可以读上来一个数据,交给 InputDispatcher 去分发给 App ,那么是不是屏幕采样率越高越好呢?也不一定,比如上面那张图,虽然 InputReader 每隔 6.25ms 就可以读上来一个数据给 InputDispatcher 去分发给 App ,但是从 WaitQueue 的表现来看,应用并没有消耗这个 Input 事件,这是为什么呢?

原因在于应用消耗 Input 事件的时机是 Vsync 信号来了之后,刷新率为 60Hz 的屏幕,一般系统也是 60 fps ,也就是说两个 Vsync 的间隔在 16.6ms ,这期间如果有两个或者三个 Input 事件,那么必然有一个或者两个要被抛弃掉,只拿最新的那个。也就是说:

  1. 在屏幕刷新率和系统 FPS 都是 60 的时候,盲目提高触摸屏的采样率,是没有太大的效果的,反而有可能出现上面图中那样,有的 Vsync 周期中有两个 Input 事件,而有的 Vsync 周期中有三个 Input 事件,这样造成事件不均匀,可能会使 UI 产生抖动
  2. 在屏幕刷新率和系统 FPS 都是 60 的时候,使用 120Hz 采样率的触摸屏就可以了
  3. 如果在屏幕刷新率和系统 FPS 都是 90 的时候 ,那么 120Hz 采样率的触摸屏显然不够用了,这时候应该采用 180Hz 采样率的屏幕

Input 调试信息

Dumpsys Input 主要是 Debug 用,我们也可以来看一下其中的一些关键信息,到时候遇到了问题也可以从这里面找 , 其命令如下:

1
adb shell dumpsys input

其中的输出比较多,我们终点截取 Device 信息、InputReader、InputDispatcher 三段来看就可以了

Device 信息

主要是目前连接上的 Device 信息,下面摘取的是 touch 相关的

1
2
3
4
5
6
7
8
9
10
11
12
13
    3: main_touch
      Classes: 0x00000015
      Path: /dev/input/event6
      Enabled: true
      Descriptor: 4055b8a032ccf50ef66dbe2ff99f3b2474e9eab5
      Location: main_touch/input0
      ControllerNumber: 0
      UniqueId: 
      Identifier: bus=0x0000, vendor=0xbeef, product=0xdead, version=0x28bb
      KeyLayoutFile: /system/usr/keylayout/main_touch.kl
      KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
      ConfigurationFile: 
      HaveKeyboardLayoutOverlay: false

Input Reader 状态

InputReader 这里就是当前 Input 事件的一些展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  Device 3: main_touch
    Generation: 24
    IsExternal: false
    HasMic:     false
    Sources: 0x00005103
    KeyboardType: 1
    Motion Ranges:
      X: source=0x00005002, min=0.000, max=1079.000, flat=0.000, fuzz=0.000, resolution=0.000
      Y: source=0x00005002, min=0.000, max=2231.000, flat=0.000, fuzz=0.000, resolution=0.000
      PRESSURE: source=0x00005002, min=0.000, max=1.000, flat=0.000, fuzz=0.000, resolution=0.000
      SIZE: source=0x00005002, min=0.000, max=1.000, flat=0.000, fuzz=0.000, resolution=0.000
      TOUCH_MAJOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOUCH_MINOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOOL_MAJOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOOL_MINOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
    Keyboard Input Mapper:
      Parameters:
        HasAssociatedDisplay: false
        OrientationAware: false
        HandlesKeyRepeat: false
      KeyboardType: 1
      Orientation: 0
      KeyDowns: 0 keys currently down
      MetaState: 0x0
      DownTime: 521271703875000
    Touch Input Mapper (mode - direct):
      Parameters:
        GestureMode: multi-touch
        DeviceType: touchScreen
        AssociatedDisplay: hasAssociatedDisplay=true, isExternal=false, displayId=''
        OrientationAware: true
      Raw Touch Axes:
        X: min=0, max=1080, flat=0, fuzz=0, resolution=0
        Y: min=0, max=2232, flat=0, fuzz=0, resolution=0
        Pressure: min=0, max=127, flat=0, fuzz=0, resolution=0
        TouchMajor: min=0, max=512, flat=0, fuzz=0, resolution=0
        TouchMinor: unknown range
        ToolMajor: unknown range
        ToolMinor: unknown range
        Orientation: unknown range
        Distance: unknown range
        TiltX: unknown range
        TiltY: unknown range
        TrackingId: min=0, max=65535, flat=0, fuzz=0, resolution=0
        Slot: min=0, max=20, flat=0, fuzz=0, resolution=0
      Calibration:
        touch.size.calibration: geometric
        touch.pressure.calibration: physical
        touch.orientation.calibration: none
        touch.distance.calibration: none
        touch.coverage.calibration: none
      Affine Transformation:
        X scale: 1.000
        X ymix: 0.000
        X offset: 0.000
        Y xmix: 0.000
        Y scale: 1.000
        Y offset: 0.000
      Viewport: displayId=0, orientation=0, logicalFrame=[0, 0, 1080, 2232], physicalFrame=[0, 0, 1080, 2232], deviceSize=[1080, 2232]
      SurfaceWidth: 1080px
      SurfaceHeight: 2232px
      SurfaceLeft: 0
      SurfaceTop: 0
      PhysicalWidth: 1080px
      PhysicalHeight: 2232px
      PhysicalLeft: 0
      PhysicalTop: 0
      SurfaceOrientation: 0
      Translation and Scaling Factors:
        XTranslate: 0.000
        YTranslate: 0.000
        XScale: 0.999
        YScale: 1.000
        XPrecision: 1.001
        YPrecision: 1.000
        GeometricScale: 0.999
        PressureScale: 0.008
        SizeScale: 0.002
        OrientationScale: 0.000
        DistanceScale: 0.000
        HaveTilt: false
        TiltXCenter: 0.000
        TiltXScale: 0.000
        TiltYCenter: 0.000
        TiltYScale: 0.000
      Last Raw Button State: 0x00000000
      Last Raw Touch: pointerCount=1
        [0]: id=0, x=660, y=1338, pressure=44, touchMajor=44, touchMinor=44, toolMajor=0, toolMinor=0, orientation=0, tiltX=0, tiltY=0, distance=0, toolType=1, isHovering=false
      Last Cooked Button State: 0x00000000
      Last Cooked Touch: pointerCount=1
        [0]: id=0, x=659.389, y=1337.401, pressure=0.346, touchMajor=43.970, touchMinor=43.970, toolMajor=43.970, toolMinor=43.970, orientation=0.000, tilt=0.000, distance=0.000, toolType=1, isHovering=false
      Stylus Fusion:
        ExternalStylusConnected: false
        External Stylus ID: -1
        External Stylus Data Timeout: 9223372036854775807
      External Stylus State:
        When: 9223372036854775807
        Pressure: 0.000000
        Button State: 0x00000000
        Tool Type: 0

InputDispatcher 状态

InputDispatch 这里的重要信息主要包括

  1. FocusedApplication :当前获取焦点的应用
  2. FocusedWindow : 当前获取焦点的窗口
  3. TouchStatesByDisplay
  4. Windows :所有的 Window
  5. MonitoringChannels :Window 对应的 Channel
  6. Connections :所有的连接
  7. AppSwitch: not pending
  8. Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Input Dispatcher State:
  DispatchEnabled: 1
  DispatchFrozen: 0
  FocusedApplication: name='AppWindowToken{ac6ec28 token=Token{a38a4b ActivityRecord{7230f1a u0 com.meizu.flyme.launcher/.Launcher t13}}}', dispatchingTimeout=5000.000ms
  FocusedWindow: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}'
  TouchStatesByDisplay:
    0: down=true, split=true, deviceId=3, source=0x00005002
      Windows:
        0: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', pointerIds=0x80000000, targetFlags=0x105
        1: name='Window{8cb8f7 u0 com.android.systemui.ImageWallpaper}', pointerIds=0x0, targetFlags=0x4102
  Windows:
    2: name='Window{ba2fc6b u0 NavigationBar}', displayId=0, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x21840068, type=0x000007e3, layer=0, frame=[0,2136][1080,2232], scale=1.000000, touchableRegion=[0,2136][1080,2232], inputFeatures=0x00000000, ownerPid=26514, ownerUid=10033, dispatchingTimeout=5000.000ms
    3: name='Window{72b7776 u0 StatusBar}', displayId=0, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x81840048, type=0x000007d0, layer=0, frame=[0,0][1080,84], scale=1.000000, touchableRegion=[0,0][1080,84], inputFeatures=0x00000000, ownerPid=26514, ownerUid=10033, dispatchingTimeout=5000.000ms
    9: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', displayId=0, paused=false, hasFocus=true, hasWallpaper=true, visible=true, canReceiveKeys=true, flags=0x81910120, type=0x00000001, layer=0, frame=[0,0][1080,2232], scale=1.000000, touchableRegion=[0,0][1080,2232], inputFeatures=0x00000000, ownerPid=27619, ownerUid=10021, dispatchingTimeout=5000.000ms
  MonitoringChannels:
    0: 'WindowManager (server)'
  RecentQueue: length=10
    MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (524.5, 1306.4)]), policyFlags=0x62000000, age=61.2ms
    MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (543.5, 1309.4)]), policyFlags=0x62000000, age=54.7ms
  PendingEvent: <none>
  InboundQueue: <empty>
  ReplacedKeys: <empty>
  Connections:
    0: channelName='WindowManager (server)', windowName='monitor', status=NORMAL, monitor=true, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    5: channelName='72b7776 StatusBar (server)', windowName='Window{72b7776 u0 StatusBar}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    6: channelName='ba2fc6b NavigationBar (server)', windowName='Window{ba2fc6b u0 NavigationBar}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    12: channelName='3c007ad com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher (server)', windowName='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: length=3
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (634.4, 1329.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=17.4ms, wait=16.8ms
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (647.4, 1333.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=11.1ms, wait=10.4ms
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (659.4, 1337.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=5.2ms, wait=4.6ms
  AppSwitch: not pending
  Configuration:
    KeyRepeatDelay: 50.0ms
    KeyRepeatTimeout: 500.0ms

参考

本文部分图文参考和拷贝自下面几篇文章,同时下面几篇文章讲解了 Input 流程的细节部分,推荐大家在看完这篇文章后,如果对代码细节感兴趣,可以仔细研读下面这几篇非常棒的文章。

  1. http://gityuan.com/2016/12/11/input-reader/
  2. http://gityuan.com/2016/12/10/input-manager/
  3. http://gityuan.com/2016/12/17/input-dispatcher/
  4. https://zhuanlan.zhihu.com/p/29386642

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件

本文其他地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - Input 解读

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android 桌面被杀问题分析案例

作者 Gracker
2019年9月17日 18:58

写这篇文章的契机是因为一个实际遇到的问题 , 这个问题其实不难 , 不过在分析了这个问题然后写日记的时候 , 我突然觉得这个问题分析的过程有必要记录一下 , 分享给大家 . 分析过程中有用到一些工具 , 一些方法 , 也从另外一个聪明的小伙伴梅明那里学到了一些分析技巧和工具的使用技巧 .

这篇文章中分析过程包括我之前在Android 中的卡顿丢帧原因概述 - 方法论 里面提到的一些工具 , 包括 : 复现视频 \ Event Log \ Android Studio 源码和 App Debug \ Android Studio Profile \ Systrace \ Dumpsys \ PS 等 . 大多数工具大家都在开发过程中使用过 , 这次分析正是使用了这些工具相互配合 , 最终找到的问题的原因.

大家看下来可能会觉得 , 这么简单一个问题还需要写一篇文章 ? 我写这篇文章的目的一是为了记录给自己 , 二是觉得分析过程比较有普遍性 , 包括分析思路和工具的使用 , 如果可以帮助到大家 , 那么最好不过了 , 如果你也有好的思路或者独家调试技巧 , 欢迎大家扫描关于我 里面的讨论群二维码加入群聊 , 共同进步!

现象

这个问题是测试直接报过来的 , Bug 描述是典型的按现象描述 : “从应用返回桌面 , 桌面图标加载慢“. 测试这边提供了录制的视频和抓取的 Log , 以及对应的 Systrace 等. 既然现象和 Log 都在 , 那么就开始分析吧.

分析过程

确定问题发生的时间点

  1. 由于测试提供的复现视频 , 首先看复现视频 , 确定时间发生的时间
  2. 根据视频里面的大概时间(精确到分) , 查看对应的 EventLog ,跟视频比对,确定发生的确切时间点 (精确到秒)
  3. 查看 EventLog 和 MainLog , 还原发生时候的用户操作 ,这个例子里面就发现启动和我信这个 App 之后,Launcher 被杀了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
EventLog
// 启动 com.jx.cmcc.ict.ibelieve 这个 App
09-10 10:14:48.877 1456 2269 I am_set_resumed_activity: [0,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity,resumeTopActivityInnerLocked]
09-10 10:14:48.886 1456 2269 I am_resume_activity: [0,80317506,54938,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity]
09-10 10:14:48.891 1456 1485 I sysui_count: [window_time_0,0]
09-10 10:14:48.891 1456 1485 I sysui_multi_action: [757,803,799,window_time_0,802,0]
09-10 10:14:48.902 1456 2269 I am_uid_stopped: 10021

// 这里桌面被杀
09-10 10:14:48.903 1456 2269 I am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

// 这里开始从 App 返回桌面
09-10 10:14:51.990 1456 1791 I am_pause_activity: [0,80317506,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity]
09-10 10:14:51.994 1456 1791 I am_task_to_front: [0,54923]
09-10 10:14:51.996 13674 13674 I am_on_paused_called: [0,com.jx.cmcc.ict.ibelieve.ui.MainTabActivity,handlePauseActivity]
09-10 10:14:52.013 1456 2270 I am_uid_running: 10021

// 重新创建桌面进程
09-10 10:14:52.025 1456 2270 I am_proc_start: [0,14013,10021,com.meizu.flyme.launcher,activity,com.meizu.flyme.launcher/.Launcher]
09-10 10:14:52.045 1456 2270 I am_proc_bound: [0,14013,com.meizu.flyme.launcher]
09-10 10:14:52.069 1456 2270 I am_uid_active: 10021
09-10 10:14:52.069 1456 2270 I am_restart_activity: [0,238217861,54923,com.meizu.flyme.launcher/.Launcher]

// 桌面显示
09-10 10:14:52.071 1456 2270 I am_set_resumed_activity: [0,com.meizu.flyme.launcher/.Launcher,minimalResumeActivityLocked]
09-10 10:14:52.335 14013 14013 I am_on_resume_called: [0,com.meizu.flyme.launcher.Launcher,LAUNCH_ACTIVITY]
09-10 10:14:52.437 1456 1504 I am_activity_launch_time: [0,238217861,com.meizu.flyme.launcher/.Launcher,413,413]

那么这里就可以简单还原问题了 , 测试报的是从应用返回桌面 , 桌面图标加载慢 , 从 Event Log 来看 , 桌面显示慢 , 是因为桌面被杀了 , 所以从 App 返回的时候 , 桌面需要重新加载 , 从桌面进程创建到桌面完全显示 , 花费了 413ms(实际到桌面完全显示,花费了至少 2s 左右,因为 Launcher 冷启动还要重新加载内容).

分析被杀原因

从上面的分析来看 , 我们需要找到 Launcher 被杀的原因 , 从现象上来看 , 似乎是和 com.jx.cmcc.ict.ibelieve 这个进程有关系 , 但是我们目前是没有办法确认的 .

这里我们重点看这个这个 Event Log

1
am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

这里可以看到 Launcher 被杀的原因是 kill background , 查看对应的源码可知,reason = kill background 是 AMS.killBackgroundProcesses 这里发出的.

ActivityManagerService.killBackgroundProcesses

1
2
3
4
5
6
7
public void killBackgroundProcesses(final String packageName, int userId) {
......
synchronized (this) {
killPackageProcessesLocked(packageName, appId, targetUserId,
ProcessList.SERVICE_ADJ, false, true, true, false, "kill background");
}
}

对源码比较熟悉的同学可以很快知道 , AMS.killBackgroundProcesses 这个接口会提供给三方应用去调用 , 其 Binder 的客户端在 ActivityManager.killBackgroundProcesses 这里

ActivityManager.killBackgroundProcesses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Have the system immediately kill all background processes associated
* with the given package. This is the same as the kernel killing those
* processes to reclaim memory; the system will take care of restarting
* these processes in the future as needed.
*
* @param packageName The name of the package whose processes are to
* be killed.
*/
@RequiresPermission(Manifest.permission.KILL_BACKGROUND_PROCESSES)
public void killBackgroundProcesses(String packageName) {
try {
getService().killBackgroundProcesses(packageName,
mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

对 SystemServer 进程进行断点 Debug

知道了上面的代码逻辑 , 我们需要做的就是找到在这个场景下 , 是哪个应用调用 ActivityManager.killBackgroundProcesses 杀掉了桌面. 由于不知道具体是哪个应用(这里虽然我们怀疑是 com.jx.cmcc.ict.ibelieve , 但是没有证据) , 我们先对 SystemServer 进程进行 Debug .

1.首先对源码进行 debug , 首先在 Android 中点击 debug 按钮 , 选择 system_process 这个进程(就是我们所说的 SystemServer) , 然后点击 OK . 代码的断点我们打在上面列出的 ActivityManagerService.killBackgroundProcesses 方法里面.

2.点击启动怀疑的 App ( 可以从 EventLog 和视频里面倒推,找到比较可疑的 App , 安装后进行本地测试复现 , 这里选择了视频中出现的几个应用,包括我们之前怀疑的 com.jx.cmcc.ict.ibelieve- 和我信 ) , 点击其他的应用都不会进入到这个断点, 而在点击 和我信 这个 App 启动后走到的断点

3.这里我们可以看到调用栈是一个 Binder 调用 , 我们需要找到这个 Binder 调用的客户端. 在 AS 里面继续操作 , 点击如下图的计算器按钮 , 输入 getRealCallingPid() 点击下面的 Evaluate , 就可以看到结果. result = 29771

4.通过 PS 命令 , 查看这个 pid 对应的 app

可以看到就是这个应用调用的 killBackgroundProcesses

对 App 进程进行断点 Debug

为了进一步调查,我们对这个 app 进行 debug , 由于没有源码,我们直接把断点打到 android/app/ActivityManager.killBackgroundProcesses 这里(因为这里是客户端代码 , 所以调试 App 进程的时候 , 可以直接打断点 )

本地安装这个应用进行调试, 发现登录后,再次启动, 桌面必会被杀 ,确定就是这个 App 的问题

到了这一步我们已经基本上确定问题就是这个 App 引起的了 , 不过如果我们想看比较详细的调用情况 , 可以使用 Android Studio Profile

使用 Android Studio Profiler 工具

打开 Android Studio , 点击 Profiler 按钮 , 点击 + 号 , 选择 com.jx.cmcc.ict.ibelieve 这个进程 , 然后点击 CPU 这一栏

这里选择了 Trace Java Methods , 然后点击旁边的 Record , 就可以开始进行操作 , 操作结束后 , 点击 Stop , AS 会自动开始解析.

解析结果我们可以看这里

最下面就是刚刚操作所对应的详细函数调用栈 , 以真正运行的顺序展示在我们面前(我经常会用这个工具来查看源码逻辑和三方应用的代码逻辑 , 不管是学习还是解决问题 , 都是一个非常好的方法)

我们使用 ctrl+f 进行搜索 killBackgroundProcesses , 如果有的话 , 会以高亮显示, 我们只需要用鼠标放大就可以看到详细的调用栈

可以看到这个 App 在 loadComplete 回调里面执行了 killBackground 方法.(到了这里,应用开发者就已经知道是哪里的问题了,修复起来飞快)

权限问题分析

如上面所示 , 调用 killBackgroundProcesses 是需要Manifest.permission.KILL_BACKGROUND_PROCESSES 这个权限的 .

1
2
3
@RequiresPermission(Manifest.permission.KILL_BACKGROUND_PROCESSES)
public void killBackgroundProcesses(String packageName) {
}

执行 adb shell dumpsys package com.jx.cmcc.ict.ibelieve 查看 com.jx.cmcc.ict.ibelieve 这个进程所申请的权限 , 发现这个应用在安装的时候就申请了KILL_BACKGROUND_PROCESSES 这个权限 , 且默认是授予的.

1
2
3
4
5
6
install permissions:
......
android.permission.ACCESS_NETWORK_STATE: granted=true
android.permission.KILL_BACKGROUND_PROCESSES: granted=true
android.permission.WRITE_USER_DICTIONARY: granted=true
......

对应的权限级别为 normal

也就是说 , 所有的第三方应用都可以默认有这个权限 , 只要你申请 . 这个案例里面 , 就是因为这个 App 申请了这个权限 , 且执行了错误的行为 , 导致把桌面杀掉 , 严重影响用户体验. Sad !

Systrace 工具可以找出来 Kill 桌面的元凶么?

由于经常使用 Systrace , 那么 Systrace 是否可以找到元凶呢? 答案是可以 (这里如果对如何在 Systrace 上查看唤醒信息不了解 , 可以看一下这篇文章分析 Systrace 预备知识). 我们录制一段 Systrace , 安装下面的顺序去看

1.首先看 system_server 进程对应的 trace ,找到 killProcessGroup 对应的点 , 查看其唤醒情况 , 可以看到是 19688 这个线程唤醒执行 AMS 的 killProcessGroup

在 Systrace 中搜索 19688 , 可以看到是 Binder:1295_1E , 1295 就是 SystemServer

查看对应的 Binder:1295_1E , 看看是哪个线程唤醒这个线程

搜索 7289这个线程 , 可以看到这个线程就是和我信这个 App 的主线程。

查看 7289 , 确定就是 com.jx.cmcc.ict.ibelieve 这个进程 . 也就是和我信这个 App(瘤子).

这里也可以反推出来这个 Kill 是 和我信 这个 App 发起的 , 进一步确认可以使用上面 AS 的 MethodTrace

总结

从上面的分析来看 , 这个问题是由于应用申请了不恰当的权限并错误使用对应的函数导致的一个严重影响用户使用的问题. 一般分析到这一步 , 我们的工作就基本上结束了 , 后续只需要和商店沟通 , 跟 App 开发者联系进行修改即可.

不过令我感到意外的是 android.permission.KILL_BACKGROUND_PROCESSES 这个权限 Google 居然放的这么松 , 我一直以为这个权限是要专门申请以防止 App 滥用或者卵用的(毕竟涉及到其他 App 的生死存亡).

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 框架问题分析案例 - 谁杀了桌面?

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

❌
❌