欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android UI 渲染机制的演进,你需要了解什么?

程序员文章站 2024-03-24 13:21:22
...

前言
如今UI 渲染可能是诸多性能问题中最容易被察觉到的,Android 开发既要面对各式各样的手机屏幕尺寸和分辨率,还要与“凶残”的产品和 UI 设计师过招。

在正确实现复杂、炫酷的 UI 设计的同时,还需要保证流程的用户体验。更加不幸的是,最近几年这个趋势似乎愈演愈烈:刘海屏、水滴屏、全面屏,还有即将推出的的柔性折叠屏,UI 适配将变得越来越复杂。
UI 渲染的背景知识

Android 的图形渲染框架十分复杂,不同版本的差异也比较大。但是无论怎么样,它们最终都是为了将我们代码中的 View 或者元素显示到屏幕上。

而屏幕作为直接面对用户的手机硬件,类似厚度、色彩、功耗等都是厂家非常关注的。从早期的黑白屏功能机,到现在的超大的全面屏,我们先来看看手机屏幕的发展历程。

1、屏幕与适配

Android 的碎片化问题由来已久,并且另每个 Android 开发痛心疾首,而屏幕的差异化正是这些碎片化问题的“中心”。屏幕从 3 英寸到 10 英寸,分辨率从 320 到 2160 应有尽有,对我们 UI 适配造成很大困难。

除此之外,材质也是屏幕至关重要的一个评判因素。目前智能手机主流的屏幕可分为两大类:一种是 LCD(Liquid Crystal Display),即液晶显示器;另一种是 OLED(Organic Light-Emitting Diode )即有机发光二极管。

最新的旗舰机例如 iPhone 11 Pro / Max 已经开始使用超视网膜 OLED 显示屏技术,OLED 提供令人惊叹的高对比度和高分辨率,相比 LCD 屏幕,OLED 无需背光组件,而是由每个像素自行发光,因此显示屏变得更薄。

另外 OLED 屏幕在可弯曲程度以及耗电方面都更具有优势。正因为这些优势,全面屏、曲面屏以及未来的柔性折叠屏,使用的都是 OLED 材质。关于 OLED 与 LCD 的具体差别,可以参考《OLED 和 LCD 区别》和《手机屏幕的前世今生,可能比你想象的还精彩》,不过目前 OLED 的成本相比 LCD 要高很多。

对于屏幕适配,Android 推荐使用 dp 作为尺寸单位来适配 UI,通过 dp 加上自适应布局基本可以解决屏幕碎片化问题,这也是 Android 推荐使用的屏幕兼容性适配方案。因此每个 Android 开发都应该清楚 px、dp、dpi、ppi、density 这些概念。

2、CPU 与 GPU

除了屏幕,UI 渲染还要依赖另外两个核心的硬件:CPU 和 GPU。

CPU(Central Processing Unit,*处理器),是计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元;
GPU(Graphics Processin Unit,图形处理器),是一种专门用于图像运算的处理器,在计算机系统中通常被称为 "显卡"的核心部件就是 GPU。

UI 组件在绘制到屏幕之前,都需要经过 Rasterization(栅格化)操作,而栅格化又是一个非常耗时的操作。
Android UI 渲染机制的演进,你需要了解什么?
Rasterization 栅格化是绘制那些 Button、Shape、Path、String、Bitmap 等显示组件最基础的操作。栅格化将这些 UI 组件拆分到显示器的不同像素上进行显示。这是一个非常耗时的操作,GPU 的引入就是为了加快栅格化。

GPU 的由来

在没有 GPU 的时代,UI 的绘制任务完全由 CPU 完成, CPU 既要负责 UI 绘制还要负责内存管理、逻辑运算等其他任务,这就导致 CPU 的任务繁多,因此性能也会大打折扣。

CPU 与 GPU 在结构设计上完全不同,如下图:
Android UI 渲染机制的演进,你需要了解什么?
一、黄色 Control 为控制器,用于协调控制整个 CPU 的运行,包括指令读取、控制其他模块的运行等;
二、绿色的 ALU(Arithmetic Logic Unit)是算数逻辑单元,用于进行数学、逻辑运算;
三、橙色的 Cache 和 DRAM 分别为高速缓存和 RAM,用于存储信息。

从结构图可以看出,CPU 的控制器较为复杂,而 ALU 数量较少,因此 CPU 更擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。

而 GPU 的设计正是为实现大量数学运算。GPU 的控制器比较简单,但包含大量 ALU。GPU 中的 ALU 使用了并行设计,且具有较多的浮点运算单元。可以帮助我们加快栅格化操作。
Android UI 渲染机制的演进,你需要了解什么?
从图中可以看到,软件绘制使用 Skia 库,它是一款能在低端设备,如手机呈现高质量的 2D 跨平台图形框架,类似 Chrome、Flutter 内部使用的都是 Skia 库。

硬件绘制的思想就是通过底层软件代码,将 CPU 不擅长的图形计算转换成 GPU 专用指令,由 GPU 完成绘制任务。

3. OpenGL 与 Vulkan

一、OpenGL(Open Graphics Library):OpenGL 是一个跨平台的图形API,它为 3D 图形处理硬件指定了一个标准的软件接口。
二、OpenGL ES (Embedded Systems):OpenGL ES 是针对嵌入式设备的 OpenGL 规范的一种变体。Android 支持多个版本的 OpenGL ES API。
三、Vulkan:它是一个低开销、跨平台的 3D 图形和计算 API。Vulkan 的目标是跨所有平台的高性能实时 3D 图形应用程序,旨在提供更高的性能和更均衡的 CPU / GPU 使用。

硬件加速绘制就是通过 GPU 来进行渲染,GPU 作为一个硬件,用户空间是无法直接使用的,它是由 GPU 厂商按照 OpenGL 规范实现的驱动间接进行使用。

也就是说,如果一个设备支持 GPU 硬件加速渲染,那么当 Android 应用程序调用 Open GL 接口来绘制 UI 时,Android 应用程序的 UI 就是通过硬件加速技术进行渲染的。

在官方硬件加速的文档中,可以看到很多 OpenGL API 都有相应的 Android API Level 限制。
Android UI 渲染机制的演进,你需要了解什么?
这主要是受 OpenGL ES 版本与系统支持的限制,直到 Android P,仍然有 3 个 API 是没有支持的。

对于不支持的 API,只能使用软件绘制模式,渲染的性能将会大大降低。
Android UI 渲染机制的演进,你需要了解什么?
Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本的同时,还添加了对 Vulkan 的支持。

Vulkan 的设计目标是取代 OpenGL,Vulkan 是一个相当低级别的 API,并且提供并行的任务处理。Vulkan 还能够渲染 2D 图形应用程序。

除了其较低的 CPU 使用率,Vulkan 还能够更好地在多个 CPU 内核之间分配工作。在功耗、多核优化提升绘图调用上有着非常明显的优势。

2、Android 的图形组件

Android 的 UI 渲染性能是 Google 长期以来非常重视的,基本每次 Google I/O 都会花很多篇幅讲这一块内容。每个开发者都希望自己的应用都可以做到 60 fps 如丝般顺滑,在了解 Android 的渲染之前,需要先了解下 Android 图形系统的整体架构,以及它包含的主要组件。
Android UI 渲染机制的演进,你需要了解什么?
一、Image Stream Produces:图像流生产方,应用程序内绘制到 Surface 的图形(xml / Java 实现的图形,或者视频)
二、Hardware Composer:硬件合成器,它是显示控制器的硬件抽象。
三、Gralloc:Graphic memory allocator 用来分配图形缓冲区内存。

如果把应用程序图形渲染过程当做一次绘画过程,那么绘画过程中 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。它将 WindowManger 提供的所有 Surface,通过硬件合成器 Hardware Composer 合成并输出到显示屏。

使用画笔 Skia / OpenGL 将内容绘制到 Surface 上,绘制的过程中如果使用 Open GL 渲染,那便是硬件加速,否则纯靠 CPU 绘制渲染栅格化的过程就叫软件绘制。

对于硬件绘制,我们通过调用 OpenGL ES 接口利用 GPU 完成绘制。

3、Why 60 fps?

关于 Android 的渲染,大家肯定听说过每秒 60 帧和 16ms 的限制问题,你是否有想过为什么是这些数字?

如果你是对于性能要求较高的开发者,这样的技术细节是非常值得深入了解的。

由于人类眼睛特殊的生理结构,并不像相机那样有图像快照送到大脑;相反,大脑在不停地处理眼睛传递给他的视觉信号,所以对于人类的大脑来说并没有帧或者快照的概念,人类眼睛对于运动的概念来自于静止的帧。

一、12 fps:每秒达到 10 ~ 12 帧以上才可以被感知到运动及变化,但是这样的速率是非常不流畅的,只有帧率超过每秒 24 帧的时候,才会被察觉为流畅的运动及变化。
二、24fps:每秒 24 帧在电影界是黄金标准,24 帧的速度足够使画面运动的非常流畅,而且 24 帧的电影预算也能满足成本的要求,这也是为什么在过去的 50 年里,绝大多数的电影都使用 24 帧每秒的速率。

现在,每秒 30 帧对于电影来说绰绰有余,但是对于那些复杂绚丽的电影特效,它的视觉效果还是难以令人信服。

60fps:实际上每秒 60fps 的速度才是真正的黄金标准,60 帧的速度非常流畅,绝大多数人都察觉不到比 60 帧还高的视觉体验。
Android UI 渲染机制的演进,你需要了解什么?
Android 系统每间隔 16ms 发出一次 VSYNC 信号,触发对 UI 的渲染任务;为了能够实现流畅的画面,这就意味着应该始终让应用保持在 60 帧每秒,即每帧工作的准备时间仅有 16ms。

4、Android 渲染的演进

跟耗电一样,Android 的 UI 渲染性能也是 Google 长期以来非常重视的,基本每次 Google I/O 都会花很多篇幅讲这一块。每个开发者都希望自己的应用可以做到 60 fps 如丝般顺滑,不过相比 iOS 系统,Android 的渲染性能一直被人诟病。

Android 系统为了弥补跟 iOS 的差距,在每个版本都做了大量的优化。接下来我们通过演进的方式看看 Android 系统在渲染方面都做了哪些努力。

1. Android 4.0 开启硬件加速
在 Android 3.0 之前,或者没有启用硬件加速时,系统都会使用软件方式来渲染 UI。
Android UI 渲染机制的演进,你需要了解什么?
整个流程如上图所示:

一、Surface。每个 View 都由某一个窗口管理,而每一个窗口都会关联有一个 Surface。
二、Canvas。通过 Surface 的 lock 函数获得一个 Canvas,Canvas 可以简单理解为 Skia 底层接口的封装。
三、Graphic Buffer。SurfaceFlinger 会帮助我们托管一个 BufferQueue,我们从 BufferQueue 中拿到 Graphic Buffer,然后通过 Canvas 以及 Skia 将绘制内容栅格化到上面。
四、SurfaceFlinger。通过 Swap Buffer 把 Front Graphic Buffer 的内容交给 SurfaceFlinger,最后硬件合成器 Hardware Composer 合成并输出到显示屏。

整个渲染流程看上去比较简单,但是正如前面所说,CPU 对于图形处理并不是那么高效,这个过程完全没有利用 GPU 的高性能。

硬件加速绘制
Android UI 渲染机制的演进,你需要了解什么?
硬件加速绘制与软件绘制整个流程差异非常大,最核心就是通过 GPU 完成 Graphic Buffer 的内容绘制。此外硬件绘制还引入了一个 DisplayList 的概念,每个 View 内部都有一个 DisplayList,当某个 View 需要重绘时,将它标记为 Dirty。

当需要重绘时,仅仅只需要重绘一个 View 的 DisplayList,而不是像软件绘制那样需要向上递归。这样可以大大减少绘图的操作数量,因而提高了渲染效率。
Android UI 渲染机制的演进,你需要了解什么?
2. Android 4.1 Project Butter

优化是无止境的,Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启了这个机制。

Project Butter 主要包含三个组成部分,VSYNC、Triple Buffer 和 Choreographer。

VSYNC 信号

其中 VSYNC(Vertical Synchronization)是理解 Project Butter 的核心。对于 Android 4.0,CPU 可能会因为在忙其他的事情,导致没来得及处理 UI 绘制。

为了解决这个问题,Project Butter 引入了 VSYNC,它类似于时钟中断,每收到 VSYNC 中断,CPU 会立即准备 Buffer 数据,由于大部分显示设备刷新频率都是 60 Hz(一秒刷新 60 次),也就是说一帧数据的准备都要在 16ms 内完成。
Android UI 渲染机制的演进,你需要了解什么?
这样应用总是在 VSYNC 边界上开始绘制,而 SurfaceFlinger 总是在 VSYNC 边界上进行合成。这样可以消除卡顿,并提升图形的视觉表现。

Triple Buffering(三缓冲机制)

在 Android 4.1 之前,Android 使用双缓冲机制。

怎么理解呢?

每个 Surface 都会有一个 BufferQueue 缓存队列,但是这个队列会由 SurfaceFlinger 管理,通过匿名共享内存机制与 App 应用层交互。
Android UI 渲染机制的演进,你需要了解什么?
整个流程如下:

一、每个 Surface 对应的 BufferQueue 内部都有两个 Graphic Buffer,一个用于绘制一个用于显示。应用会把内容先绘制到离屏缓冲区(OffScreen Buffer),在需要显示时,才把离屏缓冲区的内容通过 Swap Buffer 复制到 Front Graphic Buffer 中。
二、这样 SurfaceFlinge 就拿到了某个 Surface 最终要显示的内容,但是同一时间我们可能会有多个 Surface。这里面可能是不同应用的 Surface,也可能是同一个应用里面类似 SurfaceView 和 TextureView,它们都会有自己单独的 Surface。
三、这个时候 SurfaceFlinger 把所有 Surface 要显示的内容统一交给 Hardware Composer,它会根据位置、Z - Order 顺序等信息合成为最终屏幕需要显示的内容,而这个内容交给系统的帧缓冲区 Frame Buffer 来显示(Frame Buffer 是非常底层的,可以理解为屏幕显示的抽象)。

如果你理解了双缓冲机制的原理,那就非常容易理解什么是三缓冲区了。如果只有两个 Graphic Buffer 缓存区 A 和 B,如果 CPU / GPU 绘制过程较长,超过了一个 VSYNC 信号周期,因为缓冲区 B 中的数据还没有准备完成,所以只能继续展示 A 缓冲区的内容,这样缓冲区 A 和 B 都分别被显示设备和 GPU 占用,CPU 无法准备下一帧的数据。
Android UI 渲染机制的演进,你需要了解什么?
如果再提供一个缓冲区,CPU、GPU 和显示设备都能使用各自的缓冲区工作,互不影响。简单来说,三缓冲机制就是在双缓冲机制的基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用了一个 Graphic Buffer 所占用的内存。
Android UI 渲染机制的演进,你需要了解什么?
Choreographer

Choreographer 本质是一个 Java 类,如果直译的话为舞蹈指导,看到这个词不得不赞叹设计者除了 Coding 之外的广泛视野。

舞蹈是有节奏的,节奏使舞蹈的每个动作更加协调和连贯;视图刷新也是如此,Choreographer 可以接收系统的 VSYNC 信号,业界一般通过它来监控应用的帧率。

Choreographer 是线程单例,而且它具有处理当前线程 Looper 的能力。

private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        Looper looper = Looper.myLooper();
        if (looper == null) {
            //抛出异常。
        }
        return new Choreographer(looper);
    }
};

3. Android 5.0:RenderThread

经过 Android 4.1 的 Project Butter 黄油计划之后,Android 的渲染性能有了很大的改善。不过你有没有注意到这样一个问题,虽然利用了 GPU 的图形高性能运算,但是从计算 DisplayList,到通过 GPU 绘制到 Frame Buffer,整个计算和绘制都在 UI 主线程中完成。
Android UI 渲染机制的演进,你需要了解什么?
UI 线程“既当爹又当妈”,任务过于繁重。如果整个渲染过程比较耗时,可能造成无法响应用户的操作,进而出现卡顿的情况。

GPU 对图形的绘制渲染能力更胜一筹,如果使用 GPU 并在不同线程绘制渲染图形,那么整个流程会更加顺畅。

正因如此,在 Android 5.0 引入两个比较大的改变。

一个是引入了 RenderNode 的概念,它对 DisplayList 及一些 View 显示属性都做了进一步封装。另一个是引入了 RenderThread,所有的 GL 命令执行都放到这个线程上,渲染线程在 RenderNode 中存有渲染帧的所有信息,可以做一些属性动画,这样即便主线程有耗时操作的时候也可以保证动画流程。

总结
今天,我们主要介绍了 UI 渲染的理论知识,通过演进的方式进一步加深对 Android 渲染机制的理解,这对接下来要讨论的 UI 渲染优化会有很大的帮助。

想获取Android核心知识笔记可点击我的github了解更多进阶的可能。