android 卡顿、ANR优化(1)屏幕刷新机制
前言:
本文通过阅读各种文章和源码总结出来的,如有不对,还望指出
目录
正文
基础概念
视觉暂留
逐行扫描
帧
CPU/GPU/Surface:
帧率、刷新率、画面撕裂
画面撕裂
Android屏幕刷新机制的演变
单缓存(Android4.0之前)
双缓存
VSync(垂直同步)
三缓存
源码解析
正文
扯这个机制之前,先了解几个基础概念
基础概念
视觉暂留
物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1-0.4秒左右的图像,这种现象被称为视觉暂留现象。是人眼具有的一种性质。人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。但当物体移去时,视神经对物体的印象不会立即消失,而要延续0.1 -0.4秒的时间,人眼的这种性质被称为“眼睛的视觉暂留”。
逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点。
帧
在视频领域中,帧表示一副画面,就比如
在纸上看电影:闭关37天,爆肝手绘700张画,完成一段星爷的搞笑喜剧
嗯,就这样的,一张纸表示一个画面,表示一帧,通过快速翻动,利用视觉暂留的原理实现了电影或者动画的呈现
CPU/GPU/Surface:
- Surface,这里只是简单说明,后续会更新个文章专门讲这个,通俗的讲,这个就是Android的view跟其子类都是绘制在上面的,可以说没有这个Surface的渲染,就没有android那些花里胡哨的布局界面
- CPU:
中央处理器
,主要用于计算数据,在Android中主要用于三大绘制流程中Surface的计算过程,起着生产者的作用 - GPU,
图像处理器
,主要用于游戏画面的渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buffer中,让显示器进行读取,起着消费者的作用。
帧率、刷新率、画面撕裂
- 帧率,也叫帧数,俗称fps,就我们游戏中的fps就是这玩意,指的是GPU1秒内渲染多少画面到buffer中,单位是fps,比如60 fps就是指的是1秒内可以渲染60帧画面到buffer中
- 刷新率,指的是屏幕在1秒内从buffer中读取数据的次数,单位是HZ,常见屏幕刷新率为60Hz,和帧率不一样刷新率是个固定值,更硬件参数有关
画面撕裂
简单点说就是显示器把两帧或两帧以上的数据同时显示在同一个画面的现象,比如这样
画面撕裂的原因:我们知道屏幕刷新率是固定的,假设为60HZ,正常情况下当我们的GPU的帧率也是60fps的时候,GPU绘制完一帧,屏幕刷新一帧,这样是不会出问题的,但是随着GPU显卡性能的提升,GPU的帧率超过60fps后,就会出现画面撕裂的情况,
就比如,GPU的帧率是120fps,每秒钟可以处理120张画面到buffer中,然后就会出现的问题就是每1/120秒就会有一张画面进入buffer中,下一个1/120秒,下一站画面就会把上面一张给取代,然后屏幕的刷新率是60Hz,就可以理解为它一秒内只能扫描到60张画面进行显示。这样就会出现画面撕裂了,因为屏幕提取画面是从上到下一行一行(逐行扫描)把画面显示出来的,本来要1/60秒才能显示完,结果显示一半的时候1/120秒,下一张画面就塞进来了,这时候屏幕肯定会照样会从buffer中拿到画面进行显示的,这样就会造成一半上一张的,一半是下一张的情况
所以其本质是帧率和屏幕刷新率的不一致导致的撕裂。
Android屏幕刷新机制的演变
单缓存(Android4.0之前)
那可能大家要说了,等屏幕一帧刷新完成后,再将新的一帧存到buffer中不就可以了,那你要知道,早期的4.0之前设备是只有一个buffer,且其并没有buffer同步的概念,屏幕读取buffer中的数据时,GPU是不知道的,屏幕读取的同时,GPU也在写入,导致buffer被覆盖,出现同一画面使用的是不同帧的数据。用图来表示,就是这样的:
那既然是因为使用同一个Buffer引起的画面撕裂,使用两个buffer不就可以了?
谷歌当时也觉得可行,然后说干就干,于是开启了黄油计划
双缓存
针对上面的问题关键:图像绘制和屏幕读取这一帧数据使用的是一块Buffer
可以想到的一种解决方案是:不让它们使用同一块Buffer
,用两块让它们各自为战不就好了,这么想的思路确实是对的。分析下这个具体过程:
当图像绘制和屏幕显示有各自的Buffer
后,GPU
将绘制完的一帧图像写入到后缓存(Back Buffer),显示器显示的时候只会去扫描前缓存的数据(Frame Buffer)
,在显示器未扫描完一帧前,前缓存区内数据不改变,屏幕就只会显示一帧的数据,避免了撕裂。
但这样做的最关键一步是,什么时候去交换两块Buffer
的数据?
等 Back buffer准备完成一帧数据以后就进行?肯定是不行的,因为此时屏幕还没有完整显示上一帧内容,这样弄肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。
当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 屏幕撕裂的状况。大致流程如下:
VSync(垂直同步)
VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。
所以说V-sync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。
不过,需要注意的是:开启垂直同步后,就算GPU准备好了Back Buffer
的数据,但屏幕没有逐行扫描完前缓冲区的,就不允许发生帧传递
。GPU就空载着,等待显示器扫描完毕后的VBlank阶段
。
意思就是说,开启VSync后,GPU的帧率被强制锁定为跟屏幕刷新率一样,这就解释了在玩游戏的时候,如果开启了垂直功能,游戏中显示的帧率一直处于一个帧率之下,这个时候显示帧率值就是屏幕刷新率。
这样就能解决问题了嘛?我们来通过一张具体的流程图来看看
Jank
在下面的图中,你将会经常看到Jank
一词语,它术语翻译,叫做卡顿。卡顿很容易理解了,比如我们在打游戏时,经常会遇到同一帧画面在那显示很久没有变化,这就是所谓的Jank
场景1
先看下最原始的,只有双缓冲,没有VSync
影响下,它会发生什么:
图中Display
为显示屏, VSync
仅仅指双缓冲的交换。
(1)Display
显示第0帧,此时 CPU/GPU
渲染第1帧画面,并且在 Display
显示下一帧前完成。
(2)Display
正常渲染第一帧
(3)出于某种原因,如 CPU
资源被占用,系统没有及时处理第2帧数据,当 Display
显示下一帧时,由于数据没处理完,所以依然显示第1帧,即发生“Jank” ,
上图出现的情况就是第2帧没有在显示前及时处理,导致屏幕多显示第一帧一次,导致后面的帧都延时了。根本原因是因为第2帧的数据没能在VBlank
时(即本次完成到下次扫描开始前的时间间隙)完成。
上图可以看到的是由于CPU资源被抢,导致第2帧的数据处理时机太晚,假设在双缓存交换完成后,CPU资源可以立刻为处理第二帧所用,就可以处理完成该帧的数据(当前前提是该帧的处理数据不超过刷新一帧的时间),也就避免了Jank的出现。
场景2
在双缓冲下,有了VSync
会怎么样呢?
如图,当且仅当收到VSync
通知(比如16ms触发一次),CPU
和GPU
立刻开始计算然后把数据写入Buffer
。VSync
同步信号的出现让绘制速度和屏幕刷新速度保持一致,使CPU
和GPU
充分利用了这16.6 ms的时间,减少了jank。
场景3
但是如果界面比较复杂,CPU/GPU处理时间真的超过16.6ms的话,就会发生:
图中可以看出当第1个 VSync
到来时GPU
还在处理数据,这时缓冲区在处理数据B,被占用了,此时的VBlank阶段
就无法进行缓冲区交换,屏幕依然显示前缓冲区的数据A,发生了jank
。当下一个信号到来时,此时 GPU 已经处理完了,那么就可以交换缓冲区,此时屏幕就会显示交互后缓冲区的数据B了。
由于硬件性能限制,我们无法改变 CPU/GPU 渲染的时间,所以第一次的Jank是无法避免的,但是在第二次信号来的时候,由于GPU占用了后缓冲区,没能实现缓冲区交换,导致屏幕依然显示上一帧A。由于此时,后缓冲区被占用了,就算此时CPU是空闲的也不能处理下一帧数据。增大了后期Jank的概率,比如图中第二个Jank的出现。
出现该问题本质的原因是,两个缓冲区各自被GPU/CPU、屏幕显示所占用。导致下一帧的数据不能被处理。
三缓存
找到问题的本质了,那很容易想到,再加一个Buffer
(这里叫它中Buffer
)参与,让添加的这个中Buffer
和后Buffer
交换,这样既不会影响到显示器读取前Buffer
,又可以在后Buffer
缓冲区不能处理时,让中Buffer
来处理。像下图这样:
当第一个信号到来时,前缓冲区在显示A、后缓冲区在处理B,它们都被占用。此时 CPU 就可以使用中缓冲区,来处理下一帧数据C。这样的话,C数据可以提前处理完成,之前第二次发生的Jank
就不存在了,有效的降低了Jank出现的几率。
到这里,可以看出,不管是双缓冲和三缓冲,都会有卡顿、延时问题,只是三缓冲下,减少了卡顿的次数。
那又有人要说了,那就再多开几个不就可以了,是的,buffer越多jank越少,但是你得考虑性价比: 3 buffer已经可以最大限度的避免jank的发生了,再多的buffer起到的作用就微乎其微,反而因为buffer的数量太多,浪费更多内存,得不偿失。
源码解析
我们都知道,View在绘制的时候,最终都要调用ViewRootImpl
的scheduleTraversals
方法(==这个后面会写篇文章扯扯它怎么到这来的),会往MessageQueue
插入同步屏障消息,绘制完成后会移除同步屏障消息。同步屏障消息不懂的看看handler解析(3)-同步消息、异步消息、同步屏障_handler同步消息和异步消息_沙滩捡贝壳的小孩的博客-CSDN博客
@UnsupportedAppUsagevoid scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;//插入同步屏障消息mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);if (!mUnbufferedInputDispatch) {scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}void unscheduleTraversals() {if (mTraversalScheduled) {mTraversalScheduled = false;//移除同步屏障消息mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}}
为了保证View的绘制过程不被主线程其它任务影响,View在绘制之前会先往MessageQueue插入同步屏障消息,然后再注册Vsync信号监听,Choreographer$FrameDisplayEventReceiver
就是用来接收vsync信号回调的
Choreographer$FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiverimplements Runnable {...@Overridepublic void onVsync(long timestampNanos, long physicalDisplayId, int frame) {...//mTimestampNanos = timestampNanos;mFrame = frame;Message msg = Message.obtain(mHandler, this);//1、发送异步消息msg.setAsynchronous(true);mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);}@Overridepublic void run() {// 2、doFrame优先执行doFrame(mTimestampNanos, mFrame);}}
收到Vsync信号回调,注释1会往主线程MessageQueue
post一个异步消息,保证注释2的doFrame
优先执行。
doFrame
才是View真正开始绘制的地方,会调用ViewRootImpl
的doTraversal
、performTraversals
,
而performTraversals
里面会调用我们熟悉的View的onMeasure
、onLayout
、onDraw
。
参考文章:
通俗易懂的Android屏幕刷新机制 - IM Geek开发者社区-移动开发者社区-开源社区-IM Geek官网
“一文读懂“系列:Android屏幕刷新机制_android 刷新 原理_程序员一东的博客-CSDN博客
视觉暂留_百度百科
android 卡顿、ANR优化(1)屏幕刷新机制相关推荐
- 西瓜卡顿 ANR 优化治理及监控体系建设
背景 卡顿 & ANR 在各 APP 中都是非常影响用户体验的问题,关于其的分析和治理一直也是个老生常谈的话题.过去调查卡顿 & ANR 问题主要依赖上报的堆栈和 traceInfo ...
- Android卡顿检测及优化
前言 之前在项目中做过一些Android卡顿以及性能优化的工作,但是一直没时间总结,趁着这段时间把这部分总结一下. 卡顿 在应用开发中如果留意到log的话有时候可能会发下下面的log信息: I/Cho ...
- 深入解析:Android卡顿检测及优化项目实战经验总结,任君白嫖
前言 之前在项目中做过一些Android卡顿以及性能优化的工作,但是一直没时间总结,趁着这段时间把这部分总结一下. GitHub系统教程学习地址:https://github.com/Timdk857 ...
- 深入探索Android卡顿优化(下)
前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 在上篇文章中,笔者带领大家学习了卡顿优化分析方法与工具.自动化卡顿检测方案及优化这两块内容. ...
- 深入探索Android卡顿优化
由于卡顿优化这一主题包含的内容太多,为了更详细地进行讲解,因此,笔者将它分为了上.下两篇.本篇,即为<深入探索Android卡顿优化>的上篇. 本篇包含的主要内容如下所示: 卡顿优化分析方 ...
- Android卡顿优化分析
本篇包含的主要内容如下所示: 1.卡顿优化分析方法与工具 2.自动化卡顿检测方案及优化 在某个 App 的时候,有时我们会看到某个 App 运行起来,即出现了卡现象,如何去定义发生了卡现象呢?马上来了 ...
- Android 卡顿优化之 Skipped * frames 掉帧的计算
Android 卡顿优化之 Skipped * frames 掉帧的计算 有时候看日志的时候,可能会在日志中看到类似下文的打印: Skipped 30 frames! The application ...
- Android卡顿掉帧问题分析之原理篇
当用户抱怨手机在使用过程中存在卡顿问题的时候,会严重影响用户对手机品牌的好感和应用APP的体验,从而导致用户对手机品牌的忠诚度降低或应用APP的装机留存率下降.所以无论是手机设备厂商还是应用APP开发 ...
- 卡顿监测 · 方案篇 · Android卡顿监测指导原则
一.引言 Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享卡顿监测 · 方案篇 · Android卡顿监测指导原则.小木箱从七个维度将Android卡顿监测技术方案解释清楚. 第一个维 ...
最新文章
- jQuery中文入门指南,翻译加实例,jQuery的起点教程
- SpringBoot 实战 (九) | 整合 Mybatis
- python大学课程-大学只安排了C和Python课程,是否有必要学习一下Java
- 【AI初识境】深度学习模型中的Normalization,你懂了多少?
- 90后女博士任985高校特聘教授,这次有点不一样
- 去掉 edittext 长按全选_开封消毒湿巾全选
- process调用protothread机制的相关宏定义——用HelloWorld进程诠释
- 重新写博+linux查找系列
- Linux下更新libnss3的代码,yum安装firefox错误libnssutil3.s
- 用bitbucket积累代码
- 计算机病毒是通过内存传播吗,计算机病毒的工作过程
- PTAM在Linux下编译运行
- CATIA V5汽车焊接夹具设计从基础到高级培训视频教程
- 【项目管理】项目管理四要素
- 知识分享之Golang——一个常见word、excel转换pdf的工具函数
- 1_requests请求
- word使用:默认粘贴方式的更改
- PythonNOJ习题前二十道(西北工业大学cpSkill实验平台)
- VS2015 kb2919355 解决方法汇总
- Augustus安装问题 (3.0 以上都适用)
热门文章
- centos7:在linux世界里,一切皆文件
- MIT Technology Review 2022年“全球十大突破性技术”解读
- 【转载】常备JS操作
- React给antd中TreeSelect组件左侧加自定义图标icon
- 基于乾坤的微前端+SpringBoot2.7整套解决方案的基础通用平台及组件
- 只能存储12KB数据,“码农女神”是怎样把人类送上月球的?
- FFMPEG源码编译(Windows篇)
- 笨方法学Python—ex42:对象、类及从属关系
- 理票侠打开OFD格式发票步骤
- 装虚拟机装Oracle数据库