Android图片内存管理

这两天在整理关于Android中图片的内存管理问题,这里稍微做一下总结,本文主要目的是总结知识点,不会列出详细实现方式,但是会列出从哪里能学习到这些知识点。

一、bitmap所占内存大小

首先列出一条公式,

一张bitmap占用内存 = 长度 * 宽度 * 一个像素占用的内存

此处的长度和宽度对应图片像素,而一个像素占用多少内存,这是通过我们平时经常看到的ALPHA_8RGB_565ARGB_4444ARGB_8888来设置的。这四个值是Bitmap对象的一个内部枚举类Bitmap.Config的四个属性,它们各自的特点如下:

ALPHA_8:一个像素占用一个字节,此处的8是指一个字节的透明度,图片不显示颜色值,只显示透明度,其实这个属性如何应用我还没完全探索明白,后续再研究。

RGB_565:一个像素占用2个字节,此处的565是值三个字节的RGB的三原色,这个值可以应用于无关透明与半透明的图片,可以大大降低图片所占内存。

RGB_4444:一个像素占用2个字节,该属性已经被废弃,其特点和RGB_8888类似,只是降低了图片质量和内存占用,从API=19开始,使用这个值也会默认成使用RGB_8888.

RGB_8888:一个像素占用4个字节,android系统使用该值做为默认值,毋庸置疑,该值是图片质量最高的,也是占用内存最多的。

Bitmap.Config的设置主要在两个地方使用,一个是BitmapcreateBitmap方法

public static Bitmap createBitmap(int width, int height, Config config)

另一个是BitmapFactory内部类OptioninPreferredConfig属性,平时我们经常使用isSampleSize属性来压缩图片的像素,其实还可以通过inPreferredConfig设置每个像素占用几个字节,对于不透明的图片,可以使用RGB_565减少每个像素占用的内存。

public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

二、图片压缩

平时我们都是通过isSampleSize压缩图片,这个知识点应该是总所周知的。需要注意的是,如上所说,我们同时也可以借助inPreferredConfig.

三、listview等控件中并发的问题。

当使用viewholder的模式加载listview,某些正在下载图片中的view,可能已经被回收给其他view复用了,当图片下载完,会使得在新的view上呈现其他view的图片,从而导致顺序错乱,为了解决这个并发异步的问题,需要建立url–Imageview–AsyncTask的一一对应关系,这样就可以完美解决这个问题,这个知识点可以参考以下两篇文章

http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html

http://developer.android.com/training/displaying-bitmaps/process-bitmap.html

四、图片缓存

平时大家都使用LruCache作为内存缓存,DiskLruCache作为本地缓存。前者可以在UI线程进行,后者则不可以,并且后者得考虑并发的问题,因为可能DiskLruCache初始化还没完成,已经有线程准备去操作DiskLruCache了。所以需要做类似如下操作:

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (mDiskCacheLock) {
            File cacheDir = params[0];
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // Finished initialization
            mDiskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // Wait while disk cache is started from background thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

此处提一个小技巧,为了避免LruCache跟着Activity被销毁(比如屏幕旋转),可以将LruCache放在一个设置了setRetainInstance(true)的没有UI的fragment中。以下代码片段来自于Displaying Bitmaps Efficiently几篇文章中的例子DisplayingBitmaps,其中借助RetainFragment保存ImageCache对象,ImageCache对象不只持有LruCache,还有DiskLruCache。

 public static ImageCache getInstance(
            FragmentManager fragmentManager, ImageCacheParams cacheParams) {

        // Search for, or create an instance of the non-UI RetainFragment
        final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);

        // See if we already have an ImageCache stored in RetainFragment
        ImageCache imageCache = (ImageCache) mRetainFragment.getObject();

        // No existing ImageCache, create one and store it in RetainFragment
        if (imageCache == null) {
            imageCache = new ImageCache(cacheParams);
            mRetainFragment.setObject(imageCache);
        }

        return imageCache;
 }

 private static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {

        // Check to see if we have retained the worker fragment.
        RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);

        // If not retained (or first time running), we need to create and add it.
        if (mRetainFragment == null) {
            mRetainFragment = new RetainFragment();
            fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
        }

        return mRetainFragment;

    }

    /**
     * A simple non-UI Fragment that stores a single Object and is retained over configuration
     * changes. It will be used to retain the ImageCache object.
     */
    public static class RetainFragment extends Fragment {
        private Object mObject;

        /**
         * Empty constructor as per the Fragment documentation
         */
        public RetainFragment() {}

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Make sure this Fragment is retained over a configuration change
            setRetainInstance(true);
        }

        /**
         * Store a single object in this Fragment.
         *
         * @param object The object to store
         */
        public void setObject(Object object) {
            mObject = object;
        }

        /**
         * Get the stored object.
         *
         * @return The stored object
         */
        public Object getObject() {
            return mObject;
        }
    }

五、内存回收以及内存共享

3.0之前

3.0之前,Bitmap对象保存在java堆Dalvik heap中,而图片像素数据保存在native memory中,前者有垃圾回收机制可以回收内存,recycle()方法是针对后者加快回收内存的,所以,在3.0以前,内存的回收推荐使用recycle()方法。

单个Bitmap的使用场景,我们很容易知道何时调用recycle()方法,而在listview这种类型的控件中,为了把握Bitmap被回收的时机,我们可以借助引用计数,使Bitmap在没有被使用没有被缓存时调用recycle()

3.0开始

3.0开始,Bitmap对象和像素数据都保存在java堆Dalvik heap中,二者的回收都有垃圾回收机制可以负责,所以3.0开始,recycle()并没有调用的必要性。但是依然有优化内存的方法,我们可以使用 BitmapFactory.Options.inBitmap属性,该属性可以使得多个Bitmap对象共用内存,这里说共用内存其实不准确,说复用可能更准确,其实是一个不被使用的Bitmap,其内存会被记录起来,后续被应用于其他Bitmap对象。以下是 BitmapFactory.Options.inBitmap字段的文档。

search screenshot

从API=19开始,被复用的Bitmap占用内存大小只要大于新的Bitmap即可.在API=19之前,新的Bitmap必须和被复用的Bitmap占用内存大小一样,而且新的Bitmap需要jpeg或者png格式,isSampleSize需要等于1,而且inPreferredConfig也会被复用。

六、学习资源

1,Android官网的Displaying Bitmaps Efficiently几篇文章绝对是精华中的精华,以上很多知识点启示网上很多人都有写,但是我相信都是来自阅读这几篇文章。

2,Displaying Bitmaps Efficiently这一系列文章有一个demo叫DisplayingBitmaps,这个demo结合了这几篇文章的所有知识点。