Android直播渲染卡顿原理分析

1,161 阅读10分钟

背景

从开发直播间以来,客户端就一直遇到卡顿和渲染的问题,针对卡顿也做过性能优化专项来解决这些问题。但是有些现象未深入的思考,比如:

  • 为什么Android渲染出问题时视频流是黑屏
  • 为什么UI卡顿甚至卡死的时候视频流不受影响
  • 用户反馈卡的时候是不是真的发生了卡顿?
  • 如何以技术指标来监控和衡量用户视角的卡顿?

目标

用技术指标体现用户视角是否卡顿、卡顿的影响、卡顿率。

  • 监控整体卡顿需要监控全链路,从场景上看应该包括UI、事件处理循环、流媒体(接收图元数据、解码、渲染、显示),各指标互补;
  • 同时也要技术指标与业务场景结合分析,静态页面FPS为0但并不代表卡顿,需要结合事件处理循环分析有无事件处理
  • FPS高未必流畅,与每帧画面的渲染时长以及两帧画面间隔有关系

业界方案分两类:

  • 工具:以开发调优为切入点的工具类产品比如Perfdog、Android Studio profile、Xcode Instruments,无法监控线上生产环境的卡顿;
  • 自研:各厂自研的APM、流媒体卡顿监控,与业务结合相对紧密不够通用;

渲染基础知识

硬件基础

移动端设备渲染中,有三个核心硬件会参与其中:CPU、GPU、屏幕。下面我们来简单介绍下三者的概念: CPU:即中央处理器,是系统的运算和控制单元,渲染过程中负责界面的尺寸测量、布局计算、软解码等步骤。

GPU:即图形处理器,有非常强的并行计算和浮点计算能力,一般用来对纹理数据进行渲染和合成。

屏幕:由一个个像素点组成,以固定的频率(比如每帧16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点。

总结一句话就是:CPU计算好图元信息提交至GPU,GPU渲染完成后将渲染结果存入帧缓冲区,最后屏幕从缓冲区中读取数据并显示,像下图这样:

image2022-7-14_10-36-51.png

屏幕成像原理

下面我们来看一下屏幕成像的一些技术概念:

屏幕刷新频率:是指设备刷新屏幕的频率,常见的有60Hz、120Hz、144Hz等,最新的也有可变刷新率设备,会随内容的展示而改变。

VSYNC:即垂直同步信号,是由硬件时钟产生的一个脉冲信号,为了协调CPU计算和屏幕绘制时间,使其同时开始绘制,能避免画面撕裂的问题。

双缓冲:为了解决单缓冲的效率问题,GPU 通常会引入两个缓冲区,前缓冲和后缓冲。UI先在后缓冲中绘制,然后再和前缓冲交换,渲染到显示设备中。

三缓冲:在双缓冲基础上引入三块缓冲区,使CPU、GPU的利用率进一步提高,降低产生画面卡顿的概率。

现在我们知道,为了使屏幕UI显示流畅,系统做了很多优化,从双缓冲到三缓冲,从没有同步到引入VSYNC信号,下面我们看一下优化的效果。

不使用VSYNC信号

image2022-7-6_20-41-41.png 图中有三个时间轴:Display、CPU、CPU。Display按照固定时间来取数据的,暂定每个间隔是16ms,也就是每秒60帧。

每一个数字相同的方块表示同一帧,每一帧都按照CPU→GPU→Display的流程来处理。

从图中可以看出,在没有VSYNC信号的时候,CPU绘制的开始时间不确定,导致在第3个16ms时,帧2还没有渲染完成,导致卡顿。

使用VSYNC信号

image2022-7-6_20-42-46.png 在加入VSYNC信号同步后,每收到VSYNC信号,CPU就开始处理各帧数据,已经解决了刷新不同步的问题。

从图中可以看出,CPU和GPU的渲染时间都控制在16ms内,渲染很完美,但假如CPU和GPU的渲染时间超过16ms,就会有新的问题,看下面的图:

image2022-7-6_15-24-4.png 1)在第一个16ms时间段,CPU、GPU根据VSYNC信号开始绘制,Display显示A帧。

2)在第二个16ms时间段,Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示,这时CPU无所事事,因为A Buffer被Display使用,B Buffer被GPU使用。

于是第二个16ms和第四个16ms都会发生卡顿,这个问题本质上是缓冲区不够导致的,所以系统引入了三缓冲机制。

三缓冲+VSYNC信号

image2022-7-6_20-41-41.png 从图上可以看出来,引入三缓冲机制后,第二个16ms还会卡顿,但后续不会再卡顿,降低了卡顿的发生次数,当前Android和iOS都引入了类似的机制。

Android端渲染原理

UI渲染

整体框架

UI渲染指应用界面在屏幕显示的过程,我们知道整个过程在硬件上需要CPU、GPU、屏幕三者的协同工作,在应用中需要很多部分共同完成渲染工作:

  • App进程:和各个服务交互,管理绘制工作
  • WindowsManager服务:提供窗口,并绑定画布和窗口
  • SurfaceFlinger服务:提供画布,并把各应用画布合成一张图
  • HAL:硬件抽象层,简单理解就是屏幕

具体如下图所示:

1111.jpeg 1)第一条线:App->Wms→SF,App进程在创建Activity后,通过与WindowManagerService交互,向SurfaceFlinger申请创建Surface。创建成功后,App端对应的得到为SharedBufferClient的对象,用来控制共享内存的写入

2)第二条线:App->SC,SF->SC,App负责UI测量、布局、绘制,SurfaceFinger负责UI渲染合成,两个进程通过共享内存(SharedClient)来交换数据

3)第三条线:SF→HAL,SurfaceFinger合成内容后,提供给HAL,最终会提供给硬件帧缓冲区,刷新到屏幕上

4)隐藏的线:VSYNC信号,HWComposer基于硬件来产生VSYNC信号的,App进程通过监听VSYNC信号来开始绘制,SurfaceFlinger通过监听VSYNC信号控制层的合成。

UI视图绘制流程

B18FE4A6-21F0-44C7-9863-EEFE04AE724C.jpeg 先看一张图,图中是Android应用的View结构,这些UI元素是以树形结构来组织的,即它们存在着父子关系,其中子UI元素位于父UI元素里面,常规的渲染分3个步骤:

1)测量:从根视图开始,依次计算每一个视图的宽高,最终视图期望的窗口尺寸

2)布局:从根视图开始,依次计算每个子视图在父视图中的位置

3)绘制:从根视图开始,按照绘制背景、绘制自己、绘制子视图的流程来进行内容绘制

现在我们知道UI的绘制流程,那UI如何监听VSYNC信号来触发绘制的呢?负责这块的是Choreographer类

1)ViewRootImpl的requestLayout()方法会调用Choreographer的postCallback(),会在主线程添加同步屏障,并通过Choreographer注册VSYNC信号监听

2)Choreographer通过DisplayEventReceiver向framework层注册下一个VSYNC信号

3)ViewRootImpl收到VSYNC信号后,移除同步屏障并执行doTraversal()方法,触发UI绘制

到这里我们就知道UI渲染的整个流程,从监听VSYNC信号开始,收到后开始执行App进程的绘制,完成后触发SurfaceFlinger的绘制,最后在下一个VSYNC信号到来后,渲染到屏幕上。

所以监控UI渲染的核心就在于监控VSYNC信号的接收

流媒体渲染

特殊的画布

流媒体的流是渲染在SurfaceView上的,它本质上是一个View。但不同于普通视图,SurfaceView最大的特点是它有自己的Surface,如下图所示:

67a0ff49295e4ef29490b5838a008523.png 在一个页面(图中的Activity)中,所有的普通视图共用一个画布(图中的aSurface),而SurfaceView有自己的画布(图中的mSurface),一个独立的surface对应的是一个独立的渲染线程。

所以每一个流的渲染都可以在单独线程去做,而其它视图的渲染必须在主线程,这就解释了为什么有时候UI卡顿不会影响流媒体渲染。

另外一方面,因为SurfaceView是一个普通View,所以它也有普通视图的特性:

  • 在页面窗口创建时,SurfaceView会收到回调,这时它会通过SurfaceFlinger创建Surface
  • 在UI渲染时,SurfaceView会收到绘制的回调,这时它会绘制一个纯黑色的背景,这也是Android界面是“黑屏”的根源

流媒体渲染流程

渲染流程.jpg 流媒体在经历过网络请求,解码,同步,渲染等流程后,将帧数据通过eglSwapBuffers提交给系统层进行渲染。换句话说,eglSwapBuffers是我们业务层能接触到的最后一行代码,接下来的surfaceFlinger和HWC等系统层和硬件层超过了我们的可控范围。

流媒体监控

综上,流媒体的监控,就是要监控每一帧从组帧开始到eglSwapBuffers方法返回为true渲染的全部过程,记录每个帧是被丢弃了还是完成了渲染,渲染延时是多少,帧间隔,帧率等等

落地

UI卡顿监控

1)Android系统默认的帧率是60帧/秒。APP 业务层通过监控Choreographer的 void doFrame(long frameTimeNanos)方法,来监控APP业务层是否卡顿。

2)系统每渲染一帧,就会回调一下doframe这个方法,并返回对应的时间。通过检测两帧回调之间的时间间隔可以知道是否发生了绘制卡顿。

3)通过分析MessageQueue的消息执行,可以监控系统所有消息的执行耗时情况。如屏幕点击,广播等

流媒体卡顿监控

1)计算统计一秒内eglSwapBuffers执行并返回true的次数。如果小于满帧数,则说明出现了丢帧。如果小于阈值帧,则说明流媒体发生了卡顿。则进一步进行卡顿归因。

 2)从拉流获取网络数据组帧为监控起点(记做起点),到将帧数据提交给OpenGL渲染,执行eglSwapBuffers方法返回true(记做终点)。

 3)对这一秒内的帧做ID标记,追踪这一秒内所有帧的链路行为。

 4)根据上面数据,定位出卡顿的具体环节,从而针对对应环节做优化

指标

指标释义计算方式
Big Janky Count严重卡顿次数单帧绘制耗时大于 MOVIE_FRAME_TIME*3
FPS实际渲染帧率数据获取时间周期内,实际渲染帧数/ 数据获取间隔时间
Janky Count普通卡顿次数单帧绘制耗时大于 MOVIE_FRAME_TIME*2
Latent Jank潜在卡顿根据视觉惯性,单帧耗时大于前三帧平均耗时2倍
RFPS相对帧率数据获取时间周期内,(理论满帧-实际掉帧数)/ 数据获取间隔时间
Stutter卡顿率卡顿比。当发生 jank 的帧的累计时长与区间时长的比值。

优化思路

软件方向

1)特定机型,系统,高系统负载时进行业务降级

2)系统新特性适配。如Doze适配,Android 12适配

3)线程复用,优化线程锁等待和ANR

硬件方向

1)拉高硬件执行能力:如指定线程和CPU大核绑定,以减少CPU切换开销,提高CPU缓存命中逻辑

2)功耗优化

3)硬解(GPU)替代软解(CPU)

4)Android中的“Metal”–Vulkan,重点优化CPU性能