Hiten's Blog.

Android硬件位图填坑之获取硬件画布

字数统计: 2.4k阅读时长: 10 min
2019/01/21 Share

前言

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的写法如下:

1
2
3
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控件;

1
imageView.setImageBitmap(bitmap)

一行代码搞定imageView没错,这行代码一般情况下是没有问题的,那么问题在哪里?

首先,硬件位图只支持GPU的绘制,言外之意是这个ImageView必须在开启硬件加速的Activity中,而且当前这个ImageView不能设置软件层 (software layer type);

  • 开启硬件加速的代码

    1
    2
    3
    4
    5
    6
    7
    //application级别开启硬件加速
    <application android:hardwareAccelerated="true">
    <activity ..../>
    </>

    //activity级别开启硬件加速
    <activity android:hardwareAccelerated="true"/>
  • 在View 上使用software layer type

    1
    2
    3
    ImageView imageView = …
    imageView.setImageBitmap(hardwareBitmap);
    imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

如果我们满足硬件加速和不设置software layer type这两个条件,在正真使用中还有坑,其中最大的也最频繁发生的就是通过Canvas来改变Bitmap的形状或者其他的转换;

拿圆形图片做例子

假设我们需要显示圆形图片,一般解决方案有两种:通过自定义控件处理和通过Glide等工具类直接剪裁Bitmap;当Bitmap剪裁遇到HARDWARE就是问题的开始;

  1. 通过自定义控件比如CircleImageView的方案,onDraw()方法如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @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的方式剪裁圆形图片,基本代码如下:
    1
    2
    3
    4
    5
    6
    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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()

1
2
3
4
5
6
7
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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构造方法

1
2
3
4
5
6
7
8
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()

1
2
3
4
5
6
7
/**
* @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的代码:

1
2
3
4
5
6
7
8
第一行,创建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()

1
2
3
public static Bitmap createHardwareBitmap(RenderNode node, int width, int height) {
return nCreateHardwareBitmap(node.getNativeDisplayList(), width, height);
}

该方法根据传入的RenderNode创建一个硬件加速的Bitmap并返回,要求传入的这个node必须是根root,在这里,一个完整的获取替换Canvas的流程应该是这样;

1
2
3
4
5
6
7
8
9
10
第一行,创建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,基本调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建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的创建流程,关于硬件加速的更详细的介绍,推荐大家看这篇文章https://www.jianshu.com/p/40f660e17a73

CATALOG
  1. 1. 前言
  2. 2. 何如使用Hardware Bitmap
    1. 2.1. 创建Hardware Bitmap
    2. 2.2. 使用Hardware Bitmap
  3. 3. 分析Canvas流程
    1. 3.1. drawSoftware如何获得Canvas
    2. 3.2. View体系硬件加速Canvas创建过程
  4. 4. 尝试创建一个Canvas
  5. 5. 总结