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 的过程:

Android 渲染机制_第1张图片

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

在 ViewRootImpl#drawSoftware 方法中创建一个 Canvas 对象,然后进入 View#draw 流程,Canvas 才是实际制作图像的工具,比如如何画点,如何画线,如何画文字、图片等等。

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;
  	
  	...
      
        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;
    try {
        canvas = mSurface.lockCanvas(dirty);
        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 {
        ...    
    }
} 


// /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 对象从 ViewRootImpl 传给 View,View 的各个方法(draw、dispatchDraw 和 drawChild)都只接收 Canvas 对象,每个 View 都要把其想要展示的内容传递到 Canvas 对象中。

Canvas 对象的来源有两个:

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

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

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

2 软件绘制和硬件绘制

硬件加速绘制是从 Android 4.0 开始支持的,在此之前走的都是软件渲染,也就是从 ViewRoot(Android 4.0 之前是 ViewRoot,之后才是 ViewRootImpl)中持有的 Surface 直接创建 Canvas,然后交给 View 去做具体的绘制。

Android 渲染机制_第2张图片

Skia(二维)、OpenGL(三维)是专门用来制作图形的(Bitmap),可以直接对 GPU 进行调用处理。底层的图像库很多,Android 选择的是这两个。

软件绘制使用的是 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:是一种跨平台的 3D 图形绘制规范接口,OpenGL ES 是针对嵌入式设备的,对手机做了优化;
  • Vulkan:Vulkan 是用来替换 OpenGL 的,它同时支持 2D 和 3D 绘制,也更加轻量级;

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

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

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

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

Android 渲染机制_第3张图片

  • 黄色部分: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 的引入就是为了加快栅格化。

Android 渲染机制_第4张图片

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

Android 4.0 开始引入硬件加速机制,但是还有一些 API 是不支持硬件加速的,需要进行手动关闭。

3 Android 黄油计划(Project Butter)

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

于是在 Android 4.1 中引入了 Choreographer 以及 Vsync 机制来解决这个问题。

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 帧的画面;
  • 屏幕刷新频率(Refresh Rate):单位是赫兹(Hz),表示屏幕在一秒内刷新画面的次数,刷新频率取决于硬件的固定参数,该值对于特定的设备来说是一个常量。如 60Hz、144 Hz 表示每秒刷新 60 次或 144 次。

对于一个特定的设备来说,帧率和屏幕刷新速率没有必然的关系。但是两者需要协同工作,才能正确的获取图像数据并进行绘制。

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

Android 渲染机制_第5张图片
如果上一帧的扫描没有结束,屏幕又开始扫描下一帧,就会出现扫描撕裂的情况:
Android 渲染机制_第6张图片

因此,GPU 厂商开发出了一种防止屏幕撕裂的技术方案 —— Vertical Synchronization,即 VSync,垂直同步信号或时钟中断。VSync 是一个硬件信号,它和显示器的刷新频率相对应,通常是 60Hz,每当屏幕完成一次垂直刷新,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();
    }
}

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 三缓存

以下是 Android 渲染的整体架构:
Android 渲染机制_第7张图片
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 从图像缓冲区将数据取出,进行加工及合成,最终交给 HAL 展示;
  • HAL:硬件抽象层,把图形数据展示到设备屏幕;

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

无论开发者使用哪种 API,一切内容都会渲染到 Surface(Canvas —> Surface) 上。Surface 表示缓冲队列中的生产方,缓冲队列通常被 SurfaceFlinger 消耗。如 draw 方法会把绘制指令通过 Canvas 传递给 framework 层的 RenderThread 线程。RenderThread 线程通过 surface.dequeue 的到缓冲区 graphic buffer,然后在上面通过 OpenGL 来完成真正的渲染命令,把缓冲区交还给 BufferQueue 队列中。

3.3.1 单缓存

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


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

理想的情况是帧率(GPU)和刷新频率(屏幕)相等,每绘制一帧,屏幕就显示一帧。而实际情况是,二者之间没有必然的联系,如果没有锁来控制同步,很容易出现问题。如当帧率大于刷新频率时,屏幕还没有刷新到 n-1 帧的时候,GPU 已经生成第 n 帧了,屏幕刷新的时候绘制的就是第 n 帧数据,这个时候屏幕上半部分显示的是第 n 帧数据,屏幕的下半部分显示的是第 n-1 帧之前的数据,这样显示的图像就会出现明显的偏差,也就是“tearing”,如下所示:
Android 渲染机制_第9张图片
Android 渲染机制_第10张图片
如果刷新频率大于帧率的时候,屏幕就会拿不到下一帧数据,导致黑屏。

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)。

3.3.3 三缓存(Triple Buffer)

双缓存的缺陷在于,当 CPU/GPU 绘制一帧的时间超过 16ms 时,就会产生 Jank。

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

Android 渲染机制_第11张图片
如果有第三个 Buffer 能让 CPU/GPU 在这个时候继续工作,那就完全可以避免第二个 Jank 产生了。

Android 渲染机制_第12张图片

于是有了三缓存:

工作原理同双缓冲类似,只是多了一个 Back Buffer。

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

SurfaceFlinger 是图像数据的消费者。在应用程序请求创建 Surface 的时候,SurfaceFlinger 会创建一个 Layer。Layer 是 SurfaceFlinger 操作合成的基本单元。所以,一个 Surface 对应一个 Layer。当应用程序把绘制好的 Graphic Buffer 数据放入 BufferQueue 后,接下来的工作就是 SurfaceFlinger 来完成了。

SurfaceFlinger 的作用主要是接收 Graphic Buffer,然后交给 HWComposer 或者 OpenGL 做合成,合成完成后,SurfaceFlinger 会把最终的数据提交给 FrameBuffer。

你可能感兴趣的:(android,python,opencv)