Android硬體點陣圖填坑之獲取硬體畫布
Hardware Bitmap(硬體點陣圖)
是Android8.0加入的新功能,通過設定Bitmap的config為Bitmap.Config.HARDWARE
,建立所謂的Hardware Bitmap,它不同與其他Config
的Bitmap,Hardware Bitmap對應的畫素資料是儲存在視訊記憶體中,並對圖片僅在螢幕上繪製的場景做了優化;
硬體點陣圖的介紹參考Glide文件
何如使用Hardware Bitmap
建立Hardware Bitmap
眾所周知,Bitmap的建立一般是呼叫BitmapFactory這個工廠類來實現,由於Hardware Bitmap需要配置Bitmap.Config.HARDWARE
屬性,一個基本的獲取用Hardware Bitmap的寫法如下:
val options = BitmapFactory.Options() options.inPreferredConfig = Bitmap.Config.HARDWARE val bitmap = BitmapFactory.decodeResource(resources, R.drawable.dog, options) 複製程式碼
主要是需要設定BitmapFactory.Options
的inPreferredConfig
為Bitmap.Config.HARDWARE
;
針對HARDWARE情況BitmapFactory的提示
如果設定了inPreferredConfig = Bitmap.Config.HARDWARE
,千萬不要設定options.inMutable = true
,這樣會引起報錯,因為Hardware Bitmap
是不可變的,也不能被利用;另外inBitmap
屬性也沒有必要設定,因為硬體點陣圖不需要當前程序的快取複用,如果設定inBitmap
可能會替換掉之前設定的inPreferredConfig
屬性;
使用Hardware Bitmap
通過上一步的建立,我們獲得Bitmap物件,首先我們可以通過bitmap.getConfig()
獲取到當前Bitmap是不是Hardware,其次,大多數情況下,我們是把Bitmap
設定給ImageView
控制元件;
imageView.setImageBitmap(bitmap) 複製程式碼
一行程式碼搞定imageView沒錯,這行程式碼一般情況下是沒有問題的,那麼問題在哪裡?
首先,硬體點陣圖只支援GPU的繪製,言外之意是這個ImageView必須在開啟硬體加速的Activity中,而且當前這個ImageView不能設定軟體層 (software layer type);
- 開啟硬體加速的程式碼
//application級別開啟硬體加速 <application android:hardwareAccelerated="true"> <activity ..../> </> //activity級別開啟硬體加速 <activity android:hardwareAccelerated="true"/> 複製程式碼
- 在View 上使用software layer type
ImageView imageView = … imageView.setImageBitmap(hardwareBitmap); imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 複製程式碼
如果我們滿足硬體加速和不設定software layer type這兩個條件,在正真使用中還有坑,其中最大的也最頻繁發生的就是通過Canvas來改變Bitmap的形狀或者其他的轉換;
拿圓形圖片做例子
假設我們需要顯示圓形圖片,一般解決方案有兩種:通過自定義控制元件處理和通過Glide等工具類直接剪裁Bitmap;當Bitmap剪裁遇到HARDWARE
就是問題的開始;
-
通過自定義控制元件比如
CircleImageView
的方案,onDraw()
方法如下:
@Override protected void onDraw(Canvas canvas) { if (mDisableCircularTransformation) { super.onDraw(canvas); return; } if (mBitmap == null) { return; } if (mCircleBackgroundColor != Color.TRANSPARENT) { canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint); } canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); if (mBorderWidth > 0) { canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint); } } 複製程式碼
這是CircleImageView
重新onDraw()
方法,通過自定義控制元件實現剪下圓角,在設定硬體點陣圖Bitmap時,一般都沒有問題;
-
通過類似
Glide
等直接處理Bitmap
的方式剪裁圓形圖片,基本程式碼如下:
Canvas canvas = new Canvas(resultBitmap); // Draw a circle canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT); // Draw the bitmap in the circle canvas.drawBitmap(inBitmap, null, destRect, CIRCLE_CROP_BITMAP_PAINT); clear(canvas); 複製程式碼
通過類似工具類的形式直接對Bitmap
進行修改,執行到canvas.drawBitmap
就會報異常,異常資訊是java.lang.IllegalStateException: Software rendering doesn't support hardware bitmaps
;
如何避免報異常
我大致想了這麼兩個方案:
-
方案一:所有關於剪下Bitmap的操作都改成自定義控制元件,在自定義控制元件的
onDraw
中實現; -
方案二:尋找一種方案,解決掉自己建立的
Canvas
不報異常,這樣就能繼續用工具類來處理Bitmap
;
方案一技術實現比較簡單,把專案中所用用到處理Bitmap的邏輯都換成自定義控制元件,但是可能涉及到很多處程式碼的修改,是一個功夫活;
方案二實施起來有點障礙,因為除了通過new Canvas(Bitmap)獲取畫布,還能通過什麼方式能拿到Canvas,對了還有SurfaceView也是可以拿到Canvas,但是SurfaceView不支援硬體加速,所以直接就Pass了,想實現方案二我認為得弄清自定義控制元件onDraw()
方法中Canvas
從何而來;
分析Canvas流程
View.onDraw()中的Canvas從何而來
我們知道,View
的繪製流程是從ViewRootImpl.performTraversals()
這個方法開始
performTraversals()
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible; if (!cancelDraw && !newSurface) { if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); } mPendingTransitions.clear(); } //調動performDraw() performDraw(); } else { if (isViewVisible) { // Try again scheduleTraversals(); } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).endChangingAnimations(); } mPendingTransitions.clear(); } } 複製程式碼
performTraversals()
方法呼叫performDraw()
,然後performDraw()
方法中又呼叫draw(fullRedrawNeeded)
,大部門繪製的邏輯都是在draw(fullRedrawNeeded)
方法中;
draw(fullRedrawNeeded)
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) { //省略程式碼 mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this); } else { if (mAttachInfo.mThreadedRenderer != null && !mAttachInfo.mThreadedRenderer.isEnabled() && mAttachInfo.mThreadedRenderer.isRequested()) { //省略程式碼 //drawSoftware if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } } } 複製程式碼
從draw(fullRedrawNeeded)
方法可以看到,如果支援硬體加速,呼叫mAttachInfo.mThreadedRenderer.draw()
方法,否則呼叫drawSoftware()
方法,繪製的基本流程從這裡分叉;
drawSoftware如何獲得Canvas
drawSoftware()
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { // Draw with software renderer. final Canvas canvas; try { final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; final int bottom = dirty.bottom; canvas = mSurface.lockCanvas(dirty); Surface.lockCanvas() //noinspection ConstantConditions if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) { attachInfo.mIgnoreDirtyState = true; } canvas.setDensity(mDensity); } catch (Surface.OutOfResourcesException e) { handleOutOfResourcesException(e); return false; } catch (IllegalArgumentException e) { mLayoutRequested = true; return false; } //省略程式碼 } 複製程式碼
從drawSoftware()
方法可以知道,軟體繪製的流程是從Surface.lockCanvas()
獲得Canvas
物件;
View體系硬體加速Canvas建立過程
ThreadedRenderer.draw()
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) { attachInfo.mIgnoreDirtyState = true; final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer; choreographer.mFrameInfo.markDrawStart(); //呼叫updateRootDisplayList更新DisplayList updateRootDisplayList(view, callbacks); } 複製程式碼
ThreadedRenderer.updateRootDisplayList()
private void updateRootDisplayList(View view, DrawCallbacks callbacks) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()"); updateViewTreeDisplayList(view); if (mRootNodeNeedsUpdate || !mRootNode.isValid()) { //通過RootNode.start建立DisplayListCanvas DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight); try { final int saveCount = canvas.save(); canvas.translate(mInsetLeft, mInsetTop); callbacks.onPreDraw(canvas); canvas.insertReorderBarrier(); canvas.drawRenderNode(view.updateDisplayListIfDirty()); canvas.insertInorderBarrier(); callbacks.onPostDraw(canvas); canvas.restoreToCount(saveCount); mRootNodeNeedsUpdate = false; } finally { //最終呼叫end方法 mRootNode.end(canvas); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); } 複製程式碼
View.updateDisplayListIfDirty()
public RenderNode updateDisplayListIfDirty() { final RenderNode renderNode = mRenderNode; //省略程式碼 int layerType = getLayerType(); //建立DisplayListCanvas final DisplayListCanvas canvas = renderNode.start(width, height); canvas.setHighContrastText(mAttachInfo.mHighContrastText); try { //判斷layerType if (layerType == LAYER_TYPE_SOFTWARE) { buildDrawingCache(true); Bitmap cache = getDrawingCache(true); if (cache != null) { canvas.drawBitmap(cache, 0, 0, mLayerPaint); } } else { computeScroll(); canvas.translate(-mScrollX, -mScrollY); mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; // Fast path for layouts with no backgrounds if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { dispatchDraw(canvas);//dispatchDraw drawAutofilledHighlight(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } if (debugDraw()) { debugDrawFocus(canvas); } } else { //呼叫draw()方法 draw(canvas); } } } finally { renderNode.end(canvas); setDisplayListProperties(renderNode); } } else { mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; } return renderNode; } 複製程式碼
從上面基本流程可以看出,硬體加速下Canvas的建立是呼叫RenderNode.create()
方法,每個View
都有自己的RenderNode
,RenderNode
的建立是在View的構造方法中;
View構造方法
public View(Context context) { mContext = context; mResources = context != null ? context.getResources() : null; mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | FOCUSABLE_AUTO; //省略 mRenderNode = RenderNode.create(getClass().getName(), this); //省略 } 複製程式碼
RenderNode
通過呼叫靜態方法create
得到RenderNode
物件,我們繼續看RenderNode.create()
方法
RenderNode.create()
/** * @param name The name of the RenderNode, used for debugging purpose. May be null. * @return A new RenderNode. */ public static RenderNode create(String name, @Nullable View owningView) { return new RenderNode(name, owningView); } 複製程式碼
create()
方法有兩個引數,第一個name,第二個是owningView,而且是可以為空的,從註釋上來看,name只是為了除錯用,而且owningView可以為空,我們可以用反射去建立一個簡單的RenderNode
;
嘗試建立一個Canvas
回顧一下,寫出一個簡單的建立一個硬體加速Canvas的程式碼:
第一行,建立RenderNode RenderNode node = RenderNode.create("helloworld", null); 第二行,建立DisplayListCanvas final DisplayListCanvas canvas = node.start(bitmapWidth, bitmapHeight); 第三行,執行canvas的操作 canvas.xxx(); 第四行,執行node.end()方法 node.end(canvas); 複製程式碼
一個簡單的DisplayListCanvas
建立流程在腦海中浮現出來,但是還有個問題,我們執行完canvas
的繪製操作之後,生成的產物Bitmap
從哪裡得到,我們回顧和ViewRootImpl
打交道的硬體加速繪製相關的類是ThreadedRenderer
,我們剛才看了這個類的draw()
方法和updateRootDisplayList()
方法,很有意思,它還有一個這個靜態的方法createHardwareBitmap(RenderNode node, int width, int height)
;
ThreadedRenderer.createHardwareBitmap()
public static Bitmap createHardwareBitmap(RenderNode node, int width, int height) { return nCreateHardwareBitmap(node.getNativeDisplayList(), width, height); } 複製程式碼
該方法根據傳入的RenderNode
建立一個硬體加速的Bitmap並返回,要求傳入的這個node
必須是根root,在這裡,一個完整的獲取替換Canvas的流程應該是這樣;
第一行,建立RenderNode RenderNode node = RenderNode.create("helloworld", null); 第二行,建立DisplayListCanvas final DisplayListCanvas canvas = node.start(width, height); 第三行,執行canvas的操作 canvas.xxx(); 第四行,執行node.end()方法 node.end(canvas); 第五行,呼叫createHardwareBitmap生成Bitmap bitmap = ThreadedRenderer.createHardwareBitmap(node,width,height) 複製程式碼
基於上面的虛擬碼分析,我寫了一個避免反射調
優化版的Hardware Canvas
,基本呼叫如下:
//建立HardwareCanvasManager val hardwareCanvasManager = HardwareCanvasManager() try { //獲取canvas val canvas = hardwareCanvasManager.createCanvas(size, size) //畫圓形or其他繪製 canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT); //畫原圖,通過畫筆設定SRC_IN屬性 canvas.drawBitmap(inBitmap, null, destRect, CIRCLE_CROP_BITMAP_PAINT); //得到bitmap val buildBitmap = hardwareCanvasManager.buildBitmap() //將bitmap設定給ImageView iv.setImageBitmap(buildBitmap) } finally { //清理工作 hardwareCanvasManager.clean() } 複製程式碼
總結
這篇水文主要是分析View繪製下Canvas的建立流程,關於硬體加速的更詳細的介紹,推薦大家看這篇文章www.jianshu.com/p/40f660e17… 。