从整体上看Android图像显示系统

Android图形显示系统,是Android比较重要的一个子系统,和很多其他子系统的关联紧密。假如没有图形显示系统,手机可能就是个对讲机。Android图形显示系统比较复杂,这里我们从整体上理一遍,细节留待后期再去深入。

开局一张图:

 上图是Android图形显示系统的一个全貌图。

每个Layer对应着一个Surface。Surface 由每个需要显示的进程View树创建(View树本身对应一个Surface,是为主线程UI的Surface,遇到SurfaceView时挖洞,产生额外的Surface),View树的底端,实际渲染View会选取一种绘制方式,可能是Widget(系统提供默认实现),Webcore(WebView走独立的渲染流程),Canvas(用户在Canvas层级上自定义绘制函数)和 OpenGL(GLSurfaceView,独立的渲染流程,由用户使用OpenGL实现渲染。)

SurfaceFlinger用来创建Layer,并且在上层绘制完成后,合成所有需要显示的Layer,送到LCD显示。其中gralloc是申请图形内存且连接fb(framebuffer)的模块,合成Layer时,优先选用hwcomposer,在hwcomposer无法解决时,采用默认的3D合成,也即调OpenGL标准接口,将各图层绘制到fb上。

以图中间那条线为界,显示系统划分为上下两层。

  • 上层为应用级别的显示,解决如何绘制图层的问题,主要为java代码。
  • 下层为系统级别的显示,解决如何将绘制好的图层送显的问题,主要为C/C++代码。

同时,官方也提供了一个图形系统的关键组件协作图,如下所示:

 这幅图也表达了同样的图形数据的流转:OpenGL ES、MediaPlayer 等生产者生产图形数据到 Surface,Surface 通过 IGraphicBufferProducer把 GraphicBuffer 跨进程传输给消费者SurfaceFlinger。SurfaceFlinger 根据 WMS 提供的窗口信息合成所有的 Layer(对应于 Surface ),具体的合成策略由 hwcomposerHAL 模块决定并实施,最后也是由该模块送显到 Display,而 Gralloc 模块则负责分配图形缓冲区。

稍微深入:

通常来说,view在应用层measure-layout后,由cancas.draw到window上,window就是个载体,WindowManager提供的,以便在framework层区分层级。当app通过WindowManager创建一个Window时,WindowManager会为每一个Window创建一个Surface,并把该Surface传递给app以便应用在上面绘制内容。最后都是交给native层的surfaceFliger+hardwareComposer合成处理显示。

其中:Canvas 是一个2D图形 API ,是 Android View 树实际的渲染者。Canvas 又可分为Skia 软件绘制和 hwui 硬件加速绘制。

Android 4.0 之前默认是 Skia 绘制,该方式完全通过 CPU 完成绘图指令,并且全部在主线程操作,在复杂场景下单帧容易超过16ms导致卡顿。

从 Android 4.0 开始,默认开启硬件加速渲染,而且 5.0 开始把渲染操作拆分到了两个线程:主线程和渲染线程,主线程负责记录渲染指令,渲染线程负责通过 OpenGL ES 完成渲染,两个线程可以并发执行。

除了Canvas,开发者还可以在异步线程直接通过 OpenGL ES 进行渲染,一般适用于游戏、视频播放等独立场景。

从应用侧来看,不管是 Canvas ,还是 OpenGL ES,最终渲染到的目标都是 Surface ,现在比较流行的跨平台UI框架 Flutter 在 Android 平台上也是直接渲染到 Surface 。

例如:一个Activity是一个Surface、一个Dialog也是一个Surface,承载了上层的图形数据,与SurfaceFlinger侧的Layer相对应。

Native层Surface实现了ANativeWindow结构体,在构造函数中持有一个IGraphicBufferProducer,用于和 BufferQueue 进行交互。

BufferQueue 是连接 Surface 和 Layer 的纽带,当上层图形数据渲染到 Surface 时,实际是渲染到了BufferQueue中的一个GraphicBuffer,然后通过IGraphicBufferProducer 把 GraphicBuffer 提交到 BufferQueue ,让 SurfaceFlinger 进行后续的合成显示工作。

SurfaceFlinger 负责合成所有的 Layer 并送显到 Display ,这些Layer主要有两种合成方式:

  • OpenGL ES:把这些图层合成到 FrameBuffer,然后把FrameBuffer提交给hwcomposer 完成剩余合成和显示工作。
  • hwcomposer:通过HWC模块合成部分图层和FrameBuffer,并显示到Display。

Surface属于APP进程,Layer属于系统进程,如果它们之间只用一个Buffer,那么必然存在显示和性能问题,所以图形系统引入了BufferQueue,一个Buffer用于绘制,一个Buffer用于显示,双方处理完之后,交换一下Buffer,这样效率就高很多了。

BufferQueue的通信流程如下所示:

  • 生产者从BufferQueue出队一个空闲GraphicBuffer,交给上层填充图形数据;

  • 数据填充后,生产者把装载图形数据的GraphicBuffer入队到BufferQueue,也可以丢弃这块Buffer,直接cancelBuffer送回到BufferQueue;

  • 消费者通过acquireBuffer获取一个有效缓存;

  • 完成内容消费后(比如上屏),消费者调用releaseBuffer把Buffer交还给BufferQueue。

  • GraphicBuffer代表的图形缓冲区是由Gralloc模块分配的,并且可以跨进程传输(实际传输的只是一个指针)。

  • 通常而言,APP端使用的是BufferQueue的IGraphicBufferProducer接口(在Surface类里面),用于生产;SurfaceFlinger端使用的是BufferQueue的IGraphicBufferConsumer接口(在GLConsumer类里面),用于消费。

图像帧的读取显示

GraphicBuffer显示到屏幕上,也是跟像素一样的,一行一行地读取显示:

当然,屏幕上的内容需要需要不断的更新,如果在同一个Buffer进行读取和写入(合成)操作,将会导致屏幕显示多帧内容。所以硬件层除了提供一个Buffer用于屏幕显示,还提供了一个Buffer用于后台的图形合成,也就是我们常说的双缓冲:

这样,一个GraphicBuffer用于读取显示,另一个用于上层制造数据填充。当前一帧显示完毕,后一帧准备好了,屏幕将会开始读取下一帧的内容,也就是开始读取上图中的后缓冲区的内容,只需要交换两帧数据即可。

在现实中“当前一帧显示完毕,后一帧准备好了”这两个事件并非同时完成。那么,屏幕扫描缓冲区的速度和系统合成帧的速度之间有什么关系呢,首先看看下面两个概念:

  • 屏幕刷新率(HZ):代表屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ(也就是1秒刷新60帧,大约16.67毫秒刷新1帧)
  • 系统帧速率(FPS):代表了系统在一秒内合成的帧数,该值的大小由系统算法和硬件决定。

其实这里很多人也不明白,好好地显示一张图,为什么要搞60HZ的刷新屏幕。但如果只是墙上一幅画,不刷新都可以。但是屏幕要接收用户切换页面命令、视频动画等,所以要不停的刷新才行啊。为什么是60HZ而不是6HZ或者600HZ呢,因为人眼的识别速率,60HZ就能让人感觉就像在看一幅图了。其它动物不清楚,说不定其它动物看正常手机完全感觉异常卡顿或者异常迅速呢。

回到正题。我们用以下两个假设来分析两者的关系:

1、屏幕刷新速率比系统帧速率快

此时,在前缓冲区内容全部映射到屏幕上之后,后缓冲区尚未准备好下一帧,屏幕将无法读取下一帧,所以只能继续显示当前一帧的图形,造成一帧显示多次,也就是卡顿。

2、系统帧速率比屏幕刷新率快

此时,屏幕未完全把前缓冲区的一帧映射到屏幕,而系统已经在后缓冲区准备好了下一帧,并要求读取下一帧到屏幕,将会导致屏幕上半部分是上一帧的图形,而下半部分是下一帧的图形,造成屏幕上显示多帧,也就是屏幕撕裂。

上面两种问题,根本原因就是两个缓冲区的操作速率不一致,解决办法就是让屏幕控制前后缓冲区的切换,让系统帧速率配合屏幕刷新率的节奏。

那么屏幕是如何控制这个节奏的呢?

垂直同步(VSync):当屏幕从缓冲区扫描完一帧到屏幕上之后,开始扫描下一帧之前,发出的一个同步信号,该信号用来切换前缓冲区和后缓冲区。

通过上面的分析可以看出,屏幕的显示节奏是固定的,操作系统需要配合屏幕的显示,在固定的时间内准备好下一帧,以供屏幕进行显示。两者通过VSync信号来实现同步。

SurfaceFlinger-图形合成者

如果说屏幕是消费者,那么SurfaceFlinger相对屏幕来说就是生产者,其具有如下特性:

  • 作为上层应用的消费者,硬件层的生产者。
  • 负责图形的合成
  • 和ActivityManagerService一样,是一个系统服务

为了更好的理解SurfaceFlinger这个服务的工作内容,以及他是如何做到一个承上启下的作用,我们通过下面的这个界面分析:

界面很简单,包含微信、悬浮工具箱、通知栏、底部虚拟按键栏:

 我们可以先这样理解上面这幅图,上层每一个界面,其实都对应SufaceFlinger里的一个Surface对象,上层将自己的内容绘制在对应的Surface内,接着,SufaceFlinger需要将所有上层对应的Surface内的图形进行合成,具体看下图:

 SurfaceFlinger就是将多个Surface里的内容进行合成,最后提交到屏幕的后缓冲区,等待屏幕的下一个垂直同步信号的到来,再显示到屏幕上。

我们会发现SufaceFlinger通过屏幕后缓冲区与屏幕建立联系。同时通过Surface与上层建立联系。从而起到一个承上启下的作用,是Android图形系统结构中的关键组成部分。

VSync以及三缓冲

上面的也说了,整个绘制流程的节奏,分成两个生产者消费者模型,一个由屏幕和SurfaceFlinger构成,另一个由SurfaceFlinger和上层应用构成,具体流程可以用下图来描述:

 其中:

  • CPU和GPU代表上层的绘制执行者
  • Composite代表的是SurfaceFlinger对多个Surface的合成
  • Background Buffer和Front Buffer分别代表的是硬件帧缓冲区中的前缓冲和后缓冲
  • 显示屏扫描完一帧之后,会发出VSync信号来切换并显示下一帧

上面的流程中,存在一个问题,屏幕的VSync信号只是用来控制帧缓冲区的切换,并未控制上层的绘制节奏,也就是说上层的生产节奏和屏幕的显示节奏是脱离的:

上图中,横轴表示时间,纵轴表示Buffer的使用者,每个长方形表示Buffer的使用,长方形的宽度代表使用时长,VSync代表垂直同步信号,两个VSync信号之间间隔16.6ms。此图描述了Android在4.1系统版本之前,上层的绘图流程在没有VSync信号的时候,出现的绘制问题。

我们从时间为0开始看,当前屏幕显示第0帧,上层CPU开始计算第1帧的纹理,计算完成后,交由GPU进行栅格化。当下一个垂直同步信号到来,屏幕显示下一帧,这时候,上层CPU并未马上开始准备下一帧,而当CPU开始准备下一帧的时候已经太晚了,下一个VSync信号来临的时候,GPU未能绘制完第二帧的处理,导致屏幕再次显示上一帧,造成卡顿:

 因为上层不知道VSync信号已经发出,导致上层未能开始CPU的计算。google在Android 4.1系统中加入了上层接收垂直同步信号的逻辑,大致流程如下:

 也就是说,屏幕在显示完一帧后,发出的垂直同步除了通知帧缓冲区的切换之外,该消息还会发送到上层,通知上层开始绘制下一帧。

那么,上层是如何接受这个VSync消息的呢?

Choreographer VSync信号的上层接收者

Google为上层设计了一个Choreographer类,翻译成中文是“编舞者”,是希望通过它来控制上层的绘制(舞蹈)节奏。

首先看看Choreographer的类图:

可以发现,Choreographer需要向SurfaceFlinger来注册一个VSync信号的接收器DisplayEventReceiver。同时在Choreographer的内部维护了一个CallbackQueue,用来保存上层关心VSync信号的组件,包括ViewRootImpl,TextView,ValueAnimator等。

再看看上层接收VSync的时序图:

知道了Choreographer是上层用来接收VSync的角色之后,我们需要进一步了解VSync信号是如何控制上层的绘制的:
 

 一般,上层需要绘制新的UI都是因为View的requestLayout或者是invalidate方法被调用触发的,我们以这个为起点,跟踪上层View的绘制流程:

  • requestLayout或者invalidate触发更新视图请求
  • 更新请求传递到ViewRootImpl中,ViewRootImpl向主线程MessageQueue中加入一个阻塞器,该阻塞器将会拦截所有同步消息,也就是说此时,我们再通过Handler向主线程MessageQueue发送的所有Message都将无法被执行。
  • ViewRootImpl向Choreographer注册下一个VSync信号
  • Choreographer通过DisplayEventReceiver向framework层注册下一个VSync信号
  • 当底层产生下一个VSync消息时,该信号将会发送给DisplayEventReceiver,最后传递给Choreographer
  • Choreographer收到VSync信号之后,向主线程MessageQueue发送了一个异步消息,我们在第二步提到,ViewRootImpl向MessageQueue发送了一个同步消息阻塞器。这里Choreographer发送的异步消息,是不会被阻塞器拦截的。

最后,异步消息的执行者是ViewRootImpl,也就是真正开始绘制下一帧了
至此,底层的VSync控制上层的逻辑就解释完了,此时上层绘制图形的流程与VSync信号的关系可以用下图表示:

 时间从屏幕显示第0帧开始,CPU开始准备第1帧图形的处理,好了之后交给GPU进行处理,在上层收到下一个VSync之后,CPU立马开始第2帧的处理,上层绘图的节奏就和VSync信号保持一致了,整个绘图非常流畅。

然而,理想很丰满,现实很骨感,如果CPU和GPU没能在下一个VSync信号到来之前完成下一帧的绘制工作,又会是怎么样的呢?

 还是从屏幕显示第A帧开始,时间进入第一个16.6ms,CPU和GPU合成第B帧,当下一个VSync信号到来的时候,GPU未能及时完成第B帧的绘制,此时,GPU占有一个Surface里的Buffer,而同时SurfaceFlinger又持有一个Buffer用于合成显示下一帧到屏幕,这样的话,就导致Surface里的两个缓冲区都被占用了。此时SurfaceFlinger只能使用第A帧已经准备好的Buffer来合成,GPU继续在另一个缓冲区中合成第B帧,此时CPU无法开始下一帧的合成,因为缓冲区用完了。另外一个不好的事情是CPU只有在VSync信号来的时候才开始绘制下一帧,也是就是说在第二个16.6ms时间内,CPU一直处于空闲状态,未进行下一帧的计算。
只有等到第二个VSync信号来了之后,CPU才开始在绘制下一帧。如果CPU和GPU需要合成的图形太多,将会导致连续性的卡顿,如果CPU和GPU大部分时候都无法在16.6ms完成一帧的绘制,将会导致连续的卡顿现象。

google处理上述问题也很简单粗暴,两个缓冲区不行,那就三个嘛。CPU和GPU还有SurfaceFlinger各占一个Buffer,并行处理图形:

 从上图可以看出,在第一个VSync到来时,尽管SurfaceFlinger占了一个Buffer,GPU又占了一个Buffer,CPU仍然可以在第三个Buffer中开始下一帧的计算,整个显示过程就开始时卡顿了一帧,之后都是流畅的。

当然系统并非一直开启三个Buffer,因为Buffer是需要消耗资源的,并且,我们会发现,上图中,GPU处理好的图形,需要跨越两个VSync信号,才能显示。这样的话,给用户的影响是一个延迟的现象。

为了解决该问题,我们才经常被提醒需要从上层往下层了解Android绘制图形的各个细节进行优化。对于应用程序开发人员来说,重点还是上层的优化,对自己的应用程序的内存,UI,数据等进行优化。具体优化下期再说。


鸣谢:

Android图形显示系统(一)

Android 图形系统概述

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android 5.1是一个广泛使用的操作系统版本,支持HDMI输出功能。要设置HDMI分辨率,可以按照以下步骤进行: 1. 首先,将Android设备与HDMI显示器或电视连接。确保HDMI线缆正常连接。 2. 在Android设备上打开设置菜单。可以通过下拉通知栏或点击主屏幕上的设置图标来访问设置菜单。 3. 在设置菜单中,向下滚动并找到“显示”选项。点击进入显示设置页面。 4. 在显示设置页面中,可能会看到“屏幕投射”或“显示模式”的选项。点击进入相关设置。 5. 在屏幕投射或显示模式设置页面中,可以看到HDMI选项。点击进入HDMI设置。 6. 在HDMI设置页面中,通常会有分辨率选项。点击进入分辨率设置。 7. 在分辨率设置页面中,可以看到可用的分辨率选项列表。根据显示器或电视的支持能力和个人偏好,选择所需的分辨率。 8. 选择完分辨率后,点击确认或应用。系统将应用所选的分辨率设置。 9. 返回到上一个菜单或主屏幕,查看是否已成功设置HDMI分辨率。显示器或电视上的画面应该会根据所选的分辨率进行调整。 需要注意的是,不同的Android设备或系统版本可能会有略微不同的设置流程。因此,根据具体的设备和系统版本,上述步骤可能会有所变化。但总体而言,通过进入设置菜单,找到显示设置,再进入HDMI设置,选择适当的分辨率,就可以完成Android 5.1设置HDMI分辨率的流程。 ### 回答2: Android 5.1版本的设备在设置HDMI分辨率时,可以按照以下步骤进行操作: 1. 连接HDMI线缆:首先,将一端的HDMI线缆插入Android设备的HDMI输出接口,另一端插入显示设备(如电视或投影仪)的HDMI输入接口。 2. 打开设置界面:在Android设备上,找到并点击打开“设置”应用程序,通常可以在应用程序列表中找到该选项。 3. 进入显示设置:在“设置”主界面中,向下滚动并找到“显示”选项,点击进入显示设置界面。 4. 选择HDMI设置:在显示设置界面中,找到并点击“HDMI”选项,这将打开HDMI设置界面。 5. 选择分辨率:在HDMI设置界面中,通常会显示可用的HDMI分辨率选项。根据你的显示设备和个人需求,选择适当的分辨率选项。 6. 保存设置:选择完分辨率后,点击界面上的“保存”或“应用”按钮,以保存并应用新的HDMI分辨率设置。 7. 测试分辨率:你可以通过在显示设备上观察图像是否清晰和完整来测试新的HDMI分辨率设置。如果满意,设置流程就结束了。 请注意,以上步骤仅适用于Android 5.1版本的系统设备,不同的Android版本或设备类型可能略有不同。确保你的设备支持HDMI输出功能并运行在Android 5.1版本或更高版本。 ### 回答3: 要设置Android 5.1的HDMI分辨率,可以按照以下流程操作: 1. 首先,确保你的Android设备已连接到HDMI显示器上。 2. 在设备上滑动屏幕,进入主菜单,找到并点击“设置”图标。 3. 在设置菜单中,向下滚动找到“显示”或类似的选项,并点击进入。 4. 在显示设置菜单中,找到“屏幕分辨率”或类似的选项,并点击进入。 5. 系统会列出可用的屏幕分辨率选项。根据你的需求和HDMI显示器的能力,选择一个适合的分辨率。 6. 点击选中分辨率后,系统会提示你是否确认应用此分辨率。确认后,系统会应用新的分辨率设置。 7. 返回到主菜单或桌面,你会注意到HDMI显示器的分辨率已经改变为你所设置的分辨率。 需要注意的是,不同的Android设备可能会在设置菜单的布局和选项名称上有所区别,具体操作可能会有所不同。但一般来说,通过“设置”-“显示”-“屏幕分辨率”可以找到相关的选项。 如果你在Android设备上找不到上述选项,可能是因为该设备的固件版本或制造商定制的界面导致了菜单布局的差异。在这种情况下,你可能需要查阅设备的用户手册或进行在线搜索来了解具体的设置方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值