二十七、UI卡顿的分析和处理

概述

对于安卓app的UI性能分析,systrace是目前使用最广的工具。它能够帮助开发者分析多个模块的运行状态,以及详细信息,比如 SurfaceFlinger,View刷新机制等。

AndroidSDK 本身就提供了 systrace.py 脚本,可以设置数据的采集方式,并收集相关程序运行数据。最终生成一个html文件,供开发者分析程序性能问题。

基本使用

AndroidSdk中提供了 脚本文件位置在: Android/sdk/platform-tools/systrace

基本的使用命令为: python systrace.py --time=10 -o my_systrace.html

详细的参数如下:

由于这是python的脚本,使用之前,必须先在电脑上安装好 python环境,这个并非本文重点,不展开讨论。

上面的命令最终会生成一个 my_systrace.html 文件,需要使用 浏览器打开,推荐chrome 或者以 chrome为内核的其他浏览器。

比如上图,黄色的1处,就是UI渲染时不流畅的表现。绿色2处才正常。

由于分析这个html文件有一些门槛,需要提前对安卓屏幕渲染的流程有一定认知。所以下面将从基础开始讲。

屏幕渲染基础知识

CPU && GPU && 双缓冲机制

关键词:UI线程 , RenderThread线程 , SurfaceFlinger

前者用来进行计算,后者用来进行渲染。 我们在做自定义View的时候有提到过 硬件加速,开启硬件加速,就是用GPU直接来渲染组件。

通常情况下,View的 measure测量,layout布局,draw 绘制 3个流程都是由 CPU来完成的。另外,CPU还负责用户的输入事件的处理,View的动画等,这部分工作都是在UI线程中完成。

当CPU完成了 上面提到的所有工作之后,就会将 结果提交给 渲染线程RenderThread。 之后,工作就交接给了GPU,它会对 这些数据进行栅格化操作。并将数据缓存到 一个 buffer中。最后,手机屏幕从 buffer中读取数据,并显示出来。

实际上,真正对 Buffer中的数据进行合成处理的,是SurfaceFlinger.

仔细分析一下上面的流程,GPU不断向 buffer中存入数据,而屏幕不停地从 buffer中取得数据,这样就存在一个矛盾,就是有可能 GPU还没写入完毕,屏幕就来获取,取得的数据可能会不完整,就造成了屏幕上显示的内容错乱。

为了避免这种问题,安卓引入了双缓冲机制,利用两片缓存区,一个专门用来屏幕读取的FrameBuffer,一个专门供GPU写入数据的 BackBuffer, 在GPU写入完毕之后,通过swap事件,将两个缓存区的内容调换。

双缓冲机制,要求 定期交换 FrameBuffer和BackBuffer的数据,从而保证屏幕上显示最新内容。

当GPU正在向BackBuffer写入数据时,它会将 BackBuffer锁定。

此时,如果到达了 定期交换的时机,那么,本次swap事件将会被忽略。

直接导致的结果,就是屏幕上显示的是 上一个帧的数据。这也就是 我们常说的 丢帧

为了保证App能够流畅工作,我们需要保证 每一帧在16MS之内处理完成 CPU 和 GPU 的工作,包括绘制渲染等。完美的屏幕刷新应该如下图所示:

每一次 的屏幕刷新(swap事件)都发生在 CPU GPU 工作完成之后

VSYNC垂直同步机制

通过以上的双缓冲机制,大部分情况下已经能够满足屏幕刷新的体验要求。但是特殊情况下还是有例外。 这是因为,屏幕刷新率和GPU绘制的频率并不一定是一致的。

  • 屏幕刷新率
    • 指的是 手机屏幕每秒可以刷新多少次
    • 手机出厂之后,屏幕刷新率就确定了,无法改变
  • GPU绘制频率,
    • 指的是 GPU每秒可以绘制多少帧

上面说的特殊情况如下图:

GPU正在工作时,到达了屏幕刷新(swap 事件)的时间节点。这也就导致了丢帧。上图中第三帧显示的还是 老内容,无法使用到最新的数据。

这是由于,屏幕刷新的时机是一定的,但是 一个View的刷新时机,是可以通过代码去控制的,而我们开发者无法保证view刷新时机与 屏幕刷新的时机保持一致。

为了解决这个问题,安卓又引入了 Vsync(垂直同步)机制 :

每隔16MS,硬件层发出 vsync信号,应用层收到这个信号之后,就执行UI的渲染流程,同时 vsync也会触发 SurfaceFlinger从buffer中读取数据,进行合成,显示到屏幕上

简单来说就是: 将GPU 和 CPU 工作的开始时间与 屏幕刷新的时机强制同步

加入vsync之后,屏幕刷新的频率和vsync信号的频率一致,可以优化 刷新时机错位导致的丢帧 问题。

choregrapher 编舞者

有了vsync机制之后,如果我们有view要刷新数据,必须等到下一次vsync信号,才能执行刷新。

那么 软件层是如何 接收 硬件发出的Vsync信号并执行view的刷新操作的呢?

回顾View的绘制流程:

所有Viewinvalidate,都会执行到 ViewRootImpl.javarequestLayout()

requestLayout() 会执行 view的测量布局绘制流程,其核心代码就是在 scheduleTraversals()中. 看下图: 最核心的一句代码,mChoregrapher.postCallback(...)

继续跟踪代码:

从上图红框内可以看出,我们的view刷新动作,最终会形成一个 事件,进入到一个 mCallbackQueue 队列中。

当 Choregrapher 接收到 vsync信号时,它会从 队列中将事件取出并执行。

那么问题就到了 choregrapher 怎么知道 vsync信号的到来的呢?

上图中,除了红框内的 队列处理之外,还有一句代码:scheduleFrameLocked(now); 它最终执行到了一个native (nativeScheduleVsync)方法。

这个native方法实际上就是向系统订阅了一次vsync信号。Android系统每隔16.6MS会发出一次Vsync信号,只有订阅了信号的app能够接收到。

回顾整个流程,我们去view.invalidate()想要刷新UI,Choregrapher 先帮我们注册一个事件,并且同时订阅一个 vsync信号,等到信号到来时,执行我们的UI刷新动作,由此保证刷新UI的时机与屏幕刷新的时机同步。

注意,订阅一次,只能收到一次,如果要收到多次,就只能订阅多次。如果 View存在动画特效,那么必须每个变化都会去订阅一次 Vsync信号。

还有一个未解之谜,那就是 Choregrapher 注册 vsync 信号之后,是在哪儿接收 vsync信号的。

答案就是:FrameDisplayEventReceiver 广播接收器。

上图中,onVsync方法会在vsync信号到来之后执行,执行的内容也很简单,就是 发送了一个 Message到 主线程的 handler中。

Message.obtain(mHandler,this)的源代码如下

由于传入的是第二个参数是 FrameDisplayEventReceiver.this,它实现了Runnable接口,所以接下来该看的就是 FrameDisplayEventReceiverrun方法:

上图就是最终的执行代码,它是 Choregrapher.javadoFrame方法。 它主要完成两大操作:

  1. 进行掉帧逻辑计算,并添加 性能分析的trace日志。
  2. 执行各种callback ,从这里可以看出,其实一帧中就做了这 INPUT,ANIMATION,TRAVERSAL 三件事。

Choregrapher小结

承上

承接了应用层的各种 callback输入,包括 input,animation,traversal绘制。但是它不会立即执行,而是先将他们存入一个 CallbackQueue队列中。

启下

它内部的 FrameDisplayEventReceiver负责接收Vsync信号,当收到信号之后,调用的doCallback,从 CallbackQueue中取出各种事件并执行,其中包括 负责 UI绘制的 traversalRunnable

骚操作

利用 Choregrapher 实现帧渲染监控

Choregrapher会在doFrame方法中执行各种callback,其中包括UI绘制的 traversalRunnable,并且向外部提供了 FrameCallback接口,可以用它来监听doFrame方法的执行过程。

我们可以在自己的app代码中,主动调用 Choregrapher.getInstance().postFrameCallback 来判断是否发生丢帧。

postFrameCallback 实际上 和 view的requestLayout一样是平级关系,都会往 ChoregrapherCallbackQueue中插入一个callback,只是他们的类型不同,postFrameCallbackanimation事件,而requestLayout发送的是 traversal事件

正常情况下,doFrame方法只会打印出 ~~doFrame~~,但是如果执行时间超过了16.6MS, 那么还会打印出 发生丢帧! 注意,由于每一次订阅vsync都只会接收到一次vsync信号,所以,最后一句,就是在递归订阅Vsync,始终保持 渲染丢帧的监控状态。

安卓利用同步屏障优化UI性能

View的 invalidate会向 Choregrapher发送 traversalCallback之前,会执行MessageQueue.postSyncBarrier() 设置同步屏障

handler中的Message分为同步消息和异步消息两类,比如下图就是一个异步消息。

通常情况下,Looper在不停的从MessageQueue中获取消息的时候不区分同步和异步,但是,如果设置了同步屏障,异步消息的优先级会提高,MessageQueue只会在next中提取出 异步消息。而 TraversalRunnable 会被封装到一个异步Message中,因此,View的绘制操作会被优先执行。

再来分析 systrace.html

在了解到本文的所有关于渲染的基础知识之后,我们再来分析 systrace.html

这是一个典型的由于animation动画引起的卡顿。 很明显,图中1处,这一帧之所以不流畅,是因为 inflate 和 decodeBitmap 消耗了太多时间。这里可以直接定位到 某个类的某一个方法在执行过长时间 的绘制 。

检索我们的代码,可以找到有问题的代码源头。从而进行优化。

上图中 inflate 方法在通过反射创建 View,decodeBitmap 在解析一个图片内容,前者可以通过 缓存View来优化,后者也可以通过缓存 bitmap到内存中来优化。

除了上面提到的 动画卡顿之外,还有:

  • 自定义View的draw方法耗时过长,

  • 自定义View的 measure 和 layout耗时过长

  • decodeBitmap 转换图片耗时过长

总结

本文讲述了大量的基础知识,主要包括:

  • CPU GPU 屏幕 的协同工作原理
  • 为了解决Buffer读取问题,引入了 双缓冲机制
  • 为了解决刷新时机与屏幕刷新时机不同步的问题,引入了 Vsync垂直同步机制
  • 详细讲述了 垂直同步机制的实现核心类 Choregrapher的工作原理

最后,列举了一些常见的 可能导致屏幕丢帧的代码问题。

相关推荐
大前端爱好者1 小时前
React 19 新特性详解
前端
随云6321 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
J老熊2 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
我爱学Python!2 小时前
面试问我LLM中的RAG,秒过!!!
人工智能·面试·llm·prompt·ai大模型·rag·大模型应用
OLDERHARD2 小时前
Java - LeetCode面试经典150题 - 矩阵 (四)
java·leetcode·面试
寻找09之夏2 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
银氨溶液3 小时前
MySql数据引擎InnoDB引起的锁问题
数据库·mysql·面试·求职
多多米10053 小时前
初学Vue(2)
前端·javascript·vue.js
柏箱3 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑3 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法