Android 圖形渲染應用層簡述
一. 圖形渲染方式
Android 影象渲染有兩種方式一是 CPU 渲染, 另一種是 GPU 渲染
一) CPU 渲染
CPU 渲染稱之為軟體繪製, Android CPU 渲染引擎框架為 Skia , 它是一款在底端裝置上呈現高質量的 2D 跨平臺圖形框架, Google 的 Chrome、Flutter 內部都有使用這個圖形渲染框架
二) GPU 渲染
GPU 渲染稱之為硬體繪製(即開啟硬體加速)
1. OpenGL
市面上最常用於圖形渲染的引擎莫過於 OpenGL 了, Android 系統架構中的外部連結庫中有 OpenGL ES 的依賴, 並且提供了應用層的 API, 用於做高效能的 2D/3D 圖形渲染, Android 中對 OpenGL 的支援如下
OpenGL 版本支援
Android 版本 | OpenGL ES 支援 |
---|---|
Android 1.0 | OpenGL ES 1.0、1.1 |
Android 2.2 | OpenGL ES 2.0 |
Android 4.3 | OpenGL ES 3.0 |
Android 5.0 | OpenGL ES 3.1 |
Android 7.0 | OpenGL ES 3.2 |
OpenGL API 支援

OpenGL API 支援
2. Vulkan
Android 7.0 之後除了新增 OpenGL ES3.2 的支援, 同時添加了 Vulkan 影象引擎, Vulkan 是用於高效能 3D 圖形的低開銷、跨平臺 API, 它與 OpenGL 不同, 它被新增到 Android 執行時庫中, 目前支援面稍窄
二. 圖性渲染元件
Android 的圖形渲染是一個生產者消費者模型, Android 圖形渲染的元件結構圖如下

圖形渲染模型
影象生產者
生產者為 Media Player 視訊解碼器, OpenGL ES 等產生的影象快取資料, 他們通過 BufferData 的形式傳遞到緩衝佇列中
影象消耗者
影象資料的消耗者主要是 SurfaceFlinger, 該系統服務會消耗 Surface 提供的資料緩衝流, 並且使用視窗管理器中提供的資料, 把他們合併輸出繪製到螢幕上
視窗管理器
控制視窗的 Android 系統服務,它是檢視容器。視窗總是由 Surface 提供支援。該服務會監督生命週期、輸入和聚焦事件、螢幕方向、轉換、動畫、位置、變形、Z-Order 以及視窗的其他許多方面。視窗管理器會將所有視窗元資料傳送到 SurfaceFlinger,以便 SurfaceFlinger 可以使用該資料在顯示部分合成 Surface。
各個元件之間的對映關係
-
畫筆: Skia 和 OpenGL, 我們通過 Canvas API 進行繪製最終都會呼叫到外部連結庫的 Skia 和 OpenGL
- Skia: 2D 影象繪製, 關閉硬體加速時使用該引擎
- OpenGL: 2D/3D 影象繪製, 開啟硬體加速時使用該引擎
-
畫紙: Surface, Android 中所有的元素都在 Surface 這張畫紙上進行繪製
- Window 是 View 的容器, 每個 Window 會關聯一個 Surface
- WindowManager 用於管理所有的 Window
- 它將 Window 的 Surface 資訊傳遞給 Graphic Buffer
- 將 Window 其他資料資訊傳送給 SurfaceFlinger
-
畫板: Graphic Buffer, 它用於影象資料的緩衝, 將資料傳送給 SurfaceFlinger 進行繪製
- 4.1 之前使用雙緩衝機制, 4.1 之後使用三緩衝機制
-
顯示: SurfaceFlinger, 它將 WindowManager 提供的所有的 Surface, 通過硬體合成輸出到螢幕上
三. 影象渲染流程
熟悉 View 繪製的三大流程可知, View 的繪製發起在 ViewRootImpl 的 performDraw 中, 我們直接從這裡分析
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { private void performTraversals(){ if (!cancelDraw && !newSurface) { ...... // 呼叫了 performDraw performDraw(); } else { ...... } } private void performDraw() { try { // 執行繪製 boolean canUseAsync = draw(fullRedrawNeeded); ...... } finally { ...... } } private boolean draw(boolean fullRedrawNeeded) { ...... final Rect dirty = mDirty; if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { // 若開啟了硬體加速, 則使用 OpenGL 的 ThreadedRenderer 進行繪製 if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) { ...... mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback); } else { // 若我們沒有開啟硬體加速, 則呼叫 drawSoftware if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) { return false; } } } ....... } }
好的可以看到, View 的繪製, 可能有兩種實現方式
- 呼叫 ThreadedRenderer.draw() 進行 GPU 繪製
- 呼叫 ViewRootImpl.drawSoftware() 進行 CPU 繪製
GPU 繪製又稱之為 硬體加速繪製 , 在 Android 4.0 之後是系統預設開啟的, 我們先分析硬體繪製原理
一) 硬體繪製
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { private boolean draw(boolean fullRedrawNeeded) { ...... final Rect dirty = mDirty; if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) { ...... mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback); } } ....... } }
從 ViewRootImpl.draw 的程式碼中, 我們知道硬體繪製只有在 mAttachInfo 的 mThreadedRenderer 有效的情況下才會執行
因此在想了解硬體繪製流程之前, 需要搞清楚 mThreadedRenderer 它是如何初始化並且賦值的, 也就是說硬體繪製是如何開啟的?
1. 硬體繪製的開啟
這需要從 ViewRootImpl 的建立說起, ViewRootImpl 是用於管理 View 樹與其依附 Window 的一個媒介, 當我們呼叫 WindowManager.addView 時便會建立一個 ViewRootImpl 來管理即將新增到 Window 中的 View
public final class WindowManagerGlobal { public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ViewRootImpl root; synchronized (mLock) { // 建立了 ViewRootImpl 的例項 root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); // 新增到快取中 mViews.add(view); mRoots.add(root); mParams.add(wparams); try { // 將要新增的檢視新增到 ViewRootImpl 中 root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { ...... } } } } public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { final View.AttachInfo mAttachInfo; public ViewRootImpl(Context context, Display display) { ...... // 構建了一個 View.AttachInfo 用於描述這個 View 樹與其 Window 的依附關係 mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); ...... } public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { if (mSurfaceHolder == null) { // 根據 attrs 判讀是否開啟硬體加速 enableHardwareAcceleration(attrs); } } private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) { mAttachInfo.mHardwareAccelerated = false; mAttachInfo.mHardwareAccelerationRequested = false; ...... final boolean hardwareAccelerated = (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0; if (hardwareAccelerated) { ...... if (fakeHwAccelerated) { ...... } else if (!ThreadedRenderer.sRendererDisabled || (ThreadedRenderer.sSystemRendererDisabled && forceHwAccelerated)) { ...... // 建立硬體加速的渲染器 mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent, attrs.getTitle().toString()); mAttachInfo.mThreadedRenderer.setWideGamut(wideGamut); if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mHardwareAccelerated = mAttachInfo.mHardwareAccelerationRequested = true; } } } } }
好的, 這個硬體加速渲染器是通過 WindowManager.LayoutParams 來決定的, 他會給 mAttachInfo 的 mThreadedRenderer 屬性建立一個渲染器執行緒的描述
好的, 有了 mThreadedRenderer 這個物件, 接下來就可以探索 mThreadedRenderer.draw 是如何進行硬體繪製的了
2. 硬體繪製流程
硬體渲染開啟之後, 在 ViewRootImpl.draw 中就會執行 mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback); 進行 View 的繪製
public final class ThreadedRenderer { void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks, FrameDrawingCallback frameDrawingCallback) { ...... // 1. 構建根檢視的渲染資料 updateRootDisplayList(view, callbacks); ...... // 2. 通知 RenderThread 執行緒繪製 int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length); ...... } private void updateRootDisplayList(View view, DrawCallbacks callbacks) { // 1.1 構建 View 樹的渲染資料 updateViewTreeDisplayList(view); // 1.2 檢視需要更新 || 當前的根渲染器中的資料已經無效了 if (mRootNodeNeedsUpdate || !mRootNode.isValid()) { // 構建當前 Window 對應的 Surface 的畫筆 DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight); try { ...... // 讓 Surface 畫筆的資料指向根檢視 DecorView 中的資料 canvas.drawRenderNode(view.updateDisplayListIfDirty()); ...... // 表示當前 Window 的檢視資料更新完畢, 不需要更新了 mRootNodeNeedsUpdate = false; } finally { // 將 Canvas 畫筆中的資料, 儲存到渲染器中 mRootNode.end(canvas); } } } private void updateViewTreeDisplayList(View view) { // 在根 View 的 Flag 中新增一個 Drawn 指令 view.mPrivateFlags |= View.PFLAG_DRAWN; // 若根檢視被設定了 INVALIDATE, 則說明需要重新構建顯示列表 view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED; view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; // 如果 View 的顯示區域已經無效了, 則更新 View 的顯示列表 view.updateDisplayListIfDirty(); ...... } }
好的, 可以看到 ThreadedRenderer 主要進行了兩個操作
- 呼叫 updateRootDisplayList 構建根檢視的渲染資料
- 呼叫 View.updateViewTreeDisplayList(), 更新 View 樹的渲染器資料
- 將 View 樹的渲染器資料儲存到當前 Window 的畫布 Surface 渲染器中
- 將 Surface 渲染器中的資料傳送到 RenderThread 進行真正的渲染
好的, 可以看到這裡呼叫了 View.updateViewTreeDisplayList() 對 View 樹的渲染資料的進行構建, 接下來我們就看看他是如何操作的
View 的渲染資料的構建
public class View { final RenderNode mRenderNode; public View(Context context) { ...... // 可見在硬體繪製中, 每一個 View 對應著一個渲染器中的結點 mRenderNode = RenderNode.create(getClass().getName(), this); ...... } public RenderNode updateDisplayListIfDirty() { final RenderNode renderNode = mRenderNode; ..... if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.isValid() || (mRecreateDisplayList)) { // 1. 當前 View 渲染器中資料依舊是有效的 && 沒有要求重繪 if (renderNode.isValid() && !mRecreateDisplayList) { // 1.2 將更新 Render 的操作分發給子 View, 該方法會在 ViewGroup 中重寫 dispatchGetDisplayList(); // 1.3 直接跳過使用 Canvas 繪製的操作 return renderNode; } // 2. 走到這裡說明當前 View 的渲染資料已經失效了, 需要重新構建渲染資料 mRecreateDisplayList = true; int width = mRight - mLeft; int height = mBottom - mTop; int layerType = getLayerType(); // 2.1 通過渲染器獲取一個 Canvas 畫筆, 畫筆的可作用的區域為 width, height final DisplayListCanvas canvas = renderNode.start(width, height); try { if (layerType == LAYER_TYPE_SOFTWARE) { ...... } else { ...... // 2.2 當前 View 的 Draw 可跳過, 直接分發去構建子 View 渲染資料 if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { dispatchDraw(canvas); } else { // 2.3 繪製自身 draw(canvas); } } } finally { // 2.4 表示當前 View 的渲染資料已經儲存在 Canvas 中了 // 將它的資料傳遞到渲染器 renderNode.end(canvas); } } else { ...... } return renderNode; } }
好的, View.updateDisplayListIfDirty 方法從名字上來理解是 若 View 展示列表無效了則更新它, 事實上它做的也是如此, 只不過這個 DisplayList 稱之為 渲染資料 更為合適, 它主要做了如下操作
- 當前 View 渲染資料依舊是有效的 並且沒有要求重繪
- 將更新渲染資料的操作分發給子 View
- 遍歷結束之後直接返回 當前現有的渲染器結點物件
- 當前 View 渲染資料無效了
- 通過渲染器構建可渲染區域的畫筆 Canvas
- 呼叫 view.draw 進行渲染資料的構建
- 這個方法留在軟體渲染的時候再分析
- 當前 View 的渲染資料重新構建好了, 則將它儲存在渲染器 renderNode 中
好的, DecorView 的 updateDisplayListIfDirty 操作完成之後, 當前 Window 的 Surface 中所有的渲染資料就更新了, 之後再呼叫 ThreadedRenderer.nSyncAndDrawFrame 就可以將資料傳送到 SurfaceFlinger 提供的 Graphic Buffer 中等待其展示到螢幕上了
3.硬體繪製流程圖

硬體繪製流程圖
- 當呼叫 View.draw 時首先獲取當前 Window 的畫布 Surface
- 獲取當前畫布 Surface 的渲染器 RenderNode, 通過他來構建一個畫筆 Canvas
- View.updateDisplayListIfDirty 完成後, 畫筆 Canvas 便會將資料輸出到其對應的渲染器中
- 最終 Window 的 Surface 畫布中便會儲存 DecorView 的渲染資料
- GPU 從 SurfaceFlinger 託管的 BufferQueue 中獲取一個 Graphic Buffer
- Surface 將渲染器中的資料傳送到 GPU, GPU 將渲染資料柵格化到 Graphic Buffer 中
- GPU 將 Graphic Buffer 重新發送給 SurfaceFlinger 的 BufferQueue 中
- SurfaceFlinger 將柵格化的資料傳送給螢幕呈現出來
二) 軟體渲染
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { private boolean draw(boolean fullRedrawNeeded) { ...... final Rect dirty = mDirty; if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) { ...... } else { // 若我們沒有開啟硬體加速, 則呼叫 drawSoftware if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) { return false; } } } ....... } }
可以看到軟體渲染呼叫了 drawSoftware 方法, 接下來我們繼續探究軟體渲染是如何執行的
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { final Canvas canvas; try { ...... // 1. 通過 Surface 的 lock 操作, 獲取一個畫筆, 這個畫筆是 Skia 的上層封裝 canvas = mSurface.lockCanvas(dirty); ...... } ...... try { ...... try { ...... // 2. 呼叫了 DecorView 的 draw 方法 mView.draw(canvas); ...... } finally { ....... } } finally { // 3. 解鎖畫筆, 將資料傳送給 SurfaceFlinger surface.unlockCanvasAndPost(canvas); ...... } return true; } }
好的, 可以看到軟體繪製主要有三部
- 首先是通過 Surface 的 lockCanvas 獲取一個畫筆 Canvas, 它是 Android 2D 影象庫 Skia 的一個上層封裝
- 然後呼叫了 View 的 draw 方法
- 最後呼叫了 unlockCanvasAndPost 解鎖畫筆, 將資料同步給 SurfaceFinger 緩衝區, 進行渲染
public class View { public void draw(Canvas canvas) { ...... /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * *1. Draw the background *2. If necessary, save the canvas' layers to prepare for fading *3. Draw view's content *4. Draw children *5. If necessary, draw the fading edges and restore layers *6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) { onDraw(canvas); } // Step 4, draw the children dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); return; } ...... } }
好的, 很簡單這就是 View 的繪製流程的分發, 這裡不再贅述了
軟體繪製流程圖

軟體繪製流程圖
- View 想要繪製的時候, 首先要獲取 Window 對應的 Surface 畫紙
- 通過 Surface.lock 操作獲取一個 Canvas 畫筆, 這個 Canvas 是 Skia 的上層封裝
- 使用 Skia 畫筆在 Surface 畫紙上進行繪製
- SurfaceFlinger 會託管一個 BufferQueue, 我們從 BufferQueue 中獲取到 Graphic Buffer 畫板
- 將我們使用 Skia 畫筆在 Surface 畫紙上繪製的內容柵格化到 Graphic Buffer 畫板上
- 將填充了資料畫板傳送給 SurfaceFlinger 進行繪製
- SurfaceFlinger 將畫板內容渲染到手機螢幕上
四. 硬體繪製與軟體繪製差異
從渲染機制
- 硬體繪製使用的是 OpenGL/ Vulkan, 支援 3D 高效能圖形繪製
- 軟體繪製使用的是 Skia, 僅支援 2D 圖形繪製
渲染效率上
-
硬體繪製
- 在 Android 5.0 之後引入了 RendererThread, 它將 OpenGL 圖形柵格化的操作全部投遞到了這個執行緒
- 硬體繪製會跳過渲染資料無變更的 View, 直接分發給子檢視
-
軟體繪製
- 在將資料投入 SurfaceFlinger 之前, 所有的操作均在主執行緒執行
- 不會跳過無變化的 View
因此硬體繪製較之軟體繪製會更加流暢
從內測消耗上
硬體繪製消耗的記憶體要高於軟體繪製, 但在當下大記憶體手機時代, 用空間去換時間還是非常值得的
從相容性上
- 硬體繪製的 OpenGL 在各個 Android 版本的支援上會有一些不同, 常有因為相容性出現的系統 bug
- 軟體繪製的 Skia 庫從 Android 1.0 便屹立不倒, 因此它的相容性要好於硬體繪製
五. 總結
至此, 對 Andorid 圖形渲染的操作, 總體上有了一些輪廓, 上述程式碼都在應用框架層, 對於 SurfaceFlinger 與當前程序的通訊以及渲染原理可以參考 老羅的文章 , 筆者從中受益良多