硬件加速,直觀上說就是依賴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; }
默認情況下Skia的繪製沒有采用GPU渲染的方式(雖然Skia也能用GPU渲染),也就說默認drawSoftware工作完全由CPU來完成,不會牽扯到GPU的操作,但是8.0之後,Google逐漸加重了Skia,開始讓Skia接手OpenGL,間接統一調用,將來還可能是Skia同Vulkan的結合,不過這裡不是重點。重點看下HardwareRenderer所進行的硬件加速繪製。
HardwareRenderer硬件加速繪製模型
開頭說過,硬件加速繪製包括兩個階段:構建階段+繪製階段,所謂構建就是遞歸遍歷所有視圖,將需要的操作緩存下來,之後再交給單獨的Render線程利用OpenGL渲染。在Android硬件加速框架中,View視圖被抽象成RenderNode節點,View中的繪製都會被抽象成一個個DrawOp(DisplayListOp),比如View中drawLine,構建中就會被抽象成一個DrawLintOp,drawBitmap操作會被抽象成DrawBitmapOp,每個子View的繪製被抽象成DrawRenderNodeOp,每個DrawOp有對應的OpenGL繪製命令,同時內部也握著繪圖所需要的數據。如下所示:
如此以來,每個View不僅僅握有自己DrawOp List,同時還拿著子View的繪製入口,如此遞歸,便能夠統計到所有的繪製Op,很多分析都稱為Display List,源碼中也是這麼來命名類的,不過這裡其實更像是一個樹,而不僅僅是List,示意如下:
構建完成後,就可以將這個繪圖Op樹交給Render線程進行繪製,這裡是同軟件繪製很不同的地方,軟件繪製時,View一般都在主線程中完成繪製,而硬件加速,除非特殊要求,一般都是在單獨線程中完成繪製,如此以來就分擔了主線程很多壓力,提高了UI線程的響應速度。
知道整個模型後,就代碼來簡單瞭解下實現流程,先看下遞歸構建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); } }
可以看出,對於硬件加速的場景,請求SurfaceFlinger內存分配的時機會稍微提前,而不是像軟件繪製,由Surface的lockCanvas發起,主要目的是:預先分配slot位置,避免在渲染的時候再申請,一是避免分配失敗,浪費了CPU之前的準備工作,二是也可以將渲染線程個工作簡化,減少延時。不過,還是會存在另一個問題,一個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]中,示意圖如下:
合併之後,DeferredDisplayList Vector<Batch * > mBatches 包含全部整合後的繪製命令,之後渲染即可,需要注意的是這裡的合併並不是多個變一個,只是做了一個集合,主要是方便使用各資源紋理等,比如繪製文字的時候,需要根據文字的紋理進行渲染,而這個時候就需要查詢文字的紋理座標系,合併到一起方便統一處理,一次渲染,減少資源加載的浪費,當然對於理解硬件加速的整體流程,這個合併操作可以完全無視,甚至可以直觀認為,構建完之後,就可以直接渲染,它的主要特點是在另一個Render線程使用OpenGL進行繪製,這個是它最重要的特點。而mBatches中所有的DrawOp都會通過OpenGL被繪製到GraphicBuffer中,最後通過swapBuffers通知SurfaceFlinger合成。
總結
軟件繪製同硬件加速的區別主要是在繪製上,內存分配、圖層合成等整體流程是一樣的,只不過硬件加速相比軟件繪製算法更加合理,同時採用單獨的渲染線程,減輕了主線程的負擔。