android 卡顿、ANR优化(1)屏幕刷新机制

news2024/10/2 3:15:07

前言: 

本文通过阅读各种文章和源码总结出来的,如有不对,还望指出

目录

正文

基础概念

视觉暂留

逐行扫描

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触发一次),CPUGPU 立刻开始计算然后把数据写入BufferVSync同步信号的出现让绘制速度和屏幕刷新速度保持一致,使CPUGPU 充分利用了这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在绘制的时候,最终都要调用ViewRootImplscheduleTraversals方法(==这个后面会写篇文章扯扯它怎么到这来的),会往MessageQueue插入同步屏障消息,绘制完成后会移除同步屏障消息。同步屏障消息不懂的看看handler解析(3)-同步消息、异步消息、同步屏障_handler同步消息和异步消息_沙滩捡贝壳的小孩的博客-CSDN博客


    @UnsupportedAppUsage
    void 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 DisplayEventReceiver
            implements Runnable {
        ...
        @Override
        public 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);
        }

        @Override
        public void run() {
            // 2、doFrame优先执行
            doFrame(mTimestampNanos, mFrame);
        }
    }

收到Vsync信号回调,注释1会往主线程MessageQueue post一个异步消息,保证注释2的doFrame优先执行。

doFrame才是View真正开始绘制的地方,会调用ViewRootImpldoTraversalperformTraversals

performTraversals里面会调用我们熟悉的View的onMeasureonLayoutonDraw

参考文章:

通俗易懂的Android屏幕刷新机制 - IM Geek开发者社区-移动开发者社区-开源社区-IM Geek官网

“一文读懂“系列:Android屏幕刷新机制_android 刷新 原理_程序员一东的博客-CSDN博客

视觉暂留_百度百科

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/397796.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

限流算法详解

限流是我们经常会碰到的东西,顾名思义就是限制流量。它能保证我们的系统不会被突然的流量打爆,保证系统的稳定运行。像我们生活中,地铁就会有很多护栏,弯弯绕绕的,这个就是一种限流。像我们抢茅台,肯定大部…

案例17-环境混用带来的影响

目录一、背景介绍背景事故二、思路&方案三、过程四、总结nginx做转发fastdfs(文件上传下载)五、升华一、背景介绍 本篇博客主要介绍开发中项目使用依赖项环境闭一只带来的恶劣影响,在错误中成长进步。 背景 本公司另外一个产品开发God…

爱因斯坦求和约定 含代码einsum

目录 一、简介 1.哑标 2.自由标 二、torch实现 1.计算迹 2.取矩阵对角线 3.计算外积 4.batch矩阵乘法 5.带有子列表和省略号 一、简介 爱因斯坦求和约定(Einstein summation convention)是一种标记的约定, 又称为爱因斯坦标记法(Einstein notation), 可以基于一些约定…

position:absolute详解

position:absolute详解 日常开发中,经常会涉及元素的定位,我们都知道,绝对定位相对于最近position不为static的父级元素来定位,但其中定位的位置还是有细微的差别的。 绝对定位根据left和top属性来规定绝对定位元素的位置。 基…

2023年前端面试题集锦

2023年又是行情惨淡的一年,为此我从 「枇杷村IT面试宝典」小程序里收集了一些题目,更多题目可以扫下方二维码查看 现做个总结如下: 1. 在JavaScript中, 0 -0的结果是什么? 结果为true! 严格等于比较的是值和类型&…

tcpdump写文件-w文件大小为0字节问题处理

一同事找来,说用tcpdump在一台linux服务器上抓包写文件,文件大小为0,不知道是什么原因造成,让协助解决。 自己登陆服务器试了一下,发现问题确实如此 不用-w,让打印在平面上,发现正常 以为权限不…

虹科案例 | 如何可持续的对变压器进行温度监控?

为了延长变压器的使用寿命,需要一个测量系统来监测内部整个绕组区域的温度。它必须明确温度升高发生的位置及其强度。您可以在此处了解为什么会这样以及如何在实践中实施? PART 1 变压器多点测温问题 变压器的工作温度越高,使用寿命越短。这里主要存在…

C++核心知识(五)—— 继承和派生

1. 继承概述1.1 为什么需要继承网页类 class IndexPage{ public://网页头部void Header(){cout << "网页头部!" << endl;}//网页左侧菜单void LeftNavigation(){cout << "左侧导航菜单!" << endl;}//网页主体部分void MainBody(){…

[SQL Statements] 基本的SQL知识 之DDL针对表结构和表空间的基本操作

[SQL Statements] 基本的SQL知识 之DDL针对表结构和表空间的基本操作 什么是数据库的表以及表空间 在MySQL中&#xff0c;一个数据库可以包含多个表&#xff0c;每个表是由若干个列&#xff08;column&#xff09;和行&#xff08;row&#xff09;组成的。表是存储数据的基本…

2023年clang12编译问题与解决的记录

最近编译clang12以及尝试基于clang开发一个C的静态代码分析工具&#xff0c;如下是遇到的环境相关的编译问题与解决方案。在此做个记录&#xff0c;同时供可能会遇到同样问题的同学参考 环境说明 注&#xff1a;如下是最终编译成功的环境 clang分支&#xff1a; branch : a…

HTTP 请求头中的 Remote_Addr,X-Forwarded-For,X-Real-IP | Spring Cloud 13

一、$remote_addr 表示发出请求的客户端主机的 IP 地址&#xff0c;但它的值不是由客户端提供的&#xff0c;而是Nginx与客户端进行TCP连接过程中&#xff0c;获得的客户端的真实地址 IP 地址&#xff0c;REMOTE_ADDR 无法伪造&#xff0c;因为建立 TCP 连接需要三次握手&…

Java学习教程,Java基础教程(从入门到精通)

Java 是一门面向对象编程语言&#xff0c;不仅吸收了 C 语言的各种优点&#xff0c;还摒弃了 C 里难以理解的多继承、指针等概念。Java 不但可以用来开发网站后台、PC 客户端和 Android APP&#xff0c;还在数据分析、网络爬虫、云计算领域大显身手。 从学术的角度讲&#xff…

VR全景云展厅,实现7*24小时的线上宣传能力!

数字化时代&#xff0c;虚拟现实技术的应用越来越广泛&#xff0c;其中VR全景云展厅是一种新兴的展示方式&#xff0c;具有独特的展示优势。随着VR技术的不断发展&#xff0c;越来越多的企业、机构和个人开始使用VR全景云展厅来展示他们的产品和服务。一、展厅营销痛点1、实地到…

内网渗透-基础环境

解决依赖&#xff0c;scope安装 打开要给cmd powershell 打开远程 Set-ExecutionPolicy RemoteSigned -scope CurrentUser; 我试了好多装这东西还是得科学上网&#xff0c;不然不好用 iwr -useb get.scoop.sh | iex 查看下载过的软件 安装sudo 安装git 这里一定要配置bu…

105.第十九章 MySQL数据库 -- MySQL半同步复制、复制过滤器、复制的问题和解决方案(十五)

6.1.6 半同步复制 Mysql的主从复制它的复制机制我们称为所谓的异步复制,这里面提到了一个概念异步,那什么叫异步复制呢,所谓异步复制实际上说白了就是在用户发请求到数据库做一些修改的时候,那我们在前面讲过主从复制,如果我们有一个主节点,另外带若干个从节点,假设有2个…

Typescript 全栈最值得学习的技术栈 TRPC

如果你想成为一个 Typescript 全栈工程师&#xff0c;那么你可能需要关注一下 tRPC 框架。本文总共会接触到以下主要技术栈。Next.jsTRPCPrismaZodAuth.js不是介绍 tRPC 吗&#xff0c;怎么突然出现这么多技术栈。好吧&#xff0c;主要这些技术栈都与 typescript 相关&#xff…

知道一个服务器IP应该怎么进入

首先我是国内&#xff0c;访问国外的网站比如谷歌等&#xff0c;访问特别慢&#xff0c;有时候甚至登录不进去。现在知道了一个台湾或者国外的服务器应该怎么登录进去呢&#xff1f;知道服务器IP之后&#xff0c;你还需要知道服务器的远程端口帐号密码才能登录的。知道上面信息…

Java 19和IntelliJ IDEA,如何和谐共生?

Java仍然是目前比较流行的编程语言&#xff0c;它更短的发布节奏让开发者每六个月左右就可以试用新的语言或平台功能&#xff0c;IntelliJ IDEA帮助我们更流畅地发现和使用这些新功能。IntelliJ IDEA v2022.3正式版下载(Q技术交流&#xff1a;786598704&#xff09;在本文中&am…

小樽C++ 多章⑧ (叁) 指针与字符串、(肆) 函数与指针

目录 叁、函数与字符串 肆、函数与指针 4.1 指针作为函数参数 4.2 函数返回指针 4.3 函数指针与函数指针数组 4.4 结构体指针 ​​​​​​​​​​​​​​小樽C 多章⑧ (壹) 指针变量https://blog.csdn.net/weixin_44775255/article/details/129031168 小樽C 多章⑧ …

数据可视化的正确逻辑和关键点

由于移动互联网和手机的普及发展&#xff0c;截至2022年6月&#xff0c;我国短视频的用户规模达9.62亿&#xff1b;即时通信用户规模达10.27亿&#xff1b;网络新闻用户规模达7.88亿&#xff1b;网络直播用户规模达7.16亿......这些数据都意味着互联网已经涉及我们的方方面面&a…