Android Systrace 基礎知識(9)-MainThread 和 RenderThread 解讀

本文是 Systrace 系列文章的第九篇,主要是是介紹 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主線程渲染線程。文章會從 Systrace 的角度來看 MainThread 和 RenderThread 的工作流程,以及涉及到的相關知識:卡頓、軟件渲染、掉幀計算等

本系列的目的是通過 Systrace 這個工具,從另外一個角度來看待 Android 系統整體的運行,同時也從另外一個角度來對 Framework 進行學習。也許你看了很多講 Framework 的文章,但是總是記不住代碼,或者不清楚其運行的流程,也許從 Systrace 這個圖形化的角度,你可以理解的更深入一些

系列文章目錄

  1. Systrace 簡介
  2. Systrace 基礎知識 - Systrace 預備知識
  3. Systrace 基礎知識 - Why 60 fps ?
  4. Systrace 基礎知識 - SystemServer 解讀
  5. Systrace 基礎知識 - SurfaceFlinger 解讀
  6. Systrace 基礎知識 - Input 解讀
  7. Systrace 基礎知識 - Vsync 解讀
  8. Systrace 基礎知識 - Vsync-App :基於 Choreographer 的渲染機制詳解
  9. Systrace 基礎知識 - MainThread 和 RenderThread 解讀
  10. Systrace 基礎知識 - Binder 和鎖競爭解讀
  11. Systrace 基礎知識 - Triple Buffer 解讀
  12. Systrace 基礎知識 - CPU Info 解讀

正文

這裏以滑動列表爲例 ,我們截取主線程和渲染線程一幀的工作流程(每一幀都會遵循這個流程,不過有的幀需要處理的事情多,有的幀需要處理的事情少) ,重點看 “UI Thread ” 和 RenderThread 這兩行

App 一幀的工作流
App 一幀的工作流

這張圖對應的工作流程如下

  1. 主線程處於 Sleep 狀態,等待 Vsync 信號
  2. Vsync 信號到來,主線程被喚醒,Choreographer 回調 FrameDisplayEventReceiver.onVsync 開始一幀的繪製
  3. 處理 App 這一幀的 Input 事件(如果有的話)
  4. 處理 App 這一幀的 Animation 事件(如果有的話)
  5. 處理 App 這一幀的 Traversal 事件(如果有的話)
  6. 主線程與渲染線程同步渲染數據,同步結束後,主線程結束一幀的繪製,可以繼續處理下一個 Message(如果有的話,IdleHandler 如果不爲空,這時候也會觸發處理),或者進入 Sleep 狀態等待下一個 Vsync
  7. 渲染線程首先需要從 BufferQueue 裏面取一個 Buffer(dequeueBuffer) , 進行數據處理之後,調用 OpenGL 相關的函數,真正地進行渲染操作,然後將這個渲染好的 Buffer 還給 BufferQueue (queueBuffer) , SurfaceFlinger 在 Vsync-SF 到了之後,將所有準備好的 Buffer 取出進行合成(這個流程在講 SurfaceFlinger 的時候會提到)

上面這個流程在 Android 基於 Choreographer 的渲染機制詳解 這篇文章裏面已經介紹的很詳細了,包括每一幀的 doFrame 都在做什麼、卡頓計算的原理、APM 相關. 沒有看過這篇文章的同學,建議先去掃一眼

那麼這篇文章我們主要從 Android 基於 Choreographer 的渲染機制詳解 這篇文章沒有講到的幾個點來入手,幫你更好地理解主線程和渲染線程

  1. 主線程的發展
  2. 主線程的創建
  3. 渲染線程的創建
  4. 主線程和渲染線程的分工
  5. 遊戲的主線程與渲染線程
  6. Flutter 的主線程和渲染線程

主線程的創建

Android App 的進程是基於 Linux 的,其管理也是基於 Linux 的進程管理機制,所以其創建也是調用了 fork 函數

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

pid_t pid = fork();

Fork 出來的進程,我們這裏可以把他看做主線程,但是這個線程還沒有和 Android 進行連接,所以無法處理 Android App 的 Message ;由於 Android App 線程運行基於消息機制 ,那麼這個 Fork 出來的主線程需要和 Android 的 Message 消息綁定,才能處理 Android App 的各種 Message

這裏就引入了 ActivityThread ,確切的說,ActivityThread 應該起名叫 ProcessThread 更貼切一些。ActivityThread 連接了 Fork 出來的進程和 App 的 Message ,他們的通力配合組成了我們熟知的 Android App 主線程。所以說 ActivityThread 其實並不是一個 Thread,而是他初始化了 Message 機制所需要的 MessageQueue、Looper、Handler ,而且其 Handler 負責處理大部分 Message 消息,所以我們習慣上覺得 ActivityThread 是主線程,其實他只是主線程的一個邏輯處理單元。

ActivityThread 的創建

App 進程 fork 出來之後,回到 App 進程,查找 ActivityThread 的 Main函數

com/android/internal/os/ZygoteInit.java

static final Runnable childZygoteInit(
        int targetSdkVersion, String[] argv, ClassLoader classLoader) {
    RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv);
    return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader);
}

這裏的 startClass 就是 ActivityThread,找到之後調用,邏輯就到了 ActivityThread的main函數

android/app/ActivityThread.java

public static void main(String[] args) {
    //1. 初始化 Looper、MessageQueue
    Looper.prepareMainLooper();
    // 2. 初始化 ActivityThread
    ActivityThread thread = new ActivityThread();
    // 3. 主要是調用 AMS.attachApplicationLocked,同步進程信息,做一些初始化工作
    thread.attach(false, startSeq);
    // 4. 獲取主線程的 Handler,這裏是 H ,基本上 App 的 Message 都會在這個 Handler 裏面進行處理 
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // 5. 初始化完成,Looper 開始工作
    Looper.loop();
}

註釋裏面都很清楚,這裏就不詳細說了,main 函數處理完成之後,主線程就算是正式上線開始工作,其 Systrace 流程如下:

主線程工作流
主線程工作流

ActivityThread 的功能

另外我們經常說的,Android 四大組件都是運行在主線程上的,其實這裏也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了

class H extends Handler { //摘抄了部分
    public static final int BIND_APPLICATION        = 110;
    public static final int EXIT_APPLICATION        = 111;
    public static final int RECEIVER                = 113;
    public static final int CREATE_SERVICE          = 114;
    public static final int STOP_SERVICE            = 116;
    public static final int BIND_SERVICE            = 121;
    public static final int UNBIND_SERVICE          = 122;
    public static final int DUMP_SERVICE            = 123;
    public static final int REMOVE_PROVIDER         = 131;
    public static final int DISPATCH_PACKAGE_BROADCAST = 133;
    public static final int DUMP_PROVIDER           = 141;
    public static final int UNSTABLE_PROVIDER_DIED  = 142;
    public static final int INSTALL_PROVIDER        = 145;
    public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
}

可以看到,進程創建、Activity 啓動、Service 的管理、Receiver 的管理、Provider 的管理這些都會在這裏處理,然後進到具體的 handleXXX

主線程的工作
主線程的工作

渲染線程的創建和發展

主線程講完了我們來講渲染線程,渲染線程也就是 RenderThread ,最初的 Android 版本里面是沒有渲染線程的,渲染工作都是在主線程完成,使用的也都是 CPU ,調用的是 libSkia 這個庫,RenderThread 是在 Android Lollipop 中新加入的組件,負責承擔一部分之前主線程的渲染工作,減輕主線程的負擔

軟件繪製

我們一般提到的硬件加速,指的就是 GPU 加速,這裏可以理解爲用 RenderThread 調用 GPU 來進行渲染加速 。 硬件加速在目前的 Android 中是默認開啓的, 所以如果我們什麼都不設置,那麼我們的進程默認都會有主線程和渲染線程(有可見的內容)。我們如果在 App 的 AndroidManifest 裏面,在 Application 標籤裏面加一個

android:hardwareAccelerated="false"

我們就可以關閉硬件加速,系統檢測到你這個 App 關閉了硬件加速,就不會初始化 RenderThread ,直接 cpu 調用 libSkia 來進行渲染。其 Systrace 的表現如下

純軟件渲染
純軟件渲染

與這篇文章開頭的開了硬件加速的那個圖對比,可以看到主線程由於要進行渲染工作,所以執行的時間變長了,也更容易出現卡頓,同時幀與幀直接的空閒間隔也變短了,使得其他 Message 的執行時間被壓縮

硬件加速繪製

正常情況下,硬件加速是開啓的,主線程的 draw 函數並沒有真正的執行 drawCall ,而是把要 draw 的內容記錄到 DIsplayList 裏面,同步到 RenderThread 中,一旦同步完成,主線程就可以被釋放出來做其他的事情,RenderThread 則繼續進行渲染工作

硬件加速渲染
硬件加速渲染

渲染線程初始化

渲染線程初始化在真正需要 draw 內容的時候,一般我們啓動一個 Activity ,在第一個 draw 執行的時候,會去檢測渲染線程是否初始化,如果沒有則去進行初始化

android/view/ViewRootImpl.java

mAttachInfo.mThreadedRenderer.initializeIfNeeded(
        mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);

後續直接調用 draw

android/graphics/HardwareRenderer.java

mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();
updateRootDisplayList(view, callbacks);

if (attachInfo.mPendingAnimatingRenderNodes != null) {
    final int count = attachInfo.mPendingAnimatingRenderNodes.size();
    for (int i = 0; i < count; i++) {
        registerAnimatingRenderNode(
                attachInfo.mPendingAnimatingRenderNodes.get(i));
    }
    attachInfo.mPendingAnimatingRenderNodes.clear();
    attachInfo.mPendingAnimatingRenderNodes = null;
}

int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
    setEnabled(false);
    attachInfo.mViewRootImpl.mSurface.release();
    attachInfo.mViewRootImpl.invalidate();
}
if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
    attachInfo.mViewRootImpl.invalidate();
}

}

上面的 draw 只是更新 DIsplayList ,更新結束後,調用 syncAndDrawFrame ,通知渲染線程開始工作,主線程釋放。渲染線程的核心實現在 libhwui 庫裏面,其代碼位於 frameworks/base/libs/hwui

frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

int RenderProxy::syncAndDrawFrame() {
    return mDrawFrameTask.drawFrame();
}

關於 RenderThread 的工作流程這裏就不細說了,後續會有專門的篇幅來講解這個,目前 hwui 這一塊的流程也有很多優秀的文章,大家可以對照文章和源碼來看,其核心流程在 Systrace 上的表現如下:

渲染線程核心方法
渲染線程核心方法

主線程和渲染線程的分工

主線程負責處理進程 Message、處理 Input 事件、處理 Animation 邏輯、處理 Measure、Layout、Draw ,更新 DIsplayList ,但是不涉及 SurfaceFlinger 打交道;渲染線程負責渲染渲染相關的工作,一部分工作也是 CPU 來完成的,一部分操作是調用 OpenGL 函數來完成的

當啓動硬件加速後,在 Measure、Layout、Draw 的 Draw 這個環節,Android 使用 DisplayList 進行繪製而非直接使用 CPU 繪製每一幀。DisplayList 是一系列繪製操作的記錄,抽象爲 RenderNode 類,這樣間接的進行繪製操作的優點如下

  1. DisplayList 可以按需多次繪製而無須同業務邏輯交互
  2. 特定的繪製操作(如 translation, scale 等)可以作用於整個 DisplayList 而無須重新分發繪製操作
  3. 當知曉了所有繪製操作後,可以針對其進行優化:例如,所有的文本可以一起進行繪製一次
  4. 可以將對 DisplayList 的處理轉移至另一個線程(也就是 RenderThread)
  5. 主線程在 sync 結束後可以處理其他的 Message,而不用等待 RenderThread 結束

RenderThread 的具體流程大家可以看這篇文章 : http://www.cocoachina.com/articles/35302

遊戲的主線程與渲染線程

遊戲大多使用單獨的渲染線程,有單獨的 Surface ,直接跟 SurfaceFlinger 進行交互,其主線程的存在感比較低,絕大部分的邏輯都是自己在自己的渲染線程裏面實現的。

大家可以看一下王者榮耀對應的 Systrace ,重點看應用進程和 SurfaceFlinger 進程(30fps)

王者榮耀
王者榮耀

可以看到王者榮耀主線程的主要工作,就是把 Input 事件傳給 Unity 的渲染線程,渲染線程收到 Input 事件之後,進行邏輯處理,畫面更新等。

Flutter
Flutter

Flutter 的主線程和渲染線程

這裏提一下 Flutter App 在 Systrace 上的表現,由於 Flutter 的渲染是基於 libSkia 的,所以它也沒有 RenderThread ,而是他自建的 RenderEngine , Flutter 比較重要的兩個線程是 ui 線程和 gpu 線程,對應到下面提到的  Framework 和 Engine 兩層

Flutter
Flutter

Flutter 中也會監聽 Vsync 信號 ,其 VsyncView 中會以 postFrameCallback 的形式,監聽 doFrame 回調,然後調用 nativeOnVsync ,將 Vsync 到來的信息傳給 Flutter UI 線程,開始一幀的繪製。

Flutter
Flutter

可以看到 Flutter 的思路跟遊戲開發的思路差不多,不依賴具體的平臺,自建渲染管道,更新快,跨平臺優勢明顯。

Flutter SDK 自帶 Skia 庫,不用等系統升級就可以用到最新的 Skia 庫,而且 Google 團隊在 Skia 上做了很多優化,所以官方號稱性能可以媲美原生應用

Flutter
Flutter

Flutter 的框架分爲 Framework 和 Engine 兩層,應用是基於 Framework 層開發的,Framework 負責渲染中的 Build,Layout,Paint,生成 Layer 等環節。Engine 層是 C++實現的渲染引擎,負責把 Framework 生成的 Layer 組合,生成紋理,然後通過 Open GL 接口向 GPU 提交渲染數據。

Flutter
Flutter

當需要更新 UI 的時候,Framework 通知 Engine,Engine 會等到下個 Vsync 信號到達的時候,會通知 Framework,然後 Framework 會進行 animations, build,layout,compositing,paint,最後生成 layer 提交給 Engine。Engine 會把 layer 進行組合,生成紋理,最後通過 Open Gl 接口提交數據給 GPU,GPU 經過處理後在顯示器上面顯示。整個流程如下圖:

Flutter
Flutter

性能

如果主線程需要處理所有任務,則執行耗時較長的操作(例如,網絡訪問或數據庫查詢)將會阻塞整個界面線程。一旦被阻塞,線程將無法分派任何事件,包括繪圖事件。主線程執行超時通常會帶來兩個問題

  1. 卡頓:如果主線程 + 渲染線程每一幀的執行都超過 16.6ms(60fps 的情況下),那麼就可能會出現掉幀。
  2. 卡死:如果界面線程被阻塞超過幾秒鐘時間(根據組件不同 , 這裏的閾值也不同),用戶會看到 “應用無響應” (ANR) 對話框(部分廠商屏蔽了這個彈框,會直接 Crash 到桌面)

對於用戶來說,這兩個情況都是用戶不願意看到的,所以對於 App 開發者來說,兩個問題是發版本之前必須要解決的,ANR 這個由於有詳細的調用棧,所以相對來說比較好定位;但是間歇性卡頓這個,可能就需要使用工具來進行分析了:Systrace + TraceView,所以理解主線程和渲染線程的關係和他們的工作原理是非常重要的,這也是本系列的一個初衷

另外關於卡頓,可以參考下面三篇文章,你的 App 卡頓不一定是你 App 的問題,也有可能是系統的問題,不過不管怎麼說,首先要會分析卡頓問題。

  1. Android 中的卡頓丟幀原因概述 - 方法論
  2. Android 中的卡頓丟幀原因概述 - 系統篇
  3. Android 中的卡頓丟幀原因概述 - 應用篇

參考

  1. https://juejin.im/post/5a9e01c3f265da239d48ce32
  2. http://www.cocoachina.com/articles/35302
  3. https://juejin.im/post/5b7767fef265da43803bdc65
  4. http://gityuan.com/2019/06/15/flutter_ui_draw/
  5. https://developer.android.google.cn/guide/components/processes-and-threads

附件

本文涉及到的附件也上傳了,各位下載後解壓,使用 Chrome 瀏覽器打開即可

點此鏈接下載文章所涉及到的 Systrace 附件

關於我 && 博客

  1. 關於我 , 非常希望和大家一起交流 , 共同進步 .
  2. 博客內容導航
  3. 優秀博客文章記錄 - Android 性能優化必知必會

一個人可以走的更快 , 一羣人可以走的更遠

本文使用 mdnice 排版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章