在Android系统中,图片都是以Bitmap的形式呈现在UI界面上的。Bitmap通常是比较消耗内存的,而Anroid APP 都有内存大小限制,所以当我们显示比较大的图片时很容易出现内存溢出的情况。下面就重点讲一讲如何更好地处理Bitmap图片。
在很多情况下,提供的图片尺寸都要比展示在用户界面上的尺寸大。高分辨率的图片展示在低分辨率屏幕的手机上时,即使按照图片的尺寸完全加载,对用户来说也是没什么效果的,看到的仍然是低分辨率的图像。这个时候我们就要考虑有什么办法能够降低内存的使用,而又不影响图片展示的效果。显然,我们可以对图片进行压缩,使之最接近手机屏幕的分辨率。
一种方法就是我们不先全部加载图片到内存,而是预先读取这张图片的尺寸和类型,然后和手机屏幕或者要显示图片的ImageView的尺寸比较,然后决定压缩的比例,这样可以避免内存溢出。代码如下:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//true 表示仅读取尺寸信息
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
我们已经知道了要加载的图片有多大了,同时我们还要知道目标尺寸该是多大。计算目标尺寸的大小不能仅仅考虑ImageView的尺寸,还要从以下几个方面考虑:
考虑以上因素,可以得出目标尺寸大小。通常情况下,我们的目标尺寸取得都是ImageView的尺寸。计算压缩比例的代码如下:
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 图片的原始宽高
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;
// 反复比较计算出最接近目标尺寸但是要保证不小于目标尺寸的压缩比例
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
需要注意的一点,上述代码计算比例的时候都是2的幂次方增大,这是因为Android系统要求压缩比例必须是2的幂次方,即使给的值不是2的幂次方,系统也会自动转为最接近的2的幂次方数。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 首先将inJustDecodeBounds 设置为true,去读取尺寸信息
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算压缩比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 将inJustDecodeBounds设置为false,加载压缩后的图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
上面一节讲述的加载图片的方法都是运行在UI线程中。但是如果我们要加载的图片很大的话,这个过程可能是很耗时的,这样就有可能导致ANR 。下面我们讲一讲如何在非UI线程中加载大图。
一种简单的方法就是使用系统提供的异步任务类AsyncTask
。直接贴代码:
/** * 异步加载图片类 * Integer 要加载的图片资源id * Bitmap 得到的Bitmap对象 */
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// 若引用,保证ImageView可以被正常回收
imageViewReference = new WeakReference<ImageView>(imageView);
}
// 在后台加载图片
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// 完成后返回Bitmap给UI线程
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {//若imageview还没被回收则设置图片,否则啥都不做
imageView.setImageBitmap(bitmap);
}
}
}
}
注意我们在上述代码中用到了弱引用。弱引用的作用就是保证不会妨碍ImageView的垃圾回收。因为我们是异步加载的,有可能整个Activity都销毁了,那么ImageView应该被回收。如果我们使用强引用,则ImageView不能够被回收。用了若引用后,我们在用它时就要先检测它是否还存在,有没有被销毁,如果销毁了,我们也没必要给其设置图片了。
我们可以抽象出一个异步加载图片的方法,如下:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
使用异步加载图片的机制后,我们就得考虑并发的问题。在ListView 和ImageView中,由于其使用了复用机制,并发加载图片可能导致图片错位现象。下面我们提供一种检测方案,确保图片不错位。
创建一个专用的占位Drawable,用来存储异步任务的引用,并且绑定到目标ImageView,显示占位图。如下代码所示:
static class AsyncDrawable extends BitmapDrawable {
//弱引用,保存异步任务类
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
我们需要修改一下异步加载类,修改后如下:
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);//获取ImageView绑定的任务
if (this == bitmapWorkerTask && imageView != null) {
//若绑定的任务没有变更则设置图片
imageView.setImageBitmap(bitmap);
}
}
}
}
//获取和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;
}
同时我们需要修改抽象出来的加载图像的方法,在加载图像之前检测当前ImageView是否已经有绑定的任务。
//修改后的加载图片的方法
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
//创建占位Drawable
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
//取消重复任务的方法
public static boolean cancelPotentialWork(int data, ImageView imageView) {
//获取与相应ImageView的task
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// 如果还没有设置图片,或者要设置的图片和原来的不一样
if (bitmapData == 0 || bitmapData != data) {
// 取消原先的任务
bitmapWorkerTask.cancel(true);
} else {
//否则表示同样的任务已经在进行
return false;
}
}
return true;
}
完成了上面的步骤之后,我们就可以在ListView或GridView加载图片的时候简单地调用loadBitmap方法了。保证图片不错位的逻辑和异步加载的逻辑都隐藏在了loadBitmap方法中。