普通视图

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

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性能优化知识星球 : 欢迎加入,多谢支持~

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

微信扫一扫

关于 Android 系统流畅性的一些思考

作者 Gracker
2018年8月13日 12:01

最近一直想写一些关于 Android 系统流畅度的东西,因为流畅度这个东西,是消费者最直接能体验到的,再加上 Android 一直为人诟病的越用越卡顿,使得大家在提到安卓机的时候,都会有一丝阴影。这也是大部分人买手机首先会考虑 iPhone 的一个原因。

由于 Google 对 Android 持开放态度,各个手机厂商生产不同产品定位的机器,以及各个 Android 应用的质量良莠不齐,导致影响 Android 流畅度的因素非常非常多,并非大家简单的以为是系统没有优化好,很多时候你会发现,不同 SOC 但是相同的系统,体验完全就是两种。

所以我想和大家聊聊影响 Android 系统流畅性的一些原因,后续大家遇到卡顿的问题,也不会单纯把锅甩给系统,或许你卸载一个 App 就解决了呢.

我想从下面几个方面展开聊这个话题:

  1. 硬件层面
  2. 系统层面
  3. 应用层面
  4. 流畅度优化闭环

准备好了,那就开始吧,欢迎你加入讨论

1. 硬件层面

CPU

cpu 是手机硬件里面最核心的一个器件,这也是把 cpu 作为第一个来说的主要原因,cpu 之所以重要,是因为 Android 系统的运行过程中,大部分是跟 cpu 打交道,cpu 的能力强弱,直接决定了这款手机的档次。

手机 cpu 目前主要有高通、华为、三星、联发科四家在做,每家都有高中低档,高端 cpu 的排名大概是 高通>华为>三星>联发科, 具体的排名可以去这里看(仅供参考):http://www.mydrivers.com/zhuanti/tianti/01/

GPU

各个厂商提供的 SOC 里面,通常包含了 cpu 和 gpu ,所以大部分情况下,只要一说 cpu,其 gpu 也是对应确定的,比如高通骁龙845 SOC 带的 gpu 就是 Adreno 630。

gpu 的能力强弱更多的影响的是 gpu 强相关的应用和游戏,比如绝地求生-刺激战场 、 崩坏3 、极品飞车、狂野飙车等。反而王者荣耀这样的游戏更多的是吃 cpu 而不是 gpu。

Ram

随着 Android 版本的更新,以及硬件的更新换代,Android 系统对内存的需求越来越强,目前4G内存的手机基本上已经成了标配,旗舰机器没个 6G 或者 8G 你都不好意思说自己是旗舰。

内存主要影响系统行为,内存越大,系统就越可以以空间换时间:后台可以缓存更多的进程,杀进程不再那么激进;可以根据用户习惯预加载一些文件或者进程;各种虚拟机、hwui、进程的参数可以往宽松里调。反馈到用户那里,就是快。

当然如果后台进程多了又没有管住在后台跑,那么又会很耗电,有点得不偿失,这也是为什么国内的系统都会对进程管理这一块进行魔改。

UFS && EMMC

ufs 和 emmc 都是面向移动端的 flash 标准,我们最长听说的就是 emmc5.1 和 ufs 2.1 ,具体可以参考这篇文章:https://zhuanlan.zhihu.com/p/26431201

对于用户来说,ufs 和 emmc 的差异主要在文件读取速度、视频加载速度、文件拷贝等方面,总之能上ufs就别考虑 emmc。

不过有时候这个也是需要 SOC 支持的,比如高通 660,就不支持 ufs,能不买就别买吧。

屏幕分辨率

我们最常见的屏幕分辨率是 1080P,即 1920*1080. 对用户来说,屏幕分辨率除了会影响视觉感官外,还会在系统某些地方有差异,比如截图、录屏、合成等操作。越高的屏幕分辨率,在这里的耗时就越久,也越耗电。

这也是部分 2K 手机在某些场景下,把分辨率降低到 1080P 去运行的原因。比较失败的一个例子就是当年的魅族 MX4 Pro ,在硬件性能不足以支撑 2K 的情况下,强行上了 2K 屏幕,导致很多情况下,用户反馈又卡又耗电。

电池大小

电池大小决定着续航,也决定着手机设计,手机厂家往往需要在这两者之间找一个平衡,在电池技术没有突破的情况下,就算各家都有快充,还是建议用户在选购手机的时候,尽量选大容量电池的手机,比如 Oppo Find X 或者 Vivo NEX,或者华为 Mate 10.

SoC 平台

SoC的全称叫做:System-on-a-Chip,除了我们之前说的 cpu、gpu,Soc 上还有很多器件,具体可以看这篇文章:https://zhuanlan.zhihu.com/p/37634251
,这里就不展开讲了。

SoC 是整个手机最重要的部分,是一切体验的基础。现在高通、三星、MTK 给手机厂家提供的硬件就是 Soc ,以及其配套的 Android 适配系统。手机厂商拿到这个之后,在其基础上做整机的设计,系统这边会在配套的 Android 适配系统上做移植,也就是把各家系统差异化的东西移植到新系统上。

从目前的高通、三星、MTK 三家的适配系统的质量来看,高通提供的适配系统是功能最完善的,高通在 AOSP 的基础上,加上了高通自己的非常多的优化代码,并提供了完善的参数供手机厂商去配置,总的来说开发起来是很舒服的,本身系统的问题不会太多,加上高通的文档完善,支持速度快,国内那么多手机厂商都在用高通也就不足为奇了。

至于专利费,该给的要给啊。

我们经常会说,如果魅族早点用高通的 Soc,早 TM 上市了。

2. 系统层面

应用的管控策略

大部分 Android 应用开发者对国内的手机厂商恨的咬牙切齿,最大的原因就是国内系统对应用管控这一块进行了大量的魔改,除非你是 QQ 或者微信,否则灭了屏结果都一样。

国内厂商这么做,不是没有原因的,国内应用厂商的全家桶相互唤醒,已经到了一种丧心病狂的地步,牵一发而动全身,一点都不夸张。

我们遇到的很多用户反馈的整机卡顿问题,抓 Trace 和 Log 来看,都是后台有应用在乱跑,或者后台大量的进程常驻,内存根本不够,而这些普通用户根本就不知道怎么去处理。

所以国内厂商一般会在系统里面做限制,以保障用户的基础体验:

  1. 除非必须,一个应用偷偷拉起来另外一个应用的行为是不被允许的
  2. 除非必须,一个应用常驻后台是不被允许的
  3. 除非必须,一个应用在灭屏后在后台乱跑是不被允许的
  4. 除非必须,一个应用在后台长时间占用 cpu 是不被允许的
  5. 除非必须,一个应用弹窗是不被允许的

另外手机厂商会有其他的逻辑清理后台的应用,尽管你是合理存在的。

对进程的严格管控,也导致了国内系统的体验有一定的影响,首当其冲的是通知,如果一个应用没有接入这个手机厂商提供的 push sdk,那么他这辈子别想发通知给用户了,如果接入了手机厂商提供的 sdk(目前大部分应用的普遍做法),由于应用不在后台,用户点击通知要等好久才可以进入到对应的界面,毫无用户体验可言。

内存策略

手机厂商常常会根据手机的内存大小来定制各种不同的策略,比如后台应用的缓存个数、LowMemoryKiller 的阈值、杀进程模块的阈值、显示模块的缓存大小阈值、用户最常用应用的个数等。

很多低端机用户反馈卡顿,我们查看发现,内存是造成卡顿的主要元凶,在低内存的机器上,由于内存不足,系统会频繁杀后台,同时也有频繁的内存->文件,文件->内存 的操作,Trace 上很多 BlockIO,很多平时执行很快的操作,现在执行要很久,再加上部分进程被杀之后马上重启,重启之后又被杀,cpu 占用很高,此时就会很卡。

随着 Android 系统和应用的更新,只会越来越吃内存,目前4G内存是标配,明年或许 6G 才是标配了,能上 8G 尽量上 8G。

进程调度策略

进程调度策略有时候也会影响用户的流畅性,当应用的渲染链路上,有哪个环节因为某些原因,没有被调度到的时候,很大可能会造成卡顿。

调度不到在 Trace 上的表现是 Runnable,常见的调度不到的情况有:

  1. 同时运行的进程太多,cpu 这边的几个核处理的任务基本都是满的
  2. 进程优先级较低
  3. 调度器过于不灵敏,不能及时响应大任务

另外由于 cpu 引起的卡顿情况还有:

  1. 从大核心掉落到小核心上,小核心处理能力不足,会造成短暂的卡顿
  2. 触发温控或者触发低电量,此时某些系统会限制大核的使用,导致卡顿
  3. 系统锁也是造成卡顿的一大元凶,尤其是 wms 锁和 ams 锁,再加上 binder 通信,relayoutWindow 了解一下?
  4. 核心频率不足,导致函数执行时间过长导致卡顿
  5. 大核心被占用,任务又调度不到小核,导致卡顿

系统调优往往需要针对上面的情况做对应的处理,给用户一个好的用户体验。具体的调优方式,往往跟系统和 Soc 强相关,又涉及到 Kernel 和 功耗,改起来是牵一发而动全身,需要非常谨慎。

渲染线程和主线程

Android 应用的渲染链路上最重要的就是主线程和渲染线程,主线程就是应用启动时创建的 MainThread,对应的也会创建一个 RenderThread(硬件加速默认开启),我们平时比较看重的 GPU Profile 那条线,基本就包含了主线程和渲染线程的各个阶段的执行时间,从 GPU Profile ,就可以很容易看到应用的瓶颈

大部分应用的卡顿都发生在主线程和渲染线程上,比如:

  1. 较长时间的 input 事件处理
  2. 较长时间的动画事件处理,比如 ListView 的新 Item 的生成
  3. 复杂界面的 Measure、 Layout、Draw
  4. 较大 Bitmap 的频繁 upload
  5. 复杂渲染指令的执行

很多编程的不好的实现,都可以在上面几个步骤里面体现出来,这些都可以通过 Systrace 看出来。

当前应用的渲染链路上的一切优先级都应该是最高的,后台的进程不应该对其造成影响,这也是系统优化的核心要素,不过要做到这一点也是比较难的,你很难考虑到所有的情况,比如有的用户的使用环境就是很复杂,而且都是必须的,这时候就不是很好处理。

TripleBuffer

之前有提到 TripleBuffer,这个是 Project Butter 引进的,其中 Vsyncv 和 TripleBuffer 的引进使得 Android 的流畅度上了一个台阶,关于这个可以参考这篇文章 : https://niorgai.github.io/2017/03/21/Android-Draw-System/

对于用户来说这个是透明的,影响的是 GPU Profile 的展示,有时候如果有一条线超过 16 ms 的警戒线,它不一定代表着卡顿,这就是 TrileBuffer 的作用。

后续我会有文章专门讲这个,如何判断是真正的卡顿。

虚拟机 - Art 和 Dalvik

对用户来说,Art 虚拟机相比 Dalvik 虚拟机,最大的提升就是解放了应用的主线程,主线程不再频繁被 GC 线程 Stop ,相应卡顿也减少了很多。

当然 Art 带来的好处不止这一点,Art 随着几个大版本的缝缝补补,已经在很多地方远远超过了 Dalvik,有兴趣的可以自己查一下。

温控 && 低电量

之前提到,一旦触发温控或者低电量,系统会对资源做一定的限制,防止手机无限制过热或者快速关机。这限制就包括

  1. 降低 cpu、gpu 最高频率
  2. 减少可运行的 cpu 的核心数
  3. 杀掉部分后台进程
  4. 关闭部分特效
  5. 限制网络连接

总之,这些限制或多或少会对用户造成影响,最大的影响就是卡顿,这就是很多人会遇到打游戏的时候突然很卡的一个原因。

所以说选购手机的时候,除了要看 Soc,还要看散热是否做的够好,电池是否做的够大。

3. 应用层面

复杂的布局

复杂的布局往往是应用卡顿的最主要的元凶之一,复杂的布局意味着更长的 Measure、Layout、Draw ,这会拖慢主线程的执行速度

ListView、RecyclerView 的新的 Item 在初始化的时候也会有类似的问题,由于此时一般是在滑动,这时候的卡顿感会更明显,用户也更容易察觉,这个从 Trace 上也很容易看出来。

过多的业务逻辑

过多业务逻辑导致的卡顿和响应慢的问题,拿淘宝来举例子最合适不过了,每次你冷启动淘宝的时候,进入主界面马上滑动,总感觉跟吃了屎一样,点按钮点不动,滑界面滑不动,虽然最近的版本有优化,不过你找个低端 Android 机,还是原来的配方。

淘宝在启动的时候,需要动态加载很多东西,导致主界面响应很慢,很多东西要动态加载完成后才可以操作,后台还有大量的 dex2oat 操作,可以说是很忙了。

内存颠簸

频繁申请和释放内存,会导致内存颠簸,从 AS 的内存监视器可以看到这一点,短时间内内存曲线上下跳动非常频繁,这时候你需要检查一下是否代码写的有问题。

慢网络

慢网络指的是用户请求网络耗时很久,这会导致用户在某些界面等待内容需要很久,比如知乎经常会出现这种情况,在用户看来,这就是卡了。

不合理的设计

设计和性能往往不可兼得,需要从两者之间做取舍,设计师的设计往往很炫酷,互相嵌套的动画往往是程序员的噩梦,为了实现这些复杂的效果,程序员往往需要复杂的代码来实现,这对应用的渲染链路的压力是非常大的,而且在不同性能的机器上表现差异很大,高端机用户觉得这个效果棒棒哒,低端机用户卡的要骂娘。

程序员需要有这方面的知识和数据,好与设计师动之以情晓之以理。

不过用户是很挑剔的,现在的用户对性能的要求越来越高,哪怕是低端机用户,所以合适的设计应该考虑到这部分用户、或者针对低端机用户做区分。

代码实现错误

俗称 bug ,很多程序员不喜欢解决性能问题,因为这个东西解决起来,性价比很低,拿我司的程序员来说,解一个性能问题的时间,可以解决十几个界面显示的问题,还未必能真的解决。

不过由于代码实现错误引起的性能问题,必须要最高优先级解决。

4. 流畅度优化闭环

实验室监控 && 模拟用户

开发阶段就用各项数据来做监控和对比,尽量模拟用户的使用环境,尽早暴漏性能问题,早日解决。

用户流畅度数据收集

在用户使用阶段,收集性能数据,针对这些数据做分析,找出用户最多遇到的性能问题,针对性地做优化。需要注意此时不能在用户阶段手机太多的信息,否则会导致观察者效应

至于需要收集的数据,则需要根据相关度模型来做判断,卡顿发生的时候,系统的哪些指标是可能导致卡顿的原因,那么这些指标就是我们收集的数据。

另外用户的场景判断也非常重要,需要知道用户是在哪个场景出现的卡顿,一旦用户的数量到了一定的级别,这个是很容易发现问题的。

针对性地优化

大数据发现问题后,后续就是针对性地进行优化,把用户最常遇到性能问题的场景进行排序,对最常见的场景进行调研和优化。很多时候需要与应用开发厂商进行沟通,

然后需要把这些场景纳入到实验室监控环境里,做到 实验室监控 —> 模拟用户 — > 大数据收集 —> 针对性优化 —> 实验室监控补充. 这样一个闭环。

关于我 && 博客

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

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

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

微信扫一扫

❌
❌