从0到1搞懂:Android 渲染原理

1. 背景

本文旨在帮助同学们理解 Android app 是如何将内容显示到屏幕上的。从而在未来遇到相关问题时,能够减少学习成本。

在讲解具体的显示原理之前,需要先介绍一些前置内容,掌握这些内容有利于我们后续的理解。

2. 前置知识

2.1 系统启动流程

了解 Android 启动流程对理解本文主要内容有较大的帮助,所以我们结合下图,挑一些启动流程的重点进行介绍:

  1. init 进程

    在 Android 开机到 Linux 内核启动后,第一件事情就是启动 init 进程。它是第一个用户空间进程,进程号为 1。它有很多重要的职责,其中下文提到的 Zygote 与 SurfaceFlinger 进程就是由它启动的。

  2. SurfaceFlinger 进程

    由 init 进程启动,职责包括 "请求/监听/分发 vsync 信号"、"合成与送显"。下文会详细介绍。

  3. Zygote 进程

    由 init 进程启动,应用程序进程与 SystemServer 进程由它通过 fork 进行创建。同时,它创建了虚拟机,所以由它 fork 而来的应用程序进程和 SystemServer 可以在内部获取一个虚拟机副本。

  4. WindowManagerService 系统 服务

    由 SystemServer 进程创建的系统服务,职责包括:

    • 窗口管理:例如窗口的大小、层级。
    • Surface管理:Surface 是 App 获取渲染缓冲区的媒介,下文会详细介绍。
  5. Launcer 进程(桌面)

    由 AMS 启动,并通过 Zygote fork 创建进程。它的职责包括 "显示系统已经安装的应用程序"、"启动其他应用程序进程"。

2.2 应用程序启动流程

在我们点击 Launcher(桌面)的 App 图标后,便开始了 App 的启动流程。流程图如下:

其中,在应用程序进程启动后,会走到 ActivityThread::main 方法,内部开启了主线程的消息循环

java 复制代码
// frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
    // ...
    //创建主线程的Looper以及MessageQueue
    Looper.prepareMainLooper();
    // ...
    //开启主线程的消息循环
    Looper.loop();
}

在 AMS 通知启动根 Activity 后,便开始初始化根 Activity。流程图如下:

  • ApplicationThread 用于 App 与 AMS 间的 Binder 通信。AMS 通知创建根 Activity 后,ApplicationThread 会发送消息给主线程创建根 Activity。
  • 之后会调用一系列生命周期相关的方法。其中在 performCreate 时初始化了 DecorView(根View);在 handleResumeActivity 时初始化了 ViewRootImpl,它的作用下文会详细介绍。

2.3 Android 屏幕刷新机制

显示器(液晶/OLED屏)在刷新时,会逐行扫描帧,更新像素,也就是 "从上到下,从左到右"

这里有几个概念需要介绍:

  • 一幅画面。由多个像素点组成,每一个像素点都保存了像素值,比如RGB色值。
  • 例如,屏幕的分辨率是1920 x 1080,那么一帧就是1920 x 1080个像素点元素的数组。
  1. FrameBuffer
  • 帧缓冲区。上层绘制完的帧会放到 FrameBuffer 中,显示器从 FrameBuffer 中读取帧刷新。
  1. 屏幕刷新率
  • 一秒钟内能展示多少幅完整画面,单位为 hz。
  • 例如,60hz 就是1秒钟能刷新60次。
  1. 帧率
  • 一秒钟内生成帧的速度,单位为 fps。
  • 例如,100fps 就是1秒钟能生成100帧。
  1. Jank

卡顿,即连续展示同一帧。

在理想情况下,屏幕刷新率与帧率一致,那么我们可以看见流畅的画面。但如果 屏幕刷新率与帧率不一致 ,就可能造成 画面撕裂。如下图所示:

本质原因是:显示器在读取 FrameBuffer 刷新的过程中,FrameBuffer 的数据被更新了,导致 同一时刻展示了两帧的内容

为了解决画面撕裂,Android 引入了 双缓冲 机制:

  • 显示器从 前缓冲区 读取帧。
  • 上层将绘制好的帧写入 后缓冲区

那何时交换缓冲区呢?

当显示器扫描完一屏之后,在进入下一轮扫描之前有一个 空隙 ,这段空隙时间叫做 VBI (Vertical Blanking Interval)。在 VBI 期间,正好是 交换缓冲区 的最佳时间。

除了画面撕裂,早期的 Android 手机还很容易出现 Jank。如下图:

上游并不知道何时开始绘制数据,导致显示器连续展示了相同的帧。

  • CPU/GPU 在第一个 VBI 到来前渲染完了第 [1] 帧。
  • 在第一个 VBI 期间,缓冲区交换,显示器开始绘制第 [1] 帧。
  • 但由于没有一个机制约束什么时候开始渲染,CPU/GPU 并没有立即开始渲染工作,而是等到第二个 VBI 快到时才开始渲染,导致显示器连续显示第 [1] 帧,产生了 Jank。

在 Android 4.1 以后引入了 VSync 机制,如下图:

在 VBI 期间交换缓冲区,并通知上游开始产生下一帧,很大程度地缓解了 Jank。

与此同时,也发现了 双缓冲的弊端,如下图:

  • 系统只有两个缓冲区,BufferA、BufferB。
  • 一开始显示器扫描 BufferA 更新像素,CPU/GPU 准备 BufferB。
  • 但由于 GPU 准备时间较长,无法及时释放 BufferA。
  • 导致下一个 vsync 信号来时,CPU 无法获取 BufferA 进行渲染。最终导致连续两次 Jank。

为了进一步优化体验,Android 引入了 三缓冲 机制,提高了 CPU/GPU 的利用率,如下图:

第 1 个 Vsync 信号到来时,BufferA 在展示,BufferB 还未释放,CPU 可以使用 BufferC 准备帧数据,提高了利用率,可见比双缓冲时减少了一次 Jank。

在 Android 中,FrameBuffer 与 BufferQueue 一起组成了多缓冲结构。其中 BufferQueue 在下文会介绍。

2.4 GPU 简述

在 2.3 小节当中,我们看到帧的绘制是由 CPU 与 GPU 一起完成的。CPU 不是负责计算的吗,为什么要引入 GPU 呢?

我们对比 CPU 与 GPU 的结构图,可看出:

  1. CPU 拥有 更多缓存空间复杂控制单元 ,具备 更快访问数据更快处理逻辑分支 的特点。
  2. GPU 拥有 更多计算单元 ,具备 更强的计算能力

所以,使用 GPU 渲染图形的根本原因就是:速度。因为渲染的过程不涉及复杂的逻辑判断。

GPU 渲染过程

其中 Application 阶段由 CPU 负责,其他阶段由 GPU 负责。

  1. Application(应用处理阶段)

主要是 准备图像信息(图元) ,例如三角形、矩形及其颜色等。图元中的三角形、线段、点分别对应三个 Vertex、两个 Vertex、一个 Vertex,用于描述形状与位置。例如当调用 Canvas::drawLine 时,相应的图元就包含两个 Vertex。

  1. Geometry(几何处理阶段)
  • 顶点着色器(Vertex Shader):这个阶段中会将图元中的顶点信息进行视角转换、添加光照信息、增加纹理等操作。
  • 形状装配(Shape Assembly):这个阶段会 将 Vertex 连接成相对应的形状
  • 几何着色器(Geometry Shader):额外添加额外的Vertex,将原始图元 转换成新图元 ,以构建一个不一样的模型。简单来说就是基于通过三角形、线段和点 构建更复杂的几何图形
  1. Rasterization(光栅化阶段)

计算出 每个图元所覆盖的像素信息 等,从而将像素划分成不同的部分。

  1. Pixel (像素处理阶段)
  • 片段着色器(Fragment Shader):这个阶段的目的是 给每一个像素 Pixel 赋予颜色
  • 测试与混合(Tests and Blending):将所有图元进行 混合

经过上述处理,便得到了一个 Bitmap ,里面包含了经过处理、蕴含大量信息的 像素点集合

GPU 的接口

近年来,GPU 增加了处理器指令和存储器硬件,从而允许对 GPU 进行编程。

Android 中常用的图形绘制库为 OpenGL、Skia、Vulkan 等。

2.5 HWC 简述(硬件抽象层)

物理构成包括:DPU 或 2D渲染芯片。

职责:合成送显发送 vsync 信号

什么是合成?

如下图,当 Android 手机显示一个应用时,通常有三层:顶部状态栏、App、底部状态栏。它们在绘制时各自利用 GPU 生成自身的图像信息(Bitmap)。随后,由 HWC/GPU 进行合成。

为什么不使用 GPU 合成?

官方文档 对 HWC 的描述包括:

  1. 配合使用 HWC 合成会显著提高合成效率。

  2. 在合成时,将所有图层交由 HWC,HWC 会判断哪些图层使用 GPU 合成,哪些使用硬件合成,从而达到最高效。

3. Android 显示整体流程

3.1 官方描述

官方文档 对渲染流程的描述如下:

  • 在 Android 平台上创建的 每个窗口 都由 Surface 提供支持。
  • 无论开发者使用什么渲染 API,一切内容都会渲染到 surface 上。
  • Surface 表示图像的生产方,SurfaceFlinger 表示消费方,利用 WMS 提供的信息将可见的 Surface 合成到屏幕上。
  • 相应的生产者与消费者模型如下。

3.2 解读

官方的描述比较难以理解,我们结合下图更具体地了解一下:

  1. 当显示器处于 VBI 期间,HWC 模块发送 vsync 信号给 SF,SF 将信号转发给 App。
  2. 每个 App 持有至少一个 Surface,在收到信号后,利用 Surface 向 BufferQueue 请求 Buffer ,然后利用 GPU 进行绘制。绘制完成后,将 Buffer 放回 BufferQueue 中等待合成。
  3. SF 在收到 vsync 信号后隔一段时间,待 Apps 都绘制完成后,向 HWC 询问每个 Layer 的合成情况,获取 Layer 中要合成的 Buffer,然后利用 GPU/HWC 进行合成。最后将 需要HWC合成的Buffer 与 部分GPU合成的Buffer 一起打包给 HWC。
  4. HWC 将所有图层合成并在下一次 vsync 时更新 FrameBuffer。
  5. 显示器 在执行完本次刷新并过了一个 VBI 后,开始下一次刷新,将 FrameBuffer 的内容显示到屏幕上。

注:本文一个 App 默认当作仅创建一个窗口。

4. SurfaceFlinger 工作原理

上文提到,SF 的职责包括:

  1. 监听 HWC 发送的 vsync 信号并分发给 app。
  2. 合成所有图层。

我们来依次了解下相关的代码。

4.1 监听 HWC vsync 信号

  • 在 2.1 中,介绍到 init 进程启动了 SF 进程。
  • SF 初始化时,会向 HWC 注册一个 vsync 回调。
cpp 复制代码
// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
void SurfaceFlinger::init() {
    getBE().mHwc->registerCallback(this, ...); // 注:SF 实现了 HWC2::ComposerCallback 接口
}
  • 当 HWC 产生 Vsync 信号时,会回调 SF 的 onVsyncReceived 方法。
cpp 复制代码
// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
void SurfaceFlinger::onVsyncReceived(..., int64_t timestamp) {
    // 将信号分发给app和SF
    mPrimaryDispSync.addResyncSample(timestamp);
}

4.2 分发 vsync 信号

  • 在监听到 vsync 信号后,SF 需要将信号分发给 app,让 app 开始渲染

  • 在 app 渲染完成后,SF 将所有图层进行合成与送显

  • 我们先来猜测一下具体的流程:

    • 在第 0 个 vsync 信号到达时,SF 通知各 app 进行绘制
    • 在第 1 个 vsync 信号到达时,SF 进行合成
    • 在第 3 个 vsync 信号到达时,显示框架才更新画面
  • 上述流程,展示一幅画面至少要 2 帧。对此,SF 通过 控制偏移量 的手段,让渲染与合成在一个周期内完成。
  • 如下图所示,SurfaceFlinger 在接收到 VSYNC 信号后,并不会在下一个 vsync 信号到来时才开始合成,而是 等待一段时间 便进行合成,此时各图层均已渲染完成。
复制代码
如果 app 渲染时间比较长,会不会错过合成呢?

答:通常 app 在渲染完后会通过 SF 向 HWC 请求下一次 vsync,以此保证渲染内容就算跟不上这一帧,也会再下一帧被显示。
  • 有了上述基础后,我们下面看一下 SF 中分发过程具体的代码设计。
  • 上文介绍到,HWC vsync 信号到来后,SF 会调用 DispSync.addResyncSample 方法将信号同步给 app 与 SF。
cpp 复制代码
// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
    void SurfaceFlinger::onVsyncReceived(..., int64_t timestamp) {
        // ...
        needsHwVsync = mPrimaryDispSync.addResyncSample(timestamp);
        // ...
    }
  • DispSync.addResyncSample 会唤醒 DispSyncThread 线程。该线程在 DispSync 初始化时启动,主要负责:

    • 收集响应该信号的监听者(gatherCallbackInvocationsLocked)。因为 app、SF 都有偏移量,并不是立即响应信号的。
    • 回调监听者注册的回调方法(fireCallbackInvocations)
  • 下面我们结合代码,来了解具体的执行顺序:

    • 当 vsync 信号到来时,线程在 // 1 处被唤醒
    • 线程执行 // 3 处,由于偏移量的存在,第一次没有合适监听者
    • 线程继续执行到 // 4 处,取得 app 要监听的时刻(vsync发送时间 + app偏移量),然后在 // 2 处继续阻塞等待
    • 等待了一段时间后,线程恢复执行,执行到 // 3 处,找到满足条件的监听者,也就是 app 设置的监听器,执行回调通知app进行渲染
    • 随后又回到 // 2 处被阻塞
    • 同上,等到 SF 要监听的时刻(vsync发送时间 + SF偏移量),通知 SF 进行合成
cpp 复制代码
// frameworks/native/services/surfaceflinger/Scheduler/DispSync.cpp
class DispSyncThread : public Thread {
public:
    virtual bool threadLoop() {
        while (true) {
            targetTime = computeNextEventTimeLocked(now);  // 4

            if (now < targetTime) {
                if (targetTime == INT64_MAX) {
                    mCond.wait(mMutex);  // 1
                } else {
                    mCond.waitRelative(mMutex, targetTime - now);  // 2
                }
            }

            now = systemTime(SYSTEM_TIME_MONOTONIC);
            callbackInvocations = gatherCallbackInvocationsLocked(now); // 3
            if (callbackInvocations.size() > 0) {
                fireCallbackInvocations(callbackInvocations);
            }
        }
    }

private:
    Vector<CallbackInvocation> gatherCallbackInvocationsLocked(nsecs_t now) {
        Vector<CallbackInvocation> callbackInvocations;
        // 根据偏移量,计算已经到时的所有Listener
        for (size_t i = 0; i < mEventListeners.size(); i++) {
            nsecs_t t = computeListenerNextEventTimeLocked(mEventListeners[i], ...);

            if (t < now) {
                CallbackInvocation ci;
                ci.mCallback = mEventListeners[i].mCallback;
                ci.mEventTime = t;
                callbackInvocations.push(ci);
                mEventListeners.editItemAt(i).mLastEventTime = t;
            }
        }

        return callbackInvocations;
    }

    void fireCallbackInvocations(const Vector<CallbackInvocation>& callbacks) {
        // 调用监听者的回调
        for (size_t i = 0; i < callbacks.size(); i++) {
            callbacks[i].mCallback->onDispSyncEvent(callbacks[i].mEventTime);
        }
    }
}

4.3 vsync 信号的监听者

  • 上一节提到,vsync 信号到来后,会由 DispSyncThread 线程分发给监听者。下面我们看一下是在哪里注册监听者的。
  • 监听者需要实现 DispSync.Callback 类,DispSyncSource 继承了这个类,并支持设置 偏移量
  • 在 SF 的初始化中,为 app 和 SF 都创建了一个 DispSyncSource 对象,并设置了相应的偏移量 mPhaseOffset。其中 app 监听器的偏移量为 0,意味着 vsync 到来后立刻通知 app 进行渲染。
cpp 复制代码
// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
void SurfaceFlinger::init() {
    // app的监听器(偏移量为0)
    mEventThreadSource = std::make_unique<DispSyncSource>(&mPrimaryDispSync, SurfaceFlinger::vsyncPhaseOffsetNs, true, "app");
    // sf的监听器
    mSfEventThreadSource = std::make_unique<DispSyncSource>(&mPrimaryDispSync, SurfaceFlinger::sfVsyncPhaseOffsetNs, true, "sf");
}
  • 同时创建了两个线程来管理监听器的变化
cpp 复制代码
// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
void SurfaceFlinger::init() {
    // app
    mEventThread = std::make_unique<impl::EventThread>(mEventThreadSource.get(), [this]() { resyncWithRateLimit(); },
            impl::EventThread::InterceptVSyncsCallback(), "appEventThread");
    // sf
    mSFEventThread = std::make_unique<impl::EventThread>(mSfEventThreadSource.get(), [this]() { resyncWithRateLimit(); },
            [this](nsecs_t timestamp) { mInterceptor->saveVSyncEvent(timestamp); }, "sfEventThread");
}
  • EventThread 的作用:支持外部注册多个 Connection 来监听 vsync。因为可能不止一个 app 要监听 vsync 信号刷新,所以需要同时支持多个连接。

  • 我们来看看 EventThread 具体实现:

    • 线程 // 1 处在阻塞等待唤醒。
    • DispSyncThread 分发 vsync 信号给监听者(4.2节代码块)后,EventThread 在 // 2 处被唤醒。
    • 随后在 // 3 处通知注册的 Connection(各App与SF)。
cpp 复制代码
// frameworks/native/services/surfaceflinger/EventThread.cpp
void EventThread::threadMain() NO_THREAD_SAFETY_ANALYSIS {
    while (mKeepRunning) {
        signalConnections = waitForEventLocked(&lock, &event); // 1 阻塞等待连接与vsync

        const size_t count = signalConnections.size();
        for (size_t i = 0; i < count; i++) {
            const sp<Connection>& conn(signalConnections[i]); 
            conn->postEvent(event); // 3 通知连接
        }
    }
}

void EventThread::onVSyncEvent(nsecs_t timestamp) {
    mCondition.notify_all(); // 2 唤醒
}

status_t EventThread::Connection::postEvent(const DisplayEventReceiver::Event& event) {
    ssize_t size = DisplayEventReceiver::sendEvents(&mChannel, &event, 1);
    return size < 0 ? status_t(size) : status_t(NO_ERROR);
}
  • 完整分发与监听流程如下:
  • 从中也可以看出,如果 app 想要监听 vsync,需要向 EventThread(app) 注册 Connection。

4.4 合成与送显

SF 在初始时会向 EventThread(sf) 注册 Connection,从而监听 vsync 信号进行合成。本节我们看下合成相关的内容。

3.2 节描述到,SurfaceFlinger 首先委托 GPU 合成部分图层,随后委托 HWC 合成所有图层并送显。

那如何判断各图层 使用 GPU合成 还是 硬件合成? 官方文档描述道,不同的场景不同的组合都会影响合成效率,SF 在合成前会将各图层交由 HWC 判断用哪种方式进行合成。

我们来看看相关简化后的代码:EventThread(SF) 发送 vsync 信号后,最终会执行到 handleMessageRefresh 方法进行合成。具体可回顾 3.2 节流程图。

cpp 复制代码
// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
void SurfaceFlinger::handleMessageRefresh() {
    // 判断哪些图层使用 GPU合成、哪些使用 硬件合成
    setUpHWComposer();
    // 使用GPU合成、送显
    doComposition();
}

void SurfaceFlinger::setUpHWComposer() {
    // ...
    for (size_t i = 0; i < currentLayers.size(); i++) {
        const auto& layer = currentLayers[i];
        // 请求HWC判断合成方式
        if (!layer->createHwcLayer(getBE().mHwc.get(), hwcId)) {
            // 标记该图层使用GPU合成
            layer->forceClientComposition(hwcId);
            continue;
        }
    }
}

void SurfaceFlinger::doComposition() {
    // 使用GPU合成部分Layer
    doDisplayComposition(hw, dirtyRegion);
    // 委托HWC合成所有Layer,并送显
    postFramebuffer();
}

4.5 小结

  1. SurfaceFlinger 监听并分发 vsync 信号,app 收到信号后执行渲染 ,一段时间的 偏移SF 开始合成
  2. SF 开启了两个 EventThread 线程,用于向 渲染Connection/合成Connection 分发 vsync。 App 如果想要监听 Vsync 信号,需要向 SF 的 EventThread(app) 注册 Connection
  3. SurfaceFlinger 会委托 HWC 判断图层的合成方式(硬件/GPU)。部分图层利用 GPU 合成后,将所有图层都交由 HWC 进行合成与送显。

5. App 绘制原理

上一节介绍了 SF 的工作流程,本节介绍一下 App 在渲染时的工作流程。主要涉及三个部分:

  1. App 如何监听 SF 传递的信号?
  2. App 如何向 SF 传输绘制数据?
  3. App 绘制的过程。

5.1 监听 vsync 信号

  • 在 2.2 节中提到 app 启动后会走到 Activity::handleResumeActivity 方法。追踪该方法的调用链,创建了 FrameDisplayEventReceiver 对象,它是接收 vsync 信号的核心。
cpp 复制代码
// ActivityThread.java
public void handleResumeActivity(...) {
    // ...
    wm.addView(decor, l);
}

// WindowManagerGlobal.java
public void addView(View view, ...) {
    // ...
    root = new ViewRootImpl(...);
    // ...
    // root.setView(view , wparams , panelParentView , userId) ;
}

// ViewRootImpl.java
public ViewRootImpl(@UiContext Context context , Display display , IWindowSession session , WindowLayout windowLayout) {
    mChoreographer = Choreographer.getInstance() ;
}

// Choreographer.java
private Choreographer(Looper looper , int vsyncSource , long layerHandle) {
    // ...
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper , vsyncSource , layerHandle)
            : null ;
}
  • FrameDisplayEventReceiver 类比较复杂,用下面的类图来描述其结构。
  • 它对外暴露两个接口方法:scheduleVsync(...) 与 onVsync(...),分别用于 订阅响应 vsync。
  • 在 native 层的 DisplayEventReceiver 对象初始化时,建立了与 SF 的连接,并设置了 vsync 回调。也就是向 SF::mEventThread(app) 创建一个 Connection。符合 4.3 节流程图的描述。
cpp 复制代码
// frameworks/native/libs/gui/DisplayEventReceiver.cpp
DisplayEventReceiver::DisplayEventReceiver(ISurfaceComposer::VsyncSource vsyncSource) {
    // ...
    // 创建一个监听vsync的连接
    mEventConnection = sf->createDisplayEventConnection(vsyncSource);
}

// frameworks/native/services/surfaceflinger/Surfaceflinger.cpp
sp<IDisplayEventConnection> SurfaceFlinger::createDisplayEventConnection(...) {
    // ...
    // 此处为 EventThread(app)
    return mEventThread->createEventConnection();
}

// frameworks/native/libs/gui/DisplayEventReceiver.cpp
status_t DisplayEventDispatcher::initialize() {
    // ...
    // 设置回调
    int rc = mLooper->addFd(mReceiver.getFd(), 0, Looper::EVENT_INPUT, this, NULL);
}
  • 在上游调 View::requestLayout/invalidate 后:

    • 会调用至 FrameDisplayEventReceiver::scheduleVsync 订阅下一帧的监听。
    • scheduleVsync 方法内部会一路调用至 SF 中的 EventThread::requestNextVsync,恢复连接状态尝试唤醒 SF 中的 EventThread。
    • 当下一帧到来时,EventThread 被唤醒并分发 vsync 给 app。详见 4.3 小节。
cpp 复制代码
// frameworks/native/services/surfaceflinger/EventThread.cpp
void EventThread::requestNextVsync(const sp<EventThread::Connection>& connection) {
    // ...
    if (connection->count < 0) {
        // 1.将连接状态调整为 "活跃"
        connection->count = 0;
        // 2.唤醒EventThread线程
        mCondition.notify_all();
    }
}
  • vsync 到来 时,会一路回调至 FrameDisplayEventReceiver::onVsync,通知 App 开始测绘。 回调路径:NativeDisplayEventReceiver::dispatchVsync -> DisplayEventDispatcher::handleEvent -> DisplayEventReceiver::onVsync

5.2 与 SF 间的数据传输

从 3.2 节流程图可以看出,App 通过 SurfaceBufferQueue 中获取 GraphicBuffer,绘制完后将 GraphicBuffer 放回 BufferQueue。随后,SF 利用 Layer 从 BufferQueue 中获取绘制好的 Buffer 进行合成。

可见,Surface、BufferQueue、Layer 是 App 与 SF 之间的 数据传输纽带 。所以,下面主要探讨它们的 创建流程与联系

  • ViewRootImpl 持有一个 Surface.java 对象。
java 复制代码
// ViewRootImpl.java
public final Surface mSurface = new Surface() ;

public Surface() {}
  • Surface.java 的构造函数没有做任何事情。那它是怎么与 SF 建立联系的呢?秘密藏在成员变量 mNativeObject 上。
java 复制代码
public class Surface implements Parcelable {
    long mNativeObject ;
}
  • 该成员变量的创建有些复杂,我们结合下图说明。

    • ActivityThread::handleResumeActivity 一路会调用到 ViewRootImpl::setView 方法,方法内会跨进程调用 WMS::addWindow 来添加窗口。
    • WMS 会为该窗口创建一个 Session(会话) ,用于 后续向 SF 请求创建 Surface
    • 后续 App 执行 ViewRootImpl::performTraversals 测绘时,会走到 ViewRootImpl::relayoutWindow,进而调用到 WMS::relayoutWindow 方法。
    • WMS::relayoutWindow 方法会创建 SurfaceControl 对象,其内部利用 SurfaceComposerClient 向 SF 请求创建 生产者-消费者模型。
    • SF 收到消息后,创建 Layer、Producer、Consumer、BufferQueue 对象。
    • WMS::relayoutWindow 方法后续创建一个 Surface.cpp 对象,内部持有 Producer 的引用。将这个 Surface.cpp 对象绑定到 Surface.java 的 mNativeObject 引用上。
    • 至此,Surface、BufferQueue、Layer 三者便联系了起来,App 与 SF 之间可以进行数据传输了。

5.3 绘制流程

上文讲到,在调用 View::requestLayout/invalidate 后,会向 SF 注册下一帧的监听。

当下一帧到来时,会走到 FrameDisplayEventReceiver::onVsync,内部会向主线程抛一个 测绘任务

java 复制代码
// Choreographer的内部类FrameDisplayEventReceiver.java
@Override
public void onVsync(long timestampNanos , long physicalDisplayId , int frame , VsyncEventData vsyncEventData) {
    // ...
    Message msg = Message.obtain(mHandler , this) ;
    msg.setAsynchronous(true) ;
    // 向主线程抛测绘任务
    mHandler.sendMessageAtTime(msg , timestampNanos / TimeUtils.NANOS_PER_MS) ;
}

接下来我们跟踪后续的流程,看看 App 是如何进行绘制的。

  • 首先,主线程会执行到 ViewRootImpl::doTraversal 进行测绘。
java 复制代码
// ViewRootImpl.java
void doTraversal() {
    if (mTraversalScheduled) {
        // ...
        performTraversals() ;
    }
}
  • performTraversals 内部会依次调用 measure、layout、draw。
java 复制代码
// ViewRootImpl.java
private void performTraversals() {
    // ...
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // ...
    performLayout(lp, mWidth, mHeight);
    // ...
    performDraw();
}
  • 我们重点关注 performDraw 方法,其伪代码如下。
java 复制代码
// ViewRootImpl.java
private boolean performDraw() {
    // ...
    boolean canUseAsync = draw(fullRedrawNeeded , usingAsyncReport && mSyncBuffer) ;
}

private boolean draw(boolean fullRedrawNeeded , boolean forceDraw) {
    // ...
    if (isHardwareEnabled()) {
        // 硬件绘制
        mAttachInfo.mThreadedRenderer.draw(mView , mAttachInfo , this) ;
    } else {
        // 软件绘制
        drawSoftware(...)
    }
}
  • 我们发现,绘制流程分为 硬件绘制软件绘制。系统默认开启硬件绘制,可以通过调整 AndroidManifest.xml 中的属性来调整绘制方式。
xml 复制代码
android.R.attr #hardwareAccelerated

接下来我们依次看看硬件绘制与软件绘制的具体过程。

5.3.1 软件绘制

5.3.1.1 原理

软件绘制的流程比较简单,就是:

  1. 通过 Surface 获取一个绘制缓冲区,为其绑定一个 Bitmap。
  2. 利用 OpenGL/Skia/Vulkan 框架提供的能力,在主线程绘制 Bitmap。
  3. 向 SF 提交缓冲区进行合成。

代码入口:

java 复制代码
// ViewRootImpl.java
private boolean drawSoftware(...) {
    // ...
    // 1.获取绘制缓冲区,为其绑定一个Bitmap
    canvas = mSurface.lockCanvas(dirty) ;

    try {
        // 2.绘制Bitmap
        mView.draw(canvas) ;
    } finally {
        // 3.向SF提交缓冲区
        surface.unlockCanvasAndPost(canvas) ;
    }
}
5.3.1.2 获取绘制缓冲区,绑定Bitmap
cpp 复制代码
// frameworks/base/core/jni/android_view_Surface.cpp
static jlong nativeLockCanvas(jobject canvasObj, ...) {
    // ...

    // 1.内部调用获取GraphicBuffer
    ANativeWindow_Buffer buffer;
    status_t err = surface->lock(&buffer, dirtyRectPtr);

    // 2.创建Bitmap
    SkBitmap bitmap;
    ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
    bitmap.setInfo(info, bpr);

    // 3.绑定Bitmap与buffer
    bitmap.setPixels(outBuffer.bits);

    // 4.绑定Bitmap与canvas
    nativeCanvas->setBitmap(bitmap);
}

// frameworks/native/libs/gui/Surface.cpp
status_t Surface::lock(...) {
    // ...
    // 获取绘制缓冲区
    ANativeWindowBuffer* out;
    status_t err = dequeueBuffer(&out, &fenceFd);
    sp<GraphicBuffer> backBuffer(GraphicBuffer::getSelf(out));
    // ...
    // 保存到引用 mLockedBuffer
    mLockedBuffer = backBuffer;
}
5.3.1.3 绘制Bitmap

例如,View 中调用了 Canvas::drawRect 方法,最终会调用到 SkBitmapDevice::drawRect 方法,在 Bitmap 上绘制一个矩形。

cpp 复制代码
// external/skia/src/core/SkBitmapDevice.cpp
void SkBitmapDevice::drawRect(const SkRect& r, const SkPaint& paint) {
    BDDraw(this).drawRect(r, paint);
}

注:SkBitmapDevice 是 Skia 提供给软件绘制使用的类,它是在当前线程进行 同步绘制 ; 硬件绘制则使用 SkGpuDevice ,会发射绘制指令给 GPU 进行 异步绘制

5.3.1.4 提交缓冲区

最终会走到 Surface::unlockAndPost 方法,向 SF 提交绘制缓冲区。由 SF 进行后续的合成。

cpp 复制代码
// frameworks/native/libs/gui/Surface.cpp
status_t Surface::unlockAndPost(){
    // ...
    err = queueBuffer(mLockedBuffer.get(), fd);
}

通过上述代码,也证实了:

Surface 是绘制缓冲区的 生产者 ,暴露了 queue/dequeue 两个接口,窗口可以利用它获取绘制缓冲区进行绘制。

5.3.2 硬件绘制

5.3.2.1 原理
  • 硬件绘制开始后,UI 线程递归 所有 View,将 绘制操作缓存 下来,然后统一给单独的 Render 线程 利用 GPU 渲染。
  • 例如,调用一次 drawPoint (...) 方法会被抽象成一个 DrawPointsOp子 View 的绘制被抽象成一个 DrawRenderNodeOp
  • 递归完后,就得到一棵 多叉树,每个节点保存着自身及子 View 的所有绘制操作。
  • 随后 UI 线程 将操作树交给 RenderThread 进行绘制:

    • 首先 RenderThreadSF 申请 Buffer
    • 随后 RenderThread 将操作树 转化为 GPU 相关指令发射给 GPU
    • GPU 异步绘制到 Buffer 的图像中。
    • RenderThread 并不会等待 GPU 绘制完成,在发射完指令后便直接提交 Buffer ,通知 SF 进行合成,然后便结束去执行别的任务了。

GPU 还没绘制完,就通知 SF 合成,会不会有问题?

答:内部借助 Fence 同步机制,保证了 SF 不会合成为渲染完的图层。

5.3.2.2 UI线程 与 RenderThread 之间的通信方式

从上一节中可以看出,UI线程 与 RenderThread 间有频繁的联系,先来看看它们之间是如何通信的。

我们看 ViewRootImpl 的类图,有几个需要关注的地方:

  1. ViewRootImpl 间接持有了 mRootNode。

    • 这是 DrawOp树根节点
    • 在 UI线程 构建完 DrawOp树 后,会将根节点赋值给 mRootNode。
  2. RenderProxy 的是 UI线程 与 RenderThread 通信的代理对象

    • 初始化 RenderProxy 时,会尝试 开启 RenderThread 、并 将 mRootNode 传给 RenderThread
    cpp 复制代码
    // frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
    RenderProxy::RenderProxy(RenderNode* rootRenderNode, ...): 
       mRenderThread(RenderThread::getInstance() ), ...
    {
       mContext = mRenderThread.queue().runSync([&]() -> CanvasContext* {
           return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
       });
       // ...
    }
    cpp 复制代码
    // frameworks\base\libs\hwui\renderthread\RenderThread.cpp
    RenderThread& RenderThread::getInstance() {
       //...
       thread->start("RenderThread");
    }
    • 在测绘前,会通过 RenderProxy 向 RenderThread 传递 Surface。这样 RenderThread 便可以向 SF 申请与提交 Buffer,从而进行渲染。
    java 复制代码
    // ViewRootImpl.java
    public final class ViewRootImpl {
        private void performTraversals() {
            // ...
            mAttachInfo.mThreadedRenderer.initialize(mSurface) ;
        }
    }
    cpp 复制代码
    // initialize 会调用到该方法
    void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) {
        // 为RenderThread绑定Surface。因为app不止一个窗口。 
        mRenderThread.queue().post([this, win = window, enableTimeout]() mutable {
            mContext->setSurface(win, enableTimeout);           
        });
    }
  3. 在UI线程 构建完成 后,会通过 RenderProxy 通知 RenderThread,RenderThread 便利用 mRootNode 执行后续的绘制。

5.3.2.3 UI线程 构建 DrawOp 树

在建立起通信后,UI线程 便可以 构建 DrawOp 树给 RenderThread 绘制了。本小节我们看看 UI线程 是如何构建 DrawOp 树的。

  • 下面代码是硬件绘制的入口。
java 复制代码
// ViewRootImpl.java
public final class ViewRootImpl {
    private boolean draw(boolean fullRedrawNeeded , boolean forceDraw) {
        // ...
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (isHardwareEnabled()) {
                // 硬件绘制
                mAttachInfo.mThreadedRenderer.draw(mView , mAttachInfo , this) ;
            } else {
                // 软件绘制
                // ...
            }
        }
    }
}
  • 追踪方法,会走到 ThreadedRenderer.updateRootDisplayList(...) 方法。我们看看简化后的代码。
java 复制代码
// ThreadedRenderer.java
private void updateRootDisplayList(View view , ...) {
    RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth , mSurfaceHeight) ;
    try {
        // 1.从 DecorView 开始构建 DrawOp 树
        // 2.将 DrawOp 树交由 RecordingCanvas 做一些处理
    canvas.drawRenderNode(view.updateDisplayListIfDirty()) ;
    } finally {
        // 3.将 RecordingCanvas 处理后的 DrawOp 树赋值给 mRootNode
        mRootNode.endRecording() ;
    }
}
  • 再看看 View 中收集 DrawOp 的方法。注意到:

    • 每个 View 都有一个 RenderNode ,用于记录 自身及子View 的绘制操作。
    • 如果某个 子View 使用 软件绘制,UI线程 直接将该View与其子View绘制到一个 Bitmap 上,随后记录一个BitmapOp,后续由 RenderThread 统一合并绘制。
java 复制代码
// View.java
public class View {
    final RenderNode mRenderNode ;

    public RenderNode updateDisplayListIfDirty() {
        final RecordingCanvas canvas = renderNode.beginRecording(width , height) ;
        try {
            // ...
            if (layerType == LAYER_TYPE_SOFTWARE) {
                // 软件绘制(自身及子View)
                buildDrawingCache(true) ;
            Bitmap cache = getDrawingCache(true) ;
                canvas.drawBitmap(cache , 0, 0, mLayerPaint) ;
            } else {
                // 硬件绘制(自身及子View)
                draw(canvas) ;
            }
        } finally {
            // 构建完后,将子树保存到 mRenderNode
            renderNode.endRecording() ;
        }
    }
}
  • 在硬件绘制的 draw(...) 方法当中,View中调用的 drawXXX(...) 方法会被记录为一个个 DrawOp。

    • 例如,UI线程 在执行 drawRect (...) 时,并不会真正绘制,而是记录一个 RectOp 操作到 Canvas 内部当中。
    cpp 复制代码
    // frameworks/base/libs/hwui/RecordingCanvas.cpp
    void RecordingCanvas::drawRect(...) {
        // ...
        addOp(alloc().create_trivial<RectOp>(Rect(left, top, right, bottom), *(mState.currentSnapshot()->transform), getRecordedClip(),refPaint(&paint)));
    }
  • 在每个 View 构建完后,会调用 RenderNode.endRecording(...) 将 Canvas 中保存到 View 自身的成员变量 mRenderNode 中。

    java 复制代码
    // RenderNode.java
    public void endRecording() {
        // ...
        RecordingCanvas canvas = mCurrentRecordingCanvas ;
        canvas.finishRecording(this) ;
    }
  • 上述递归完成后,DecorView 的成员变量 mRootNode 便指向了一棵 DrawOp 树,里面存储着 自身及子View 的所有绘制操作。

  • 在 DecorView 构建完后,会将 DrawOp 树保存到 ThreadedRenderer.mRootNode 当中。

    java 复制代码
    // ThreadedRenderer.java
    private void updateRootDisplayList(View view , ...) {
        RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth , mSurfaceHeight) ;
        try {
            // 1.从 DecorView 开始构建 DrawOp 树
            // 2.将 DrawOp 树交由 RecordingCanvas 做一些处理
        canvas.drawRenderNode(view.updateDisplayListIfDirty()) ;
        } finally {
            // 3.将 RecordingCanvas 处理后的 DrawOp 树赋值给 mRootNode
            mRootNode.endRecording();
        }
    }
  • 上一节讲到,RenderThread 也持有 mRootNode 的引用,所以 RenderThread 此时已经有构建好的数据了。接下来我们看看如何通知 RenderThread 与渲染的。

5.3.2.4 通知 RenderThread
  • RenderThread 被开启后,便循环等待消息的到来。消息到来后,从队列中取出消息进行处理。

    cpp 复制代码
    // frameworks/base/libs/hwui/renderthread/RenderThread.cpp
    bool RenderThread::threadLoop() {
        // ...
        while (true) {
            waitForWork();
            processQueue();
        }
    }
  • UI线程 构建好 DrawOp 后,便会通知 RenderThread 进行渲染

    java 复制代码
    // frameworks\base\core\java\android\view\ThreadedRenderer.java
    void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
        // ...
        
        // 构建 DrawOp 树
        // ...
        
        // 通知 RenderThread
        int syncResult = syncAndDrawFrame(frameInfo);
    }
  • 追踪 syncAndDrawFrame,最终会调用到下述方法,向 RenderThread 的队列提交一个绘制任务。并阻塞等待 RenderThread 同步一些窗口信息。

    cpp 复制代码
    // frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp
    void DrawFrameTask::postAndWait() {
        // ...
        mRenderThread->queue().post([this]() { run(); });
        mSignal.wait(mLock);
    }
  • 信息同步完成后,UI线程 的所有工作已完成 ,剩下的 渲染、提交Buffer 的工作由 RenderThread 完成

5.3.2.5 RenderThread 进行渲染

RenderThread 在收到渲染任务后,会走到下述方法。

cpp 复制代码
// frameworks/base/libs/hwui/renderthread/CanvasContext.cpp
void CanvasContext::draw() {
    // ...
    // 1.绑定Surface、通过Surface获取Buffer
    Frame frame = mRenderPipeline->getFrame();
    // 2.遍历操作树,向GPU发射相应的绘制指令
    bool drew = mRenderPipeline->draw(mRenderNodes, ...);
    // 3.直接向SF提交Buffer,并不会等待GPU渲染完成
    bool didSwap = mRenderPipeline->swapBuffers(...);
}

其中对于注释2,会遍历每一个 DrawOp,都会调用到其 draw 方法,内部会发射对应的 GPU 指令,让 GPU 进行异步渲染。

例如,我们之前保存了一个绘制矩形的 DrawOp,RenderThread 在绘制时会相应地调用到 Skia 渲染框架的代码,向 GPU 发射绘制矩形的指令(也就是包含四个 Vertex 的图元)。

cpp 复制代码
// external/skqp/src/gpu/SkGpuDevice.cpp
void SkGpuDevice::drawRect(const SkRect& rect, const SkPaint& paint) {
    // 向 GPU 发射绘制矩形的指令
    fRenderTargetContext->drawRect(this->clip(), std::move(grPaint), GrAA(paint.isAntiAlias()),
                                   this->ctm(), rect, &style);
}

可以看出,无论是软件绘制还是硬件绘制,内部都是利用了 Skia 框架提供的能力。分别使用了 SkBitmapDevice、SkGpuDevice 这两个类。

5.4 小结

  1. 在执行 ActivityThread::handleResumeActivity 时,App 便具备了接收 vsync 的能力,该能力体现在 FrameDisplayEventReceiver 类中。

  2. 在执行 ActivityThread::handleResumeActivity 时,初始化了 ViewRootImpl::mSurface 对象,App 便具备获取绘制缓冲区的能力。

  3. App的绘制分为 软件绘制 与 硬件绘制。其中:

    • 软件绘制:生成一个 Bitmap,利用 OpenGL/Skia/Vulkan 提供的能力,在主线程将内容绘制到 Bitmap 上,随后通过 Surface 将 Buffer 提交给 SF 合成。

    • 硬件绘制:UI 线程会递归所有 View,将绘制操作缓存下来,然后统一给单独的 Render 线程利用 GPU 渲染。随后 RenderThread 将 Buffer 提交给 SF 合成。

6. 总结

至此,我们已经介绍完了下图中的每个环节,可以结合下图自己尝试着总结一下。如果有疑问,可以再看看 3.2 小节。

7. 参考资料

  1. Android 源码
  2. 官方文档
  3. 《Android 进阶解密》-刘望舒
  4. 计算机图形渲染原理
  5. 易保山图形系统系列
  6. Avengong 渲染系列
  7. Android 屏幕刷新机制
  8. 苍耳叔叔图形系统系列
  9. Android显示系统之---多缓冲和Vsync
  10. 学不会Android显示系统?那是因为你还没有看过这篇文章
  11. Android 重学系列 图元的合成(下)
  12. 从Activity创建到View呈现中间发生了什么?
  13. 深入Android系统(十二)Android图形显示系统-1-显示原理与Surface
  14. Android 源码 图形系统之硬件渲染器同步和绘制帧
  15. Android渲染(五)_view的绘制流程
  16. 理解Android硬件加速的小白文
  17. Android硬件加速(二)-RenderThread与OpenGL GPU渲染
  18. 【Graphics & SF】【硬件加速】2、DisplayList构建过程分析【Android 13】
  19. ...
相关推荐
该怎么办呢14 分钟前
原生android实现定位java实现
android·java·gitee
Android小码家30 分钟前
Live555+Windows+MSys2 编译Androidso库和运行使用(三,实战篇)
android·live555
Tsing72241 分钟前
Android vulkan示例
android
每次的天空2 小时前
高性能 Android 自定义 View:数据渲染与事件分发的双重优化
android
KdanMin2 小时前
Android 13组合键截屏功能的彻底移除实战
android
_祝你今天愉快2 小时前
安卓源码学习之【导航方式切换分析及实战】
android·源码
&有梦想的咸鱼&2 小时前
Android Compose 框架物理动画之弹簧动画(Spring、SpringSpec)深入剖析(二十七)
android·java·spring
Wgllss2 小时前
Android Compose轻松绘制地图可视化图表,带点击事件,可扩展二次开发
android·架构·android jetpack
SHUIPING_YANG3 小时前
MySQL 慢查询日志开启与问题排查指南
android·mysql·adb
tracyZhang4 小时前
NativeAllocationRegistry----通过绑定Java对象辅助回收native对象内存的机制
android