從零開始仿寫一個抖音App——Android繪製機制以及Surface家族原始碼全解析

何時夕發表於2019-02-15

本文首發於微信公眾號——世界上有意思的事,搬運轉載請註明出處,否則將追究版權責任。微訊號:a1018998632,交流qq群:859640274

GitHub地址

大家好,新的一年又正式開始了,筆者在這裡給大家拜個晚年。最近寫的文章需要涉及到的知識很多,所以更新比較慢,望大家海涵。最近申請了一個微信公眾號:世界上有意思的事,二維碼在文章底部,以後我的文章首發都會在這個公眾號上面。簡書、掘金會在幾個小時之後同步,所以希望大家能來關注我的公眾號,大量乾貨在這裡等著你。進入微信公眾號 世界上有意思的事 傳送訊息:**Android繪製機制以及Surface家族原始碼全解析,**即可獲取本文的 pdf 版。

本篇文章分為以下章節,讀者可以按需閱讀

  • 1.Android繪製機制概覽
  • 2.Android繪製機制原始碼分析
  • 3.Surface家族原始碼全解析
  • 4.總結

閱讀須知

  • 1.進入微信公眾號 世界上有意思的事 傳送訊息:**Android繪製機制以及Surface家族原始碼全解析,**即可獲取本文的 pdf 版。
  • 2.本文分析的原始碼版本是 Android 7.0,建議結合原始碼閱讀本文
  • 3.推薦一個 Android 原始碼閱讀網站:Android 原始碼閱讀網站
  • 4.因為很多 java 層和 c++ 層的類命名都相同,所以後續例如:Surface.lockCanvas 表示 java 層的呼叫,Surface::lockCanvas 表示 c++ 層的呼叫
  • 5.本文的一些縮寫:SF——>SurfaceFlinger、WMS——>WindowManagerService、MessageQueue——>MQ、GL——>OpenGL、BQ——>BufferQueue、ST——>SurfaceTexture、TV——>TextureView、SV——>SurfceView
  • 6.本文是視訊編輯SDK開發的重要前置知識,建議讀透

一、Android繪製機制概覽

在深入各種原始碼之前,我們需要了解 Android 繪製機制的整體架構,以及一些概念

1.Android螢幕重新整理機制

圖1:螢幕重新整理.jpg

圖1就是 Android 螢幕顯示的抽象示意圖,這裡我來解釋一下:

  • 1.首先圖的橫軸是時間,縱軸從下到上分別表示:CPU 處理、GPU 處理、螢幕顯示,這三個步驟也就是我們寫的程式碼到影象顯示在螢幕上的流程。
  • 2.我們都知道要讓手機不卡頓一個顯而易見的標準就是:**螢幕上每隔一定的 ms 就能顯示下一幀影象。**在這裡這個時間由底層控制,也就是圖中兩個 VSync 的間隔時間——16ms
  • 3.Android 中引入了下面這些特性來保證螢幕上的資料每隔 16ms 來重新整理一次。
    • 1.一個固定的脈衝訊號——VSync,這個東西由底層保證,每一次 VSync 訊號來了 CPU 就開始執行繪製程式碼(例如執行 View.draw 之類的方法),當 CPU 的資料準備好了,就將這些資料交給 GPU 讓其在一塊記憶體緩衝區上進行影象的繪製。當 GPU 繪製好了就將影象顯示到螢幕上。
    • 2.三緩衝,圖中的 A、B、C 表示的是三塊記憶體緩衝區。因為不是每次 CPU、GPU 的資料處理都能在16ms 內完成的,所以為了不浪費時間而且不丟棄之前做完的工作。CPU、GPU 的工作會依次反應在 A、B、C 三塊記憶體緩衝區中。而螢幕每次都取當前已經準備好的記憶體緩衝區。三緩衝較雙緩衝的問題就是:我們的操作最終顯示到螢幕上的時候會延遲16ms,這可能也是 Android 不如 ios ”跟手“的一個原因
  • 4.由我們可以得出兩個簡單的結論:
    • 1.ui執行緒太忙了,使得 CPU 16ms 內沒有處理好資料會導致丟幀。
    • 2.需要繪製的影象太複雜,導致 GPU 16ms 沒有繪製好影象也會導致丟幀。

2.Android影象繪製方式

問大家一個問題:平時我們開發過程中可以用哪些工具在螢幕上繪製影象?大家一定可以回答出很多東西:View、Drawable、xml、SV、GLSurfaceView、TV、Canvas等等。其實 Android 上面一共只有兩種常用的繪圖機制,上面列舉出來的東西都是由這兩種機制演變而來的。這一節我就簡單歸納介紹一下。

Android 的兩種常用繪圖機制:

  • 1.Skia圖形庫:Skia官網,Skia是一個開源的二維圖形庫,提供各種常用的API,並可在多種軟硬體平臺上執行。在沒開啟硬體加速的時候用到 Canvas 的地方在底層都會呼叫到 Skia 庫中去。在上面我們列舉的方式裡面:View、Drawable、xml、SV、Canvas 最終使用的都是 Skia 庫。另外 Skia 最終是使用 CPU 來對影象進行最終繪製,所以效率比較低。
  • 2.GL:GL 是用於渲染 2D、3D 向量圖形的跨語言、跨平臺的應用程式程式設計介面。在開啟硬體加速的時候用到 Canvas 的地方最終會呼叫到 GL ES 庫中。沒開啟硬體加速的時候上面我們列舉的方式中:GLSurfaceView、TV 最終使用的是 GL ES另外 GL 使用的是 GPU 來對影象進行繪製,所以效率比較高。
  • 3.在後續的章節裡我會從原始碼上分析上面各種繪圖方式的呼叫鏈。
  • 4.其實在 7.0 之後 Android 中新增了 Vulkan。Vulkan 是用於高效能 3D 圖形的低開銷、跨平臺 API。與 GL ES 一樣,Vulkan 提供多種用於在應用中建立高質量的實時圖形的工具。不過目前很少用到,所以本篇文章中我們不討論它。

3.Android繪製中的生產者和消費者

android 的繪製機制中存在著一系列生產者消費者,這一節我將介紹一下這個機制中相關的概念。

  • 1.BQ:如圖2所示,BQ 是一個儲存著記憶體塊的佇列。
    • 1.生產者:它可以使用兩個 api,queuedequeue
      • 1.**dequeue:**需要一塊記憶體來繪製影象的時候,可以從佇列的尾部拿出一塊記憶體進行繪製。
      • 2.**queue:**當影象繪製完畢的時候,可以將該記憶體新增到佇列的頭部
    • 2.**消費者:**它也可以使用兩個 api,acquirerelease
      • 1.**acquire:**當需要一塊已經繪製完成的記憶體再對其進行處理的時候,可以從佇列的頭部拿出一塊記憶體。
      • 2.**release:**當對影象記憶體處理完畢的時候,可以將記憶體重置然後放回佇列的尾部

圖2:BufferQueue.png

  • 2.影象記憶體的生產者:
    • 1.Surface:Surface 是 BQ 的生產者。當我們使用 lockCanvasunlockCanvasAndPost 的時候,就是先從 BQ 中取出一塊記憶體然後呼叫 Canvas/GL 的 api 對記憶體進行繪製,最後將記憶體放回 BQ 中。
    • 2.我們知道了 Surface 是生產者,那麼像 View、SV、GLSurfaceView 這些間接或者直接用到了 Surface 的東西就都是生產者了。
  • 3.影象記憶體的消費者:
    • 1.SF:它有自己的程式,在 Android 系統啟動的時候就像其他各種 Service 一樣被建立了。它的作用是接受來自多個來源的記憶體緩衝區,對它們進行合成,然後傳送到顯示裝置。大多數應用通常在螢幕上有三個層:螢幕頂部的狀態列、底部或側面的導航欄以及應用的介面。所以應用的 Surface 生產的記憶體就會被它所消耗。
    • 2.**ST:**這個東西是常常用在 TV 中。它可以在我們的應用中使用。它在建立的時候會建立一個自己的 BQ。我們可以通過 ST 來建立一個 Surface 然後通過 Surface 向 BQ 中提供影象記憶體。此時 ST 就可以消耗這些影象記憶體。它可以使用 GL 來對這些被消耗的影象記憶體進行二次處理,然後讓這些被處理之後的影象記憶體在通過 GLSurfaceView 之類的東西顯示到螢幕上。

二、Android繪製機制原始碼分析

這一章我們來從原始碼上分析 View 是如何繪製到螢幕上面的,前面的 measure、layout、draw 等等 framework 層的東西我不會著重分析,我主要分析 cpp 層的東西。

圖3:Android繪製機制.png

其實原始碼的主要流程都在圖3中,我下面講的東西算是對圖3的補充和說明。另外強烈建議結合 Android 原始碼閱讀本章節。

  • 1.首先我們的入口是 ViewRootImpl.scheduleTraversals。看過原始碼的同學應該知道,類似 invalidate、requestLayout 等等需要改變 View 的顯示的操作,最終都會層層向上呼叫最終呼叫到這個方法上。
    • 1.在這個方法裡主要呼叫到的方法就是 Choreographer.postCallback 這個方法傳入了 mTraversalRunnable,表示在某個時間點會呼叫這個 Runnable 中的 run()。我們在第一章中講過 VSync 的相關知識,而 Choreographer 就是 VSync 在程式碼層面的實現。
      • 1.postCallback 根據 scheduleFrameLocked——>scheduleVsyncLocked——>scheduleVsyncLocked 的呼叫鏈,最終會呼叫到 DisplayEventReceiver.nativeScheduleVsync 向native 層申請下一個 VSync 訊號。
  • 2.16ms 後 VSync 訊號會從 native 層向上觸發,這裡是想 ui 的 loop 中新增了一個 msg。這樣的話呼叫鏈就是:DisplayEventReceiver.dispatchVsync——>DisplayEventReceiver.onVsync——>Choreographer.doFrame。
    • 1.到了 doFrame 這裡就會呼叫前面 postCallback 中傳入的 mTraversalRunnable 的 run()。我們知道 run() 中會呼叫 ViewRootImpl.doTraversal 方法。
      • 1.這樣就會呼叫到 ViewRootImpl.performTraversals 中去。我想大家應該對這個方法很熟悉,這個方法就是呼叫 measure、layout、draw 的方法。已經分析爛了的東西這裡我就不說了。
      • 2.我們直接看 ViewRootImpl.draw 方法,這裡會有兩種繪製方式。就像我們在第一章中說的那樣。如果沒有開啟硬體加速那麼就使用 Skia 庫以 CPU 來繪製影象,這裡的入口方法是 drawSoftware。如果開啟了硬體加速那麼就是用 GL ES 來以 GPU 繪製影象,這裡入口方法是 ThreadedRenderer.draw。
        • 1.drawSoftware:這個方法比較簡單就是建立一個 Surface,然後 lockCanvas 得到一個 Canvas,然後在各個層級的 View 中呼叫 Canvas 最終呼叫 Skia 庫來在 Surface 上提供的影象記憶體中繪製影象,畫完之後呼叫 unlockCanvasAndPost 來提交影象記憶體。這裡更詳細的流程我會在第三章中分析 Surface 的時候分析。
        • 2.ThreadedRenderer.draw:這個方法是硬體加速下的影象繪製入口,裡面最終都是呼叫到 GL 的 api。在深入之前我們先了解一下硬體加速繪製的幾個階段:
          • 1.硬體加速繪製的五個階段:
            • 1.APP在UI執行緒使用 Canvas 遞迴構建 GL 渲染需要的命令及資料
            • 2.CPU 將準備好的資料共享給 GPU
            • 3.CPU 通知GPU渲染,這裡一般不會阻塞等待GPU渲染結束,因為效率很低。CPU 通知結束後就返回繼續執行其他任務。當然使用 glFinish 可以阻塞執行。
            • 4.swapBuffers,並通知 SF 圖層合成
            • 5.SF 開始合成圖層,如果之前提交的GPU渲染任務沒結束,則等待GPU渲染完成,再合成(Fence機制),合成依然是依賴GPU
          • 2.硬體加速繪製程式碼分析:
            • 1.我們先看 draw() 裡面呼叫的 updateRootDisplayList:
              • 1.這個方法的第一個呼叫鏈是這樣的 updateViewTreeDisplayList——>View.updateDisplayListIfDirty——>View.draw。如圖4,這裡聰明的同學一看就知道是一個遞迴操作。View 的 draw 會遞迴到子 View 中。然後各個 View 會呼叫 Canvas 的 api 將繪製操作儲存在 Canvas 中。那麼這裡的 Canvas 是怎麼來的呢?其實在每個 View 建立的時候內部會建立一個 RenderNode 。這個物件可以建立一個 DisplayListCanvas 來作為 Canvas 給各個 View 在繪製的時候使用。而每個子 View 呼叫 draw(Canvas, ViewGroup, long) 的時候都會得到 parentView 傳遞下來的 DisplayListCanvas,然後在本 View.draw(Canvas) 呼叫結束之後,將DisplayListCanvas 的操作儲存到本 View 的 RenderNode 中。最後呼叫 parentView 的 DisplayListCanvas.drawRenderNode 將本 View 的 RenderNode 存入 parentView 的 RenderNode 中。如此遞迴,最終將所有繪製操作存入 RootView 的 RenderNode 中。至此 RootView 中就包含了一個 DrawOp 樹。
              • 圖4:硬體加速下的Canvas繪製流程.png
              • 2.我們回到 updateRootDisplayList 這裡後續就是將 RootView 的 DrawOp 樹 交給 ViewRootImpl 的 RenderNode 方便後面進行操作。
            • 2.再回到 draw() 中,這裡下一個呼叫的重要的方法是 nSyncAndDrawFrame:
              • 1.這個方法最終會呼叫到 c++ 層的 RenderProxy::syncAndDrawFrame 方法。在瞭解這裡的呼叫鏈之前。我先介紹一下 Android 5.0 之後出現的渲染執行緒的概念,再來講解呼叫鏈。
                • 1.首先渲染執行緒的實現類是 RenderThread.cpp 它是和 ui 執行緒類似,是一個事件驅動的 Loop。它的也有佇列,佇列中儲存著 DrawFrameTask.cpp 物件。RenderProxy.cpp 是 RenderThread.cpp 給 java 層的代理。ThreadRender 所有關於渲染執行緒的請求都會交給 RenderProxy.cpp 然後由它向 RenderThread.cpp 提交 task。
                • 2.瞭解了渲染執行緒的概念,我們再來講講呼叫鏈:syncAndDrawFrame——> DrawFrameTask::drawFrame——>DrawFrameTask::postAndWait。這裡做的事情很簡單,向 RenderTheas.cpp 的佇列中插入一個 DrawFrameTask.cpp,然後阻塞當前的 ui 執行緒。
            • 3.ui 執行緒被阻塞之後,渲染執行緒會呼叫到上一次插入到佇列裡的 DrawFrameTask.run 方法。
              • 1.run() 這裡會先呼叫 syncFrameState,這個方法主要是用於同步 java 層的各種資料。
                • 1.第一步是先用 mLayers::apply 來同步資料,這個 mLayers 在 java 層的體現是 TV。這裡的分析我們會在第三章中著重分析,這裡先略過。
                • 2.第二步是呼叫 CanvasContext::prepareTree 來將前面在 java 層構建的 DrawOp 樹同步到 c++ 層,以便後續執行 OpengGL 的命令。這裡關鍵的呼叫鏈是:CanvasContext::prepareTree——>RenderNode::prepareTree——>RenderNode::prepareTreeImpl。由前面我們可以知道 RenderNode.java 已經構建了一個 DrawOp 樹。但是之前只是呼叫 RenderNode::setStagingDisplayList 暫存在 RenderNode::mStagingDisplayListData 中的。因為 java 層在執行過程中還會出現多次meausre、layout的,還有資料還可能發生改變。所以當走到這裡的時候資料已經確定了,所以可以開始同步資料。prepareTreeImpl 同步資料主要有三個步驟:
                  • 1.呼叫 pushStagingDisplayListChanges 同步當前 RenderNode.cpp 的屬性,也就是把 mStagingDisplayListData 賦值給 mDisplayListData
                  • 2.呼叫 prepareSubTree 遞迴處理子 RenderNode.cpp。
                  • 3.這裡會有一個同步成功和同步失敗的問題,一般來說這裡的資料都會同步成功的。但是在 RenderNode::prepareSubTree 中會有一個步驟是把 RenderNode 用到的 Bitmap 封裝成紋理,一旦這裡 Bitmap 太大或者數量太多那麼同步就會失敗。注意這裡同步失敗只是會與 Bitmap 有關,其他的 DrawOp 資料無論如何都會同步成功的。
                • 3.如果這裡同步成功了的話,那麼 ui thread 就會被喚醒,反之則暫時不喚醒。
              • 2.run() 中將資料同步完成之後,就會呼叫 CanvasContext.draw,這個方法主要有三個操作:
                • 1.mEglManager::beginFrame,其實是標記當前上下文,並且申請繪製記憶體,因為一個程式中可能存在多個window,也就是多個 EglSurface,那麼我們首先需要標記處理哪個,然後向 SF 申請記憶體,這裡 EglSurface 是生產者,SF 是消費者。
                • 2.根據前面的 RenderNode 的 DrawOp 樹,遞迴呼叫 OpenGLRender 中的 GL API,進行繪製。
                • 3.通過 swapBuffers 將繪製好的資料提交給 SF 去合成,值得注意的是此時可能 GPU 並沒有完成當前的渲染任務,但是為了提高效率,這裡可以不用阻塞渲染執行緒。
              • 3.當所有的繪製操作都通過 GL 提交給了 GPU 的時候,如果前面資料同步失敗了,那麼這個時候需要喚醒 ui thread。

三、Surface家族原始碼全解析

上一章我們講了 Android 的整個繪製機制,但是其中涉及到 Surface 的部分都是簡單帶過。所以這一章我就來解析一下 Surface 家族中各個成員的原始碼。

1.Surface的建立與繪製

(1).Surface的建立

圖5:Surface 建立.png

**這裡我們以 View 的建立流程為例,講述一下 Surface 在這個過程中的建立流程,Surface 的建立流程如圖5所示。 **

  • 1.首先我們需要知道的是 ViewRootImpl 在建立的時候會使用 new Surface 來建立一個 java 層的空殼。這個空殼會在後面被初始化。
  • 2.然後入口的呼叫鏈是這樣的:ViewRootImpl.performTraversals——>ViewRootImpl.relayoutWindow——>WindowManagerService.relayoutWindow——>WindowManagerService.createSurfaceControl。
    • 1.createSurfaceControl 這個方法裡首先會 WindowStateAnimator.createSurfaceLocked——>new WindowSurfaceController——>new SurfaceControlWithBackground——>new SurfaceControl——>SurfaceControl.nativeCreate——>android_view_SurfaceControl::nativeCreate 來建立一個SurfaceControl.cpp 這個東西是初始化 Surface 的引數。
      • 1.nativeCreate 的第一步是建立一個 SurfaceComposerClient.cpp 它其實是 SF 所在程式的代理,我們可以通過這個類對 SF 進行操作。在呼叫 new SurfaceComposerClient.cpp 的建構函式之後,首先會觸發 onFirstRef,這裡面則會使用 ComposerService::getComposerService() 獲取 SF 的服務。然後遠端呼叫 ComposerService::createConnection 也就是 SF::createConnection 來建立一個 ISurfaceComposer 作為 SurfaceComposerClient 與 SF 程式互動的介面,這裡用到的是 Binder 機制
      • 2.SurfaceComposerClient 建立完畢之後,就可以呼叫 SurfaceComposerClient::createSurface——>Client::createSurface 來向 SF 程式傳送建立 Surface 的請求了。SF 程式也是事件驅動模式,所以 Client::createSurface 中呼叫 SF::postMessageSync 傳送了一個呼叫 SF::createLayer 方法的訊息給事件佇列。 而 createLayer 最終會呼叫 createNormalLayer 中。這個方法會返回一個 IGraphicBufferProducer 給 SurfaceControl.cpp。**記得我們在前面講的 生產者——消費者 模式嗎?**這裡的 IGraphicBufferProducer 就是 SF 的 BQ 分離出來的生產者,我們後續就可以通過這個 IGraphicBufferProducer 向 SF 的 BQ 中新增通過 Surface 繪製好的影象記憶體了。
    • 2.回到 createSurfaceControl 這裡建立了一個 SurfaceControl.java 之後,下一步就是初始化 Surface.java。這裡就比較簡單了,就是通過呼叫鏈:SurfaceController.getSurface(Surface)——>Surface.copyFrom(SurfaceControl)——>Surface.nativeCreateFromSurfaceControl——>SurfaceControl::getSurface。將 SurfaceControl.cpp 中的 IGraphicBufferProducer 作為引數建立一個 Surface.cpp 交給 Surface.java。

(2).Surface的繪製

我們在第二章裡面說到 View 的繪製根據是否硬體加速分為,軟體繪製硬體繪製兩種。當時我們分析了硬體繪製,軟體繪製略過了。其實軟體繪製與硬體繪製的區別就在於是使用 CPU 進行繪製計算還是使用 GPU 進行繪製計算。這一小節的 Surface 繪製其實就是軟體繪製,也就是 ViewRootImpl.drawSoftware 中的內容。

圖6:Surface 繪製.png

我們都知道 Surface 可以通過 lockCanvas 和 unlockCanvasAndPost 這兩個 api 來再通過 Canvas 來繪製影象,這一節我就通過這兩個 api 來講講 Surface 的繪製流程,整個流程如圖6所示。

  • 1.首先我們從 lockCanvas 這個入口開始呼叫鏈是:lockCanvas——>nativeLockCanvas——>Surface::lock——>Surface::dequeueBuffer,這裡最終會使用我們在 Surface 建立的時候得到的 BufferQueueProducer(IGraphicBufferProducer) 來想 SF 請求一塊空白的影象記憶體。得到了影象記憶體之後,將記憶體傳入 new SkBitmap.cpp 中建立對應物件,一共後面使用 Skia 庫繪製影象。這裡的 SkBitmap.cpp 會被交給 SkCanvas.cpp 而 SkCanvas.cpp 物件就是 Canvas.java 在 c++ 層的操作物件。
  • 2.上面我們通過 lockCanvas 獲取到了一個 Canvas 物件。當我們呼叫 Canvas 的各種 api 的時候其實最終會代用到 c++ 層的 Skia 庫,通過 cpu 對影象記憶體進行繪製。
  • 3.當我們繪製完之後就可以呼叫 unlockCanvasAndPost 來通知 SF 合成影象,呼叫鏈是:unlockCanvasAndPost——>nativeUnlockCanvasAndPost——>Surface::queueBuffer,與 lockCanvas 中相反這裡是最終是通過 BufferQueueProducer::queueBuffer 將影象記憶體放回到佇列中,除此之外這裡還呼叫 IConsumerListener::onFrameAvailable 來通知 SF 程式來重新整理影象,呼叫鏈是:onFrameAvailable——>SF.signalLayerUpdate——>MQ.invalidate——>MQ.Handler.dispatchInvalidate——>MQ.Handler.handleMessage——>onMessageReceived:因為 SF 程式採用的也是事件驅動模型,所以這裡和 ui thread 類似也是通過 looper + 事件 的形式觸發 SurfaceFinger 對影象的重新整理的。注意:這裡的 IConsumerListener 是在 createNormalLayer 的時候建立的 Layer.cpp

2.SurfaceView的建立與使用

其實只要瞭解了 Surface 的建立與使用,那麼 SV 就很簡單了,SV.updateSurface 中會建立或者更新 Surface。在 SV 上繪製也是呼叫 Surface 的兩個 api。這裡我就簡單將 View 與 SV 比較一下。

  • 1.原理:一般的 Activity 包含的多個 View 會組成 View Hierachy 的樹形結構,只有最頂層的 DecorView,也就是根結點檢視,才是對 WMS 可見的。這個 DecorView 在 WMS 中有一個對應的 WindowState。相應地,在 SF 中有對應的 Layer。而 SV 自帶一個 Surface,這個 Surface 在 WMS 中有自己對應的 WindowState,在 SF 中也會有自己的 Layer。
  • 2.好處:在 App 端 Surface 仍在 View Hierachy 中,但在 Server 端(WMS 和 SF)中,它與宿主視窗是分離的。這樣的好處是對這個 Surface 的渲染可以放到單獨執行緒去做,渲染時可以有自己的 GL context。這對於一些遊戲、視訊等效能相關的應用非常有益,因為它不會影響主執行緒對事件的響應。
  • 3.壞處:因為這個 Surface 不在 View Hierachy 中,它的顯示也不受 View 的屬性控制,所以不能進行平移,縮放等變換,也不能放在其它 ViewGroup 中,一些 View 中的特性也無法使用。

3.SurfaceTexture的建立與使用

ST 我們平時可能用的不是很多,但是其實它是 TV 的核心元件。它可以將 Surface 提供的影象流轉換為 GL 的紋理,然後對該內容進行二次處理,最終被處理的內容可以被交給其他消費者消費。這一節我們就來從原始碼層次解析一下 ST。

圖7:SurfaceTexture概覽圖.png

圖7是 ST 與 Surface、SV、TV 等等元件結合的概覽圖,我這裡簡單解釋一下:

  • 1.最左邊表示原始影象流的生成方式:Video(本地/網路視訊流)、Camera(攝像頭拍攝的視訊流)、Software/Hardware Render(使用 Skia/GL 繪製的影象流)。
  • 2.最左邊的原始影象流可以被交給 Surface,而 Surface 是 BQ 的生產者,GLConsumer(java層的體現就是 ST) 是 BQ 的消費者。
  • 3.GLConsumer 拿到了 Surface 的原始影象流,可以通過 GL 轉化為 texture。最終以合理的方式消耗掉。
  • 4.消耗 GLConsumer 中的 texture 的方式就是使用 SV、TV 等等方式或者顯示在螢幕上或者用於其他地方。

圖8:SurfaceTexture建立以及使用.png

我將根據圖8的流程來講解 ST 的建立與使用

  • 1.首先我們從 ST.java 的建立開始,也就是圖中的黃色方框。注意 ST 的建立需要傳入一個 GLES20.glGenTextures 建立的 Texture 的 Id。Surface 提供的影象記憶體最終也是會被掛靠在這個 Texture 中的。

    • 1.建立的的呼叫鏈:SurfaceTexture.nativeInit——>SurfaceTexture::SurfaceTexture_init。
    • 2.SurfaceTexture_init 這個方法裡關鍵步驟如下:
      • 1.建立 IGraphicBufferProducer 和 IGraphicBufferConsumer 根據前面我們知道這兩個類是 BQ 的生產者和消費者。
      • 2.呼叫 BQ.createBufferQueue 將上面建立的生產者和消費者傳入,建立一個 BQ。
      • 3.建立一個 GLConsumer 這個東西相當於 IGraphicBufferConsumer 封裝類也是 ST 在 c++ 層的最終實現類。
      • 4.呼叫 SurfaceTexture_setSurfaceTexture 為 ST.java 的 mSurfaceTexture 賦值,這裡被賦值的物件就是上面建立的 GLConsumer。
      • 5.呼叫 SurfaceTexture_setProducer 同樣為 ST 的 mProducer 賦值。
      • 6.呼叫 SurfaceTexture_setFrameAvailableListener 為 GLConsumer 新增一個 java 層的 OnFrameAvailableListener 作為監聽影象流的回撥。這個 OnFrameAvailableListener 是呼叫 ST.setOnFrameAvailableListener 新增的。
  • 2.ST 初始化完畢之後,我們此時需要一個 Surface 來作為生產者為 ST 提供影象流。

    • 1.還記得 Surface.java 有個建構函式是需要以 ST 作為引數的嗎?我們就從這個函式入手,呼叫鏈如下:new Surface(ST)——>Surface.nativeCreateFromSurfaceTexture——>android_view_Surface::

      nativeCreateFromSurfaceTexture。

    • 2.nativeCreateFromSurfaceTexture 中的關鍵步驟如下:

      • 1.呼叫 SurfaceTexture_getProducer 獲取 ST.java 中儲存的 mProducer 物件在 c++ 層的指標。
      • 2.通過上面獲取的 IGraphicBufferProducer 物件來建立 Surface.cpp 物件。
  • 3.建立了一個負責提供影象流的 Surface 之後我們就可以使用它的兩個 api 來繪製影象以提供影象流了。

    • 1.lockCanvas 和 unlockCanvasAndPost 這兩個 api 我在這裡就不重複解析了。
    • 2.根據前面講解的 Surface 的繪製 我們知道呼叫了 unlockCanvasAndPost 之後會觸發一個 IConsumerListener.onFrameAvailable 的回撥。在 View 的繪製流程中我們知道這裡的回撥最終會觸發 SF 的影象合成。那麼這裡的回撥的實現類是誰呢?你猜的沒錯此時的實現類是 GLConsumer 中被設定的 java 層的 OnFrameAvailableListener,我們在 Surface.cpp 建立的時候會傳入一個 IGraphicBufferProducer 它最終會通過呼叫鏈: BQ::ProxyConsumerListener::onFrameAvailable——>ConsumerBase::onFrameAvailable——>FrameAvailableListener::onFrameAvailable——>JNISurfaceTextureContext::onFrameAvailable——>OnFrameAvailableListener.onFrameAvailable 呼叫到 java 層。
    • 3.當然 OnFrameAvailableListener 回撥的東西是由我們自己來寫的。
      • 1.我們知道 SurfaceTexture 的最終目的是將影象流繫結到我們最開始定義的 texture 中去。SurfaceTexture 正好提供了 updateTexImage 方法來重新整理 texture。
      • 2.那麼我們就在 OnFrameAvailableListener 中直接呼叫 updateTexImage 嗎?這裡其實是不一定的。因為我們知道 Surface 的繪製可以在任意執行緒,也就是說 OnFrameAvailableListener 的回撥也是在任意執行緒中觸發的。而 texture 的更新需要在當初建立這個 texture 的 GL 環境中進行。此外 Android 中 GL 環境和執行緒是一一對應的。所以只有 Surface 繪製的執行緒與 GL 環境執行緒為同一個的時候,我們才能在回撥中呼叫 updateTexImage。
      • 3.不管如何最終還是要呼叫 updateTexImage 的,我們再來看看它內部是怎麼個實現。首先呼叫鏈是這樣的:SurfaceTexture.updateTexImage——>SurfaceTexture::nativeUpdateTexImage——>GLConsumer::updateTexImage。GLConsumer::updateTexImage 中重要的步驟如下:
        • 1.呼叫 acquireBufferLocked 獲取 BQ 頭部最新的影象記憶體。
        • 2.呼叫 glBindTexture 將獲取到的影象記憶體繫結到 texture 中。
        • 3.呼叫 updateAndReleaseLocked 重置前面獲取到的影象記憶體,然後將其放回到 BQ 的尾部
      • 4.至此我們在 Surface 中繪製的影象流就被 SurfaceTexture 轉化成了一個 texture。至於繼續對 texture 進行顯示和處理之類的事情就可以交給 TV 或者 GLSurfaceView 了。

4.TextureView和建立與使用

它可以將內容流直接投影到 View 中,可以用於實現視訊預覽等功能。和 SV 不同,它不會在WMS中單獨建立視窗,而是作為 View Hierachy 中的一個普通 View,因此可以和其它普通 View 一樣進行移動,旋轉,縮放,動畫等變化。值得注意的是 TV 必須在硬體加速的視窗中。它顯示的內容流資料可以來自 App 程式或是遠端程式。這一節我們就來從原始碼上分析它。因為 TV 需要硬體加速,所以它最終也是由渲染執行緒繪製的,而我們在第一章中講述了 ThreadRender.java、RenderProxy.cpp、RenderThread.cpp 等等類在渲染執行緒中的作用,所以這一小節關於渲染執行緒的東西就直接使用而不再贅述了。

圖9:TextureView建立和使用.png

和前面一樣,本小節接下來的分析也都是順著圖9來的

  • 1.因為 TV 我們可以將其看成一個普通的 View,所以這裡我們可以直接從 TV 的 draw 方法開始分析。draw 中有下面這些關鍵的步驟:
    • 1.呼叫 getHardwareLayer 來獲取硬體加速層:
      • 1.ThreadRender.createTextureLayer——>ThreadRender.nCreateTextureLayer——>RenderProxt::createTextureLayer——>new DeferredLayerUpdater.cpp:這裡第一個呼叫鏈建立了一個 DeferredLayerUpdater.cpp 物件,這個物件會在後面用於 ST 的 texture 更新。
      • 2.ThreadRender,createTextureLayer 中建立完 DeferredLayerUpdater.cpp 還需要呼叫 HardwareLayer.adoptTextureLayer 將 ThreadRender,java 和 DeferredLayerUpdater.cpp 在 java層封裝一下,以便後續使用。
      • 3.建立了 HardwareLayer 後,如果 ST 還沒建立的話,那麼就會去建立一個 ST。這裡的建立流程上一節講過了也不再贅述。同理這裡會呼叫 nCreateNativeWindow 在 c++ 層建立一個 ANativeWindow(也就是 java 層的 Surface.java)。這個 Surface 主要是為了讓 TV 能夠提供 lockCanvas 和 unlockCanvasAndPost 這裡兩個 Api 而建立的。
      • 4.除了初始化 ST 的一系列操作,這裡還會呼叫 HardwareLayer.setSurfaceTexture 為 DeferredLayerUpdater 設定 ST。因為後面 DeferredLayerUpdater 更新 ST 的 texture 的時候需要用到 ST。
      • 5.接下來會呼叫 applyUpdate 但是因為是初次建立,所以這裡先略過。
      • 6.然後會呼叫 ST.setOnFrameAvailableListener(mUpdateListener) 來為圖形流新增監聽,而這裡的 mUpdateListener 實現就在 TV 裡面,這裡的實現等我們呼叫到了再說。
      • 7.至此 ST 其實已經初始化完成了,所以可以呼叫 mListener.onSurfaceTextureAvailable 來回撥外部的程式碼了。
    • 2.呼叫 DisplayListCanvas.drawHardwareLayer 在當前 View Hierachay 的 Surface 上面新增繪製一個獨立的 layer(簡單來說就是一個繪圖的區域)。**注意:這裡的 layer 與 我們在前面說的 SV 在 SF 中存在的 Layer 是兩個不同的概念。 **
  • 2.TV 建立好了,接下來我們就可以為其提供影象流。這裡我們就用 lockCanvas 和 unlockCanvasAndPost 來提供吧。注意:其實 ST 和這裡的 TV 不僅僅可以使用上面兩個 api 來提供影象流,還可以將 Surface 轉化成 EGLSurface 使用 GL 來提供影象流,更常見還有攝像頭提供的影象流,這些我在這裡就不展開了,交給讀者自己去探索。
    • 1.首先 lockCanvas 和 unlockCanvasAndPost 這兩個 api 的原始碼流程和回撥流程我就不再贅述了,在 ST 和 Surface 的解析中都有。
    • 2.我們直接看 TV 中 OnFrameAvailableListener 的實現程式碼。
      • 1.這裡先呼叫 updateLayer 將 mUpdateLayer 置為 true,表示下一次 VSync 訊號過來的時候 TV 需要重新整理。注意 TV 是在 View Hierarchy 中的,所以需要與 View 一起重新整理。
      • 2.呼叫 invalidate 觸發 ViewRootImpl 的重繪流程。
    • 3.16ms 之後 VSync 訊號來了,經過一系列方法之後呼叫又回到了 TV.draw 中。此時因為 ST 已經建立,所以最主要的程式碼就是 TV.applyUpdate 方法。
      • 1.先呼叫 HardwareLayer.nPrerare——>RenderProxt::pushLayerUpdate 將前面建立的 DeferredLayerUpdater 存入 RenderProxt.cpp 中。
      • 2.呼叫 HardwareLayer.nUpdateSurfaceTexture 將 DeferredLayerUpdater 中的 mUpdateTexImage 置為 true。為啥不在這裡就直接更新 texture 呢?因為此時的執行緒還是 ui thread,而 texture 的更新需要在渲染執行緒做。
    • 4.我們回憶一下第二章分析 Android繪製機制 原始碼,當時我們講過當 View Hierarchy 的 draw 操作完成之後,經過一系列呼叫就會進入到渲染執行緒中。這裡我們也不再贅述了,我們直接看 RenderProxt::syncAndDrawFrame——>DrawFrameTask::drawFrame——>DrawFrameTask::syncFrameState 就是在渲染執行緒中執行的。它會觸發我們儲存在 RenderProxy.cpp 中的 DeferredLayerUpdater::apply 方法,這個方法有這些關鍵步驟
      • 1.GLConsumer::attachToContext,將當前的 ST 與 texture 繫結。
      • 2.GLConsumer::updateTexImage,像前面講的 ST 一樣將影象流更新到 texture 中
      • 3.LayerRenderer::updateTextureLayer
  • 3.到這裡 TV 從建立到影象流轉換的原始碼都解析完了,剩下的就是 Android繪製機制 中講的那樣使用 GL 進行真正的繪製了。

四、總結

這篇文章真是寫死我了!!過年有三分之二的時間花在這個上面,感覺這篇文章真的是筆者有史以來寫過的最有乾貨的一篇文章了,希望能夠對大家有所幫助。另外看在我這麼辛苦的份上,希望大家能順手關注一下我的公眾號:世界上有意思的事,二維碼就在文章底部,跪謝!!

連載文章

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、網際網路、程式設計師、計算機程式設計。下面是我的微信公眾號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事

參考文獻

相關文章