Android UI 显示原理

问题:介绍一下 Android 屏幕显示原理,开发编写的 View 控件,是怎么变成屏幕上显示的图像的?

思路是先整体串讲,宏观的把Android UI 显示原理的关键知识点都涉及到,然后再细化具体介绍,知识点如下:

  • Activity 显示原理(Window/DecorView/ViewRoot)

  • UI 刷新机制(Choreographer/vSync)

  • UI 绘制原理(Measure/Layout/Draw)

  • Surface 原理(Surface/SurfaceFlinger)

Activity显示原理

Android的显示的大致流程:Android显示原理的架构是C/S架构,S是系统层的服务SurfaceFlinger来扮演,它由C++语言编写;C一部分是Java提供给应用层使用的API,一部分是C++编写的底层实现。应用层将测量,布局,绘制后的数据给到SurfaceFlinger,SurfaceFlinger会将数据渲染到屏幕上,并配合Android的刷新机制来不断的刷新数据,最终显示出一帧帧的画面。那么应用层绘制的数据,如何传递到系统层服务SurfaceFlinger的呢?不同进程之间的通信,采用的肯定是跨进程的通信传输数据。Android采用的是一个叫SharedClient的匿名共享内存,来实现进程间数据的传输,它的底层实现仍是Binder机制。由于不同进程间是直接操作内存,因此匿名共享内存比一般的进程间通信的效率要高。

Android系统的绘制原理

绘制任务由应用发起,最终通过系统层绘制到硬件屏幕上,也就是说应用进程绘制后,通过跨进程通信机制把需要显示的数据传到系统层,由系统层中的SurfaceFlinger服务绘制到屏幕上;

1.应用层

一个Android应用程序窗口里面包含了很多UI元素,这些UI元素是以树形结构来组织的,即它们存在着父子关系,其中,子UI元素位于父UI元素里面,如下图:

在绘制一个Android应用程序窗口的UI之前,我们首先要确定它里面的各个子UI元素在父UI元素里面的大小以及位置。确定各个子UI元素在父UI元素里面的大小以及位置的过程又称为测量过程和布局过程。Android每个View绘制的三个核心步骤:通过Measure和Layout确定当前需要绘制View的大小和位置,通过绘制到surface,在Android系统绘制源码是在ViewRootImpl的performTraversals方法开始的,通过该方法会执行Measure和layout和draw方法;Measure和layout都是递归获取View的大小和位置,都是以深度为优先级,可以看出层级越深元素越多,耗时也就越长;
测量、布局没有太多要说的,这里要着重说一下绘制。Android目前有两种绘制模型:基于软件的绘制模型硬件加速的绘制模型(从Android 3.0开始全面支持)。
基于软件的绘制模型下,CPU主导绘图,视图按照两个步骤绘制
(1)让View层次结构失效;
(2) 绘制View层次结构;
当应用程序需要更新它的部分UI时,都会调用内容发生改变的View对象的invalidate()方法。无效(invalidation)消息请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。不幸的是,这种方法有两个缺点:
(1)绘制了不需要重绘的视图(与脏区域相交的区域);
(2)掩盖了一些应用的bug(由于会重绘与脏区域相交的区域);
注意:在View对象的属性发生变化时,如背景色或TextView对象中的文本等,Android系统会自动的调用该View对象的invalidate()方法。
基于硬件加速的绘制模式下,GPU主导绘图,绘制按照三个步骤绘制:
(1)让View层次结构失效
(2)记录、更新显示列表
(3)绘制显示列表
这种模式下,Android系统依然会使用invalidate()方法和draw()方法来请求屏幕更新和展现View对象。但Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。另一个优化是,Android系统只需要针对由invalidate()方法调用所标记的View对象的脏区进行记录和更新显示列表。没有失效的View对象则能重放先前显示列表记录的绘制指令来进行简单的重绘工作。使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,以便下次重用,然后再调用OpenGL完成绘制。硬件加速提高了Android系统显示和刷新的速度,但它也不是万能的,它有三个缺陷:
耗电:GPU功耗比CPU高;
兼容问题:某些接口或者函数不支持硬件加速;
内存大:使用OpenGL接口至少需要8MB内存;
所以是否使用硬件加速需要考虑接口是否支持,通过结合产品形态,如TV版本不需要考虑功耗问题,TV屏幕大,使用硬件加速实现更好的显示效果;

2.系统层:

真正把需要显示器的数据渲染到屏幕上,是通过系统级进程汇总的SurfaceFlinger服务来实现的,SurfaceFlinger主要工作:
(1)响应客户端事件,创建Layer与客户端Surface建立连接;
(2)接收客户端数据及属性,修改Layer属性,如尺寸、颜色、透明度等;
(3)将创建的layer内容刷新到屏幕上;
(4)维持layer序列,并对Layer最终输出做出裁剪计算;
既然是两个不同进程,那么肯定需要跨进程通信机制来实现数据传输,Android显示系统使用匿名共享内存:SharedClient,每个应用和SurfaceFlinger之间都会创建一个SharedClient,如下图所示,在每个SharedClient中,最多会创建31个SharedBufferStack,每个Surface对应一个SharedBufferStack,也就是一个Window;一个SharedClient对应一个Android应用程序,而一个Android应用程序可能包含多个窗口,即Surface,也就是说SharedClient包含的是SharedBufferStack集合,因为最多可以创建31个SharedBufferStack,也就意味着一个Android应用最多可以还包含31个窗口,同时每个SharedBufferStack中又包含两个(低于4.1版本)或者三个(4.1及以上版本)缓冲区,即显示刷新机制中双缓冲和三重缓冲机制;

最后总结起来显示整体流程分为三个模块:应用层绘制到缓存区,SurfaceFlinger把缓存区数据渲染到屏幕,由于是两个不同的进程,所以Android使用匿名共享内存SharedClient缓存需要显示的数据来达到目的;SurfaceFlinger把缓存区的数据渲染到屏幕主要是驱动层的事情。

3.Android是如何将view绘制到屏幕?

大致流程如下:
(1)首先是CPU准备数据,TextView,Button等等控件通过cpu计算转换为内存中的polygons(多边图形)和texture(纹理)。
(2)其次,cpu通过OpenGL的接口将纹理数据传递给GPU渲染处理,由于图形API不允许CPU直接与GPU通信,而是通过中间图形驱动层来连接两部分,驱动层维护了一个队列,CPU把display list添加到队列中,GPU从这个队列去除数据进行绘制,最终在屏幕上显示出来;在这个过程中,由DisplayList这个结构负责保存绘制用到的所有信息,在Displaylist无需重新创建或改变的情况下,GPU可以直接使用这里的数据进行渲染.当View中的某些可见组件,那么之前的DisplayList就无法继续使用了,我们需要回头重新创建一个DisplayList并且重新执行渲染指令并更新到屏幕上。
(3)最后,GPU对图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
名词解释:
栅格化:栅格化把button、textview等组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作。
DisplayList:DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。

4. Why 60fps?

知道了绘制流程之后,那么到底绘制一个单元多长时间才合理,我们通常都会提到60fps与16ms,可是知道为何会是以程序是否达到60fps来作为App性能的衡量标准吗?这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms=1000/60的时间来处理所有的任务。Android系统每隔16ms发现VSYNC信号,触发对UI的进行渲染,如果每次渲染都成功,就可以达到流畅画面所需要的60FPS;如果某个操作话费时间超过16ms,那么得到VSYNC信号时就无法进行正常的渲染,就样就会发生丢帧;刷新不及时是引起卡顿的一个主要原因,下边接收系统怎么刷新以及在什么情况下会导致卡顿发生;

5.显示刷新机制

Android系统一直在不断的优化、更新,但直到4.0版本发布,有关UI显示不流畅的问题仍未得到根本解决;从Android4.1版本开始,Android对显示系统进行了重构,引入了三个核心元素:VSYNC, Tripple Buffer和Choreographer。VSYNC是Vertical Synchronized的缩写,是一种定时中断;Tripple Buffer是显示数据的缓冲区;Choreographer起调度作用,将绘制工作统一到VSYNC的某个时间点上,使应用的绘制工作有序进行。
名词解释:
双缓冲:显示内容的数据内存,双缓冲意味着要使用两个缓冲区(SharedBufferStack中),其中一个称为Front Buffer,另外一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。
三缓冲:即Triple Buffer。利用CPU/GPU的空闲时间准备数据,用于弥补在VSYNC+双缓冲配合使用的缺陷;
VSYNC:当双缓冲的介绍了解到,只有当另外一个buffer准备好之后,才能去刷新,这就需要CPU以主动查询方式来保证数据是否准备好,因为这种机制效率很低,引入VSYNC,一旦受到VSYNC定时中断,CPU就开始处理各帧数据;没有VSYNC信号,CPU不知道何时去处理UI绘制,引入VSYNC核心目的就是解决刷新不同步的问题;
Choreographer:收到VSYNC信号时,调用用户设置的回调函数,一种有三种类型的回调:
CALLBACK_INPUT:优先级最高,与输入事件有关;
CALLBACK_ANIMATON:第二优先级,与动画有关;
CALLBACK_TRAVERSAL:最低优先级,与UI控件绘制有关;
View的onclick、onDraw等等都是从Choreographer.doFrame开始执行的;关于Choreographer可以参考Android Choreographer 源码分析,讲的特别好;

5.1.没有VSYNC信号同步

没有VSYNC信号同步.png

(1)第一个16ms开始:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者都在正常工作。
(2)进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU却并未及时去绘制第2帧数据(前面的空白区表示CPU和GPU忙其它的事),直到在本周期快结束时,CPU/GPU才去处理第2帧数据。
(3)进入第三个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank),导致错过了显示第二帧。
通过上述分析可知,此处发生Jank的关键问题在于,为何第1个16ms段内,CPU/GPU没有及时处理第2帧数据?原因很简单,CPU可能是在忙别的事情,不知道该到处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了。 为解决这个问题,Android 4.1中引入了VSYNC,核心目的是解决刷新不同步的问题。

5.2.有VSYNC信号同步

在加入VSYNC信号同步后,每收到VSYNC中断,CPU就开始处理各帧数据。已经解决了刷新不同步的问题。
但是上图中仍然存在一个问题:CPU和GPU处理数据的速度似乎都能在16ms内完成,而且还有时间空余,也就是说,CPU/GPU的FPS(帧率)要高于Display的FPS。由于CPU/GPU只在收到VSYNC时才开始数据处理,故它们的FPS被拉低到与Display的FPS相同。但这种处理并没有什么问题,因为Android设备的Display FPS一般是60,其对应的显示效果非常平滑。
但如果CPU/GPU的FPS小于Display的FPS,情况又不同了,将会发生如下图的情况:

 

(1)在第二个16ms时间段,Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。
(2)同理,在第二个16ms时间段内,CPU无所事事,因为A Buffer被Display在使用。B Buffer被GPU在使用。注意,一旦过了VSYNC时间点,CPU就不能被触发以处理绘制工作了。

5.3.引入Triple Buffer

为什么CPU不能在第二个16ms处开始绘制工作呢?原因就是只有两个Buffer(Android 4.1之前)。如果有第三个Buffer的存在,CPU就能直接使用它,而不至于空闲。于是在Android4.1以后,引出了第三个缓冲区:Tripple Buffer。Tripple Buffer利用CPU/GPU的空闲等待时间提前准备好数据,并不一定会使用。
引入Triple Buffer效果如下图所示:

上图中,第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示A帧一次,但后续显示就比较顺畅了。
是不是Buffer越多越好呢?回答是否定的。由上图可知,在第二个时间段内,CPU绘制的第C帧数据要到第四个16ms才能显示,这比双Buffer情况多了16ms延迟。所以,Buffer最好还是两个,三个足矣。,目前基本都是这么解释的,但是这个解释我认为不对,如果不引入Triple Buffer,使用双缓冲技术,第二个阶段那一帧数据也是需要到第四个16ms才显示,使用双缓冲显示过程如下:第二个16ms还是显示第一帧A;因为发生丢帧,第二个16ms也是显示第一帧A;第三个16ms应该显示第二帧B;所以第三帧最早也是显示在第四个16ms,和引入Triple Buffer效果一样,所以我认为目前网上的解释都不对。需要管理的缓存越多,管理起来越复杂,所以并不是缓存越多越好;
从以上分析,Android系统在显示机制上解决了AndroidUI不流畅问题,但在实际应用开发过程中仍然存在卡顿现象,因为VSYNC中断处理的线程游戏那几一定要高,否则接收到VSYNC终端不能及时处理,也是徒劳无功;

6.卡顿根本原因

从Android显示原理可以看到,影响绘制的根本原因有以下两方面:
(1)绘制任务太重,绘制一帧内容耗时过长;
(2)主线程太忙,导致VSYNC信号到来时还没有准备好数据导致丢帧;
耗太长,需要从UI布局和绘制上来具体分析;
绘制工作是在主线程,主线程的主要职责是处理用户交互,在屏幕上绘制像素,并加载显示相关的数据,主线主要做以下几方面的工作:
(1)UI生命周期控制
(2)系统事件处理
(3)消息处理
(4)界面布局
(5)界面绘制
(6)界面刷新
除了这些之外,尽量避免将其他处理放到主线程中,特别是复杂的数据计算和网络请求;
更多卡顿问题可参考Android性能优化-App卡顿

另外:针对 UI 绘制请求与绘制时机,简单的分析一下 UI 刷新机制,并不涉及 vSync 信号的生成等底层内容:

接收 UI 重绘请求的 ViewRootImpl

ViewRootImpl 中的 performTraversals 中会依次调用 performMeasure、performLayout、performDraw,分别对应于 measure、layout、draw,由顶而下的进行界面绘制逻辑。

调用 View 控件 requestLayout、invalidate 等方法请求 UI 重绘时,会统一调用到 ViewRootImpl 的 scheduleTraversals 方法,代码如下:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

其中 postSyncBarrier 插入一个消息屏障 block 普通消息,以保证主线程可以优先来执行接下来的绘制工作。mTraversalRunnable 的实现如下:

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        performTraversals();
    }
}

通过 mTraversalScheduled 变量可以看出,不是每次调用 requestLayout、invalidate 方法就会触发一次 UI 重绘的,而是要等 mTraversalRunnable 被执行后才会接收下一次的重绘请求。

在 mTraversalRunnable 中调用了 performTraversals() 进行真正的 UI 绘制,而 UI 真正绘制的时机则取决于 mChoreographer 触发回调的时机。

触发 UI 绘制的 Choreographer

ViewRootImpl 接收 UI 重绘请求后,将真正的 UI 绘制时机交给了 Choreographer,而 Choreographer 会在每次 vSync 信号到来时执行 UI 绘制。

调用 Choreographer 的 postCallback 方法将 UI 绘制 TraversalRunnable 传入后,会进一步调用 Choreographer 的 postCallbackDelayedInternal 方法,代码如下:

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

首先将 UI 绘制 action 记录到 mCallbackQueues 队列中,然后根据处理时间决定立即调用 scheduleFrameLocked ,或发送异步消息延时调用 scheduleFrameLocked。

scheduleFrameLocked 方法关键代码如下:

private void scheduleFrameLocked(long now) {
    // If running on the Looper thread, then schedule the vsync immediately,
    // otherwise post a message to schedule the vsync from the UI thread
    // as soon as possible.
    if (isRunningOnLooperThreadLocked()) {
        scheduleVsyncLocked();
    } else {
        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtFrontOfQueue(msg);
    }
}

如注释所示,scheduleFrameLocked 中需要切换到指定线程中调用 scheduleVsyncLocked:

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}

scheduleVsync 表示要接受下一次 vSync 信号,等到 vSync 信号到来时会由 SurfaceFlinger 回调通知。直接来看 Choreographer 接受到 vSync 信号后的处理,关键代码如下:

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
        }
    }
    ...
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}

当要绘制的图像时间戳晚于一个帧刷新周期时,会去进一步计算异常跳过的帧数,如果跳过的帧数过大,就可以看到非常眼熟的一条日志了:"Skipped xx frames! The application may be doing too much work on its main thread"

随后通过 doCallbacks 回调触发执行 UI 绘制,也就是执行 ViewRootImpl 传过来的 TraversalRunnable、调用 performTraversals 方法,由顶而下的执行界面绘制逻辑。

 

 

转载:https://www.jianshu.com/p/a978a6250f9e

https://mp.weixin.qq.com/s/4R-ZroWE689cPrA3846j5Q

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值