Android 进阶——图形显示系统之底层图像显示原理小结
Posted CrazyMo_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 进阶——图形显示系统之底层图像显示原理小结相关的知识,希望对你有一定的参考价值。
文章大纲
引言
在android中,当我们谈到 布局优化、卡顿优化 时,通常都知道 需要减少布局层级、减少主线程耗时操作,这样可以减少丢帧。如果丢帧比较严重,那么界面可能会有明显的卡顿感。我们知道 通常手机刷新是每秒60次,即每隔16.6ms刷新一次。,接下来系列文章将全面介绍图形显示和绘制的相关知识,包括Choreographer机制等等。
一、图形显示系统概述
一个典型的显示系统中一般由CPU、GPU、Display三个部分组成,其中 CPU负责计算帧数据,把计算好的数据交给GPU,GPU会对图形数据进行栅格化处理和渲染,渲染好后放到Frame Buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把Frame Buffer里的数据呈现到屏幕上。曾经在一篇文章看过一个生动的比喻,如果把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用是:
- 画笔——Skia 或者 OpenGL。我们可以用 Skia 画笔绘制 2D 图形,也可以用 OpenGL 来绘制 2D/3D 图形。正如前面所说,前者使用 CPU 绘制,后者使用 GPU 绘制。
- 画纸——Surface。所有的元素都在 Surface 这张画纸上进行绘制和渲染。在 Android 中,Window 是 View 的容器,每个窗口都会关联一个 Surface。而 WindowManager 则负责管理这些窗口,并且把它们的数据传递给 SurfaceFlinger。
- 画板——Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,在 Android 4.1 之前使用的是双缓冲机制;在 Android 4.1 之后,使用的是三缓冲机制。
- 显示——SurfaceFlinger,它将 WindowManager 提供的所有 Surface通过硬件合成器 Hardware Composer 合成并输出到显示屏。
二、图形显示系统基础理论
1、屏幕刷新率
Linux 通常使用Frame Buffer(一块显示驱动程序内部缓冲区在内存的映射区域)来缓存显示的数据,一旦用户进程把图像数据复制到Frame Buffer中,显示驱动程序就会一个像素一个像素地扫描整个Frame Buffer,并根据其中的值更新屏幕上的像素点的颜色,显示驱动程序的更新动作是固定且重复的(与硬件相关),整个更新周期就是传说中的刷新率
。
2、逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程即 1000 / 60 ≈ 16ms。
3、帧率 (Frame Rate)
表示 GPU 在一秒内绘制操作的帧数,单位 fps。例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。而 Android 系统则采用更加流程的 60 fps,即每秒钟GPU最多绘制 60 帧画面。帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。
4、画面闪烁(撕裂)的原因
当屏幕更新到一半的时候用户进程更新了Frame Buffer 中的数据,将导致屏幕上画面的上部分是前一帧的画面,下半部分变成了新的画面,于是就会让用户觉得画面有闪烁感(虽然会在下一次刷新时自动纠正过来)。因为图像绘制和屏幕读取操作使用的是同个Frame Buffer,导致屏幕刷新时可能读取到的是不完整的一帧画面。
5、双缓冲和三缓冲Frame Buffer机制
为了处理的画面闪烁感,引入了双缓冲Frame Buffer机制,即驱动程序从自己的内部缓冲区中提供了两块映射到内存的区域,绘制计算(CPU/GPU)操作共享一个Frame Buffer,显示器拥有独立的 Buffer(暂且命名为Display Buffer),GPU 处理完成后将一帧图像数据写入到 Back Buffer,当屏幕正在刷新时,Display Buffer 并不会发生变化,当且仅当Back Buffer准备就绪后,通过ioctl操作进行交换再告诉显示设备切换用于显示的Frame Buffer才会切换。但如果切换的时间点不对,在画面更新到一半时切换,还是不可避免地产生画面闪烁的异常。简而言之,双缓存是指CPU/GPU共享一个Frame Buffer,显示器独享一个Buffer并从里面读取数据显示。但CPU/GPU的处理时间较长超过了16ms的话,双缓冲机制还是会产生较多的Jank,于是三缓冲应运而生,即在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,CPU和GPU 各自拥有独立的Buffer,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。当接到VSync脉冲时Display Buffer 可以选择Frame Buffer 和 Graphic Buffer中已经准备好数据的进行交换。
三、VSync (VerticalSynchronization)
1、VSync 概述
虽然前面引入了双缓冲机制,但是依然存在切换时间点不准确导致画面闪烁的现象,虽然可以在底层通过代码逻辑去判断,但是通过ioctl轮询Frame Buffer的状态效率很低,为此采取了底层驱动固定地发送VSync信号给用户进程,用户进程接到VSync信号时就知道该切换了。垂直同步VSync的缩写),它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换(是指各自的内存地址,可以认为该操作是瞬间完成)。虽然双缓存会在VSync脉冲时交换,但CPU/GPU绘制时机是随机的。
假如是 Frame buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的,看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为Vertical Blanking Interval(VBI),则这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。
2、Android 图形界面刷新机制
Android中为了确定缓冲交换时间引入了VSync 机制,由底层模拟VSync 信号一直固定发出,当用户进程接到时就开始渲染处理,而引入缓冲机制是为了更高效和顺滑,简而言之,简而言之,当VSync信号到来时且缓冲区数据准备完毕后,就会进行缓存交换。
2.1、没有VSync 的绘制过程
如图所示在第一个时间周期(即两个VSync之间的间隔)Display显示第0帧数据,因为此时CPU已经准备好数据,GPU正好在CPU准备好后开始处理且在第一个周期内完成,Display正常渲染第0帧画面,在第二个周期的时候由于某种原因第二祯数据CPU处理比较晚,GPU在CPU处理完成后再去处理,导致GPU处理完成时已经超过了第二个时间周期,但是Display 刷新率是固定的于是只能显示第0帧,就产生了所谓的Jank
丢帧现象。
"丢"帧(掉帧)表示这一帧延迟显示,另外
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPGmaUJc-1649942747293)(图像显示.assets/image-20220309152811009.png)]
Display 表示显示设备,上面的数字表示图像祯序号,GPU方块表示GPU正在准备数据,CPU方块表示CPU正在准备数据。
2.2、结合单缓冲机制+VSync 的绘制过程
系统在收到VSync pulse后,将马上开始下一帧的渲染,即一旦收到VSync信号(16ms触发一次),CPU和GPU 才立刻开始计算然后把数据写入Back Buffer。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DnuTiMz2-1649942747294)(图像显示.assets/image-20220309154526576.png)]
可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。当VSync到来时进行双缓存的交换,交换后屏幕会取Display Buffer内的新数据,而实际此时的Back buffer 就可以供GPU准备下一帧数据了。
2.3、结合双缓冲机制+VSync 的绘制过程
如果 VSync到来时 CPU/GPU就开始操作的话,一个完整时间周期是16ms,这样应该会基本避免Jank的出现了,但CPU/GPU计算超过了16ms,如下图所示双缓冲机制的时候
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xvwJJQ5F-1649942747294)(图像显示.assets/image-20220309155740990.png)]
2.4、结合三缓冲机制+VSync 的绘制过程
由于CPU/GPU处理耗时过长(超过了一个周期),当第一个VSync信号到来时,缓冲区B中的数据还未准备完毕,Display未能完成交换只能继续显示缓冲区A中的内容,此时缓冲区A、B分别被Display 和GPU占用了,CPU在第二个VSync时无法开始准备下一祯数据而只能空闲运行,当下一个VSync信号来临时,Display 与B完成缓冲区交换,CPU才能继续处理下一祯数据,导致的结果就是相当于把屏幕的刷新率降低了。因为在第一个周期时原本应该显示第二祯的又多显示了第一帧。究其原因就是因为当两个Buffer都被占用,CPU 则无法准备下一帧的数据。那么如果再提供一个Buffer,有三个Buffer可供CPU、GPU 和Display Buffer使用,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eEvjxsk6-1649942747296)(图像显示.assets/image-20220309162408515.png)]
- 在第一个VSync时,虽然GPU还占用B缓冲区的数据还在处理,也Display只能显示A缓冲区的数据(但是C缓冲区还空闲)CPU 占用C缓冲区处理数据;
- 当第二个VSync时,B缓冲区数据处理完毕,Display 切换(与GPU缓冲区交换后,相当于**
Display 占用B,GPU占用A,CPU占用C
**)到B缓冲区并显示,与此同时CPU处理C的数据完成后与GPU交换,GPU处理C缓冲区,CPU继续使用A缓冲区处理数据; - 当第三个VSync到来时,Display切换到C 缓冲区(因为在第二个周期内GPU处理好了C缓冲),Display 切换到C ,A 缓冲被GPU占用,CPU使用B缓冲,如此循环,除了第一帧可能不可避免地产生“Jank“后续的帧显示效果都比较理想。
四、Handler异步消息与同步屏障机制
在Handler中Message按照属性不同可以分为**:异步消息
、同步消息
、同步屏障消息
**,本质上都是Message,只不过机制有所差别。
/**
* Use the @link Looper for the current thread with the specified callback interface
* and set whether the handler should be asynchronous.
*
* Handlers are synchronous by default unless this constructor is used to make
* one that is strictly asynchronous.
*
* Asynchronous messages represent interrupts or events that do not require global ordering
* with respect to synchronous messages. Asynchronous messages are not subject to
* the synchronization barriers introduced by @link MessageQueue#enqueueSyncBarrier(long).
*
* @param callback The callback interface in which to handle messages, or null.
* @param async If true, the handler calls @link Message#setAsynchronous(boolean) for
* each @link Message that is sent to it or @link Runnable that is posted to it.
*
* @hide
*/
public Handler(Callback callback, boolean async)
if (FIND_POTENTIAL_LEAKS)
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0)
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
mLooper = Looper.myLooper();
if (mLooper == null)
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
Handler的构造参数时有两个成员:消息处理回调和否是同步的消息的标识,标识为false就是同步,true就是异步。可以看到标识最终保存到成员变量mAsynchronous中,很明显其他成员方法也会使用,enqueueMessage时会给Message的flag 赋值。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)
//将Handler赋值给Message的target变量
msg.target = this;
//mAsynchronous为false,为同步消息
if (mAsynchronous)
msg.setAsynchronous(true);
return queue.enqueueMessage(msg, uptimeMillis);
1、创建异步消息的Handler
false就表示 非异步,即使用的是同步消息,mAsynchronous使用是在enqueueMessage()
public Handler()
this(null, false);
2、MessgeQueue#postSyncBarrier向消息队列插入同步屏障消息
同步消息的Handler肯定就是传入的async的值为true,既然消息处理都大同小异肯定需要插入一个机制来实现屏障的功能
/**
* Posts a synchronization barrier to the Looper's message queue.
* @hide
*/
public int postSyncBarrier()
return postSyncBarrier(SystemClock.uptimeMillis());
private int postSyncBarrier(long when)
synchronized (this)
final int token = mNextBarrierToken++;
//重点是这里没有tartget赋值
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0)
while (p != null && p.when <= when)
prev = p;
p = p.next;
if (prev != null) // invariant: p == prev.next
msg.next = p;
prev.next = msg;
else
msg.next = p;
mMessages = msg;
return token;
postSyncBarrier()返回一个int类型的数值,通过这个数值可以撤销屏障即removeSyncBarrier(),postSyncBarrier()是私有的,如果我们想调用它就得使用反射。
3、同步屏障的基本原理
同步屏障消息是主要就是挡住普通消息来保证异步消息优先处理的,我们都知道MessageQueue的next()方法是读取消息后才能被处理的,遍历消息队列时,发现了同步屏障消息,那么就只取异步消息了。
//MessageQueue.java
Message next()
...
for (;;)
...
synchronized (this)
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null)
// msg.target == null 就是同步屏障消息,那么只取异步消息
do
prevMsg = msg;
msg = msg.next;
while (msg != null && !msg.isAsynchronous());
if (msg != null)
if (now < msg.when)
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
else
// Got a message.
mBlocked = false;
if (prevMsg != null)
prevMsg.next = msg.next;
else
mMessages = msg.next;
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
else
// No more messages.
nextPollTimeoutMillis = -1;
...
4、屏障消息和普通消息
- 屏障消息和普通消息的区别在于屏障没有tartget,而普通消息有target。(因为它需要将消息分发给对应的target)
- 屏障不需要被分发,它就是用来挡住普通消息来保证异步消息优先处理的。
- 屏障和普通消息一样可以根据时间来插入到消息队列中的适当位置,并且只会挡住它后面的同步消息的分发
- 插入普通消息会唤醒消息队列,但是插入屏障不会。
以上是关于Android 进阶——图形显示系统之底层图像显示原理小结的主要内容,如果未能解决你的问题,请参考以下文章
Android 进阶——性能优化之Bitmap位图内存管理及优化
Android 进阶——性能优化之Bitmap位图内存管理及优化
Android 进阶——性能优化之Bitmap位图内存管理及优化