普通视图

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

Android Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问

作者 Gracker
2021年4月24日 19:55

不同的人对流畅性(卡顿掉帧)有不同的理解,对卡顿阈值也有不同的感知,所以有必要在开始这个系列文章之前,先把涉及到的内容说清楚,防止出现不同的理解,也方便大家带着问题去看这几篇问题,下面是一些基本的说明

  1. 对手机用户来说,卡顿包含了很多场景,比如在 滑动列表的时候掉帧应用启动白屏过长点击电源键亮屏慢界面操作没有反应然后闪退点击图标没有响应窗口动画不连贯、滑动不跟手、重启手机进入桌面卡顿 等场景,这些场景跟我们开发人员所理解的卡顿还有点不一样,开发人员会更加细分去分析这些问题,这是开发人员和用户之间的一个认知差异,这一点在处理用户(或者测试人员)的问题反馈的时候尤其需要注意
  2. 对开发人员来说,上面的场景包括了 流畅度(滑动列表的时候掉帧、窗口动画不连贯、重启手机进入桌面卡顿)、响应速度(应用启动白屏过长、点击电源键亮屏慢、滑动不跟手)、稳定性(界面操作没有反应然后闪退、点击图标没有响应)这三个大的分类。之所以这么分类,是因为每一种分类都有不太一样的分析方法和步骤,快速分辨问题是属于哪一类很重要
  3. 在技术上来说,流畅度、响应速度、稳定性(ANR)这三类之所以用户感知都是卡顿,是因为这三类问题产生的原理是一致的,都是由于主线程的 Message 在执行任务的时候超时,根据不同的超时阈值来进行划分而已,所以要理解这些问题,需要对系统的一些基本的运行机制有一定的了解,本文会介绍一些基本的运行机制
  4. 流畅性这个系列主要是分析流畅度相关的问题,响应速度和稳定性会有专门的文章介绍,在理解了流畅性相关的内容之后,再去分析响应速度和稳定性问题会事半功倍
  5. 流畅性这个系列主要是讲如何使用 Systrace (Perfetto) 工具去分析,之所以 Systrace 为切入点,是因为影响流畅度的因素很多,有 App 自身的原因、也有系统的原因。而 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 篇

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

Systrace 的 Frame 颜色是什么意思?

这里的 Frame 标记指的是应用主线程上面那个圈,共有三个颜色,每一帧的耗时不同,则标识的颜色不同

点击这个小圆圈就可以看到这一帧所对应的主线程+渲染线程(会以高亮显示,其他的则变灰显示)

绿帧

绿帧是最常见的帧,表示这一帧在一个 Vsync 周期里面完成

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2022.png

黄帧

黄帧表示这一帧耗时超过1个 Vsync 周期,但是小于 2 个 Vsync 周期。黄帧的出现表示这一帧可能存在性能问题,可能会导致卡顿情况出现

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2023.png

红帧

红帧表示这一帧耗时超过 2 个 Vsync 周期,红帧的出现表示这一帧可能存在性能问题,大概率会导致卡顿情况出现

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2024.png

没有红帧就没有掉帧?

不一定,判断是否掉帧要看 SurfaceFlinger,而不是看 App ,这部分需要有 https://www.androidperformance.com/2019/12/15/Android-Systrace-Triple-Buffer/ 这篇文章的基础

出现黄帧但是不掉帧的情况

如上所述,红帧和黄帧都表示这一帧存在性能问题,黄帧表示这一帧耗时超过一个 Vsync 周期,但是由于 Android Triple Buffer(现在的高帧率手机会配置更多的 Buffer)的存在,就算 App 主线程这一帧超过一个 Vsync 周期,也会由于多 Buffer 的缓冲,使得这一帧并不会出现掉帧

出现黄帧且掉帧的情况

这次分析的 Systrace(见附件),就是没有红帧只有黄帧,连续出现两个黄帧,第一个黄帧导致了卡顿,而第二个黄帧则没有

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2025.png

主线程为何要等待渲染线程?

还是这个 Systrace(附件) 中的情况,第一个疑点处两个黄帧,可以看到第二个黄帧的主线程耗时很久,这时候不能单纯以为是主线程的问题(因为是 Sleep 状态)

如下图所示,是因为前一帧的渲染线程超时,导致这一帧的渲染线程任务在排队等待,如(https://www.androidperformance.com/2019/11/06/Android-Systrace-MainThread-And-RenderThread/)这篇文章里面的流程,主线程是需要等待渲染线程执行完 syncFrameState 之后 unblockMainThread,然后才能继续。

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2026.png

为什么一直滑动不松手,就不会卡?

还是这个场景(桌面左右滑动),卡顿是发生在松手之后的,如果一直不松手,那么就不会出现卡顿,这是为什么?

如下图,可以看到,如果不松手,cpu 这里会有一个持续的 Boost,且此时 RenderThread 的任务都跑在 4-6 这三个大核上面,没有跑到小核,自然也不会出现卡顿情况

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2027.png

这一段 Boost 的 Timeout 是 120 ms,具体的配置每个机型都不一样,熟悉 PerfLock 的应该知道,这里就不多说了

如果不卡,怎么衡量性能好坏?

如果这个场景不卡,那么我们怎么衡量两台不同的机器在这个场景下的性能呢?

可以使用 adb shell dumpsys gfxinfo ,使用方法如下

  1. 首先确定要测试的包名,到 App 界面准备好操作
  2. 执行2-3次 adb shell dumpsys gfxinfo com.miui.home framestats reset ,这一步的目的是清除历数据
  3. 开始操作(比如使用命令行左右滑动,或者自己用手指滑动)
  4. 操作结束后,执行 adb shell dumpsys gfxinfo com.miui.home framestats 这时候会有一堆数据输出,我们只需要关注其中的一部分数据即可
  5. 重点关注
    1. Janky frames :超过 Vsync 周期的 Frame,不一定出现卡顿
    2. 95th percentile :95% 的值
    3. HISTOGRAM : 原始数值
    4. PROFILEDATA :每一帧的详细原始数据

我们拿这个场景,跟 Oppo Reno 5 来做对比,只取我们关注的一部分数据

小米 - 90 fps

xiaomi

Oppo - 90 fps

oppo

下面是一些对比,可以看到小米在桌面滑动这个场景,性能是要弱于 Oppo 的

  1. Janky frames
    1. 小米:27 (35.53%)
    2. Oppo:1 (1.11%)
  2. 95th percentile
    1. 小米:18ms
    2. Oppo:5ms

另外 GPU 的数据也比较有趣,小米的高通 865 配的 GPU 要比 Reno 5 Pro 配的 GPU 要强很多,所以 GPU 的数据小米要比 Reno 5 Pro 要好,也可以推断出这个场景的瓶颈在 CPU 而不是在 GPU

为什么录屏看不出来卡顿?

可能有下面几种情况

  1. 如果使用的手机是大于 60 fps 的,比如小米这个是 90 fps,而录屏的时候选择 60 fps 的录屏,则录屏文件会看不出来卡顿 (使用其他手机录像也会有这个问题)
  2. 如果录屏是以高帧率(90fps)录制的,但是播放的时候是使用低帧率(60fps)的设备观看的(小米就是这个情况),也不会看出来卡顿,比如用 90 fps 的规格录制视频,但是在手机上播放的时候,系统会自动切换到 60 fps, 导致看不出来卡顿

系列文章

  1. Systrace 流畅性实战 1 :了解卡顿原理
  2. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  3. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问

附件

附件已经上传到了 Github 上,可以自行下载:https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action

  1. xiaomi_launcher.zip : 桌面滑动卡顿的 Systrace 文件,这次案例主要是分析这个 Systrace 文件
  2. xiaomi_launcher_scroll_all_the_time.zip : 桌面一直按着滑动的 Systrace 文件
  3. oppo_launcher_scroll.zip :对比文件

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

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

微信扫一扫

Android Systrace 流畅性实战 2 :案例分析 - MIUI 桌面滑动卡顿分析

作者 Gracker
2021年4月24日 19:55

不同的人对流畅性(卡顿掉帧)有不同的理解,对卡顿阈值也有不同的感知,所以有必要在开始这个系列文章之前,先把涉及到的内容说清楚,防止出现不同的理解,也方便大家带着问题去看这几篇问题,下面是一些基本的说明

  1. 对手机用户来说,卡顿包含了很多场景,比如在 滑动列表的时候掉帧应用启动白屏过长点击电源键亮屏慢界面操作没有反应然后闪退点击图标没有响应窗口动画不连贯、滑动不跟手、重启手机进入桌面卡顿 等场景,这些场景跟我们开发人员所理解的卡顿还有点不一样,开发人员会更加细分去分析这些问题,这是开发人员和用户之间的一个认知差异,这一点在处理用户(或者测试人员)的问题反馈的时候尤其需要注意
  2. 对开发人员来说,上面的场景包括了 流畅度(滑动列表的时候掉帧、窗口动画不连贯、重启手机进入桌面卡顿)、响应速度(应用启动白屏过长、点击电源键亮屏慢、滑动不跟手)、稳定性(界面操作没有反应然后闪退、点击图标没有响应)这三个大的分类。之所以这么分类,是因为每一种分类都有不太一样的分析方法和步骤,快速分辨问题是属于哪一类很重要
  3. 在技术上来说,流畅度、响应速度、稳定性(ANR)这三类之所以用户感知都是卡顿,是因为这三类问题产生的原理是一致的,都是由于主线程的 Message 在执行任务的时候超时,根据不同的超时阈值来进行划分而已,所以要理解这些问题,需要对系统的一些基本的运行机制有一定的了解,本文会介绍一些基本的运行机制
  4. 流畅性这个系列主要是分析流畅度相关的问题,响应速度和稳定性会有专门的文章介绍,在理解了流畅性相关的内容之后,再去分析响应速度和稳定性问题会事半功倍
  5. 流畅性这个系列主要是讲如何使用 Systrace (Perfetto) 工具去分析,之所以 Systrace 为切入点,是因为影响流畅度的因素很多,有 App 自身的原因、也有系统的原因。而 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 篇

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

Systrace 分析卡顿问题的套路

使用 Systrace 分析卡顿问题,我们一般的流程如下

  1. 复现卡顿的场景,抓取 Systrace,可以用 shell 或者手机自带的工具来抓取

  2. 双击抓出来的 trace.html 直接在 Chrome 中打开 Systrace 文件

    1. 如果不能直接打开,可以在 Chrome 中输入 chrome://tracing/,然后把 Systrace 文件拖到里面就可以打开
    2. 或者使用 Perfetto View 中的 Open With Legacy UI 打开
  3. 分析卡顿问前,我们需要了解问题发生的背景,以提高分析 Systrace 的效率

    1. 用户(或者测试)的操作流程
    2. 卡顿复现概率
    3. 竞品机器是否也有同样的卡顿问题
  4. 分析问题之前或者分析的过程中,也可以通过检查 Systrace 来了解一些基本的信息

    1. CPU 频率、架构、Boost 信息等
    2. 是否触发温控:表现为cpu 频率被压低
    3. 是否是高负载场景:表现为 cpu 区域任务非常满
    4. 是否是低内存场景:表现为 lmkd 进程繁忙,App 进程的 HeapTaskDeamon 耗时,有很多的 Block io
  5. 定位 App 进程在 Systrace 中的位置

    1. 打开 Systrace 后,首先要首先要看的就是 App 进程,主要是 App 的主线程和渲染线程,找到 Systrace 中每一帧耗时的部分,比如下面这种,可以看到 App 的 UI Thread 的红框部分,耗时 110ms,明显是不正常的(这个案例是 Bilibili 列表滑动卡顿)image-20220228231805317

    2. 事实上,所有超过一个 Vsync 周期的 doFrame 耗时(即大家看到的黄帧和红帧),我们都需要去看一下是否真的发生的掉帧,就算没有掉帧,也要看一下原因,比如下面这个

      image-20220228231830063

    3. Vsync 周期与刷新率的对照

      1. 60fps 对应的 Vsync 周期是 16.6ms
      2. 90fps 对应的 Vsync 周期是 11.1ms
      3. 120fps 对应的 Vsync 周期是 8.3ms
  6. 分析 SurfaceFlinger 进程的主线程和 Binder 线程

    1. 由于多个 Buffer 缓冲机制存在,App 主线程和渲染线程,有时候即使超过一个 Vsync 周期,也不一定会出现卡顿,所以这里我们需要看SurfaceFlinger 进程的主线程,来确认是否真的发生了卡顿 ,比如上面 3.1 部分的图,App 主线程耗时 110 ms,对应的 SurfaceFlinger 主线程如下

      image-20220228231855706

    2. Systrace 中的 SurfaceFlinger 进程区域,对应的 App 的 Buffer 个数也是空的

      image-20220228231914525

  7. 从整机角度分析和 Binder 调用分析(不涉及可以不用看)

    1. 上面的案例,可以很容易就看到是 App 自身执行耗时,那么只需要把耗时的部分涉及到的 View 找到,进行代码或者设计方面的优化就可以了
    2. 有时候 App 进程的主线程会出现大量的 Runnable 或者 Binder 调用耗时,也会导致 App 出现卡顿,这时候就需要分析整机问题,要看具体是什么原因导致大量的 Runnable 或者 Binder 调用耗时

按照这个流程分析之后,需要再反过来看各个进程,把各个线索联系起来,推断最有可能的原因

App 掉帧的原因非常多,有 APP 本身的问题,有系统原因导致卡顿的,也有硬件层的、整机卡的,这个可以参考下面四篇文章,不同的卡顿原因在 Systrace 中会有不同的表现

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

使用 Systrace 分析卡顿问题的案例

Systrace 作为分析卡顿问题的第一手工具,给开发者提供了一个从手机全局角度去看问题的方式,通过 Systrace 工具进行分析,我们可以大致确定卡顿问题的原因:是系统导致的还是应用自身的问题

当然 Systrace 作为一个工具,再进行深入的分析的时候就会有点力不从心,需要配合 TraceView + 源码来进一步定位和解决问题,最后再使用 Systrace 进行验证

所以本文更多地是讲如何发现和分析卡顿问题,至于如何解决,就需要后续自己寻找合适的解决方案了,比如对比竞品的 Systrace 表现、优化代码逻辑、优化系统调度、优化布局等

案例说明

个人在使用小米 10 Pro 的时候,在桌面滑动这个最常用的场景里面,总会有一种卡顿的感觉,10 Pro 是 90Hz 的屏幕,FPS 也是 90,所以一旦出现卡顿,就会有很明显的感觉(个人对这个也比较敏感)。之前没怎么关注,在升级 12.5 之后,这个问题还是没有解决,所以我想看看到底是怎么回事

抓了 Systrace 之后分析发现,这个卡顿场景是一个非常好的案例,所以把这个例子拿出来作为流畅度的一个实战分享

建议大家下载附件中的 Systrace,对照文章食用最佳

  1. 鉴于卡顿问题的影响因素比较多,所以在开始之前,我把本次分析所涉及的硬件、软件版本沟通清楚,如果后续此场景有优化,此文章也不会进行修改,以文章附件中的 Systrace 为准
  2. 硬件:小米 10 Pro
  3. 软件:MIUI 12.5.3 稳定版
  4. 小米桌面版本:RELEASE-4.21.11.2922-03151646

从 Input 事件开始

这次抓的 Systrace 我只滑动了一次,所以比较好定位,滑动的 input 事件由一个 Input Down 事件 + 若干个 Input Move 事件 + 一个 Input Up 事件组成

在 Systrace 中,SystemServer 中的 InputDispatcher 和 InputReader 线程都有体现,我们这里主要看在 App 主线程中的体现

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%208.png

如上图,App 主线程上的 deliverInputEvent 标识了应用处理 input 事件的过程,input up 之后,就进入了 Fling 阶段,这部分的基础知识可以查看下面这两篇文章

  1. https://www.androidperformance.com/2019/11/04/Android-Systrace-Input/
  2. https://www.androidperformance.com/2020/08/20/weibo-imageload-opt-on-huawei/

分析主线程

由于这次卡顿主要是松手之后才出现的,所以我们主要看 Input Up 之后的这段

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%209.png

主线程上面的 Frame 有颜色进行标注,一般有绿、黄、红三种颜色,上面的 Systrace 里面,没有红色的帧,只有绿色和黄色。那么黄色就一定是卡顿么?红色就一定是卡顿么?其实不一定,单单通过主线程,我们并不能确定是否卡顿,这个在下面会讲

从主线程我们没法确定是否发生了卡顿,我们找出了三个可疑的点,接下来我们看一下 RenderThread

分析渲染线程

放大第一个可疑点,可以看到,这一帧总耗时在 19ms, RenderThread 耗时 16ms,且 RenderThread 的 cpu 状态都是 running(绿色),那么这一帧这么耗时的原因大概率是下面两个原因导致的:

  1. RenderThread 本身耗时,任务比较繁忙
  2. RenderThread 的任务受 CPU 影响(可能是频率低了、或者是跑到小核了)

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2010.png

由于只是可疑点,所以我们先不去看 cpu 相关的,先查看 SurfaceFlinger 进程,确定这里有卡顿发生

分析 SurfaceFlinger

对于 Systrace 中 SurfaceFlinger 部分解读不熟悉的可以先预习一下这篇文章 https://www.androidperformance.com/2020/02/14/Android-Systrace-SurfaceFlinger/

这里我们主要看两个点

  1. App 对应的 BufferQueue 的 Buffer 情况。通过这个我们可以知道在 SurfaceFlinger 端,App 是否有可用的 Buffer 提供给 SurfaceFlinger 进行合成
  2. SurfaceFlinger 主线程的合成情况。通过查看 SurfaceFlinger 在 sf-vsync 到来的时候是否进行了合成工作,就可以判断这一帧是否出现了卡顿。

判断是否卡顿的标准如下

  1. 如果 SurfaceFlinger 主线程没有合成任务,而且 App 在这一个 Vsync 周期(vsync-app)进行了正常的工作,但是对应的 App 的 BufferQueue 里面没有可用的 Buffer,那么说明这一帧卡了 — 卡顿出现
    这种情况如下图所示(也是上图中第一个疑点所在的位置)

    /images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2011.png

  2. 如果 SurfaceFlinger 进行了合成,而且 App 在这一个 Vsync 周期(vsync-app)进行了正常的工作,但是对应的 App 的 BufferQueue 里面没有可用的 Buffer,那么这一帧也是卡了,之所以 SurfaceFlinger 会正常合成,是因为有其他的 App 提供了可用来合成的 Buffer — 卡顿出现
    这种情况如下图所示(也在附件的 Systrace 里面)
    /images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2012.png

  3. 如果 SurfaceFlinger 进行了合成,而且 App 在这一个 Vsync 周期(vsync-app)进行了正常的工作,而且对应的 App 的 BufferQueue 里面有可用的 Buffer,那么这一帧就会正常合成,此时没有卡顿出现 — 正常情况
    正常情况如下,作为对比还是贴上来方便大家对比
    /images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2013.png

回到本例的第一个疑点的地方,我们通过 SurfaceFlinger 端的分析,发现这一帧确实是掉了,原因是 App 没有准备好可用的 Buffer 供 SurfaceFlinger 来合成,那么接下来就需要看为什么这一帧 App 没有可用的 Buffer 给到 SurfaceFlinger

回到渲染线程

上面我们分析这一帧所对应的 MainThread + RenderThread 耗时在 19ms,且 RenderThread 耗时就在 16ms,那么我们来看 RenderThread 的情况

出现这种情况主要是有下面两个原因

  1. RenderThread 本身耗时,任务比较繁忙
  2. RenderThread 的任务受 CPU 影响(可能是频率低了、或者是跑到小核了)

但是桌面滑动这个场景,负载并不高,且松手之后并没有多余的操作,View 更新之类的,本身耗时比前一帧多了将近 3 倍,可以推断不是自身负载加重导致的耗时

那么就需要看此时的 RenderThread 的 cpu 情况:

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2014.png

既然在 Running 情况,我们就去 CPU Info 区域查看这一段时间这个任务的调度情况

分析 CPU 区域的信息

查看 CPU (Kernel 区域,这部分的基础知识可以查看 Android Systrace 基础知识 - CPU Info 解读Android Systrace 基础知识 – 分析 Systrace 预备知识)这两篇文章

回到这个案例,我们可以看到 App 对应的 RenderThread 大部分跑在 cpu 2 和 cpu 0 上,也就是小核上(这个机型是高通骁龙 865,有四个小核+3 个大核+1 个超大核)

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2015.png

其此时对应的频率也已经达到了小核的最高频率(1.8Ghz)

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2016.png

且此时没有 cpu boost 介入

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2017.png

那么这里我们猜想,之所以这一帧 RenderThread 如此耗时,是因为小核就算跑满了,也没法在这么短的时间内完成任务

那么接下来要验证我们的猜想,需要进行下面两个步骤

  1. 对比其他正常的帧,是否有跑在小核的。如果有且没有出现掉帧,那么说明我们的猜想是错误的
  2. 对比其他几个异常的帧,看看掉帧的原因是否也是因为 RenderThread 任务跑到了小核导致的。如果不是,那么就需要做其他的假设猜想

在用同样的流程分析了后面几个掉帧之后,我们发现

  1. 对比其他正常的帧,没有在小核跑的,包括掉帧后的下一帧,调度器马上把 RenderThread 摆到了大核,没有出现连续掉帧的情况
  2. 对比其他几个异常的帧,都是由于 RenderThread 跑到了小核,但是小核的性能不足导致 RenderThread 执行耗时,最终引起卡顿

至此,这一次的卡顿分析我们就找到了原因:RenderThread 掉到了小核

至于 RenderThread 的任务为啥跑着跑着就掉到了小核,这个跟调度器是有关系的,大小核直接的调度跟任务的负载有关系,任务从大核掉到小核、或者从小核迁移到大核,调度器这边都是有参数和算法来控制的,所以后续的优化可能需要从这方面去入手

  1. 调整大小核迁移的阈值参数或者修改调度器算法
  2. 参考竞品表现,看看竞品在这个场景的性能指标,调度情况等,分析竞品可能使用的策略

Triple Buffer 在这个场景发挥了什么作用?

Triple-Buffer-的作用 这篇文章,讲到了 Triple Buffer 几个作用

  1. 缓解掉帧
  2. 减少主线程和渲染线程等待时间
  3. 降低 GPU 和 SurfaceFlinger 瓶颈

那么在桌面滑动卡顿这个案例里面,Triple Buffer 发挥了什么作用呢?结论是:有的场景没有发挥作用,反而有副作用,导致卡顿现象更明显,下面是分析流程

可以看文章中 Triple Buffer 缓解掉帧 的原理:

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2018.png

在分析小米桌面滑动卡顿这个案例的时候,我发现在有一个问题,小米桌面对应的 App 的 BufferQueue,有时候会出现可用 Buffer 从 2 →0 ,这相当于直接把一个 Buffer 给抛弃掉了,如下图所示

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2019.png

这样的话,如果在后续的桌面 Fling 过程中,又出现了一次 RenderThread 耗时,那么就会以卡顿的形式直接体现出来,这样也就失去了 Triple Buffer 的缓解掉帧的作用了

下图可以看到,由于丢弃了一个 Buffer,导致再一次出现 RenderThread 耗时的时候,表现依然是无 Buffer 可用,出现掉帧

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2020.png

仔细看前面这段丢弃 Buffer 的逻辑,也很容易想到,这里本身就已经丢了一帧了,还把这个耗时帧所对应的 Buffer 给丢弃了(也可能丢弃的是第二帧),不管是哪种情况,滑动时候的每一帧的内容都是计算好的(参考 List Fling 的计算过程),如果把其中一帧丢了,再加上本身 SurfaceFlinger 卡的那一下,卡顿感会非常明显

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%2021.png

举个例子,以滑动为例,offset 指的是离屏幕一个左边的距离

  1. 正常情况下,滑动的时候,offset 是:2→4→6→8→10→12
  2. 掉了一帧的情况下,滑动的 Offset 是:2→4→6→6→8→10→12 (假设 计算 8 的这一帧超时了,就会看到两个 6 ,这是掉了一帧的情况)
  3. 像上图里面,如果直接扔掉了那个耗时的帧,就会出现下面这种 Offset:2→4→6→6→10→12 ,直接从 6 跳到了 10,相当于卡了 1 次,步子扯大了一次,感官上会觉得卡+跳跃

系列文章

  1. Systrace 流畅性实战 1 :了解卡顿原理
  2. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  3. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问

附件

附件已经上传到了 Github 上,可以自行下载:https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action

  1. xiaomi_launcher.zip : 桌面滑动卡顿的 Systrace 文件,这次案例主要是分析这个 Systrace 文件
  2. xiaomi_launcher_scroll_all_the_time.zip : 桌面一直按着滑动的 Systrace 文件
  3. oppo_launcher_scroll.zip :对比文件

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

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

微信扫一扫

Android Systrace 流畅性实战 1 :了解卡顿原理

作者 Gracker
2021年4月24日 19:55

不同的人对流畅性(卡顿掉帧)有不同的理解,对卡顿阈值也有不同的感知,所以有必要在开始这个系列文章之前,先把涉及到的内容说清楚,防止出现不同的理解,也方便大家带着问题去看这几篇问题,下面是一些基本的说明

  1. 对手机用户来说,卡顿包含了很多场景,比如在 滑动列表的时候掉帧应用启动白屏过长点击电源键亮屏慢界面操作没有反应然后闪退点击图标没有响应窗口动画不连贯、滑动不跟手、重启手机进入桌面卡顿 等场景,这些场景跟我们开发人员所理解的卡顿还有点不一样,开发人员会更加细分去分析这些问题,这是开发人员和用户之间的一个认知差异,这一点在处理用户(或者测试人员)的问题反馈的时候尤其需要注意
  2. 对开发人员来说,上面的场景包括了 流畅度(滑动列表的时候掉帧、窗口动画不连贯、重启手机进入桌面卡顿)、响应速度(应用启动白屏过长、点击电源键亮屏慢、滑动不跟手)、稳定性(界面操作没有反应然后闪退、点击图标没有响应)这三个大的分类。之所以这么分类,是因为每一种分类都有不太一样的分析方法和步骤,快速分辨问题是属于哪一类很重要
  3. 在技术上来说,流畅度、响应速度、稳定性(ANR)这三类之所以用户感知都是卡顿,是因为这三类问题产生的原理是一致的,都是由于主线程的 Message 在执行任务的时候超时,根据不同的超时阈值来进行划分而已,所以要理解这些问题,需要对系统的一些基本的运行机制有一定的了解,本文会介绍一些基本的运行机制
  4. 流畅性这个系列主要是分析流畅度相关的问题,响应速度和稳定性会有专门的文章介绍,在理解了流畅性相关的内容之后,再去分析响应速度和稳定性问题会事半功倍
  5. 流畅性这个系列主要是讲如何使用 Systrace (Perfetto) 工具去分析,之所以 Systrace 为切入点,是因为影响流畅度的因素很多,有 App 自身的原因、也有系统的原因。而 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 篇

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

了解卡顿原理

卡顿现象及影响

如文章开头所述,本文主要是分析流畅度相关的问题。流畅度是一个定义,我们评价一个场景的流畅度的时候,往往会使用 fps 来表示。比如 60 fps,意思是每秒画面更新 60 次;120 fps,意思是每秒画面更新 120 次。如果 120 fps 的情况下,每秒画面只更新了 110 次(连续动画的过程),这种情况我们就称之为掉帧,其表现就是卡顿,fps 对应的也从 120 降低到了 110 ,这些都可以被精确地监控到

同时掉帧帧的原因非常多,有 APP 本身的问题,有系统原因导致卡顿的,也有硬件层的、整机卡的,这个可以参考下面四篇文章

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

用户在使用手机的过程中,卡顿是最容易被感受到的

  1. 偶尔出现的小卡顿会降低用户的使用体验,比如刷微博的时候卡了一下,比如返回桌面动画卡顿这种
  2. 整机出现卡顿的则会让手机无法使用
  3. 现在的高帧率时代,如果用户习惯了 120 fps ,在用户比较容易感知的场景下突然切换到 60 fps,用户也会有明显的感知,并觉得出现了卡顿

所以不管是应用还是系统,都应该尽量避免出现卡顿,发现的卡顿问题最好优先进行解决

卡顿定义

应用一帧渲染的整体流程

为了知道卡顿是如何发生的,我们需要知道应用主线程的一帧是如何工作的

从执行顺序的角度来看

从 Choreographer 收到 Vsync 开始,到 SurfaceFlinger/HWC 合成一帧结束(后面还包含屏幕显示部分,不过是硬件相关,这里就不列出来了)

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled.png

从 Systrace 的角度来看

上面的流程图从 Systrace (Perfetto)的角度来看会更加直观

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%201.png

具体的流程参考上面两个图以及代码就会很清楚了,上述整体流程中,任何一个步骤超时都有可能导致卡顿,所以分析卡顿问题,需要从多个层面来进行分析,比如应用主线程、渲染线程、SystemServer 进程、SurfaceFlinger 进程、Linux 区域等

卡顿定义

我对卡顿的定义是:稳定帧率输出的画面出现一帧或者多帧没有绘制 。对应的应用单词是 Smooth VS Jank

比如下图中,App 主线程有在正常绘制的时候(通常是做动画或者列表滑动),有一帧没有绘制,那么我们认为这一帧有可能会导致卡顿(这里说的是有可能,由于 Triple Buffer 的存在,这里也有可能不掉帧)

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%202.png

下面从三个方面定义卡顿

  1. 从现象上来说,在 App 连续的动画播放或者手指滑动列表时(关键是连续),如果连续 2 帧或者 2 帧以上,应用的画面都没有变化,那么我们认为这里发生了卡顿
  2. 从 SurfaceFlinger 的角度来说,在 App 连续的动画播放或者手指滑动列表时(关键是连续),如果有一个 Vsync 到来的时候 ,App 没有可以用来合成的 Buffer,那么这个 Vsync 周期 SurfaceFlinger 就不会走合成的逻辑(或者是去合成其他的 Layer),那么这一帧就会显示 App 的上一帧的画面,我们认为这里发生了卡顿
  3. 从 App 的角度来看,如果渲染线程在一个 Vsync 周期内没有 queueBuffer 到 SurfaceFlinger 中 App 对应的 BufferQueue 中,那么我们认为这里发生了卡顿

这里没有提到应用主线程,是因为主线程耗时长一般会间接导致渲染线程出现延迟,加大渲染线程执行超时的风险,从而引起卡顿;而且应用导致的卡顿原因里面,大部分都是主线程耗时过长导致的

卡顿还要区分是不是逻辑卡顿逻辑卡顿指的是一帧的渲染流程都是没有问题的,也有对应的 Buffer 给到 SurfaceFlinger 去合成,但是这个 App Buffer 的内容和上一帧 App Buffer 相同(或者基本相同,肉眼无法分辨),那么用户看来就是连续两帧显示了相同的内容。这里一般来说我们也认为是发生了卡顿(不过还要区分具体的情况);逻辑卡顿主要是应用自身的代码逻辑造成的

系统运行机制简介

由于卡顿的原因比较多,如果要分析卡顿问题,首先得对 Android 系统运行的机制有一定的了解,下面简单介绍一下分析卡顿问题需要了解的系统运行机制:

  1. App 主线程运行原理
  2. Message、Handler、MessageQueue、Looper 机制
  3. 屏幕刷新机制和 Vsync
  4. Choreogrepher 机制
  5. Buffer 流程和 TripleBuffer
  6. Input 流程

系统机制 - App 主线程运行原理

App 进程在创建的时候,Fork 完成后会调用 ActivityThread 的 main 方法,进行主线程的初始化工作

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
frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
......
// 创建 Looper、Handler、MessageQueue
Looper.prepareMainLooper();
......
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
......
// 开始准备接收消息
Looper.loop();
}

// 准备主线程的 Looper
frameworks/base/core/java/android/os/Looper.java
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

// prepare 方法中会创建一个 Looper 对象
frameworks/base/core/java/android/os/Looper.java
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

// Looper 对象创建的时候,同时创建一个 MessageQueue
frameworks/base/core/java/android/os/Looper.java
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread()
}

主线程初始化完成后,主线程就有了完整的 Looper、MessageQueue、Handler,此时 ActivityThread 的 Handler 就可以开始处理 Message,包括 Application、Activity、ContentProvider、Service、Broadcast 等组件的生命周期函数,都会以 Message 的形式,在主线程按照顺序处理,这就是 App 主线程的初始化和运行原理,部分处理的 Message 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
frameworks/base/core/java/android/app/ActivityThread.java
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 SERVICE_ARGS = 115;
public static final int STOP_SERVICE = 116;

public void handleMessage(Message msg) {
switch (msg.what) {
case BIND_APPLICATION:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
AppBindData data = (AppBindData)msg.obj;
handleBindApplication(data);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
}
}
}

这部分可以看 Android Systrace 基础知识 - MainThread 和 RenderThread 解读 这篇文章

系统机制 - Message 机制

上一节应用的主线程初始化完成后,主线程就进入阻塞状态,等待 Message,一旦有 Message 发过来,主线程就会被唤醒,处理 Message,处理完成之后,如果没有其他的 Message 需要处理,那么主线程就会进入休眠阻塞状态继续等待

从下图可以看到 ,Android Message 机制的核心就是四个:HandlerLooperMessageQueueMessage

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%203.png

网上有很多关于 Message 机制代码细节的分析,所以这里只是简单介绍 Message 机制的四个核心组件的作用

  1. Handler : Handler 主要是用来处理 Message,应用可以在任何线程创建 Handler,只要在创建的时候指定对应的 Looper 即可,如果不指定,默认是在当前 Thread 对应的 Looper
  2. Looper : Looper 可以看成是一个循环器,其 loop 方法开启后,不断地从 MessageQueue 中获取 Message,对 Message 进行 Delivery 和 Dispatch,最终发给对应的 Handler 去处理。由于 Looper 中应用可以在 Message 处理前后插入自己的 printer,所以很多 APM 工具都会使用这个作为性能监控的一个切入点,具体可以参考 Tencent-Matrix 和 BlockCanary
  3. MessageQueue:MessageQueue 入上图所示,就是一个 Message 管理器,队列中是 Message,在没有 Message 的时候,MessageQueue 借助 Linux 的 nativePoll 机制,阻塞等待,直到有 Message 进入队列
  4. Message:Message 是传递消息的对象,其内部包含了要传递的内容,最常用的包括 what、arg、callback 等

从第一节 App 主线程运行原理可知,ActivityThread 的就是利用 Message 机制,处理 App 各个生命周期和组件各个生命周期的函数

系统机制 - 屏幕刷新机制 和 Vsync

首先我们需要知道什么是屏幕刷新率,简单来说,屏幕刷新率是一个硬件的概念,是说屏幕这个硬件刷新画面的频率:举例来说,60Hz 刷新率意思是:这个屏幕在 1 秒内,会刷新显示内容 60 次;那么对应的,90Hz 是说在 1 秒内刷新显示内容 90 次

与屏幕刷新率对应的,FPS 是一个软件的概念,与屏幕刷新率这个硬件概念要区分开,FPS 是由软件系统决定的 :FPS 是 Frame Per Second 的缩写,意思是每秒产生画面的个数。举例来说,60FPS 指的是每秒产生 60 个画面;90FPS 指的是每秒产生 90 个画面

VSync 是垂直同期( Vertical Synchronization )的简称。基本的思路是将你的 FPS 和显示器的刷新率同期起来。其目的是避免一种称之为”撕裂”的现象.

  1. 60 fps 的系统 , 1s 内需要生成 60 个可供显示的 Frame , 也就是说绘制一帧需要 16.67ms ( 1/60 ) , 才会不掉帧 ( FrameMiss ).
  2. 90 fps 的系统 , 1s 内生成 90 个可供显示的 Frame , 也就是说绘制一帧需要 11.11ms ( 1/90 ) , 才不会掉帧 ( FrameMiss ).

一般来说,屏幕刷新率是由屏幕控制的,FPS 则是由 Vsync 来控制的,在实际的使用场景里面,屏幕刷新率和 FPS 一般都是一一对应的,具体可以参考下面两篇文章:

  1. Android 新的流畅体验,90Hz 漫谈
  2. Android Systrace 基础知识 - Vsync 解读

系统机制 - Choreographer

上一节讲到 Vsync 控制 FPS,其实 Vsync 是通过 Choreographer 来控制应用刷新的频率的

Choreographer 的引入,主要是配合 Vsync,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 至于为什么 Vsync 周期选择是 16.6ms (60 fps) ,是因为目前大部分手机的屏幕都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每隔 16.6 ms,Vsync 信号到来唤醒 Choreographer 来做 App 的绘制操作 ,如果每个 Vsync 周期应用都能渲染完成,那么应用的 fps 就是 60,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用

Choreographer 扮演 Android 渲染链路中承上启下的角色

  1. 承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等
  2. 启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync) .

下图就是 Vsync 信号到来的时候,Choreographer 借助 Message 机制开始一帧的绘制工作流程图

这部分详细的流程可以看 Android 基于 Choreographer 的渲染机制详解 这篇文章

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%204.png

系统机制 - Buffer 流程和 TripleBuffer

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

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%205.png

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

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

知道了 Buffer 流转的过程,下面需要说明的是,在目前的大部分系统上,每个应用都有三个 Buffer 轮转使用,来减少由于 Buffer 在某个流程耗时过长导致应用无 Buffer 可用而出现卡顿情况

下图是双 Buffer 和 三 Buffer 的一个对比图

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%206.png

三 Buffer 的好处如下

  1. 缓解掉帧 :从上图 Double Buffer 和 Triple Buffer 的对比图可以看到,在这种情况下(出现连续主线程超时),三个 Buffer 的轮转有助于缓解掉帧出现的次数(从掉帧两次 -> 只掉帧一次)。,App 主线程超时不一定会导致掉帧,由于 Triple Buffer 的存在,部分 App 端的掉帧(主要是由于 GPU 导致),到 SurfaceFlinger 这里未必是掉帧,这是看 Systrace 的时候需要注意的一个点
  2. 减少主线程和渲染线程等待时间 :双 Buffer 的轮转,App 主线程有时候必须要等待 SurfaceFlinger(消费者)释放 Buffer 后,才能获取 Buffer 进行生产,这时候就有个问题,现在大部分手机 SurfaceFlinger 和 App 同时收到 Vsync 信号,如果出现 App 主线程等待 SurfaceFlinger(消费者)释放 Buffer,那么势必会让 App 主线程的执行时间延后
  3. 降低 GPU 和 SurfaceFlinger 瓶颈 :这个比较好理解,双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现 SurfaceFlinger 无法及时合成而导致掉帧;在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间

坏处就是 Buffer 多了会占用内存

这部分详细的流程可以看 Android Systrace 基础知识 - Triple Buffer 解读 这篇文章

系统机制 - Input 流程

Android 系统是由事件驱动的,而 input 是最常见的事件之一,用户的点击、滑动、长按等操作,都属于 input 事件驱动,其中的核心就是 InputReader 和 InputDispatcher。InputReader 和 InputDispatcher 是跑在 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 就可以看到整个事件的流程

上面流程对应的 Systrace 如下

/images/Systrace-Smooth%20e5d284a979a447ad8b45ff021d6e41cf/Untitled%207.png

这部分详细的流程可以看 Android Systrace 基础知识 - Input 解读 这篇文章

系列文章

  1. Systrace 流畅性实战 1 :了解卡顿原理
  2. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  3. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问

附件

附件已经上传到了 Github 上,可以自行下载:https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action

  1. xiaomi_launcher.zip : 桌面滑动卡顿的 Systrace 文件,这次案例主要是分析这个 Systrace 文件
  2. xiaomi_launcher_scroll_all_the_time.zip : 桌面一直按着滑动的 Systrace 文件
  3. oppo_launcher_scroll.zip :对比文件

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

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

微信扫一扫

华为手机刷微博体验更好?技术角度的一些分析和思考

作者 Gracker
2020年8月20日 13:17

技术群里的小伙伴发了一条微博, https://weibo.com/1808884742/IApbpEVQr, 博主 @王波粒 发现, Mate 30 Pro 有个很特别的现象(建议先去看一下视频)

但是这个视频描述和底下的猜测都不对,我这边总结一下这个现象: 微博这个 App 在华为的手机上,在主页列表上下滑动的情况下依然可以流畅加载图片,而同一个版本的微博客户端,安装到其他手机上,在主页列表上下滑动的情况下,则必须要等到滑动停止之后才会加载图片

下面就针对这个现象,从技术的角度来深入分析产生这种现象的原因,以及我们能从里面学到什么

这个现象有什么特别呢 ?

  1. 从技术上讲,滑动列表停止后再加载图片是目前列表滑动优化中一个比较常见的优化项,很多主流 App 也都是这么做的 ,做这种处理主要是因为 如果在列表滑动的时候,碰到图片视频就加载,那么会加载很多无用的图片&&视频,浪费资源不说,还可能会影响真正用户看到的图片的加载速度 (加载一般都有并行上限和队列,队列里面无效的图片太多,后来的图片就得排队等待)。 这里比较 特别的就是同一个版本的微博 APK,在华为的机型上与在其他机型上表现不一致,作为一个系统优化工程师,这个还是值得去搞清楚的(大胆猜测是微博针对华为的机型做了优化),那么这个优化的内容是什么?
  2. 从用户体验的角度来讲,列表滑动的同时加载图片,用户可以更早地看到图片,减少图片占位白图的显示时间,可以提升滑动的体验
  3. 第三个现象就得认真体验才会感觉到: 华为手机上的微博在松手后的滑动曲线和其他手机上的微博在松手后的滑动曲线是不一样的,华为的微博列表松手后的滑动曲线速度更慢,更柔和,结束的时候也不会太突兀,与系统默认的列表滑动曲线明显不一样

上面三个是从现象上来说的,下面就从技术上来验证,从最后的结果来看,华为和微博的合作毫无疑问是很成功的,可以作为一个案例推广到其他头部 App,同时作为 Android 开发者,对华为这种非常细致的体验优化真的是非常敬佩

背景备注

  1. 由于 “列表滑动的同时加载图片” 这个功能由微博官方服务器控制,可以随时开启或者关闭,所以文章中所说的 “同一个版本的微博客户端,安装到其他手机上,在主页列表上下滑动的情况下,则必须要等到滑动停止之后才会加载图片” 这个现象在 “列表滑动的同时加载图片” 这个功能开启后,现象就会变成 “主页列表上下滑动的时候就会加载图片”
  2. 在 2020-6 月左右分析这个问题的时候,“列表滑动的同时加载图片” 这个功能还是关闭的,只有华为手机做了优化才有效果,其他手机是 “滑动停止之后才会加载图片”
  3. 在 2020-8 月再看这个问题的时候,“列表滑动的同时加载图片” 这个功能在其他手机上已经开启
  4. 华为的 PerfSDK 还有效果么?答案是有,具体分析可以看下文,因为有了这个 SDK,不仅对微博有好处(减少图片加载个数),对华为也有好处(提升微博主页列表在华为手机上的滑动体验,即 Fling 曲线优化) ;而粗暴开启 “列表滑动的同时加载图片” 的其他手机,如果性能不足,开启后反而会增加卡顿出现的概率(微博官方应该有性能监控数据可以看到)
  5. 反编译的微博版本:10.8.1

结论先行

“微博这个 App 在华为的手机上,在主页列表上下滑动的情况下依然可以流畅加载图片” 这个现象是因为华为和微博做了联合优化,主要是为了优化微博列表滑动时候的用户体验,其优化点如下

  1. 华为提供了一个简单的接口打包成 SDK 提供给微博,这个接口可以让微博的列表监听到列表的当前速度(Velocity),在速度高于阈值或者低于阈值的时候,都会及时通知 App
  2. 微博拿到这个速度回调之后,就可以根据列表的滑动速度来决定是否要在滑动过程中加载图片,一旦列表的滑动速度低于设定的阈值,就开启图片加载;一旦列表的滑动速度高于设定的阈值,就关闭图片加载
  3. 华为检测到这个应用使用了 SDK,就可以将优化过后的滑动曲线应用在这个 App 的列表 Fling 阶段,提升用户体验

对细节感兴趣的同学可以继续阅读,有能力的同学看完后可以修改 Framework 相关代码,编译一个 SDK,然后自己写个 Demo 接入 SDK,就可以打通我下面所说的所有内容了,我自己在 AOSP 的代码上实现了一遍,Demo 也可以正常运行,有兴趣可以跟我私下交流

微博+华为是怎么优化?

现象分析

我们在滑动微博列表的时候,一个滑动操作主要由下面三部分组成

  1. 手指接触屏幕,上下滑动微博主页列表,但是手指 没有离开屏幕 ,这个阶段我们称之为阶段一,技术术语为 SCROLL_STATE_TOUCH_SCROLL
  2. 手指上下滑动的时候 离开屏幕 (必须有一个上滑或者下滑的速度),微博列表有了一个惯性,根据惯性的方向继续滑动,这个阶段我们称之为阶段二,技术术语为 SCROLL_STATE_FLING
  3. 列表惯性滑动后停止,这 个 阶段我们称之为阶段三 , 技术术语为 SCROLL_STATE_I DLE

而华为和微博的优化主要在阶段一和阶段二

阶段一优化

  1. 优化前:只要手指不离开屏幕,图片加载功能关闭
  2. 优化后:只要手指不离开屏幕,列表就不会滑动太快,这时候图片加载功能开启

阶段二优化

  1. 滑动图片加载优化
    1. 优化前:只要列表滑动不停止,图片加载功能关闭
    2. 优化后:图片加载功能是否开启取决于当前列表滑动的速度
      1. 列表滑动速度太快,这时候图片加载功能关闭
      2. 列表滑动速度掉落到一个阈值,图片加载功能开启
  2. 列表 Fling 曲线优化
    1. 优化前:列表滑动的曲线是默认值,滑动时间比较短,停止的时候比较突兀,不柔和
    2. 优化后:列表滑动的曲线是华为经过优化的,滑动时间比较长,停止的时候比较柔,不突兀,比较接近 iPhone 的列表滑动曲线

技术分析

技术分析的代码主要来源于微博 apk 的反编译,微博版本 10.8.1,通过反编译的代码可以看到, 微博主页在初始化的时候,会接入华为提供的 PerfSDK,从而获得监听列表滑动速度的能力

阶段一优化的技术分析

列表的 ScrollStateChange 是标识列表状态的一个回调,微博在 ScrollStateChange 这个回调中会根据当前的状态来决定是否加载图片, 从下面的代码逻辑来看

  1. 滑动图片加载优化生效 的时候,如果 State != 2,那么就允许 ImageLoader 加载图片,State 为 2 也就是 SCROLL_STATE_FLING,熟悉列表滑动的同学应该知道,SCROLL_STATE_FLING  就是 滑动列表的时候手指松手后列表继续滑动的那一段 ,叫 fling,毕竟只有 fling 的时候才有 Velocity,松手后会根据这个值的大小计算滑动曲线和滑动时长
  2. 滑动图片加载优化不生效 的时候,就到了常规的列表滑动优化:即列表停止之后才开始加载图片 :State !=0,0 即 SCROLL_STATE_IDLE

阶段二优化的技术分析

微博的主页在初始化的时候,会给首页的 ListView 注册一个 HwPerfVelocityCallback,从名字可以看出来,这个回调是监听 Velocity 的,也就是滑动的速度,两个回调:

  1. HwPerfonVelocityDownToThreshold : 当速度降低到阈值之后,打开 ImageLoader 的图片加载功能
  2. HwPerfonVelocityUpToThreshold: 当速度升高到阈值之后,关闭 ImageLoader 的图片加载功能

下图为反编译后的源码

至于滑动曲线,则需要查看华为的 Framework 的代码,由于代码量比较大,这里只贴一下 OverScroller.java 中的 update 方法,具体感兴趣的可以自己去翻一番华为的 Framework 代码

计算 Distance 的代码

计算 Velocity 的代码

关于滑动曲线的解释,大家可以看这一篇知乎回答,其中对比了 iOS 和 Android 的滑动曲线的不同 :为什么 iOS 的过渡动画看起来很舒服?

其他厂商处理

上面图中代码最后一段还有一个判断开关, 如果 boolean a = HwPerfUtil.m14290a 这个返回的是 false,这就是说有可能华为这个优化关闭了,有可能是非华为机器,那么会 判断 Android 版本号和全局 Feature 开关

对应的 FeedAbManager 就是一个 Feature 管理器,可以在线开关某些 Feature

而 m52580k 的实现如下

可以看到这里还受到一个全局的 Feature 配置:feed_scroll_loadimage_enable,这个 Feature 是服务端可以配置的

这里就是处理其他厂商的逻辑

最后一个问题:滑动点击

滑动点击是个什么问题呢?列表在滑动的过程中,如果用户点击列表的某一个 Item,那么根据 Android 的事件分发机制,这时候列表的 Item 并不会被点击到,而是整个列表先收到点击事件,然后 触发列表滑动停止;列表停止的时候点击 Item 才会触发 Item 的点击

上面阶段二的优化中,在优化了滑动曲线之后,列表处于 Fling 状态的时间变长,如果用户想点击去某一个 Item 查看详情,那么需要先点击一下让列表停止,然后再点击一下才能进去,这就是这一节想说的 :滑动点击问题

滑动(Fling 状态)和点击其实是需要一个平衡的,这个平衡需要开发者自己去把控:

滑动(Fling 状态)的时间越短,列表越容易停下,用户点击列表越容易触发 Item 的点击,但是容易停止带来的问题就是不够柔和。想象你在粗糙的水泥地上滑出去一块石头,这石头没有滑动多久就会停止,不管是扔石头的你还是旁边看你扔石头的我,都不会觉得这有什么美感,但是没得选。这个的 代表其实就是 Android 原生的 Fling 曲线

滑动(Fling 状态)的时间越长,滑动(Fling 状态)的时间越长,列表越不容易停下,用户点击列表越不容易触发 Item 的点击,如果曲线优化的好,给人的感觉就是很柔和,符合物理规律,想象你在光滑的冰面上滑出去一块冰,冰面越滑,冰块滑动的时间就越长,越不容易停下。这其中的极端代表就是 iOS 的 Fling 曲线。说 iOS 极端是因为,iOS 的滑动曲线调的太磨叽了,时间长不说,停的异常慢,很多时候你都需要点击一下列表让他先停止,然后再进行下一步的点击动作。而小米的 MIUI12 对这个也进行了调整,效果要比 iOS 好一些,如果再和三方进行类似华为和微博的合作,体验会更上一层楼

滑动点击问题其实也可以通过厂商和 App 合作来解决,比如,当滑动到整个滑动距离的 98%(或者 95%) 之后,用户点击列表不再是让列表停止,而是列表内的 item 响应这个点击。这个思想来源于 Launcher 的代码,Launcher 的每一页在左右滑动的时候,如果滑动还没有停止但是用户比较手速快点击了某个 icon 想启动,那么这时候不会触发 Page 停止,而是直接响应 icon 的点击启动应用操作

延伸阅读

列表滑动图片加载的性能考虑

前文有提到这个问题,滑动的时候进行图片加载主要有两个问题:

  1. 如果用户滑动非常快,比如是想找昨天发的某个微博,那么今天发的所有的带图片的微博在用户滑动的时候是没必要加载的,因为用户的目标不是这些图片,而 App 去加载这些图片,而程序员是不会为用户提前加载你未看到的数据,因为加载过多的数据不仅容易发生数据复用、缓存过多、内存溢出等错误,还会对服务器造成不必要的资源请求。
  2. 如果用户滑动非常快,那么图片加载队列势必有许多无效的资源(对这一刻的用户来说),而用户真正想看的图片反而排在了加载队列后面,造成加载速度变慢,也会影响用户的体验

滑动中加载图片最大的风险其实就是造成卡顿,因为图片加载本身就是一个比较重的操作,而高帧率的手机上,一帧的时间被压缩到很短,任何小的不确定性都有可能造成卡顿

所以厂商+应用的这个优化: 快速滑动不加载图片,慢速的时候再加载,然后优化滑动曲线 ,其实对厂商和应用都是非常有益处的

列表滑动监听背景知识

下面的 AbsListView 的 OnScrollListener 里面标注了列表滑动的三个状态

  1. 滑动停止:SCROLL_STATE_IDLE
  2. 手指在屏幕上滑动:SCROLL_STATE_TOUCH_SCROLL
  3. 手指离开屏幕,列表靠惯性继续滑动:SCROLL_STATE_FLING

两个回调

  1. 列表状态变化时的回调 :onScrollStateChanged
  2. 列表滑动时候的回调:onScroll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface OnScrollListener { 

// The view is not scrolling. Note navigating the list using the trackball counts as being in the idle state since these transitions are not animated.
public static int SCROLL_STATE_IDLE = 0;

//The user is scrolling using touch, and their finger is still on the screen
public static int SCROLL_STATE_TOUCH_SCROLL = 1;

//The user had previously been scrolling using touch and had performed a fling. The animation is now coasting to a stop
public static int SCROLL_STATE_FLING = 2;

// Callback method to be invoked while the list view or grid view is being scrolled. If the view is being scrolled, this method will be called before the next frame of the scroll is rendered. In particular, it will be called before any calls to
public void onScrollStateChanged(AbsListView view, int scrollState);

// Callback method to be invoked when the list or grid has been scrolled. This will be called after the scroll has completed
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount);
}

列表滑动状态的变化

TOUCH_SCROLL、FLING、IDLE 三个状态对应的列表滑动操作如下

  1. TOUCH_SCROLL: 手指滑动 List 阶段,但是手指没有离开屏幕,这时候上下滑动都是 TOUCH_SCROLL
  2. FLING: 手指滑动 List 后抬手到 List 停止的阶段(必须有一个上滑或者下滑的速度,否则不会进入 Fling)
  3. IDLE:List 停止阶段

这三个状态的变化情况如下

  1. 手指滑动列表,停止后松手:IDLE  -> TOUCH_SCROLL -> IDLE
  2. 手指滑动列表,松手后列表继续滑动,然后停止:IDLE  -> TOUCH_SCROLL -> FLING -> IDLE

列表的 Fling 曲线计算

Fling 触发之后,每一帧都会调用 update 函数来更新 distance 和 mCurrVelocity,所以我们只需要监听 mCurrVelocity 的值,超过一定的阈值,就可以回调给 App

frameworks/base/core/java/android/widget/OverScroller.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
boolean update() { 
final long time = AnimationUtils.currentAnimationTimeMillis();
final long currentTime = time - mStartTime;
double distance = 0.0;
switch (mState) {
case SPLINE: { // Fling 状态
final float t = (float) currentTime / mSplineDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
distance = distanceCoef * mSplineDistance;
mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
break;
}
case BALLISTIC: { // 列表滑到 底 之后的 拉伸阶段
final float t = currentTime / 1000.0f;
mCurrVelocity = mVelocity + mDeceleration * t;
distance = mVelocity * t + mDeceleration * t * t / 2.0f;
break;
}
case CUBIC: { // 列表滑到底拉伸 之后的 回弹阶段
final float t = (float) (currentTime) / mDuration;
final float t2 = t * t;
final float sign = Math.signum(mVelocity);
distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
break;
}
}
mCurrentPosition = mStart + (int) Math.round(distance);
return true;
}

厂商应用联合优化

微博这个优化就是厂商和应用之间联合优化的一个案例,应用对用户体验的极致追求,让这种合作在未来会变得更加频繁,像微信、快手、抖音这些…

下面这个招聘是拼多多的一个 JD,看职位描述是专门对接厂商的优化,也可以看出应用对厂商的合作越来越重视。之前厂商和应用是魔高一尺道高一丈的关系,互相攻防导致最终体验受损的还是用户;而现在这种厂商和应用合作的关系,不仅提升了双方的体验,也会带动 Android 生态圈向好的方面去发展

本文其他地址

微信公众号 - https://mp.weixin.qq.com/s/wJKOvU7CqP3vM0TG7rO66g
知乎专栏(求个赞) - https://zhuanlan.zhihu.com/p/191460094

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

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

微信扫一扫

❌
❌