学习如何处理和加载Bitmap,显示在UI上非常的重要。如果你不重视这块,Bitmap讲很快耗尽你的内存资源,最终导致oom内存溢出。
图片什么大小和形状都有。经常图片比你的UI控件大得多。例如摄像头拍出来的照片比你手机屏幕的分辨率高得多。
BitmapFactory提供了很多通过各种各样渠道解码的方法(decodeByteArray(), decodeFile(), decodeResource(), 等等.) 。这些方法都很容易引起oom内存溢出。每个方法都可以通过BitmapFactory.Options这个类去指定解码选项。把inJustDecodeBounds设定成true,然后进行解码,这个时候就不会真的去分配内存,而是返回空的bitmap同时也会返回outWidth, outHeight 和 outMimeType.有了这三个值,我们就可以按照需要对图片进行压缩。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
除非你对图片的来源有绝对的信心,不然建议每次解码都需要检查图片的大小和类型。
你需要注意这些问题
- 预估加载整个图片需要多少内存
- 愿意从整个应用中,分配多少内存给这张图
- 目标UI控件比如ImageView 的大小
- 当前设备的屏幕分辨率和尺寸
例如把一个1024x768的图片全部加载显示在一个128x96像素的ImageView.上是没有价值的。
下面是根据目标控件传入的宽和高,计算出合适的inSampleSize值的方法。
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
使用这个方法,第一次解码设置inJustDecodeBounds为true,得到合适的inSampleSize后,设置inJustDecodeBounds为false,然后第二次解码。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
这个方法可以把很大的图,很方便的展示在很小的ImageView上。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
如果图片来源在磁盘或者网络上(或者其他内存之外的媒介),上面讨论的BitmapFactory.decode*方法不应该在主UI线程上执行。加载图片需要的时间是不可预知的,依赖于很多因素(磁盘读取速度,网速,图片大小,CPU速度等)。任何一个工作都可以阻塞UI线程,造成无响应。下面说说用子线程加载bitmaps的问题。
AsyncTask应该是大家都很熟悉的子线程通知UI线程修改的方法。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
这段代码很简单,但是可以看到好的代码习惯,比如使用软引用避免AsyncTask的引用导致内存泄漏。判断是否为空,避免空指针异常。使用代码如下:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
如果我们使用ListView 和 GridView这种重复利用子控件的UI控件时,如果我们使用上面的AsyncTask,当任务完成的时候 无法保证该子控件是否已经被重用了。此外,任务开始的顺序和完成的顺序也无法保证。
下面是解决方案,创建一个专门的 Drawable 子类来储存载入图片的任务引用。这样使用BitmapDrawable,当任务完成的时候placeholder 中的图片就能在ImageView显示了。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
在执行 BitmapWorkerTask前,创建一个AsyncDrawable 并且绑定到目标 ImageView中:
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
cancelPotentialWork方法用来检查是否已经有任务绑定到,如果已经有一个任务了就尝试取消这个任务(调用 cancel()方法)。
在少数情况下,如果新的任务和已经存在任务的数据一样,则不需要特殊额外的处理。下面是 cancelPotentialWork 方法的一种实现:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// If bitmapData is not yet set or it differs from the new data
if (bitmapData == 0 || bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
方法 getBitmapWorkerTask(),获取和 ImageView关联的任务:
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
最后一步是在BitmapWorkerTask执行更新 onPostExecute() 。
检查任务是否取消了,和当前的任务和 ImageView引用的任务是否为同一个任务。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
这套实现适用于ListView 和 GridView ,也适用于其他有子控件回收机制的控件。简单的在设置ImageView图片的地方调用loadBitmap 方法就可以。比如在GridView的getView方法中调用loadBitmap。
加载一张图片很简单,加载一大堆图片就麻烦了,比如 ListView, GridView 或者 ViewPager。LruCache 是官方推荐的缓存图片的类,低版本可以使用v4支持包。
下面是一个例子:
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache. 使用最大可以内存的八分之一作为LruCache的缓存大小
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than 这个是必须实现的,返回的是每个bitmap的大小,每次添加bitmap时都会调用
// number of items.
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
当加载bitmap到ImageView时候,LruCache先检查如果找到了键,就直接更新ImageView,否则在开启线程处理图片。
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
BitmapWorkerTask 也需要把新加载的图片的键值对放到缓存中。
class BitmapWorkerTask extends AsyncTask {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
如果使用GridView LruCache很容易超出内存限制, 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);
}
当图片加载完毕,图片应该同时缓存到memory和磁盘中,以便以后加载。
当配置发生变化,例如横竖屏切换的时候,安卓会销毁使用新的配置重建activity。为了避免图片重新处理,我们需要对代码做一些修改。
这是一个Fragment中保留LruCache不因为配置变化而重新加载的例子。
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
处理图片是每个开发者都无法避免的大问题,幸运的是已经有很多非常成熟的第三方图片加载库。笔者用过市面上主流的四种加载库。ImageLoader,Picasso,Glide,Fresco,甚至Volley的图片加载也尝试过。每个加载库各有千秋,可以根据需求做选择,大大提升开发速度,减少oom异常的概率。笔者更推荐来自facebook的Fresco,在使用过程中,渐进式显示,三级内存在低端机上体验更好。送上github上的链接: https://github.com/facebook/fresco 。中文文档非常详尽,地址: http://fresco-cn.org/ 。开发者可以根据自己的需求,选择适合自己项目的库。