Android硬體點陣圖填坑之獲取硬體畫布

HitenDev發表於2019-01-21

前言

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.OptionsinPreferredConfigBitmap.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就是問題的開始;

  1. 通過自定義控制元件比如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時,一般都沒有問題;

  1. 通過類似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都有自己的RenderNodeRenderNode的建立是在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()
}
複製程式碼

Github傳送門

總結

這篇水文主要是分析View繪製下Canvas的建立流程,關於硬體加速的更詳細的介紹,推薦大家看這篇文章www.jianshu.com/p/40f660e17…

相關文章