理解Android硬體加速的小白文

看書的小蝸牛發表於2017-11-30

硬體加速,直觀上說就是依賴GPU實現圖形繪製加速,同軟硬體加速的區別主要是圖形的繪製究竟是GPU來處理還是CPU,如果是GPU,就認為是硬體加速繪製,反之,軟體繪製。在Android中也是如此,不過相對於普通的軟體繪製,硬體加速還做了其他方面優化,不僅僅限定在繪製方面,繪製之前,在如何構建繪製區域上,硬體加速也做出了很大優化,因此硬體加速特性可以從下面兩部分來分析:

  • 1、前期策略:如何構建需要繪製的區域
  • 2、後期繪製:單獨渲染執行緒,依賴GPU進行繪製

無論是軟體繪製還是硬體加速,繪製記憶體的分配都是類似的,都是需要請求SurfaceFlinger服務分配一塊記憶體,只不過硬體加速有可能從FrameBuffer硬體緩衝區直接分配記憶體(SurfaceFlinger一直這麼幹的),兩者的繪製都是在APP端,繪製完成之後同樣需要通知SurfaceFlinger進行合成,在這個流程上沒有任何區別,真正的區別在於在APP端如何完成UI資料繪製,本文就直觀的瞭解下兩者的區別,會涉及部分原始碼,但不求甚解。

軟硬體加速的分歧點

大概從Android 4.+開始,預設情況下都是支援跟開啟了硬體加速的,也存在手機支援硬體加速,但是部分API不支援硬體加速的情況,如果使用了這些API,就需要主關閉硬體加速,或者在View層,或者在Activity層,比如Canvas的clipPath等。但是,View的繪製是軟體加速實現的還是硬體加速實現的,一般在開發的時候並不可見,那圖形繪製的時候,軟硬體的分歧點究竟在哪呢?舉個例子,有個View需要重繪,一般會呼叫View的invalidate,觸發重繪,跟著這條線走,去查一下分歧點。

檢視重繪
檢視重繪

從上面的呼叫流程可以看出,檢視重繪最後會進入ViewRootImpl的draw,這裡有個判斷點是軟硬體加速的分歧點,簡化後如下

ViewRootImpl.java

private void draw(boolean fullRedrawNeeded) {
    ...
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        <!--關鍵點1 是否開啟硬體加速-->
        if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
             ...
            dirty.setEmpty();
            mBlockResizeBuffer = false;
            <!--關鍵點2 硬體加速繪製-->
            mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
        } else {
          ...
           <!--關鍵點3 軟體繪製-->
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        ...複製程式碼

關鍵點1是啟用硬體加速的條件,必須支援硬體並且開啟了硬體加速才可以,滿足,就利用HardwareRenderer.draw,否則drawSoftware(軟體繪製)。簡答看一下這個條件,預設情況下,該條件是成立的,因為4.+之後的手機一般都支援硬體加速,而且在新增視窗的時候,ViewRootImpl會enableHardwareAcceleration開啟硬體加速,new HardwareRenderer,並初始化硬體加速環境。

private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {

    <!--根據配置,獲取硬體加速的開關-->
    // Try to enable hardware acceleration if requested
    final boolean hardwareAccelerated =
            (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
   if (hardwareAccelerated) {
        ...
            <!--新建硬體加速圖形渲染器-->
            mAttachInfo.mHardwareRenderer = HardwareRenderer.create(mContext, translucent);
            if (mAttachInfo.mHardwareRenderer != null) {
                mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString());
                mAttachInfo.mHardwareAccelerated =
                        mAttachInfo.mHardwareAccelerationRequested = true;
            }
        ...複製程式碼

其實到這裡軟體繪製跟硬體加速的分歧點已經找到了,就是ViewRootImpl在draw的時候,如果需要硬體加速就利用 HardwareRenderer進行draw,否則走軟體繪製流程,drawSoftware其實很簡單,利用Surface.lockCanvas,向SurfaceFlinger申請一塊匿名共享記憶體記憶體分配,同時獲取一個普通的SkiaCanvas,用於呼叫Skia庫,進行圖形繪製,

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        final Canvas canvas;
        try {
            <!--關鍵點1 -->
            canvas = mSurface.lockCanvas(dirty);
            ..
            <!--關鍵點2 繪製-->
                 mView.draw(canvas);
             ..
             關鍵點3 通知SurfaceFlinger進行圖層合成
                surface.unlockCanvasAndPost(canvas);
            }   ...            
           return true;  }複製程式碼

drawSoftware工作完全由CPU來完成,不會牽扯到GPU的操作,下面重點看下HardwareRenderer所進行的硬體加速繪製。

HardwareRenderer硬體加速繪製模型

開頭說過,硬體加速繪製包括兩個階段:構建階段+繪製階段,所謂構建就是遞迴遍歷所有檢視,將需要的操作快取下來,之後再交給單獨的Render執行緒利用OpenGL渲染。在Android硬體加速框架中,View檢視被抽象成RenderNode節點,View中的繪製都會被抽象成一個個DrawOp(DisplayListOp),比如View中drawLine,構建中就會被抽象成一個DrawLintOp,drawBitmap操作會被抽象成DrawBitmapOp,每個子View的繪製被抽象成DrawRenderNodeOp,每個DrawOp有對應的OpenGL繪製命令,同時內部也握著繪圖所需要的資料。如下所示:

繪圖Op抽象
繪圖Op抽象

如此以來,每個View不僅僅握有自己DrawOp List,同時還拿著子View的繪製入口,如此遞迴,便能夠統計到所有的繪製Op,很多分析都稱為Display List,原始碼中也是這麼來命名類的,不過這裡其實更像是一個樹,而不僅僅是List,示意如下:

硬體加速.jpg
硬體加速.jpg

構建完成後,就可以將這個繪圖Op樹交給Render執行緒進行繪製,這裡是同軟體繪製很不同的地方,軟體繪製時,View一般都在主執行緒中完成繪製,而硬體加速,除非特殊要求,一般都是在單獨執行緒中完成繪製,如此以來就分擔了主執行緒很多壓力,提高了UI執行緒的響應速度。

硬體加速模型.jpg
硬體加速模型.jpg

知道整個模型後,就程式碼來簡單瞭解下實現流程,先看下遞迴構建RenderNode樹及DrawOp集。

利用HardwareRenderer構建DrawOp集

HardwareRenderer是整個硬體加速繪製的入口,實現是一個ThreadedRenderer物件,從名字能看出,ThreadedRenderer應該跟一個Render執行緒息息相關,不過ThreadedRenderer是在UI執行緒中建立的,那麼與UI執行緒也必定相關,其主要作用:

  • 1、在UI執行緒中完成DrawOp集構建
  • 2、負責跟渲染執行緒通訊

可見ThreadedRenderer的作用是很重要的,簡單看一下實現:

ThreadedRenderer(Context context, boolean translucent) {
    ...
    <!--新建native node-->
    long rootNodePtr = nCreateRootRenderNode();
    mRootNode = RenderNode.adopt(rootNodePtr);
    mRootNode.setClipToBounds(false);
    <!--新建NativeProxy-->
    mNativeProxy = nCreateProxy(translucent, rootNodePtr);
    ProcessInitializer.sInstance.init(context, mNativeProxy);
    loadSystemProperties();
}複製程式碼

從上面程式碼看出,ThreadedRenderer中有一個RootNode用來標識整個DrawOp樹的根節點,有個這個根節點就可以訪問所有的繪製Op,同時還有個RenderProxy物件,這個物件就是用來跟渲染執行緒進行通訊的控制程式碼,看一下其建構函式:

RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory)
        : mRenderThread(RenderThread::getInstance())
        , mContext(nullptr) {
    SETUP_TASK(createContext);
    args->translucent = translucent;
    args->rootRenderNode = rootRenderNode;
    args->thread = &mRenderThread;
    args->contextFactory = contextFactory;
    mContext = (CanvasContext*) postAndWait(task);
    mDrawFrameTask.setContext(&mRenderThread, mContext);  
   }複製程式碼

從RenderThread::getInstance()可以看出,RenderThread是一個單例執行緒,也就是說,每個程式最多隻有一個硬體渲染執行緒,這樣就不會存在多執行緒併發訪問衝突問題,到這裡其實環境硬體渲染環境已經搭建好好了。下面就接著看ThreadedRenderer的draw函式,如何構建渲染Op樹:

@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
    attachInfo.mIgnoreDirtyState = true;

    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();
    <!--關鍵點1:構建View的DrawOp樹-->
    updateRootDisplayList(view, callbacks);

    <!--關鍵點2:通知RenderThread執行緒繪製-->
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
    ...
}複製程式碼

只關心關鍵點1 updateRootDisplayList,構建RootDisplayList,其實就是構建View的DrawOp樹,updateRootDisplayList會進而呼叫根View的updateDisplayListIfDirty,讓其遞迴子View的updateDisplayListIfDirty,從而完成DrawOp樹的建立,簡述一下流程:

private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
    <!--更新-->
    updateViewTreeDisplayList(view);
   if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
         <!--獲取DisplayListCanvas-->
        DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
        try {
        <!--利用canvas快取Op-->
            final int saveCount = canvas.save();
            canvas.translate(mInsetLeft, mInsetTop);
            callbacks.onHardwarePreDraw(canvas);

            canvas.insertReorderBarrier();
            canvas.drawRenderNode(view.updateDisplayListIfDirty());
            canvas.insertInorderBarrier();

            callbacks.onHardwarePostDraw(canvas);
            canvas.restoreToCount(saveCount);
            mRootNodeNeedsUpdate = false;
        } finally {
        <!--將所有Op填充到RootRenderNode-->
            mRootNode.end(canvas);
        }
    }
}複製程式碼
  • 利用View的RenderNode獲取一個DisplayListCanvas
  • 利用DisplayListCanvas構建並快取所有的DrawOp
  • 將DisplayListCanvas快取的DrawOp填充到RenderNode
  • 將根View的快取DrawOp設定到RootRenderNode中,完成構建

繪製流程
繪製流程

簡單看一下View遞迴構建DrawOp,並將自己填充到

 @NonNull
    public RenderNode updateDisplayListIfDirty() {
        final RenderNode renderNode = mRenderNode;
        ...
            // start 獲取一個 DisplayListCanvas 用於繪製 硬體加速 
            final DisplayListCanvas canvas = renderNode.start(width, height);
            try {
                // 是否是textureView
                final HardwareLayer layer = getHardwareLayer();
                if (layer != null && layer.isValid()) {
                    canvas.drawHardwareLayer(layer, 0, 0, mLayerPaint);
                } else if (layerType == LAYER_TYPE_SOFTWARE) {
                    // 是否強制軟體繪製
                    buildDrawingCache(true);
                    Bitmap cache = getDrawingCache(true);
                    if (cache != null) {
                        canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                    }
                } else {
                      // 如果僅僅是ViewGroup,並且自身不用繪製,直接遞迴子View
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                    } else {
                        <!--呼叫自己draw,如果是ViewGroup會遞迴子View-->
                        draw(canvas);
                    }
                }
            } finally {
                  <!--快取構建Op-->
                renderNode.end(canvas);
                setDisplayListProperties(renderNode);
            }
        }  
        return renderNode;
    }複製程式碼

TextureView跟強制軟體繪製的View比較特殊,有額外的處理,這裡不關心,直接看普通的draw,假如在View onDraw中,有個drawLine,這裡就會呼叫DisplayListCanvas的drawLine函式,DisplayListCanvas及RenderNode類圖大概如下

硬體加速類圖
硬體加速類圖

DisplayListCanvas的drawLine函式最終會進入DisplayListCanvas.cpp的drawLine,

void DisplayListCanvas::drawLines(const float* points, int count, const SkPaint& paint) {
    points = refBuffer<float>(points, count);

    addDrawOp(new (alloc()) DrawLinesOp(points, count, refPaint(&paint)));
}複製程式碼

可以看到,這裡構建了一個DrawLinesOp,並新增到DisplayListCanvas的快取列表中去,如此遞迴便可以完成DrawOp樹的構建,在構建後利用RenderNode的end函式,將DisplayListCanvas中的資料快取到RenderNode中去:

public void end(DisplayListCanvas canvas) {
    canvas.onPostDraw();
    long renderNodeData = canvas.finishRecording();
    <!--將DrawOp快取到RenderNode中去-->
    nSetDisplayListData(mNativeRenderNode, renderNodeData);
    // canvas 回收掉]
    canvas.recycle();
    mValid = true;
}複製程式碼

如此,便完成了DrawOp樹的構建,之後,利用RenderProxy向RenderThread傳送訊息,請求OpenGL執行緒進行渲染。

RenderThread渲染UI到Graphic Buffer

DrawOp樹構建完畢後,UI執行緒利用RenderProxy向RenderThread執行緒傳送一個DrawFrameTask任務請求,RenderThread被喚醒,開始渲染,大致流程如下:

  • 首先進行DrawOp的合併
  • 接著繪製特殊的Layer
  • 最後繪製其餘所有的DrawOpList
  • 呼叫swapBuffers將前面已經繪製好的圖形緩衝區提交給Surface Flinger合成和顯示。

不過再這之前先複習一下繪製記憶體的由來,畢竟之前DrawOp樹的構建只是在普通的使用者記憶體中,而部分資料對於SurfaceFlinger都是不可見的,之後又繪製到共享記憶體中的資料才會被SurfaceFlinger合成,之前分析過軟體繪製的UI是來自匿名共享記憶體,那麼硬體加速的共享記憶體來自何處呢?到這裡可能要倒回去看看ViewRootImlp

private void performTraversals() {
        ...
        if (mAttachInfo.mHardwareRenderer != null) {
            try {
                hwInitialized = mAttachInfo.mHardwareRenderer.initialize(
                        mSurface);
                if (hwInitialized && (host.mPrivateFlags
                        & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0) {
                    mSurface.allocateBuffers();
                }
            } catch (OutOfResourcesException e) {
                handleOutOfResourcesException(e);
                return;
            }
        }
      ....

/**
 * Allocate buffers ahead of time to avoid allocation delays during rendering
 * @hide
 */
public void allocateBuffers() {
    synchronized (mLock) {
        checkNotReleasedLocked();
        nativeAllocateBuffers(mNativeObject);
    }
}複製程式碼

可以看出,對於硬體加速的場景,記憶體分配的時機會稍微提前,而不是像軟體繪製事,由Surface的lockCanvas發起,主要目的是:避免在渲染的時候再申請,一是避免分配失敗,浪費了CPU之前的準備工作,二是也可以將渲染執行緒個工作簡化,在分析Android視窗管理分析(4):Android View繪製記憶體的分配、傳遞、使用的時候分析過,在分配成功後,如果有必要,會進行一次UI資料拷貝,這是區域性繪製的根基,也是保證DrawOp可以部分執行的基礎,到這裡記憶體也分配完畢。不過,還是會存在另一個問題,一個APP程式,同一時刻會有過個Surface繪圖介面,但是渲染執行緒只有一個,那麼究竟渲染那個呢?這個時候就需要將Surface與渲染執行緒(上下文)繫結。

static jboolean android_view_ThreadedRenderer_initialize(JNIEnv* env, jobject clazz,
        jlong proxyPtr, jobject jsurface) {
    RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
    sp<ANativeWindow> window = android_view_Surface_getNativeWindow(env, jsurface);
    return proxy->initialize(window);
}複製程式碼

首先通過android_view_Surface_getNativeWindowSurface獲取Surface,在Native層,Surface對應一個ANativeWindow,接著,通過RenderProxy類的成員函式initialize將前面獲得的ANativeWindow繫結到RenderThread

bool RenderProxy::initialize(const sp<ANativeWindow>& window) {
    SETUP_TASK(initialize);
    args->context = mContext;
    args->window = window.get();
    return (bool) postAndWait(task);
}複製程式碼

仍舊是向渲染執行緒傳送訊息,讓其繫結當前Window,其實就是呼叫CanvasContext的initialize函式,讓繪圖上下文繫結繪圖記憶體:

bool CanvasContext::initialize(ANativeWindow* window) {
    setSurface(window);
    if (mCanvas) return false;
    mCanvas = new OpenGLRenderer(mRenderThread.renderState());
    mCanvas->initProperties();
    return true;
}複製程式碼

CanvasContext通過setSurface將當前要渲染的Surface繫結到到RenderThread中,大概流程是通過eglApi獲得一個EGLSurface,EGLSurface封裝了一個繪圖表面,進而,通過eglApi將EGLSurface設定為當前渲染視窗,並將繪圖記憶體等資訊進行同步,之後通過RenderThread繪製的時候才能知道是在哪個視窗上進行繪製。這裡主要是跟OpenGL庫對接,所有的操作最終都會歸結到eglApi抽象介面中去。假如,這裡不是Android,是普通的Java平臺,同樣需要相似的操作,進行封裝處理,並繫結當前EGLSurface才能進行渲染,因為OpenGL是一套規範,想要使用,就必須按照這套規範走。之後,再建立一個OpenGLRenderer物件,後面執行OpenGL相關操作的時候,其實就是通過OpenGLRenderer來進行的。

繫結流程
繫結流程

上面的流程走完,有序DrawOp樹已經構建好、記憶體也已分配好、環境及場景也繫結成功,剩下的就是繪製了,不過之前說過,真正呼叫OpenGL繪製之前還有一些合併操作,這是Android硬體加速做的優化,回過頭繼續走draw流程,其實就是走OpenGLRenderer的drawRenderNode進行遞迴處理:

void OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) {
       ... 
           <!--構建deferredList-->
        DeferredDisplayList deferredList(mState.currentClipRect(), avoidOverdraw);
        DeferStateStruct deferStruct(deferredList, *this, replayFlags);
        <!--合併及分組-->
        renderNode->defer(deferStruct, 0);
        <!--繪製layer-->
        flushLayers();
        startFrame();
      <!--繪製 DrawOp樹-->
        deferredList.flush(*this, dirty);
        ...
    }複製程式碼

先看下renderNode->defer(deferStruct, 0),合併操作,DrawOp樹並不是直接被繪製的,而是首先通過DeferredDisplayList進行一個合併優化,這個是Android硬體加速中採用的一種優化手段,不僅可以減少不必要的繪製,還可以將相似的繪製集中處理,提高繪製速度。

void RenderNode::defer(DeferStateStruct& deferStruct, const int level) {  
    DeferOperationHandler handler(deferStruct, level);  
    issueOperations<DeferOperationHandler>(deferStruct.mRenderer, handler);  
}複製程式碼

RenderNode::defer其實內含遞迴操作,比如,如果當前RenderNode代表DecorView,它就會遞迴所有的子View進行合併優化處理,簡述一下合併及優化的流程及演算法,其實主要就是根據DrawOp樹構建DeferedDisplayList,defer本來就有延遲的意思,對於DrawOp的合併有兩個必要條件,

  • 1:兩個DrawOp的型別必須相同,這個型別在合併的時候被抽象為Batch ID,取值主要有以下幾種

      enum OpBatchId {  
          kOpBatch_None = 0, // Don't batch  
          kOpBatch_Bitmap,  
          kOpBatch_Patch,  
          kOpBatch_AlphaVertices,  
          kOpBatch_Vertices,  
          kOpBatch_AlphaMaskTexture,  
          kOpBatch_Text,  
          kOpBatch_ColorText,  
          kOpBatch_Count, // Add other batch ids before this  
      }; 複製程式碼
  • 2:DrawOp的Merge ID必須相同,Merge ID沒有太多限制,由每個DrawOp自定決定,不過好像只有DrawPatchOp、DrawBitmapOp、DrawTextOp比較特殊,其餘的似乎不需要考慮合併問題,即時是以上三種,合併的條件也很苛刻

在合併過程中,DrawOp被分為兩種:需要合的與不需要合併的,並分別快取在不同的列表中,無法合併的按照型別分別存放在Batch mBatchLookup[kOpBatch_Count]中,可以合併的按照型別及MergeID儲存到TinyHashMap<mergeid_t, DrawBatch> mMergingBatches[kOpBatch_Count]中,示意圖如下:

DrawOp合併操作.jpg
DrawOp合併操作.jpg

合併之後,DeferredDisplayList Vector mBatches包含全部整合後的繪製命令,之後渲染即可,需要注意的是這裡的合併並不是多個變一個,只是做了一個集合,主要是方便使用各資源紋理等,比如繪製文字的時候,需要根據文字的紋理進行渲染,而這個時候就需要查詢文字的紋理座標系,合併到一起方便統一處理,一次渲染,減少資源載入的浪費,當然對於理解硬體加速的整體流程,這個合併操作可以完全無視,甚至可以直觀認為,構建完之後,就可以直接渲染,它的主要特點是在另一個Render執行緒使用OpenGL進行繪製,這個是它最重要的特點。而mBatches中所有的DrawOp都會通過OpenGL被繪製到GraphicBuffer中,最後通過swapBuffers通知SurfaceFlinger合成。

總結

軟體繪製同硬體合成的區別主要是在繪製上,記憶體分配、合成等整體流程是一樣的,只不過硬體加速相比軟體繪製演算法更加合理,同時減輕了主執行緒的負擔。

作者:看書的小蝸牛
理解Android硬體加速的小白文

僅供參考,歡迎指正

相關文章