zoukankan      html  css  js  c++  java
  • Android系统显示原理

    Android的显示过程可以概括为:
    Android应用程序把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到屏幕上,通过Android的刷新机制来刷新数据。即应用侧绘制、系统侧渲染,通过进程间通信(IPC)把应用层需要绘制的数据传递到系统层服务,系统层服务通过显示刷新机制把数据更新到屏幕。

    两个部分(应用侧绘制、系统侧渲染)、两个机制(进程间通讯机制、显示刷新机制)

    接下来分别从 应用层、系统层和刷新机制三个方面来介绍下Android系统的显示原理。

    SurfaceFlinger:Android系统服务,负责管理Android系统的帧缓冲区,即显示屏幕。

    Surface:Android应用的每个窗口对应一个画布(Canvas),即Surface,可以理解为Android应用程序的一个窗口。

    应用层
    我们都知道一个Android的UI界面layout是整体一棵由很多不同层次的View组成的树形结构,它们存在着父子关系,子View在父View中,这些View都经过一个相同的流程最终显示到屏幕上。

    在Android中每个View的绘制中有三个核心步骤,通过Measure和Layout来确定当前需要绘制的View所在的大小和位置,通过绘制(Draw)到surface。

    1)Measure

    用深度优先原则递归得到所有View的宽、高;获取当前View的正确宽度childWidthMeasureSpec和childHeightMeasureSpec之后,可以调用它的成员函数Measure来设置它的大小。若子View是一个ViewGroup,那么它又会重复执行操作,直到它的所在子孙View的大小都测量完成为止。

    2)Layout

    用深度优先原则递归得到所有View的位置;当一个子View在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中确定的宽度和高度,就可以完全确定它在应用程序窗口中的布局。

    3)Draw

    Android支持两种绘制方式:软件绘制(CPU)和硬件绘制(GPU,要求>= Android3.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完成绘制。

    硬件加速在UI的显示和绘制的效率远远高于CPU绘制,硬件加速提高了Android系统显示和刷新的速度,但它也不是万能的,它有三个缺陷:

    耗电:GPU功耗比CPU高;
    兼容问题:某些接口和函数不支持硬件加速;
    内存大:使用OpenGL的接口至少需要8MB内存。


    系统层
    Android是通过系统级进程中的SurfaceFlinger服务来把真正需要显示的数据渲染到屏幕上。SurfaceFlinger服务使用OpenGL图形库API来将这个图形缓冲区渲染到硬件帧缓冲区中。

    SurfaceFlinger的主要工作是:

    响应客户端事件,创建Layer与客户端的Surface建立连接。
    接收客户端数据及属性,修改Layer属性,如尺寸、颜色、透明度等。
    将创建的Layer内容刷新到屏幕上。
    维持Layer的序列,并对Layer最终输出做出裁剪计算。

    因应用层和系统层分别是两个不同进程,需要一个跨进程的通信机制来实现数据传输,在Android的显示系统中,使用了Android的匿名共享内存:SharedClient。每一个应用和SurfaceFlinger之间都会创建一个SharedClient,每个SharedClient中,最多可以创建31个SharedBufferStack,每个Surface都对应一个SharedBufferStack,也就是一个window。这意味着一个Android应用程序最多可以包含31个窗口,同时每个SharedBufferStack中又包含两个(<4.1)或三个(>=4.1)缓冲区。

    总结:应用层绘制到缓冲区,SurfaceFlinger把缓存区数据渲染到屏幕,两个进程之间使用Android的匿名共享内存SharedClient 缓存需要显示的数据。

    显示刷新机制
    Android系统一直在不断的优化、更新,但直到4.0版本发布,有关UI显示不流畅的问题仍未得到根本解决。

    从Android4.1版本开始,Android对显示系统进行了重构,引入了三个核心元素:VSYNC, Tripple Buffer和Choreographer。VSYNC(垂直同步) 是Vertical Synchronized的缩写,是一种定时中断;意思是显卡输出频率与屏幕刷新频率同步的意思。Tripple Buffer是显示数据的缓冲区;Choreographer起调度作用,将绘制工作统一到VSYNC的某个时间点上,使应用的绘制工作有序进行。

    Android在绘制UI时,会采用一种称为“双缓冲”的技术,双缓冲即使用两个缓冲区(在SharedBufferStack中),其中一个称为Front Buffer,另外一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。理想情况下,一个刷新会在16ms内完成(60FPS),下图就是描述的这样一个刷新过程(Display处理前Front Buffer,CPU、GPU处理Back Buffer

    1.没有VSYNC信号同步时

    但实际运行时情况并不一定如此

    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,核心目的是解决刷新不同步的问题。

    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就不能被触发以处理绘制工作了

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

    3.引入Tripple Buffer后

    引入Tripple Buffer后的刷新时序如下图:

    上图中,第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示A帧一次,但后续显示就比较顺畅了。

    是不是Buffer越多越好呢?回答是否定的。由上图可知,在第二个时间段内,CPU绘制的第C帧数据要到第四个16ms才能显示,这比双Buffer情况多了16ms延迟。所以缓冲区并不是越多越好。

    总结
    从Android系统的显示原理可以看到,影响绘制的根本原因有以下两方面:

    1.绘制任务太重,绘制一帧内容耗时太长。(有时是UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域。)

    2. 动画使用次数过多。

    通常情况下Android需要将xml布局文件转换成GPU可以识别的绘制对象,而这些绘制对象被存放到DisplayList的数组中,当view第一次绘制的时候DisplayList被创建,view第二次绘制的时候GPU就直接从DisplayList获取绘制对象,省去了Measure,Layout的时间,但是如果我们改变了view的绘制内容那么得重新Measure,layout,DisplayList也会被重新创建,这么看来动画效果是非常消耗性能的。因为它必须经过多次重绘。

    3.主线程太忙了,导致VSYNC信号来时还没有准备好数据导致丢帧。


    实际开发中,我们应该只在主线程做以下几方面的工作:

    UI生命周期控制
    系统事件处理
    消息处理
    界面布局
    界面绘制
    界面刷新
    除了这些以外,尽量避免将其它处理放到主线程中,特别是复杂的数据计算和网络请求。

    -------------------------------------------------------------------------------------------

    ---------------------
    作者:Android面试专栏
    来源:CSDN
    原文:https://blog.csdn.net/blessed24/article/details/74277933
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    php将字符串形式的数组转化为真数组
    Mysql8.0及以上 only_full_group_by以及其他关于sql_mode原因报错详细解决方案
    php使用base64_encode和base64_decode对数据进行编码和解码
    大数据基本概念
    sonarqube安装部署
    git-修改commit信息
    NLP-Commen Sense
    索引生命周期的管理
    kibana软件具体参数配置信息
    es机器监控x-pack导致的监控存储过大的问题
  • 原文地址:https://www.cnblogs.com/Jackie-zhang/p/9871911.html
Copyright © 2011-2022 走看看