Android 渲染机制

1 Android 渲染流程

一般情况下,一个布局写好以后,使用 Activity#setContentView 调用该布局,这个 View tree 就创建好了。Activity#setContentView 其实是通过 LayoutInflate 来把布局文件转化为 View tree 的(反射)。需要注意的是,LayoutInflate 虽然可以帮助创建 View tree,但到这里也仅是以单纯的对象数据存在,这个时候是无法正确的获取 View 的 GUI(Graphical User Interface 图形用户界面)的相关属性的,如大小、位置和渲染状态。

View tree 生成的最后一步就是把根节点送到 ViewRootImpl#setView 中,之后就会进入渲染流程,入口方法是 ViewRootImpl#requestLayout,之后是 ViewRootImpl#scheduleTraversals,最后调用的是 ViewRootImpl#performTraversals,View tree 的渲染流程全都在这里,也就是常说的 measure、layout、draw。View体系与自定义View(三)—— View的绘制流程

以下为 View 的绘制流程/视图添加到 Window 的过程:

绘制流程

绘制流程

总结:文本数据(xml)—> 实例数据(java) —> 图像数据 bitmap,bitmap 才是屏幕(硬件)所需的数据。

在 ViewRootImpl#drawSoftware 方法中会通过 Surface#lockCanvas 方法创建一个 Canvas(在英文中是“画布”的意思) 对象,然后进入 View#draw 流程,Canvas 才是实际制作图像的工具,比如如何画点,如何画线,如何画文字、图片等等。

// /frameworks/base/core/java/android/view/ViewRootImpl.java
public final Surface mSurface = new Surface();
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
  	Surface surface = mSurface; // 1
  	
  	...
      
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                scalingRequired, dirty, surfaceInsets)) {
            return false;
        }
  
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, 
                             int yoff, boolean scalingRequired, 
                             Rect dirty, Rect surfaceInsets) {

    // Draw with software renderer.
    final Canvas canvas; // 2
    try {
        canvas = mSurface.lockCanvas(dirty); // 3
        canvas.setDensity(mDensity);
    } catch (Surface.OutOfResourcesException e) {
        handleOutOfResourcesException(e);
        return false;
    } catch (IllegalArgumentException e) {
        mLayoutRequested = true;    // ask wm for a new surface next time.
        return false;
    }
  
  	try {
      	...
      	mView.draw(canvas);
      	...
    }finally {
        ...    
    }
} 

一个 Canvas 对象从 ViewRootImpl 传给 View,View 的各个方法(draw、dispatchDraw 和 drawChild)都只接收 Canvas 对象,每个 View 都要把其想要展示的内容传递到 Canvas 对象中。

// /frameworks/base/core/java/android/view/View.java
public void draw(@NonNull Canvas canvas) {
  	...
}

protected void dispatchDraw(@NonNull Canvas canvas) { }

// /frameworks/base/core/java/android/view/ViewGroup.java
protected boolean drawChild(@NonNull Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

在 Canvas 中有一个 Bitmap 类型的对象,这个 Bitmap 才是真正的画布。

// /frameworks/base/graphics/java/android/graphics/Canvas.java
public class Canvas extends BaseCanvas {
  	private Bitmap mBitmap;
}

那么,Surface 是什么呢?以下是 Surface 的部分源码:

/**
  * Handle onto a raw buffer that is being managed by the screen 
  * 由屏幕管理的原始缓冲区
  */
public class Surface implements Parcelable {
  	private final Canvas mCanvas = new CompatibleCanvas();
  
    public Canvas lockCanvas(Rect inOutDirty)
          throws Surface.OutOfResourcesException, IllegalArgumentException {
      synchronized (mLock) {
          checkNotReleasedLocked();
          if (mLockedObject != 0) {
              throw new IllegalArgumentException("Surface was already locked");
          }
          mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
          return mCanvas;
      }
    }
}

从注释上可以知道,Surface 是一块原始缓冲区。 在 Android 中,所有的 View 都由窗口管理,而每个窗口都会关联一个 Surface。在屏幕上绘制内容之前,都需要先获得 Surface,然后用 2D/3D 引擎(Skia/OpenGL)在这个缓冲区上绘制内容。 绘制完成之后,会通知 SurfaceFlinger 将绘制内容(Frame Buffer)渲染到屏幕上去。关于 SurfaceFlinger,之后会做详细解释。

屏幕渲染分为软件渲染和硬件渲染,Canvas 对象的来源也有两个:

  • 一是走软件渲染时,在 ViewRootImpl 中创建,从源码上看,ViewRootImpl 本身就会创建一个 Surface 对象,然后用 Surface 获取出一个 Canvas 对象,再传递给 View,由 View 进行具体的绘制;
  • 二是走硬件加速,会由 hwui 创建 Canvas 对象;

因此,draw 的触发逻辑也有两条:

  • 没有硬件加速时,走的是 ViewRootImpl#performTraversals —> performDraw —> draw —> drawSoftware —> View#draw;
  • 启动硬件加速时,走的是 ViewRootImpl#performTraversals —> performDraw —> draw —> ThreadedRenderer.java#draw

2 软件绘制和硬件绘制

Android 4.0 开始引入硬件加速机制,之前走的都是软件渲染。如果有一些 API 是不支持硬件加速的,需要进行手动关闭。

软件绘制和硬件绘制

UI 渲染需要要依赖两个核心的硬件,CPU 和 GPU:

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

在没有 GPU 的时代,UI 的绘制任务都是由 CPU 完成的,也就是说,CPU 除了负责逻辑运算、内存管理还要负责 UI 绘制,这就导致 CPU 的任务繁重,性能也会受到影响。

CPU 和 GPU 在结构设计上完全不同,如下所示:

CPU 和 GPU

  • 黄色部分:Control 控制器,用于协调控制整个 CPU 的运行,包括读取指令、控制其他模块的运行等;
  • 绿色部分:ALU(Arithmetic Logic Unit)是算数逻辑单元,用于进行数学、逻辑运算;
  • 橙色部分:Cache 和 DRAM 分别为高速缓存和 RAM,用于存储信息;

从上图可以看出,CPU 的控制器较为复杂,而 ALU 数量较少,因此 CPU 更擅长各种复杂的逻辑运算,而不擅长数学尤其是浮点运算。而 GPU 的设计正是为了实现大量的数学运算。GPU 的控制器比较简单,但包含大量的 ALU,GPU 中的 ALU 使用了并行设计,且具有较多的浮点运算单元,可以帮助我们加快 Rasterization(栅格化)操作。

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

栅格化

因此,硬件绘制的思想就是 CPU 将 XML 数据转换成实例对象,然后将 CPU 不擅长的图形计算交由 GPU 去处理,由 GPU 完成绘制任务,以便实现更好的性能(CPU 和 GPU 都是制图者)。

底层图像库有很多,Android 选择的是 Skia(2D) 和 OpenGL(3D) 来绘制图形,图形库可以直接控制 GPU 产生图形数据(Canvas.draw —> native —>Skia/OpenGL(Canvas 命令转换为 OpenGL 指令) —> GPU)。

软件绘制使用的是 Skia 库,是一款能在低端设备,如手机上呈现高质量的 2D 跨平台图形框架,Chrome、Flutter 内部使用的都是 Skia 库。需要注意的是,软件绘制使用的是 Skia 库,但这并不代表 Skia 库不支持硬件加速,从 Android 8 开始,我们可以使用 Skia 进行硬件加速,Android 9 开始默认使用Skia 进行硬件加速。

在处理 3D 场景时,通常使用 OpenGL ES。在 Android 7.0 中添加了对 Vulkan 的支持。Vulkan 的设计目标是取代 OpenGL,Vulkan 是个相当低级别的 API,并且提供了并行的任务处理。除了较低的 CPU 的使用率,VulKan 还能够更好的在多个 CPU 内核之间分配工作。在功耗、多核优化提升会图调用上有非常明显的优势。

Skia、OpenGL、Vulkan 的区别:

  • Skia:是 2D 图形渲染库。如果想完成 3D 效果需要 OpenGL、Vulkan、Metal 进行支持。Android 8 开始 Skia 支持硬件加速,Chrome、Flutter 都是用它来完成绘制的;
  • OpenGL:是一种跨平台的 2D/3D 图形绘制规范接口,OpenGL ES 是针对嵌入式设备的,对手机做了优化;
  • Vulkan:Vulkan 是用来替换 OpenGL 的,它同时支持 2D 和 3D 绘制,也更加轻量级;

3 Android 黄油计划(Project Butter)

虽然引入了硬件加速机制,加快了渲染的时间,但是对于 GUI(Graphical User Interface 图形用户界面)的流畅度、响应度,特别是动画这一块的流畅程度和其他平台(如 Apple)差距仍然是很大的。一个重要的原因就在于,GUI 整体的渲染缺少协同。 最大的问题在于动画,动画要求连续不断的重绘,如果仅靠客户端来触发,帧率不够,由此造成的流畅度也不好。

Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启这个机制。Project Butter 主要包含三个组成部分:

  • VSync
  • Choreographer
  • TripBuffer

其中,VSync(Vertical Synchronization) 是理解 Project Butter 的核心。

3.1 VSync

帧率 vs 屏幕刷新频率:

  • 帧率(Frame Rate):单位 fps,即 GPU 在一秒内生成的帧数(图片),帧率越高越好。例如电影界采用 24 帧的速度就可以画面非常流畅了,而 Android 系统则采用更高的 60fps,即每秒生成 60 帧的画面,也就是 1000/60 ≈ 16ms 生成一帧画面;
    • 12 fps:由于人类眼睛的特殊生理结构,如果所看画面帧率高于 10~12 fps 的时候,就会认为是连贯的;
    • 24fps:有声电影的拍摄及播放帧率均为 24fps,对一般人来说是可以接受的;
    • 30fps:早起的高动态电子游戏,帧率小于 30fps 时就会显得不连贯,这是因为没有动态模糊使流畅度降低;
    • 60fps:在与手机交互过程中,如果触摸和反馈在 60fps 以下是可以被人感觉出来的,会感到画面卡顿和迟滞现象;
  • 屏幕刷新频率(Refresh Rate):单位是赫兹(Hz),表示屏幕在一秒内刷新画面的次数,刷新频率取决于硬件的固定参数,该值对于特定的设备来说是一个常量。如 60Hz、144 Hz 表示每秒刷新 60 次或 144 次。

对于一个特定的设备来说,帧率和屏幕刷新速率没有必然的关系。但是两者需要协同工作,才能正确的获取图像数据并进行绘制。比如 Android 手机的刷新频率是 60Hz,那么一帧数据需要在 16ms 内完成。

屏幕并不是一次性的显示画面的,而是从左到右(行刷新,水平刷新,Horizontal Scanning)、从上到下(屏幕刷新,垂直刷新,Vertiacl Scanning)逐行扫描显示,不过这一过程快到人眼无法察觉。以 60Hz 的刷新频率的屏幕为例,即 1000/60 ≈ 16ms,16ms 刷新一次。

屏幕刷新过程

如果上一帧的扫描没有结束,屏幕又开始扫描下一帧,就会出现扫描撕裂的情况:
tearing

因此,GPU 厂商开发出了一种防止屏幕撕裂的技术方案 —— Vertical Synchronization,即 VSync,垂直同步信号或时钟中断。VSync 是一个硬件信号,它和显示器的刷新频率相对应,每当屏幕完成一次垂直刷新,VSync 信号就会被发出,作为显示器和图形引擎之间时间同步的标准,其本质意义在于保证界面的流畅性和稳定性。

VSync 信号时需要调度的,没有调度就不会有回调。

3.2 Choreographer

Choreographer(编舞者)根据 VSync 信号来对 CPU/GPU 进行绘制指导,协调整个渲染过程,对于输入事件响应、动画和渲染在时间上进行把控,以保证流畅的用户体验。

Choreographer 在 ViewRootImpl 中的使用:

// /frameworks/base/core/java/android/view/ViewRootImpl.java
final Choreographer mChoreographer;
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

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

Choreographer 的作用:

  • 布局请求:当视图需要进行布局操作时,Choreographer 发出布局请求并协调布局操作的执行。它确保将布局请求与其他动画和绘制操作同步,避免冲突和界面不一致;
  • 绘制同步:Choreographer 负责将绘制操作与显示器的刷新同步。它通过监听系统的 VSync 信号,去定绘制操作的时机,避免图形撕裂和卡顿现象;
  • 输入事件处理:Choreographer 管理和分发用户输入事件,确保它们在正确的时间点被处理,并与动画和渲染操作同步。这有助于提供更流畅和响应敏捷的用户交互体验;
  • 动画调度:Choreographer 调度和管理应用程序中的动画效果,确保动画按照预定的帧率和时间表进行播放,并平滑地过渡到下一个动画阶段;

Choreographer 使用了以下几种机制来实现流畅的界面渲染:

  • VSync(垂直同步信号):Choreographer 监听系统发出的 VSync 信号。每当收到 VSync 信号时,Choreographer 就知道屏幕即将进行一次刷新。这样,它可以根据 VSync 信号的时间点来安排渲染和动画操作的触发和执行;
  • 时间戳(Timestamping):Choreographer 在收到 VSync 信号时,会获取一个时间戳,以记录每次 VSync 信号的时间点。这个时间戳可以用于计算渲染和动画的操作时间和持续时间,从而在合适的时机进行调度和执行;
  • 界面刷新(Frame Refresh):Choreographer 使用 VSync 信号和时间戳来决定界面的刷新时机。它根据预定的逻辑和优先级,调度动画、布局和绘制操作,以确保它们在下一次 VSync 信号到来之前完成。这样可以避免界面的撕裂或卡顿现象,提供流畅的用户体验;

其实这个 Choreogarpher 这个类本身并不会很复杂,简单来说它就是负责定时回调,主要方法有 postFrameCallback 和 removeFrameCallback,FrameCallback 是个比较简单的接口:

// /frameworks/base/core/java/android/view/Choreographer.java
public interface FrameCallback {
   public void doFrame(long frameTimeNanos);
}
3.3 TripBuffer 三缓存
3.3.1 单缓存

在没有引入 Vsync 的时候,屏幕显示图像的工作流程是这样的:

没有引入 Vsync 时

如上图所示,CPU/GPU 将需要绘制的数据存放在图像缓冲区中,屏幕从图像缓冲区中获取数据,然后刷新显示,这是典型的生产者-消费者模型。

理想的情况是帧率(GPU)和刷新频率(屏幕)相等,每绘制一帧,屏幕就显示一帧。而实际情况是,二者之间没有必然的联系,如果没有锁来控制同步,很容易出现问题。

  • 如果刷新频率大于帧率的时候,屏幕拿不到下一帧数据,就会重复绘制当前帧数据。
  • 如当帧率大于刷新频率时,屏幕还没有刷新到 n-1 帧的时候,GPU 已经生成第 n 帧了,屏幕刷新的时候绘制的就是第 n 帧数据,这个时候屏幕上半部分显示的是第 n 帧数据,屏幕的下半部分显示的是第 n-1 帧之前的数据,这样显示的图像就会出现明显的偏差,也就是“tearing”,如下所示:
    tearing
    tearing
3.3.2 双缓存(Double Buffer)

这里的双缓存和计算机组成原理中的“二级缓存”不是一回事。

为了解决单缓存的 tearing 问题,双缓存和 VSync 应运而生。双缓存的模型如下所示:

双缓存

两个缓存分别为 Back Buffer 和 Frame Buffer(帧缓冲区)。GPU 向 Back Buffer 中写数据,屏幕从 Frame Buffer 中读数据。VSync 信号负责调用 Back Buffer 到 Frame Buffer 的复制操作,可以认为该复制操作在瞬间完成。

在双缓冲模式下,工作流程是这样的:在某个时间点,一个屏幕刷新周期完成,进入短暂的刷新空白期。此时,VSync 信号产生,先完成复制操作,然后通知 CPU/GPU 绘制下一帧图像。复制操作完成后屏幕开始下一个刷新周期,即将刚复制到 Frame Buffer 的数据显示到屏幕上。

在双缓冲模型下,只有当 VSync 信号产生时,CPU/GPU 才会开始绘制。这样,当帧率大于刷新频率时,帧率就会被迫跟刷新频率保持同步,从而避免“tearing”现象。

需要注意的是,当 VSync 信号发出时,如果 CPU/GPU 正在生产帧数据,此时不会发生复制操作。当屏幕进入下一个刷新周期时,就会从 Frame Buffer 中取出“老”数据,而非正在产生的帧数据,即两个刷新周期显示的是同一帧数据,这就是“掉帧”现象(Dropped Frame,Skipped Frame,Jank)。因此,双缓存的缺陷在于,当 CPU/GPU 绘制一帧的时间超过 16ms 时,就会产生 Jank。

如下图所示,A、B 和 C 都是 Buffer。蓝色代表 CPU 生成的帧数据,绿色代表 GPU 执行生成帧数据,黄色代表生成帧完成:

double buffering

CPU/GPU 处理数据的时间过长,超过了一帧绘制的时间,在第二个时间段内,由于 GPU 还在处理 B 帧数据,无法进行数据交换,导致 A 帧被重复绘制。而 B 帧数据在绘制完成后又缺乏 VSync 信号,只能等待下一次的 VSync 信号的来临。因此,在这一过程中,有一段时间是被浪费的。

3.3.3 三缓存(Triple Buffer)

于是有了三缓存:

三缓存

工作原理同双缓冲类似,只是多了一个 Back Buffer。三缓冲机制有效的利用了等待 VSync 信号的时间,可以帮助我们减少 jank。

如果有第三个 Buffer 能让 CPU/GPU 在这个时候继续工作,那就完全可以避免第二个 Jank 产生了。

Triple buffering

需要注意的是,第三个缓存并不是总存在的,只有当需要的时候才会创建。 之所以这样,是因此三缓存会显著增加用户输入到显示的延迟时间。如上图,帧 C 是在第 2 个刷新周期产生的,却是在第 4 个周期显示的。

4 Android 渲染的整体架构

以下是 Android 渲染的整体架构:
渲染整体架构

Android 渲染的整体架构可以分为以下几部分:

  • 图像生产者(image stream producers):主要有 MediaPlayer、CameraPreview、NDK(Skia)、OpenGL ES。其中,MediaPlayer 和 Camera Preview 是通过直接读取图像源来生成图像数据。NDK(Skia)、OpenGL ES 是通过自身的绘制能力产生的图像数据。
  • 图像缓冲区(BufferQueue):一般是三缓冲区。NDK(Skia)、OpenGL ES、Vulkan 将绘制的数据存放在图像缓冲区;
  • 图像消费者(image stream consumers): SurfaceFlinger 从图像缓冲区将数据取出,通过硬件合成器 Hardware Composer 进行加工及合成 layer,最终交给 HAL 展示;
  • HAL:硬件抽象层,把图形数据展示到设备屏幕;

整个图像渲染系统就是采用了生产者-消费者模式,屏幕渲染的核心,是对图像数据的生产和消费。 生产和消费的对象是 BufferQueue 中的 Buffer。
生产者-消费者模式

前面我们已经说过,Surface 是一块原始缓冲区,每个窗口都会管理一个 Surface,屏幕在绘制内容之前,先要获得 Surface,然后在再用 2D/3D 引擎(Skia/OpenGL)在这个缓冲区上进行绘制(Surface —> Canvas)。

SurfaceFlinger 是图像数据的消费者,它的作用主要是接收 Graphic Buffer,然后交给 HWComposer 合成,合成完的数据,最终交给了 Frame Buffer(帧缓冲区)。

三缓冲机制

软件渲染

再没有硬件加速之前主要是通过 Skia 这种软件方式渲染 UI,如下所示:

软件渲染

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

硬件渲染

Android 3.0,支持硬件加速,需要手动打开,Android 4.0 就默认开启硬件加速了,开启硬件加速流程如下:

生产者-消费者模型

硬件加速绘制最核心就是通过 GPU 完成 Graphic Buffer 的内容绘制。

RenderThread 线程

经过 Android 4.1 的 Projcet Butter 黄油计划之后,Android 的渲染性有了很大的改善。不过你有没有注意到这样一个问题,虽然利用了 GPU 的图形高性能运算,但是从计算到通过 GPU 绘制到 Frame Buffer,整个计算和绘制都在 UI 主线程中完成。UI 线程任务过于繁重。如果整个渲染过程比较耗时,可能造成无法响应用户的操作,进而出现卡顿的情况。GPU 对图形的渲染能力更胜一筹,如果使用 GPU 并在不同的线程绘制渲染图形,那么整个流程会更加顺畅。

在 Android 5.0 之通过引进 RenderThread(渲染线程),我们就可将 UI 渲染工作从 Main Thread 释放出来,交由 RenderThread 来处理,从而也使得 Main Thread 可以更专注高效地处理用户输入,这样使得在提高 UI 绘制效率的同时,也使得 UI 具有更高的响应。

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值