use a recycled bitmap 的发现

 

    最近在做一个产品,里面有个用户指南的功能,该功能就是介绍怎么使用这个APP,然后是一个可以上下滚动的视图。其实就是一张图片。不过由于这张图片很大,所以用户退出这个界面的时候,必须回收资源。就是这个回收资源让我碰到了一问题。引发use a recycled bitmap的操作流程是这样子的,我进入Activity,然后得到图片并且显示出来, 退出时,在onDestroy()方法中recycle掉这个Bitmap对象。然后再次进入此界面,程序就爆了这个错误。

      先看关键代码:   在onCreate(Bundle)中

       

ImageView iv = (ImageView) findViewById(R.id.user_guide_iv);
Drawable draw = getResources().getDrawable(R.drawable.user_guide);
mUserGuideBmp = ((BitmapDrawable) draw).getBitmap();
iv.setImageBitmap(mUserGuideBmp);

 

            然后在 onDestroy()中回收资源
  

if (mUserGuideBmp!= null && !mUserGuideBmp.isRecycled()) {
	mUserGuideBmp.recycle();
	mUserGuideBmp= null;
}

 
    这些看起来都很正常,可是就是报错了。说我用了一个回收过的资源。难道我再次进入这个界面得到的还是我上次用的那个Bitmap的索引?如果是这样的,那么getResources().getDrawable()得到的也应该是我上次得到的Drawable了,因为我的Bitmap是该Drawable对象的成员变量。好吧,看getResources().getDrawable源码:

 

public Drawable getDrawable(int id) throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            return loadDrawable(value, id);
        }
    }

 明显可以看出是是loadDrawable()方法得到Drawable对象,这个方法太多了,我们看下关键性的地方

 

 boolean isColorDrawable = false;
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
                value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
        }
        final long key = isColorDrawable ? value.data :
                (((long) value.assetCookie) << 32) | value.data;

        Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);

        if (dr != null) {
            return dr;
        }

  可以看到有个getCachedDrawable(),看来第二次我们得到的Drawable对象是从缓存中得到的, 而缓存中的Drawable对象的成员变量Bitmap已经被我们给回收了,难怪会报错。知道原因了,就可以改了,我首先想到的清空下缓存,缓存没有了,Android应该会重新创建一个Drawable对象再加入缓存才对。由上面的方法可以看出缓存对象是mDrawableCache。那找下哪几个地方用到了mDrawableCache。不负众望,我在

public void updateConfiguration(Configuration config,DisplayMetrics metrics, CompatibilityInfo compat) 

方法中找到了

 clearDrawableCache(mDrawableCache, configChanges);

 这个方法明显是清空缓存的,从updateConfiguration的代码中可以看出,这个方法一定会被执行。那我们就在获取Drawable之前调用这个方法就可以了,试试

Configuration config = getResources().getConfiguration();
DisplayMetrics metrics = getResources().getDisplayMetrics();
getResources().updateConfiguration(config, metrics);

 其实我们并不是想要更改Configuration,所以传递给updateConfiguration方法的参数还是它原来的参数。

运行一下,报错,还是use a recycled bitmap的错误。晕,那好吧,看下clearDrawableCache方法

 private void clearDrawableCache(
            LongSparseArray<WeakReference<ConstantState>> cache,
            int configChanges) {
        int N = cache.size();
        if (DEBUG_CONFIG) {
            Log.d(TAG, "Cleaning up drawables config changes: 0x"
                    + Integer.toHexString(configChanges));
        }
        for (int i=0; i<N; i++) {
            WeakReference<Drawable.ConstantState> ref = cache.valueAt(i);
            if (ref != null) {
                Drawable.ConstantState cs = ref.get();
                if (cs != null) {
                    if (Configuration.needNewResources(
                            configChanges, cs.getChangingConfigurations())) {
                        if (DEBUG_CONFIG) {
                            Log.d(TAG, "FLUSHING #0x"
                                    + Long.toHexString(mDrawableCache.keyAt(i))
                                    + " / " + cs + " with changes: 0x"
                                    + Integer.toHexString(cs.getChangingConfigurations()));
                        }
                        cache.setValueAt(i, null);
                    } else if (DEBUG_CONFIG) {
                        Log.d(TAG, "(Keeping #0x"
                                + Long.toHexString(cache.keyAt(i))
                                + " / " + cs + " with changes: 0x"
                                + Integer.toHexString(cs.getChangingConfigurations())
                                + ")");
                    }
                }
            }
        }
    }

 

可以看到是下面这段代码才恩清空缓存

if (Configuration.needNewResources(
                            configChanges, cs.getChangingConfigurations())) {
                        if (DEBUG_CONFIG) {
                            Log.d(TAG, "FLUSHING #0x"
                                    + Long.toHexString(mDrawableCache.keyAt(i))
                                    + " / " + cs + " with changes: 0x"
                                    +           Integer.toHexString(cs.getChangingConfigurations()));
                        }
                        cache.setValueAt(i, null);
   } 

 

 那么

Configuration.needNewResources(configChanges,cs.getChangingConfigurations())

 方法就是判断你的配置是否改变了,可以发现配置没改变就不会清空缓存,看来android还是挺聪明的嘛。

可是我们不能为了清空缓存而去更改配置啊,那怎么办?

   突然发现我好傻,我得到Bitmap的方法换一种就可以了啊。我不要Drawable对象的成员变量保存的那个Bitmap就可以了啊。看下面获取Bitmap对象的方法:

Resources res = getResources();
Bitmap bmp = BitmapFactory.decodeResource(res, drawId);

 用这个方法获取的Bitmap对象跟上面的不同,它会每次都创建一个Bitmap对象,所以可以大胆的回收,程序通过,没问题。我可以深藏功与名啦

 

转载请注明出处:http://892848153.iteye.com/admin/blogs/2090216

 

 最后再记录一下关于图片的一些知识:

尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,

因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。

因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,

decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,

无需再使用java层的createBitmap,从而节省了java层的空间。

根据Android版本的不同,bitmap data存放的位置是不同的,3.0以前是分配在native heap上,3.0以后是分配在VM heap上。要想把图片放在native heap里面只要使用BitmapFactory.decodeStream方法解析出Bitmap就可以了。

Bitmap类的构造方法都是私有的,所以开发者不能直接new出一个Bitmap对象,只能通过BitmapFactory类的各种静态方法来实例化一个Bitmap。仔细查看BitmapFactory的源代码可以看到,生成Bitmap对象最终都是通过JNI调用方式实现的。所以,加载Bitmap到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java部分的,一部分是C部分的。这个Bitmap对象是由Java部分分配的,不用的时候系统就会自动回收了,但是那个对应的C可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用recycle()方法来释放C部分的内存。从Bitmap类的源代码也可以看到,recycle()方法里也的确是调用了JNI方法了的。(这些都是网络上的帖子,待验证)

你可能感兴趣的:(bitmap)