硬件加速,直觀上說就是依賴GPU實現圖形繪製加速,同軟硬件加速的區別主要是圖形的繪製到底是GPU來處理仍是CPU,若是是GPU,就認爲是硬件加速繪製,反之,軟件繪製。在Android中也是如此,不過相對於普通的軟件繪製,硬件加速還作了其餘方面優化,不只僅限定在繪製方面,繪製以前,在如何構建繪製區域上,硬件加速也作出了很大優化,所以硬件加速特性能夠從下面兩部分來分析:java
不管是軟件繪製仍是硬件加速,繪製內存的分配都是相似的,都是須要請求SurfaceFlinger服務分配一塊內存,只不過硬件加速有可能從FrameBuffer硬件緩衝區直接分配內存(SurfaceFlinger一直這麼幹的),二者的繪製都是在APP端,繪製完成以後一樣須要通知SurfaceFlinger進行合成,在這個流程上沒有任何區別,真正的區別在於在APP端如何完成UI數據繪製,本文就直觀的瞭解下二者的區別,會涉及部分源碼,但不求甚解。node
大概從Android 4.+開始,默認狀況下都是支持跟開啓了硬件加速的,也存在手機支持硬件加速,可是部分API不支持硬件加速的狀況,若是使用了這些API,就須要主關閉硬件加速,或者在View層,或者在Activity層,好比Canvas的clipPath等。可是,View的繪製是軟件加速實現的仍是硬件加速實現的,通常在開發的時候並不可見,那圖形繪製的時候,軟硬件的分歧點究竟在哪呢?舉個例子,有個View須要重繪,通常會調用View的invalidate,觸發重繪,跟着這條線走,去查一下分歧點。android
從上面的調用流程能夠看出,視圖重繪最後會進入ViewRootImpl的draw,這裏有個判斷點是軟硬件加速的分歧點,簡化後以下算法
ViewRootImpl.javacanvas
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庫,進行圖形繪製,bash
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所進行的硬件加速繪製。多線程
開頭說過,硬件加速繪製包括兩個階段:構建階段+繪製階段,所謂構建就是遞歸遍歷全部視圖,將須要的操做緩存下來,以後再交給單獨的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是整個硬件加速繪製的入口,實現是一個ThreadedRenderer對象,從名字能看出,ThreadedRenderer應該跟一個Render線程息息相關,不過ThreadedRenderer是在UI線程中建立的,那麼與UI線程也一定相關,其主要做用:
可見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遞歸構建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線程進行渲染。
DrawOp樹構建完畢後,UI線程利用RenderProxy向RenderThread線程發送一個DrawFrameTask任務請求,RenderThread被喚醒,開始渲染,大體流程以下:
不過再這以前先複習一下繪製內存的由來,畢竟以前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 }; 複製代碼
在合併過程當中,DrawOp被分爲兩種:須要合的與不須要合併的,並分別緩存在不一樣的列表中,沒法合併的按照類型分別存放在Batch mBatchLookup[kOpBatch_Count]中,能夠合併的按照類型及MergeID存儲到TinyHashMap<mergeid_t, DrawBatch> mMergingBatches[kOpBatch_Count]中,示意圖以下:
合併以後,DeferredDisplayList Vector mBatches包含所有整合後的繪製命令,以後渲染便可,須要注意的是這裏的合併並非多個變一個,只是作了一個集合,主要是方便使用各資源紋理等,好比繪製文字的時候,須要根據文字的紋理進行渲染,而這個時候就須要查詢文字的紋理座標系,合併到一塊兒方便統一處理,一次渲染,減小資源加載的浪費,固然對於理解硬件加速的總體流程,這個合併操做能夠徹底無視,甚至能夠直觀認爲,構建完以後,就能夠直接渲染,它的主要特色是在另外一個Render線程使用OpenGL進行繪製,這個是它最重要的特色。而mBatches中全部的DrawOp都會經過OpenGL被繪製到GraphicBuffer中,最後經過swapBuffers通知SurfaceFlinger合成。
軟件繪製同硬件合成的區別主要是在繪製上,內存分配、合成等總體流程是同樣的,只不過硬件加速相比軟件繪製算法更加合理,同時減輕了主線程的負擔。
做者:看書的小蝸牛
理解Android硬件加速的小白文
僅供參考,歡迎指正