这两天在整理关于Android中图片的内存管理问题,这里稍微做一下总结,本文主要目的是总结知识点,不会列出详细实现方式,但是会列出从哪里能学习到这些知识点。
一、bitmap所占内存大小
首先列出一条公式,
一张bitmap占用内存 = 长度 * 宽度 * 一个像素占用的内存
此处的长度和宽度对应图片像素,而一个像素占用多少内存,这是通过我们平时经常看到的ALPHA_8,RGB_565,ARGB_4444,ARGB_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的设置主要在两个地方使用,一个是Bitmap的createBitmap方法
public static Bitmap createBitmap(int width, int height, Config config)
另一个是BitmapFactory内部类Option的inPreferredConfig属性,平时我们经常使用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字段的文档。

从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结合了这几篇文章的所有知识点。