学徒浅析Android——getDrawingCache()返回为NULL的原因

本篇文章已授权微信公众号 guolin_blog(郭霖)独家发布      

       在针对WebView使用getDrawingCache()进行当前屏幕截图时,发现返回的是NULL,同时在控制台看到logcat提示了一条异常信息,意思就是当前视图大小已经超过了缓存容量。内容如下:

02-01 14:21:33.512 3461-3461/com.example.sample.job W/View: View too large to fit into drawing cache, needs 9338145 bytes, only 3686400 available

      但是自己第一印象是一屏内容不可能占这么多空间啊,于是搜各种方法修改,包括关闭硬件加速器等,但是还是不了了之,难道是自己打开的方式不对?最后在观摩诸位截屏大佬的布局时,才想明白我用的是ScrollView嵌套WebView啊。这样的布局结构导致我在使用getDrawingCache()时,系统因缓存超量直接取消了写入操作,drawingcache中成为了空值。下面给大家分享下这次问题的前世今生。

      Android的截图手段有四五种了,尤其是在5.0以后,google提供了更好了截图方式,在这里:https://github.com/weizongwei5/AndroidScreenShot_SysApi。

     getDrawingCache()是其中一种截图手段,使用方便,主要针对应用内截图,用法如下:

    view.setDrawingCacheEnabled(true);
    view.buildDrawingCache();//这句话可加可不加,因为getDrawingCache()执行的主体就是buildDrawingCache()
    Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache(), 0, 0, view.getMeasuredWidth(), view.getMeasuredHeight() - view.getPaddingBottom());
    view.setDrawingCacheEnabled(false);
    view.destroyDrawingCache();
    return bitmap;

       getDrawingCache()自身实际上是执行getDrawingCache(false),buildDrawingCache()自身实际上是执行buildDrawingCache(false),而getDrawingCache(false)操作的实体又是buildDrawingCache(false),即get操作的执行顺序也是先build再get,两个方法最终都会执行buildDrawingCacheImpl(false)来实现drawingcache的写入getDrawingCache()只是多了一步获取drawingcache对象的操作,这里的入参false都指的是自动缩放标记,用以指导是否自动适配当前屏幕的大小的,android默认是false。你也可以调用set方法把它改成true,但是会导致你的页面缓存值更大,下面是我尝试设置为true时得到的反馈信息:

buildDrawingCache(true):
02-02 17:38:11.614 5825-5825/com.example.sample.job W/View: View too large to fit into drawing cache, needs 10022400 bytes, only 3686400 available
buildDrawingCache(false):
02-02 17:44:12.725 10069-10069/com.example.sample.job W/View: View too large to fit into drawing cache, needs 9803520 bytes, only 3686400 available

        具体的实现我们可以看下android源码:

public void buildDrawingCache() {
        buildDrawingCache(false);
    }
	
    public Bitmap getDrawingCache() {
        return getDrawingCache(false);
    }
	
    public Bitmap getDrawingCache(boolean autoScale) {
        if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) {
            return null;
        }
        if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {
            buildDrawingCache(autoScale);
        }
        return autoScale ? mDrawingCache : mUnscaledDrawingCache;
    }   
	/* 

If you call {@link #buildDrawingCache()} manually without calling * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you * should cleanup the cache by calling {@link #destroyDrawingCache()} afterwards.

*

You should avoid calling this method when hardware acceleration is enabled. If * you do not need the drawing cache bitmap, calling this method will increase memory * usage and cause the view to be rendered in software once, thus negatively impacting * performance.

*/ public void buildDrawingCache(boolean autoScale) { if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ? mDrawingCache == null : mUnscaledDrawingCache == null)) { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "buildDrawingCache/SW Layer for " + getClass().getSimpleName()); } try { buildDrawingCacheImpl(autoScale); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } } private void buildDrawingCacheImpl(boolean autoScale) { mCachingFailed = false; int width = mRight - mLeft; int height = mBottom - mTop; final AttachInfo attachInfo = mAttachInfo; final boolean scalingRequired = attachInfo != null && attachInfo.mScalingRequired; if (autoScale && scalingRequired) { width = (int) ((width * attachInfo.mApplicationScale) + 0.5f); height = (int) ((height * attachInfo.mApplicationScale) + 0.5f); } final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor; final boolean opaque = drawingCacheBackgroundColor != 0 || isOpaque(); final boolean use32BitCache = attachInfo != null && attachInfo.mUse32BitDrawingCache; final long projectedBitmapSize = width * height * (opaque && !use32BitCache ? 2 : 4); final long drawingCacheSize = ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize(); if (width <= 0 || height <= 0 || projectedBitmapSize > drawingCacheSize) { if (width > 0 && height > 0) { Log.w(VIEW_LOG_TAG, getClass().getSimpleName() + " not displayed because it is" + " too large to fit into a software layer (or drawing cache), needs " + projectedBitmapSize + " bytes, only " + drawingCacheSize + " available"); } destroyDrawingCache(); mCachingFailed = true; return; } ..检测drawingCache原有数据操作.. try { bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(), width, height, quality); bitmap.setDensity(getResources().getDisplayMetrics().densityDpi); if (autoScale) { mDrawingCache = bitmap; } else { mUnscaledDrawingCache = bitmap; } if (opaque && use32BitCache) bitmap.setHasAlpha(false); } catch (OutOfMemoryError e) { // If there is not enough memory to create the bitmap cache, just // ignore the issue as bitmap caches are not required to draw the // view hierarchy if (autoScale) { mDrawingCache = null; } else { mUnscaledDrawingCache = null; } mCachingFailed = true; return; } ..执行Bitmap写入autoScale ? mDrawingCache : mUnscaledDrawingCache操作.. }

      从以上源码中,可以进一步解读到getDrawingcache为NULL的生成条件,同时也发现了logcat之前反馈的日志模板(如上图红字所述),条件共有四个:
      1、(mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING为true
      2、没有设置setDrawingCacheEnabled(true)
      3、width <= 0 || height <= 0 || projectedBitmapSize > drawingCacheSize为true
      4、OutOfMemory
      除了第一个条件,其他的都是buildDrawingCache执行时才会触发,鉴于我看到的log是由条件三触发的,那么可以断定,我的问题出在条件三上。下面来分析下条件三。既然子布局可以正常显示,那么一定是满足width>0和height>0的, drawingCacheSize肯定是一个固定值,这个从之前的警告日志中也能看出,就是当前设备系统所允许的最大绘制缓存值。projectedBitmapSize的计算方式为width * height * (opaque && !use32BitCache ? 2 : 4),顾名思义,就是当前计划缓存的图片大小,(opaque && !use32BitCache ? 2 : 4)不可能为0,也不可能是导致计划缓存值变大的主因,width就是屏幕的宽,这个没有变动的条件,那么可以肯定就是height出现了异常,对于视图高度的计算,android源码表示如下:

/**
     * The distance in pixels from the top edge of this view's parent
     * to the top edge of this view.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "layout")
    protected int mTop;
    /**
     * The distance in pixels from the top edge of this view's parent
     * to the bottom edge of this view.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "layout")
    protected int mBottom;
	
    /**
     * Return the height of your view.
     *
     * @return The height of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getHeight() {
        return mBottom - mTop;
    }

       从上可知,一个View的高度getHeight()就是底-高,其中mBottom指的是视图自身的底边到父视图顶边的距离,mTop指的是视图自身的顶边到父视图顶边的距离。如果子视图充满父视图,那么此时的getHeight()实际就可以看做是父视图的高。正常的布局中,父视图会充满屏幕,我们对子视图设置layout_height="match_parent",即表示子视图充满父布局,此时getHeight()返回的值基本小于等于设备屏幕的高度,但是在ScrollView中,设置它的子View为match_parent是没用的(并且一般设置的都是wrap_content),ScrollView的高度会随着子View高度变化而变化,此时意味着getHeight()返回的不再是屏幕可见范围的高度,而是整个加载出来的实际页面内容高度。这个数值会等同于WebView的getContentHeight()。所以在计算projectedBitmapSize时直接超了最大缓存值。此时执行的截屏操作,更像是长图截屏,将用户未看见的图片一并截取。综上所述,我遇到的getDrawingCache返回为NULL的原因是布局问题,使用了scrollview+webview的布局方式,导致在采用buildDrawingCache方法进行截图。getHeight()获得的数值远大于当前屏幕高度。

        不过举一反三的话,针对应用内的长图截屏操作(即不包含手机状态栏),可以事先在视图外部嵌套一层scrollview,使用canvas直接复制视图内容的方式实现,自己试了一下,效果还不错,大家可以试试,代码如下:

    Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    if (Build.VERSION.SDK_INT >= 11) {
        view.measure(View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(view.getHeight(), View.MeasureSpec.EXACTLY));
        view.layout((int) view.getX(), (int) view.getY(), (int) view.getX() + view.getMeasuredWidth(), (int) view.getY() + view.getMeasuredHeight());
    } else {
        view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
    }
    view.draw(canvas);
    return bitmap;

         最后谈一下,另外三个条件,条件一View提供了一个setWillNotCacheDrawing方法可以用来改变(mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING标记,但实际上这个判断组合默认是false的,你可以通过调用willNotCacheDrawing()方法进行检测;条件二是调用buildDrawingCache的必要条件,如果没有设置的话,buildDrawingCache是无法调用的;条件四,buildDrawingCache在写入操作对OutOfMemory进行异常捕获,所加的注解中却只是轻描淡写,用了“just ignore the issue”这样的描述,看来对这个问题的出现习以为常,一旦出现这个异常,就结束操作,并且置空mDrawingCache/mUnscaledDrawingCache,这个条件要是触发的话不好立刻定位到,毕竟没有直白的日志提示,要是遇到只能一点点排查了。针对条件四,我好奇的是为什么没有在return之前执行destroyDrawingCache()操作,毕竟是真的在执行写入操作啊,而条件二还没执行写入操作,但在判断projectedBitmapSize > drawingCacheSize异常后却先于return执行了destroyDrawingCache()操作,甚是奇怪。不知道大家有什么看法?这样也只能要求我们在调用getDrawingCache()后一定要调用一下destroyDrawingCache()方法。


        by the way,祝大家阖家幸福,春节快乐。



你可能感兴趣的:(Android开发)