注:看英文版的Api Guide总觉得蛋疼,现在翻译一篇比较重要的内容:Bitmap的高效加载,希望翻译过后能真正理解该篇文章的核心思想。
一般来说,我们在网络上加载的图片的的尺寸都要大于手机屏幕的分辨率,而手机的内存又极其有限,所以,在安卓开发中,对图片进行高效的处理是一块很重要的环节。在安卓开发中有条不成文的规矩:遇到图片,一定要狠狠处理。
BitmapFatory类提供了许多解析图片的方法:decodeByteArray(), decodeFile(), decodeResource() 等。这些方法可以将图片转换成我们所需要的Bitmap。但一般情况下,手机的分辨率有限,我们并不需要显示一个很大的图片,也并不需要将一个图片完整地加载进内存中,所以在加载图片的过程中需要进行一些处理。而这里,BitmapFactory提供了Option选项,它可以帮我们高效加载图片。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//这里将inJustDecodeBounds设置为true,意为不全部加载图片,只得到图片的宽和高等基本参数。
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;//得到图片的高
int imageWidth = options.outWidth;//得到图片的宽
String imageType = options.outMimeType;//得到图片的Mime类型
采用上面的代码,我们可以在不加载图片的情况下,提前得到图片的宽高,以便对其进行缩放处理。
图片的大小已经知道了, 现在需要决定是否将完整的图片都加载进来。我们需要思考如下几个部分:
1、想用多大的内存加载图片;
2、ImageView需要显示多大的尺寸;
例如,如果ImageView显示像素为128*96,显然,将一个1024*768的图片加载进内存是不合适的。加载图片的分辨率只需在128*96左右就好了。如下方法,可以确定图片的缩放比例:
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;
}
确定了缩放比例,我们就可以部分的加载图片了:
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);
// 确定缩放比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
//真正加载图片的时候一定要把这个参数设为false
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
得到Bitmap,我们就可以放入imageView进行显示了。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 128, 96));
此外,为了使APP更加流畅,我们还需将诸如BitmapFatory.decode*等方法放入非UI线程中进行处理。毕竟加载图片是个相对耗时的操作。在这里,我们可以用我们的老朋友:AsynkTask来进行处理。
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, 128, 96));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
//随时注意imageViewReference弱引用是否被回收
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
在BitmapWorkerTask中,我们定义了一个弱引用,来确保AsyncTask不会阻止ImageView被垃圾回收(WeakReference 弱引用,其中保存的对象实例可以被GC回收掉。这个类通常用于在某处保存对象引用,而又不干扰该对象被GC回收,SoftReference 软引用,它保存的对象实例,除非JVM即将OutOfMemory,否则不会被GC回收。这个特性使得它特别适合设计对象Cache),其实,在处理ImageView、Activity等大对象时,采用弱引用是个非常好的习惯。在这里,我们在后台线程对图片进行加载,加载完毕后再setImageBitmap,这样就避免了对主线程的阻塞。
然后可以这样对图片进行加载:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
ListView和GridView都继承了AbsListView,它们有一套列表项的复用功能,来优化滚动时的View重复加载。但通过AsynkTask加载列表项图片就会有一个问题,当我们在其中一个列表项加载图片的时候,有可能图片未被加载完就“滚出去了”,这个列表项可能被当做缓存应用到另一个列表项,这就造成了图片的错乱。所以我们要对ListView和GridView的图片加载进行一些并发处理。
看下官方文档的巧妙处理:首先,重写一个BitmapDrawable,它存储了ImageView所对应的BitmapWorkerTask,同样采用弱引用的方式引用BitmapWorkerTask:
static class AsyncDrawable extends BitmapDrawable {
//弱引BitmapWorkerTask
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();
}
}
接口方法loadBitmap。在执行BitmapWorkerTask之前,我们需要定义这个AsyncDrawable并绑定给ImagView:
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);
//将asyncDrawable绑定给imageView, 此时ImageView还未被加载
imageView.setImageDrawable(asyncDrawable);
//再加载imageView
task.execute(resId);
}
}
这里的AsyncDrawable将imageView与其对应的AsynkTask一一绑定。loadBitmap方法首先调用了cancelPotentialWork方法,cancelPotentialWork的目的是,如果一个列表项,它的ImageView正在加载中或加载完毕,那么就不干涉,但如果该列表项被复用,并且新加载的ImageView与之前的不同(复用前加载的Image还未完成),就将之前的AsyncTask干掉。实现代码如下:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
//得到imageView的BitmapWorkerTask
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
//如果BitmapWorkerTask为空,证明已经加载完(被回收)或未加载
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// 如果要加载的bitmapData 与bitmapWorkerTask过去加载的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;
}
从AsyncDrawable 里得到BitmapWorkerTask 的方法:
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 也要修改:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference imageViewReference;
private int data = 0;
@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);
//如是相同的BitmapWorkerTask则加载
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
我们可以在ListView或GridView adapter中的getView()方法调用loadBitmap(),由于getView()方法会频繁调用,loadBitmap会不断地检测要加载的新ImageView与过去的ImageView是否相同,如不同则把之前的任务杀掉,这样就避免了图片的错乱问题。