卡顿丢帧分析

卡顿原理

流畅度:fps。比如 60 fps,意思是每秒画面更新 60 次;120 fps,意思是每秒画面更新 120 次。如果 120 fps 的情况下,每秒画面只更新了 110 次(连续动画的过程),这种情况我们就称之为掉帧,其表现就是卡顿,fps 对应的也从 120 降低到了 110。同时掉帧的原因非常多,有 APP 本身的问题,有系统原因导致卡顿的,也有硬件层的、整机卡的。

方法论

流畅度相关工作内容概述

系统开发的过程中,由很多引起Android卡顿的原因,但是用户和测试感受最直观的是正在使用的应用掉帧和不流畅。由于测试和用户没有办法直接确定卡顿的原因,所以一般会直接将Bug提到我们这里,所有我们的角色更像是一个卡顿问题接口人,负责分析引起卡顿的原因,再把Bug分配给对应的模块负责人去解决,如框架\App\多媒体\Display\BSP等。

所以说直接由我们来解决的问题并不是很多,我们更多的时候是通过专门的分析工具,结合源码来定位和分析问题,最多使用的工具如下:

  1. Systrace\strace\ftrace : 从整个系统的层面来看问题的大致原因。

  2. MethodTrace : 可以从进程的角度 , 以详细调用栈的形式来显示。

  3. Android Studio 的 Profile 工具。

  4. MAT : 用来分析内存问题。

  5. Log : LogReport 抓取或者录制的 Log , 里面包含大量的信息 , 包括各种常规 Log (Main Log , System Log , Event Log , Kernel Log , Crash Log 等) , 也包含了厂商自己加的一些 Log ( Power Log , Performance Log 等) , 也包含事故发生时候的截图 \ 录制的视频等。

  6. 复现视频。

  7. 本地复现。

确定卡顿的根本原因,这需要对Android App开发\Android Framework知识\Display知识\Linux Kernel知识有一定的了解,知道基本的工作流畅,并能熟练使用对应的工具,区分不同的场景,迅速找到问题的原因,然后和相关模块的负责人一起讨论优化。对于一些系统全局性的方案则需要与对应的模块负责人一起分析和解决,必须要的时候我们也会开发一些Feature来解决问题。

性能问题分析的工具和套路

应用卡顿问题的原因比较多,在数据埋点还没有完善的情况下,更多的以来Systrace来从全局的角度来分析卡顿的具体原因:

  1. Systrace分析

    1. 首先确认卡顿的App。

    2. 通过App和主线程和SurfaceFlinger的主线程信息可以确定卡顿的现场。

    3. 分析 Systrace , Systrace 的分析需要一定的知识储备 : 需要知道 Systrace 每一个模块展示的内容是如何与用户感受到的内容相对应的 ; 需要知道 Systrace 上各个模块的交互式如何展示的 ; 需要知道 Binder 调用信息 ; 需要会看 Kernel 信息。

      • 如果是App主线程耗时,则分析App主线程的原因。

      • 如果是System的问题,则需要分析System_Server\SurfaceFlinger\HWC\CRTE\CPU等。

  2. TraceView + 源码分析

    1. 使用Systrace确定原因后,可以是会用TraceView结合源码查看对应的代码逻辑,Android Studio的Profile工具可以以线程为单位,进行Method的Profile,可以打出非常详细的函数调用栈,并可以与Systrace对应。

    2. 源码分析可以使用 Android Studio 进行断点调试 App 或者 Framework , 观察 Debug 信息是否与预期相符。

  3. 很多问题也需要借助 Log 工具抓上来的 Log 进行分析 , Log 分析 Log 里面一些比较重要的点 (一般从 Log 里面很难确定卡顿的原因, 但是可以结合 Systrace 做一定的辅助分析)。

    1. 截图:确定卡顿发生的时间点\卡顿的界面。

    2. Dumpsys meminfo信息。

    3. Dumpsys cpuinfo信息。

    4. “Slow dispatch” 和 “Slow delivery” Log 信息。

    5. 卡顿发生的一段时间内的 EventLog , 还原卡顿时候用户的操作。

  4. 本地尝试复现

    1. 可以录高数录像,观察细节,如果必现,可以让测试这边提供录像。

    2. 过滤Log,找到卡顿时候的异常Log。

    3. 多抓几份Systrace,有助于确定原因。

  5. 可以让测试提供 LogReport 中没有的一些信息, 来分析当时用户的手机的整体的状态。

adb shell dumpsys activity oom
adb shell dumpsys meminfo
adb shell cat /proc/buddyinfo
adb shell dumpsys cpuinfo
adb shell dumpsys input
adb shell dumpsys window

通过性能数据数据分析

由于用户反馈的不确定性 , 和内部测试的不完备性 , 通过系统或者 App 的性能埋点数据来做分析 , 是改进系统的一个好的方法。 一方面不用用户主动参与 , 一方面有大量的数据可以来做分析 , 看趋势。目前国内各大手机厂商和 App 厂商基本都有自己的 APM 平台 , 负责监控 App 或者系统的监控程度 , 来做对应的优化方案 , 比如腾讯的 Matrix 平台已经监控了下面这些内容 , 其他的 App 厂商可以直接接入。

手机厂商由于有代码权限 , 所以可以采集到更多的数据 , 比如 Kernel 相关的数据 : cpu 负载 \ io 负载 \ Memory 负载 \ FSync \ 异常监控 \ 温度监控 \ 存储大小监控 等 , 每一个大项又都有几十个小项。所以可以监控的数据会非常多 , 遇到问题也可以从多个技术指标去分析。这就需要在这方面经验非常丰富的团队 , 去定义这些监控指标 , 确定最终要收集那些信息 , 收集上来的数据如何去分析等。目前能力比较强的手机厂商 , 都在底层各个模块 , 结合硬件做优化 , 因为归根结底都是资源的分配 ; 而一些研发实力不是很强的厂商 , 则重点还是围绕在根据场景分配资源。

总结

这里简单概述了一下流畅性问题的一般分析思路和分析工具 , 而且由于我的方向主要在 Framework 和 App , 所以很多东西都是从上层的角度来说的 , 想必 Kernel 优化团队会有更好的角度和分析。

展望一下,这里想把手机厂商分为三类:

  1. 一类是苹果,自己研发芯片和核心元件,有自己的OS和生态;

  2. 二类是三星、华为,自己研发芯片和核心元件(当然华为和三星还是有所区别),共享 Android OS 和生态,当然三星在本土化这一块做的是不如华为和其他 Top 厂商的;

  3. 三类是其他 Android 手机厂商,芯片和核心元件来自于不同供应商,共享 Android OS和生态。

从技术层面看:

  1. 苹果始终会是在性能的第一阵营,可以顺利推行从硬件到 OS 到 APP 级别的任何性能保障方案;

  2. 三星、华为属于第二阵营,可以实现芯片-OS层面的整合优化;

  3. 其他 Top Android 手机厂商差距不会太大,他们有多个不同的 SoC 供应商,方案有差异,非常芯片底层的地方,往往不会去涉及,更多是做纯软件层面的策略性的优化,有价值但是不容易形成壁垒,注意这个不容易形成壁垒指的是在 top 厂商中间,一些小的厂商往往还是心有余而力不足。不过还是很期待看到有更多的突破出现。

系统篇

主要列举一些由 Android 平台自身原因导致的卡顿问题。各大国内 Android 厂商的产品由于硬件性能有高有低 , 功能实现各有差异 , 团队技术能力各有千秋 , 所以其系统的质量也有高有低 , 这里我们就来列举一下 , 由于系统的硬件和软件原因导致的性能问题。

Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化。目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些。

说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显。引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题。

Android平台性能导致的性能案例

下面会列出来一些实际的卡顿案例 , 这些导致卡顿的原因都是由于 Android 系统平台的一些问题导致的 , 有些问题在开发阶段就会暴露出来 , 这一类通常会在发给用户之前就解决掉 ; 有些问题是用户在长时间使用之后才会暴露出来 , 这一类问题最多 , 但是也比较难以解决 ; 还有一些问题 , 只有非常特殊的场景或者特殊的硬件才会暴露出来。

这些实际的案例 , 很多都可以在 Systrace 上看出来 , 所以我的很多贴图都是 Systrace 上实际被发现的问题 , Systrace 从系统全局的角度 , 来展示当前系统的运行状况 , 通常被用来 Debug Android 性能问题。

SurfaceFlinger主线程耗时

SurfaceFlinger 负责 Surface 的合成 , 一旦 SurfaceFlinger 主线程调用超时 , 就会产生掉帧。SurfaceFlinger 主线程耗时会也会导致 hwc service 和 crtc 不能及时完成, 也会阻塞应用的 binder 调用, 如 dequeueBuffer \ queueBuffer 等。

在Android系统中,SurfaceFlinger、HWC(Hardware Composer)和CRTC(Cathode Ray Tube Controller)都是与图形渲染和显示相关的重要组件。以下是它们的简介和相关概念的解释:

1. SurfaceFlinger

SurfaceFlinger是Android的合成器,它负责将多个应用程序的窗口合成成一个最终的图像,然后发送到显示硬件。它的主线程负责处理各种图形请求,包括接收来自应用的缓冲区请求、合成这些缓冲区以及进行屏幕更新。

2. HWC(Hardware Composer)

HWC是一层中介,负责将SurfaceFlinger合成的图像传递给显示硬件。它可以决定哪些图层需要由GPU处理,哪些可以直接由显示硬件处理,以提高性能和减少功耗。HWC的存在使得在显示过程中,CPU和GPU可以更高效地使用,并减少渲染延迟。

3. CRTC(Cathode Ray Tube Controller)

CRTC是显示控制器的一个组件,负责控制显示设备的输出信号。虽然名字源于显像管,但现代显示设备(如LCD或OLED)也用类似的概念。CRTC负责管理显示的帧率和同步,确保图像正确呈现。

4. dequeueBuffer 和 queueBuffer

dequeueBuffer:这是一个调用,用于从缓冲区队列中获取一个可用的缓冲区。应用程序通过这个调用来请求一个空闲的图像缓冲区,以便准备下一帧的图像。

queueBuffer:这个调用则是将准备好的缓冲区返回给SurfaceFlinger,表示该缓冲区已填充图像数据,可以被合成和显示。

主线程耗时的影响

SurfaceFlinger主线程耗时:如果SurfaceFlinger的主线程因为某种原因(如处理复杂的合成任务、等待资源等)而耗时过长,那么它就会阻塞对HWC和CRTC的调用。这意味着:

- HWC可能无法及时处理接收到的图层数据,导致显示延迟。

- CRTC也可能无法及时更新显示内容,进一步增加延迟。

- 应用的Binder调用(如dequeueBuffer和queueBuffer)可能会被阻塞,从而导致应用的渲染效率降低,用户体验变差。

总结

在Android的图形架构中,SurfaceFlinger、HWC和CRTC之间的协调非常重要。如果SurfaceFlinger的主线程耗时过长,会影响整体的图形渲染性能和响应速度,进而影响应用的运行效率和用户体验。

下图中的 SurfaceFlinger 主线程在后半部分明显超时:

SurfaceFlinger 主线程处理不及时导致应用卡顿(第一帧卡顿,后续都为黄帧)。

屏下光感截图导致SurfaceFlinger渲染不及时

有的 Android 机型使用了屏下光感 , 屏下光感的实现方法也会影响 SurfaceFlinger 主线程的运行。屏下指纹需要频繁截图 , 来区分光线和屏幕的变化 , 进行对应的亮度变化, 但是其主线程截图的方法会导致 SurfaceFlinger 主线程被截图操作所耽误, 从而导致卡顿。

HWC Service执行耗时

hwc Service 耗时也会导致 SurfaceFlinger 下一帧不会做合成操作, 导致应用的 dequeueBuffer 和 setTransationState 方法被阻塞, 导致卡顿。如下图, 可以看到 SurfaceFlinger 的掉帧情况, Binder 的阻塞情况 和 CRTC 的耗时情况。

hwc 耗时。

crtc 等待 hwc。

CRTC执行耗时

crtc 执行耗时的结果就是 SurfaceFlinger 下一帧不会做合成操作, 导致应用的 dequeueBuffer 和 setTransationState 方法被阻塞, 导致卡顿。如下图, 可以看到 SurfaceFlinger 的掉帧情况, Binder 的阻塞情况 和 CRTC 的耗时情况。

CPU调度问题
重要任务跑在小核性能不足导致卡顿

如下图,RenderThread跑到了小核,导致这一帧执行时间过长,造成卡顿图片:

如下图 , cpu 频率对性能的影响图片:

优先级低未能及时获取cpu时间片导致卡顿

在调度器看来的低优先级任务 , 在用户这里未必是低优先级任务 , 他可能正在和 App 的主线程交互 , 或者正在和 system_server 进行交互。

被RT进程抢占

App 主线程或者渲染线程被 RT 进程抢占也会导致系统卡顿或者响应慢 , Google 也意识到了这个问题 , 也在尝试在应用启动的时候 , 把 App 主线程和渲染线程的优先级也设置为 RT , 不过这个属性一直没开 , 因为会导致应用启动速度变慢。

大小核调度问题

大小核调度的问题通常表现在该跑在大核的任务跑到了小核 , 或者该在小核运行的任务却持续跑到大核 ,或者错误的被绑定在了某一个核心上。

如下图, 这是一个 CTS 问题, CTS 主线程由于被绑定到了 cpu7 , 由于 cpu7 在执行 RenderThread , 所以主线程没有调度到, 导致 CTS 失败:

触发Thermal导致限频

触发 Thermal 发热限频也有可能导致卡顿 , 这算是一种硬件级别的保护 , 如果手机已经过热 , 此时如果不进行干涉 , 那么可能会导致用户手机太烫而无法持续使用手机. 一般这个时候都会对系统的资源进行一些限制 , 比如降低 cpu\gpu 的最高频率之类的 , 这么做的话 , 势必也会对流畅性造成影响。如果你手机非常热 , 而且变卡了 , 那么放下手机休息一会 , 查杀一下后台 , 或者重启一下手机。

后台活动进程太多导致系统繁忙

后台进程活动太多,会导致系统非常繁忙, cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 , 这也是系统这边经常会碰到的问题。

CPU繁忙

Dumpsys cpuinfo可以查看一段时间内cpu的使用情况。

主线程调度不到,处于Runnable状态

当线程为 Runnable 状态的时候 , 调度器如果迟迟不能对齐进行调度 , 那么就会产生长时间的 Runnable 线程状态 , 导致错过 Vsync 而产生流畅性问题。

无关进程活跃耗时

无关进程通常是人为定义的,指的是当前前台App运行无关的进程,这些活跃进程势必会对App主进程的调度产生影响,不管这些无关进程是系统的还是App自身的,或是其他三方的App的。

CPU被占用

当后台任务过多的时候,cpu资源就会异常紧缺,如下图就是在系统低内存的时候,HeapTask和kswapD几乎沾满了整个cpu,在疯狂地想系统申请内存。

System锁

system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿。

Layer过多导致SurfaceFlinger Layer Compute耗时

Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。

所以在使用 Android 系统的时候 , 记得多用多任务清理后台任务。

Input报点不均匀

如果出现 Input 报点不均匀或者没有报点的情况, 那么主线程由于没有收到 Input 事件, 所以不去做绘制, 也会导致卡顿。如下图 , 这是一个连续滑动的 Systrace 图 , 最下面两行是 InputReader 和 InputDispatcher , 可以看到在滑动的过程中, InputReader 和 InputDispatcher 没有读出来 Input 事件, 导致卡顿。

LMK频繁工作抢占cpu

LMK工作时,会占用cpu资源,其表现主要有下面几点:

  1. CPU资源:由于LMK杀掉了进程通常都是一些Cache或者Service,这些进程由于低内存被杀之后,通常会很快就被其主进程拉起来,然后又被LMK杀掉,从而进入了一种循环。由于起进程是一件很消耗CPU的操作,所以如果后台一直有进程被杀或重启,那么前台的进程很容易出现卡顿。

  2. Memory:由于低内存的原因,很容易触发各个进程的GC,如下图的CPU状态可以看到,用于内存回收的HeapTaskDeamon出现非常频繁。

  3. IO:低内存会导致磁盘IO变多,如果频繁进行磁盘IO,由于磁盘IO很慢,那么主线程会有很多进程处于等IO的状态,也就是我们经常看到的Uninterruptible Sleep。

低内存导致IO耗时

低内存情况下,很容易出现主线程IO从而导致应用卡顿。

主线程IO导致卡顿

主线程IO导致应用启动速度慢

滑到列表IO导致卡顿

GPU合成导致SurfaceFlinger耗时

当 SurfaceFlinger 有 GPU 合成时, 其主线程的执行时间就会变长, 也会导致合成不及时而卡顿。

KSWAPD跑大核

低内存时, kswapd 由于负载比较高 , 其 cpu 占用比较高, 且经常会跑到大核上 , 导致机器发热限频, 或者抢占主线程的 cpu 时间片。

SurfaceFlinger Vsync不均匀

SurfaceFlinger 有时候会出现 Vsync 不均匀的情况, 不均匀指的是 Vsync 间隔会持续地变化, 一会大一会小, 就会导致用户看到的画面不均匀, 有卡顿感。如下图 , 可以明显看到 SurfaceFlinger 的 VSYNC-sf 这一行间隔是不一样的。这种问题一般是由于 SurfaceFlinger 这边的修改或者 HWC 的修改导致的。

三方应用使用Accessibility服务导致系统卡顿

三方应用如果使用 Accessibility 服务监听了 Input 事件的话, InputDispatcher 的行为就会与预期的出现偏差, 导致 InputDispatcher 没有及时把事件传给主线程导致卡顿。

总结

Android 原生系统是一个不断进化的过程 , 目前已经进化到了 Android Q , 每个版本都会解决非常多的性能问题 , 同时也会引进一些问题 ; 到了手机厂商这里 , 由于硬件差异和软件定制 , 会在系统中加入大量的自己的代码 , 这无疑也会影响系统的性能。

上面列出的这些影响流畅性的案例 , 只是 Android 系统开发中遇到的性能问题的冰山一角 , 任何一个问题都会对用户的使用产生影响 , 这也是为什么手机厂商越来越重视系统优化。手机厂商非常重视开发过程中和用户使用过程中遇到的性能问题 , 并开发和提出各项优化措施 , 从硬件到软件 , 从用户行为优化到系统策略动态学习。这也是为什么现在的手机厂商的系统越做越好 , 质量越来越高的一个原因 , 那些不重视质量只重视设计和产品的手机厂商 , 都渐渐地被消费者淘汰了。

应用篇

这一篇文章我们主要列举一些由于 App 自身原因导致的卡顿问题。各位用户在使用 App 的时候 , 如果遇见卡顿现象 , 先别第一时间骂手机厂商优化烂 , 先想想是不是这个 App 自己的问题。

Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化。目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些。

说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题。

Android App自身导致的性能问题

Systrace 从系统全局的角度 , 来展示当前系统的运行状况 , 通常被用来 Debug Android 性能问题。

App主线程执行时间长

主线程执行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超时都会导致卡顿 , 下面就是一些真实的案例。

Measure \ Layout 耗时\超时 (或者没有调度到)

Draw耗时

Animation回调耗时

View初始化耗时(PlayStore)

List Item初始化耗时(WeChat)

decodeBitmap耗时(或者没有调度到)

uploadBitmap耗时

这里的uploadBitmap主要是upload bitmap to gpu的操作,如果bitmap过大,或者每一帧内容都在变化,那么就需要频繁upload,导致渲染线程耗时。

BuildDrawingCache耗时

应用本身频繁调用buildDrawingCache会导致主线程执行耗时从而导致卡顿,从下图来看,主线程每一帧明显超过了Vsync周期。

微信对话框有多个动态表情的时候,也会出现这种情况导致的卡顿。

使用CPU渲染而不是GPU渲染

如果应用在Activity中设置了软件渲染,那么就不会走hwui,直接走skia,纯cpu进程渲染,由于这么做会加重UI Thread的负载,所以大部分情况下这种写法都会导致卡顿。Hardware Layer

主线程Binder耗时

Activity resume 的时候, 与 AMS 通信要持有 AMS 锁, 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作。

游戏SurfaceView内容绘制不均匀

这一项指的是游戏自身的绘制问题, 会导致总是不能满帧去跑, 如下图, 红框部分是SurfaceFlinger 显示掉帧, 原因是底下的游戏在绘制的时候, 刚好这一帧超过了 Vsync SF 的信号。这种一般是游戏自身的问题。

WebView性能不足

应用里面涉及到 WebView 的时候, 如果页面比较复杂, WebView 的性能就会比较差, 从而造成卡顿。

帧率与刷新率不匹配

如果屏幕帧率和系统的 fps 不相符 , 那么有可能会导致画面不是那么顺畅。比如使用 90 Hz 的屏幕搭配 60 fps 的动画。

应用性能跟不上高帧率屏幕和系统

部分应用由于设计比较复杂, 每一帧绘制的耗时都比较长 , 这么做的话在 60 fps 的机器上可能没有问题 , 但是在 90 fps 的机器上就会很卡, 因为从 60 -> 90 , 每帧留给应用的绘制时间从 16.6 ms 变成了 11.1 ms , 如果没有在 11.1 ms 内完成, 就会出现掉帧的情况。如下图, 这个 App 的性能比较差, 每一帧耗时都很长。

主线程IO操作

主线程操作数据库,使用 SharedPerforence 的 Commit 而不是 Apply。

WebView与主线程交互

与 WebView 进行交互的时候, 如果 WebView 出现问题, 那么也会出现卡顿。

微信文章页卡顿。

RenderThread耗时

RenderThread 自身比较耗时, 导致一帧的时长超过 Vsync 间隔。

渲染线程耗时过长阻塞了主线程的下一次 sync。

多个RenderThread同步导致主线程卡顿

有的 App 会产生多个 RenderThread ,在某些场景下 RenderThread 在 sync 的时候花费比较多的时间,导致主线程卡顿。

adb shell ps -AT | grep 10300 | grep RenderThread
u0_a170      10300 16228  6709 2693260 305172 SyS_epoll_wait      0 S RenderThread
u0_a170      10300 17394  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17395  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17396  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17397  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17399  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17400  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17401  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170      10300 17402  6709 2693260 305172 futex_wait_queue_me 0 S RenderThread

总结

Android 原生系统是一个不断进化的过程 , 目前已经进化到了 Android Q , 每个版本都会解决非常多的性能问题 , 同时也会引进一些问题 ; 到了手机厂商这里 , 由于硬件差异和软件定制 , 会在系统中加入大量的自己的代码 , 这无疑也会影响系统的性能。同样由于 Android 的开放 , App 的质量和行为也影响着整机的用户体验。

本篇主要列出了 App 自身的实现问题导致的流畅性问题 , Android App 最大的问题就是质量良莠不齐 , 不同于 App Store 这样的强力管理市场 , Android App 不仅可以在 Google Play 上面进行安装 , 也可以在其他的软件市场上面安装 , 甚至可以下载安装包自行安装 , 可以说上架的门槛非常低 , 那么质量就只能由 App 开发者自己来把握了。

许多大厂的 App 质量自然不必多说 , 他们对性能和用户体验都是非常关注的 , 但也会有需求和功能过多导致的性能问题 , 比如微信就非常占内存 ; 新版本的 QQ 要比之前版本的使用起来流畅性差好多。中小厂的 App 就更不用说了. 再加上 Android 平台的开放性 , 需要 App 玩起来黑科技 , 什么保活 \ 相互唤醒 \ 热更新 \ 跑后台任务等。站在 App 开发者的角度来说这无可厚非 , 但是系统开发者则希望系统能在用户使用的时候 , 前后台 App 都能有正常的行为 , 来保证前台 App 的用户体验。也希望 App 开发者能重视自己 App 的性能体验 , 给用户一个好印象。

系统这边发现 App 自身的性能问题 , 且在其他厂商的手机上也是一样的表现的时候 , 通常会与 App 开发者进行联系 , 沟通一起解决。

低内存篇

实际案例这里我们有列举一些由于系统低内存导致的卡顿 , 由于 Android 低内存对整机性能影响比较大 , 所以单独写一篇文章 , 来概述系统低内存对整机性能的影响。

随着 Android 系统版本的更迭 , 以及 App 的代码膨胀 , Android 系统对内存的需求越来越大 , 但是目前市面上还存在着大量的 4G 内存以下的机器 , 这部分用户就很容易遇到整机低内存的情况 , 尤其是在系统大版本更新和 App 越装越多的情况下。

Android 低内存会导致性能问题 , 具体表现就是响应慢和卡顿。比如启动一个应用要花比平时更长的时间 ; 滑动列表会掉更多帧 ; 后台的进程减少导致冷启动变多 ; 手机很容易发热发烫等 , 下面我会概述发生这些性能问题的原因 。Debug 的方法 , 以及可能的优化措施。

低内存的数据特征和行为特征

Meminfo信息

最简单的方法是使用Android系统自带的Dumpsys meminfo工具。

adb shell dumpsys meminfo
......
Total RAM: 7,658,060K (status moderate)
 Free RAM:   550,200K (   78,760K cached pss +   156,ba480K cached kernel +   314,960K free)
 Used RAM: 7,718,091K (6,118,703K used pss + 1,599,388K kernel)
 Lost RAM:  -319,863K
     ZRAM:     2,608K physical used for   301,256K in swap (4,247,544K total swap)
   Tuning: 256 (large 512), oom   322,560K, restore limit   107,520K (high-end-gfx)

如果系统处于低内存的话,会有如下特征:

  1. FreeRam的值非常少,Used Ram的值非常大。

  2. ZRAM使用率非常高(如果开了Zram的话)。

LMK && kswapd线程活跃

低内存的时候,LKMD会非常活跃,在Kernel Log里面可以看到LMK杀进程的信息:

[kswapd0] lowmemorykiller: Killing 'u.mzsyncservice' (15609) (tgid 15609), adj 906,
to free 28864kB on behalf of 'kswapd0' (91) because
cache 258652kB is below limit 261272kB for oom score 906
Free memory is -5540kB above reserved.
Free CMA is 3172kB
Total reserve is 227288kB
Total free pages is 271748kB
Total file cache is 345384kB
GFP mask is 0x14000c0

上面这段Log的意思是说,由于mem低于设定的900水位线(261272kb),所以把pid为15609的mzsyncservice这个进程杀掉(这个进程的adj是906)。

proc/meminfo

Linux Kernel展示meminfo的地方,从结果来看,当系统处于低内存的情况时候,MemFree和MemAvailable的值都很小。

shell cat proc/meminfo
MemTotal:        5630104 kB
MemFree:          148928 kB
MemAvailable:     864172 kB
Buffers:           28464 kB
Cached:          1003144 kB
SwapCached:        19844 kB
Active:          1607512 kB
Inactive:         969208 kB
Active(anon):    1187828 kB
Inactive(anon):   426192 kB
Active(file):     419684 kB
Inactive(file):   543016 kB
Unevictable:       62152 kB
Mlocked:           62152 kB
SwapTotal:       2097148 kB
SwapFree:          42576 kB
Dirty:              3604 kB
Writeback:             0 kB
AnonPages:       1602928 kB
Mapped:           996768 kB
Shmem:              7284 kB
Slab:             306440 kB
SReclaimable:      72320 kB
SUnreclaim:       234120 kB
KernelStack:       89776 kB
PageTables:       107572 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     4912200 kB
Committed_AS:   118487976 kB
VmallocTotal:   263061440 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
CmaTotal:         303104 kB
CmaFree:            3924 kB
整机卡顿 && 响应慢

低内存的时候,整机使用的时候要比低内存的时候要卡很多,点击应用或者启动App都会有不顺畅或者响应慢的感觉。

低内存对性能的具体响应

影响主线程IO操作

主线程出现大量的IO相关的问题:

  1. 反馈到Trace上就是有大量的黄色Trace State出现,例如:Uninterruptible Sleep | WakeKill - Block I/O。

  2. 查看其Block信息(kernel callsite when blocked::“wait_on_page_bit_killable+0x78/0x88)。

Linux 系统的 page cache 链表中有时会出现一些还没准备好的 page ( 即还没把磁盘中的内容完全地读出来 ) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了。只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长。

当出现大量的 IO 操作的时候,应用主线程的 Uninterruptible Sleep 也会变多,此时涉及到 io 操作(比如 view ,读文件,读配置文件、读 odex 文件),都会触发 Uninterruptible Sleep , 导致整个操作的时间变长。

出现CPU竞争

低内存会触发 Low Memory Killer 进程频繁进行扫描和杀进程,kswapd0 是一个内核工作线程,内存不足时会被唤醒,做内存回收的工作。 当内存频繁在低水位的时候,kswapd0 会被频繁唤醒,占用 cpu ,造成卡顿和耗电。

比如下面这个情况, kswapd0 占用了 855 的超大核 cpu7 ,而且是满频在跑,耗电可想而知,如果此时前台应用的主线程跑到了 cpu7 上,很大可能会出现 cpu 竞争,导致调度不到而丢帧。

HeapTaskDaemon 通常也会在低内存的时候跑的很高,来做内存相关的操作。

进程频繁查杀和重启

对 AMS 的影响主要集中在进程的查杀上面 , 由于 LMK 的介入 , 处于 Cache 状态的进程很容易被杀掉 , 然后又被他们的父进程或者其他的应用所拉起来 , 导致陷入了一种死循环。对系统 CPU \ Memory \ IO 等资源的影响非常大。

比如下面就是一次 Monkey 之后的结果 , QQ 在短时间内频繁被杀和重启。

14:32:16.932 1435 1510 I am_proc_start: [0,30387,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:16.969  1435  3420 I am_proc_bound: [0,30387,com.tencent.mobileqq]
07-23 14:32:16.979  1435  3420 I am_kill : [0,30387,com.tencent.mobileqq,901,empty #3]
07-23 14:32:16.996  1435  3420 I am_proc_died: [0,30387,com.tencent.mobileqq,901,18]
07-23 14:32:17.028  1435  1510 I am_proc_start: [0,30400,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.054  1435  3420 I am_proc_bound: [0,30400,com.tencent.mobileqq]
07-23 14:32:17.064  1435  3420 I am_kill : [0,30400,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.082  1435  3420 I am_proc_died: [0,30400,com.tencent.mobileqq,901,18]
07-23 14:32:17.114  1435  1510 I am_proc_start: [0,30413,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.139  1435  3420 I am_proc_bound: [0,30413,com.tencent.mobileqq]
07-23 14:32:17.149  1435  3420 I am_kill : [0,30413,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.166  1435  3420 I am_proc_died: [0,30413,com.tencent.mobileqq,901,18]
07-23 14:32:17.202  1435  1510 I am_proc_start: [0,30427,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.216  1435  3420 I am_proc_bound: [0,30427,com.tencent.mobileqq]
07-23 14:32:17.226  1435  3420 I am_kill : [0,30427,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.249  1435  3420 I am_proc_died: [0,30427,com.tencent.mobileqq,901,18]
07-23 14:32:17.278  1435  1510 I am_proc_start: [0,30440,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.299  1435  3420 I am_proc_bound: [0,30440,com.tencent.mobileqq]
07-23 14:32:17.309  1435  3420 I am_kill : [0,30440,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.329  1435  2116 I am_proc_died: [0,30440,com.tencent.mobileqq,901,18]
07-23 14:32:17.362  1435  1510 I am_proc_start: [0,30453,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.387  1435  2116 I am_proc_bound: [0,30453,com.tencent.mobileqq]
07-23 14:32:17.398  1435  2116 I am_kill : [0,30453,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.420  1435  2116 I am_proc_died: [0,30453,com.tencent.mobileqq,901,18]
07-23 14:32:17.447  1435  1510 I am_proc_start: [0,30466,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.474  1435  2116 I am_proc_bound: [0,30466,com.tencent.mobileqq]
07-23 14:32:17.484  1435  2116 I am_kill : [0,30466,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.507  1435  2116 I am_proc_died: [0,30466,com.tencent.mobileqq,901,18]
07-23 14:32:17.533  1435  1510 I am_proc_start: [0,30479,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.556  1435  2116 I am_proc_bound: [0,30479,com.tencent.mobileqq]
07-23 14:32:17.566  1435  2116 I am_kill : [0,30479,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.587  1435  2116 I am_proc_died: [0,30479,com.tencent.mobileqq,901,18]
07-23 14:32:17.613  1435  1510 I am_proc_start: [0,30492,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.636  1435  2116 I am_proc_bound: [0,30492,com.tencent.mobileqq]
07-23 14:32:17.646  1435  2116 I am_kill : [0,30492,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.667  1435  2116 I am_proc_died: [0,30492,com.tencent.mobileqq,901,18]

其对应的Systrace - SystemServer中可以看到AM在频繁杀QQ和起QQ。

此Trace对应的Kernel部分也可以看到繁忙的cpu。

影响内存分配和触发IO

手机经过长时间老化使用整机卡顿一下 , 或者整体比刚刚开机的时候操作要慢 , 可能是因为触发了内存回收或者 block io , 而这两者又经常有关联。内存回收可能触发了 fast path 回收 \ kswapd 回收 \ direct reclaim 回收 \ LMK杀进程回收等(fast path 回收不进行回写)。

回收的内容是匿名页 swapout 或者 file-backed 页写回和清空。(假设手机都是 swap file 都是内存,不是 disk), 涉及到 file 的,都可能操作 io,增加 block io 的概率。

还有更常见的是打开之前打开过的应用,没有第一次打开的快,需要加载或者卡一段时间。可能发生了 do_page_fault,这条路径经常见到 block io 在 wait_on_page_bit_killable(),如果是 swapout 内存,就要 swapin 了。如果是普通文件,就要 read out in pagecache/disk。

do_page_fault —> lock_page_or_retry -> wait_on_page_bit_killable 里面会判断 page 是否置位 PG_locked, 如果置位就一直阻塞, 直到 PG_locked 被清除 , 而 PG_locked 标志位是在回写开始时和 I/O 读完成时才会被清除,而 readahead 到 pagecache 功能也对 block io 产生影响,太大了增加阻塞概率。

实例

下面这个 Trace 是低内存情况下 , 抓取的一个 App 的冷启动 , 我们只取应用启动到第一帧显示的部分 ,总耗时为2s 。可以看到其 Running 的总时间是 682 ms。

低内存的启动情况

低内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 共花费了 2s。从下面的 Thread 信息那里可以看到:

  1. Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共花费 750 ms 左右(对比下面正常情况才 130 ms)。

  2. Running 的时间在 600 ms (对比下面正常情况才 624 ms , 相差不大)。

从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程也非常多 , 比如若干个 kworker 和 kswapd0。

正常内存情况下

正常内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 只需要 1.22s。从下面的 Thread 信息那里可以看到:

  1. Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共才 130 ms。

  2. Running 的时间是 624 ms。

从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程非常少。

可能的优化方案

下面列举的只是一些经验之谈 , 具体问题还是得具体分析 , 在 Android 平台上 , 对三方应用的管控是非常重要的 , 很多小白用户 , 一大堆常驻通知和后台服务 , 导致这些 App 的优先级非常高 , 很难被杀掉。导致整机的内存长时间比较低。所以做系统的必要的优化之后 , 就要着重考虑对三方应用的查杀和管控逻辑 , 尽量减少后台进程的个数 , 在必要的时候 , 清理掉无用的进程来释放内存个前台应用使用。

  1. 提高 extra_free_kbytes 值。

  2. 提高 disk I/O 读写速率,如用 UFS3.0,用固态硬盘。

  3. 避免设置太大的 read_ahead_kb 值。

  4. 使用 cgroup 的 blkio 来限制后台进程的 io 读操作,缩短前台 io 响应时间。

  5. 提前做内存回收的操作,避免在用户使用应用时碰到而感受到稍微卡顿。

  6. 增加 LMK 效率,避免无效的 kill。

  7. kswapd 周期性回收更多的 high 水位。

  8. 调整 swappiness 来平衡 pagecache 和 swap。

  9. 策略 : 针对低内存机器做特殊的策略 , 比如杀进程更加激进 (这会带来用户体验的降低 , 所以这个度需要兼顾性能和用户体验)。

  10. 策略 : 在内存不足的时候提醒用户(或者不提醒用户) , 杀掉不必要的后台进程。

  11. 策略 : 在内存严重不足且无法恢复的情况下 , 可以提示用户重启手机。

了解卡顿原理

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

  1. 对手机用户来说,卡顿包含了很多场景,比如在 滑动列表的时候掉帧应用启动白屏过长点击电源键亮屏慢界面操作没有反应然后闪退点击图标没有响应窗口动画不连贯、滑动不跟手、重启手机进入桌面卡顿 等场景,这些场景跟我们开发人员所理解的卡顿还有点不一样,开发人员会更加细分去分析这些问题,这是开发人员和用户之间的一个认知差异,这一点在处理用户(或者测试人员)的问题反馈的时候尤其需要注意。

  2. 对开发人员来说,上面的场景包括了 流畅度(滑动列表的时候掉帧、窗口动画不连贯、重启手机进入桌面卡顿)、响应速度(应用启动白屏过长、点击电源键亮屏慢、滑动不跟手)、稳定性(界面操作没有反应然后闪退、点击图标没有响应)这三个大的分类。之所以这么分类,是因为每一种分类都有不太一样的分析方法和步骤,快速分辨问题是属于哪一类很重要。

  3. 在技术上来说,流畅度、响应速度、稳定性(ANR)这三类之所以用户感知都是卡顿,是因为这三类问题产生的原理是一致的,都是由于主线程的 Message 在执行任务的时候超时,根据不同的超时阈值来进行划分而已,所以要理解这些问题,需要对系统的一些基本的运行机制有一定的了解,本文会介绍一些基本的运行机制。

  4. 流畅性这个系列主要是分析流畅度相关的问题,响应速度和稳定性会有专门的文章介绍,在理解了流畅性相关的内容之后,再去分析响应速度和稳定性问题会事半功倍。

  5. 流畅性这个系列主要是讲如何使用 Systrace (Perfetto) 工具去分析,之所以 Systrace 为切入点,是因为影响流畅度的因素很多,有 App 自身的原因、也有系统的原因。而 Systrace(Perfetto) 工具可以从一个整机运行的角度来展示问题发生的过程,方便我们去初步定位问题。

卡顿现象及影响

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

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

  1. 偶尔出现的小卡顿会降低用户的体验感,比如刷微博的时候卡了一下或返回桌面动画卡顿这类问题。

  2. 整机出现卡顿则会让手机无法使用。

  3. 现在是高帧率时代,如果用户习惯了120fps,在用户比较容易感知的场景下突然切换到60fps,用户会有明显的感知,并觉得出现了卡顿。

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

卡顿定义

应用一帧渲染的整体流程

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

从执行顺序的角度来看

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

从Systrace的角度来看

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

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

总结

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

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

如何定义卡顿:

  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. Choreographer机制。

  5. Buffer流程和TripleBuffer。

  6. Input流程。

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

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

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 如下:

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;
        }
    }
}

系统机制 - Message机制

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

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

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 等。

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 一般都是一一对应的。

系统机制 - Choreographer

Vsync是通过Choreographer来控制应用刷新的频率。

Choreographer的引入,主要是配合Vsync,给上层App的渲染提供一个稳定的Message处理时机,也就是Vsync到来的时候,系统通过对Vsync信号周期的调整,来控制每一帧绘制的时机。至于为什么Vsync周期选择是16.6ms(60fps),是因为目前大部分手机的屏幕都是60Hz的刷新率,也就是16.6ms刷新一次,系统为了配合屏幕的刷新率,将Vsync的周期也设置为16.6ms,如果每个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机制开始一帧的绘制工作流程图。

系统机制 - Buffer流程和TripleBuffer

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

在 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 的一个对比图:

三 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. 降低 GPUSurfaceFlinger 瓶颈 :这个比较好理解,双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现 SurfaceFlinger 无法及时合成而导致掉帧;在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间。

坏处就是 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 如下:

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. 是否是低内存场景:表现为Imkd进程繁忙,App进程的HeapTaskDeamon耗时,由很多Block IO。

  5. 定位App进程在Systrace中的位置:

    1. 打开Systrace后,首先要看的就是App进程,主要是App的主线程和渲染线程,找到Systrace中每一帧耗时的部分,比如下面这种,可以看到App的UI Thread的红框部分,耗时110ms,明显不是正常的。

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

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

      • 60fps对应的Vsync周期是16.6ms。

      • 90fps对应的Vsync周期是11.1ms。

      • 120fps对应的Vsync周期是8.3ms。

  6. 分析SurfaceFlinger进程的主线程和Binder线程

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

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

  7. 从整机角度分析和Binder调用分析

    1. 上面的案例,可以很容易就看到是App自身执行耗时,那么只需要把耗时的部分设计到的View找到,进行代码或者设计方面的优化就可以了。

    2. 有时候 App 进程的主线程会出现大量的 Runnable 或者 Binder 调用耗时,也会导致 App 出现卡顿,这时候就需要分析整机问题,要看具体是什么原因导致大量的 Runnable 或者 Binder 调用耗时。

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

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

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

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

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

案例说明

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

抓了 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主线程中的体现。

如上图,App 主线程上的 deliverInputEvent 标识了应用处理 input 事件的过程,input up 之后,就进入了 Fling 阶段。

分析主线程

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

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

分析渲染线程

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

  1. RenderThread本身耗时,任务比较繁忙。

  2. RenderThread的任务受CPU影响(可能是频率低了,或者是跑到小核了)。

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

分析SurfaceFlinger

这里主要看两点:

  1. App对应的BufferQueue的Buffer情况。通过这个我们可以知道在SurfaceFlinger端,App是否有可用的Buffer提供给SurfaceFlinger进行合成。

  2. SurfaceFlinger主线程的合成情况,通过查看SurfaceFlinger在sf-vsync到来的时候是否进行了合成工作,就可以判断这一帧是否出现了卡顿。

判断是否卡顿的标准如下:

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

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

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

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

回到渲染线程

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

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

  1. RenderThread本身耗时,任务比较繁忙。

  2. RenderThread的任务受CPU影响(可能是频率低了,或者是跑到小核了)。

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

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

分析CPU区域的信息

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

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

且此时没有 cpu boost 介入。

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

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

  1. 对比其他正常的帧,是否有跑在小核的。如果有且没有出现掉帧,那么说明我们的猜想是错误的。

  2. 对比其他几个异常的帧,看看掉帧的原因是否也是因为 RenderThread 任务跑到了小核导致的。如果不是,那么就需要做其他的假设猜想。

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

  1. 对比其他正常的帧,没有在小核跑的,包括掉帧后的下一帧,调度器马上把 RenderThread 摆到了大核,没有出现连续掉帧的情况。

  2. 对比其他几个异常的帧,都是由于 RenderThread 跑到了小核,但是小核的性能不足导致 RenderThread 执行耗时,最终引起卡顿。

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

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

  1. 调整大小核迁移的阈值参数或者修改调度器算法。

  2. 参考竞品表现,看看竞品在这个场景的性能指标,调度情况等,分析竞品可能使用的策略。

TripleBuffer在这个场景发挥了什么作用

Triple Buffer 几个作用:

  1. 缓解掉帧。

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

  3. 降低GPU和SurfaceFlinger瓶颈。

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

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

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

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

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

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

举个例子,以滑动为例,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 次,步子扯大了一次,感官上会觉得卡+跳跃。

Systrace的Frame颜色是什么意思

这里的 Frame 标记指的是应用主线程上面那个圈,共有三个颜色,每一帧的耗时不同,则标识的颜色不同。点击这个小圆圈就可以看到这一帧所对应的主线程+渲染线程(会以高亮显示,其他的则变灰显示)。

绿帧

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

黄帧

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

红帧

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

没有红帧就没有掉帧

不一定,判断是否掉帧要看SurfaceFlinger,而不是看App。

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

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

出现黄帧且掉帧的情况

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

主线程为什么要等待渲染线程

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

如下图所示,是因为前一帧的渲染线程超时,导致这一帧的渲染线程任务在排队等待。主线程是需要等待渲染线程执行完 syncFrameState 之后 unblockMainThread,然后才能继续。

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

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

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

这一段 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 来做对比,只取我们关注的一部分数据。

小米 - 90fps

OPPO - 90fps

下面是一些对比,可以看到小米在桌面滑动这个场景,性能是要弱于 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, 导致看不出来卡顿。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值