任何应用都逃脱不了图片展示,所以任何应用都逃脱不了Bitmap。既然逃脱不了,我决定一次性解决。本文内容源自 http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/index.html ,官方的training课程,还带有一个S级别的范例,强烈推荐。
开发面对大量图片处理的时候,bitmap如果处理不好,它会迅速的吃光你的应用的可用内存,导致一个喜闻乐见的crash:
java.lang.OutofMemoryError: bitmap size exceeds VM budget
即使小心翼翼,没有crash,界面的响应以及滑动也会有或多或少的影响。
记住图片高清的有很多,但是高清的手机屏幕并没有多少,你的手机摄像头的分辨率往往比你的手机屏幕分辨率还要高。既然这样,上面例子中我们的2592x1936高清图片,如何处理再显示在一个 100x100的ImageView上呢。直接上代码:
public static int caculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//inSampleSize 默认值设置为1 不变化
int inSampleSize = 1;
//获取Image的实际的宽高
int width = options.outWidth;
int height = options.outHeight;
//判断Image的宽或者高是否大于ImageView的宽或高
if (width > reqWidth || height > reqHeight) {
//计算inSampleSize的时候采取的是取inSampleSize的2的幂值()
while ((width / inSampleSize) > reqWidth && (height / inSampleSize) > reqHeight) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
处理图片大小核心参数就是inSampleSize
,这个参数作用方式很简单,比如上面例子中2592*1936分辨率图片的会占用20MB的内存,如果设置inSampleSize
值为4,图片就会减小为 648*484,占用内存就是648*484*4byte(这里仍然采用最大的最坑的ARGB_8888),1.25MB的内存。内存占比变为原来的十分之一都不到。现在这个参数算出来了,如何使用呢
public static Bitmap decodeScaleBitmapFromResource(Resources res,int resId, int reqWidth , int reqHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
//inJustDecodeBounds 设置为true时,执行decodeResource操作不会生成bitmap占用内存
//但是decodeResource以后可以通过options来获取图片的宽和高
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//转为false生成图片,设置inSampleSize
options.inJustDecodeBounds = false;
options.inSampleSize = caculateInSampleSize(options,reqWidth,reqHeight);;
return BitmapFactory.decodeResource(res,resId,options);
}
如何处理超大图片的系统原生方案就是这样了,同时还有一个小tip,下面这个方法可以轻松的生成 100*100 的图片,虽然这样比较死板,但是还是记一笔吧。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
上面提到的图片处理方式,当我们在加载本地的SD卡图片或者网络图片的时候,不建议在 main UI 线程进行这些操作,因为由于各种各样的原因(网络速度,本地cpu,图片大小),加载的消耗的时间无法确定。既然如此那就开线程吧,Android自带的异步操作 AsyncTask
就可以满足我们的要求
还是直接上代码
public class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{
//声明一个弱引用的ImageView
private WeakReference<ImageView> imageViewWeakReference;
private int resId;
private Context context;
public BitmapWorkerTask(ImageView imageView,Context context) {
//弱引用来保证imageview会被快速的回收
imageViewWeakReference = new WeakReference<ImageView>(imageView);
this.context = context;
}
//在后台进行生成bitmap的操作
@Override
protected Bitmap doInBackground(Integer... params) {
resId = params[0];
return Utils.decodeScaleBitmapFromResource(context.getResources(),resId,100,100);
}
//完成后判断imageview是否还在,然后set bitmap
@Override
protected void onPostExecute(Bitmap bitmap) {
if(imageViewWeakReference != null && bitmap != null){
ImageView imageView = imageViewWeakReference.get();
if(imageView != null){
imageView.setImageBitmap(bitmap);
}
}
}
}
这个图片的异步任务,需要本地的资源id,用来生成图片。使用的时候传入展示图片的视图 ImageView和上下文,它就可以在后台帮你完成所有的事情
public void loadBitmap(ImageView imageview,int resId){
BitmapWorkerTask task = new BitmapWorkerTask(imageview,this);
task.execute(resId);
}
到这里为止一整套的处理图片的并加载的流程就完成了,不过这才是第一步。以上讨论的都是单个图片的情况,实习开发往往是ListView
和GridView
这种多图并发的情况,我们的BitmapWorkerTask
还不能满足这种要求,因为当ListView或者GridView滚动的时候,他们会去回收子View。子View触发的AsyncTask无法确定是否完成,与AsyncTask所关联的View是否已经回收给其他的子View使用(listview的复用机制),更严重的问题是众多的AsyncTask们开始和结束的顺序是无法保证一致的,所以还会导致我们常常遇见的listviewt图片显示乱序的问题。
为了解决上面提到的问题,让每个ImageView都拥有自己的AsyncTask。那么用什么绑定呢? ImageView能设置的并且我们能自定义的就是Drawable了。
public class AsyncDrawable extends BitmapDrawable{
private WeakReference<BitmapWorkerTask> bitmapWorkerTaskWeakReference;
//将BitmapWorkerTask与BitmapDrawable绑定
public AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskWeakReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
//提供获取该BitmapDrawable的 BitmapWorkerTask的方法
public BitmapWorkerTask getBitmapWorkerTask(){
return bitmapWorkerTaskWeakReference.get();
}
}
自定义的BitmapDrawable让他拥有BitmapWorkerTask,还可以传入一个Bitmap作为默认显示图片,并将BitmapWorkerTask声明为弱引用(方便回收)。提供获取AsyncDrawable 的BitmapWorkerTask方法,是为了能获取ImageView的BitmapWorkerTask方便我们操作。
ImageView有了自己的AsyncTask,为了解决乱序的和回收的问题,要判断ImageView上是否有正在执行的AsyncTask,判断的方式就是resId 图片的资源Id,如果当前id与传入的id不同说明有另外一个图片在set,通过cancle()方法取消,还有一个可能性很小的情况2个id相同,这个时候不用做任何事情。
public static boolean cancelPotentialWork(int resId , ImageView imageView){
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if(bitmapWorkerTask != null){
int bitmapData = bitmapWorkerTask.resId;
//当前没有设置图片或者现在的已经有另外一个图片在set
if(bitmapData == 0 || bitmapData != resId){
//执行取消
bitmapWorkerTask.cancel(true);
}else{
//图片资源id不为0 并且新的资源id与之前的资源id相同,不作任何事情
return false;
}
}
//imageView上已经没有AsyncTask在执行
return true;
}
还有一个辅助方法,通过获取ImageView的AsyncTask
public static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView){
if(imageView != null){
Drawable drawable = imageView.getDrawable();
if(drawable instanceof AsyncDrawable){
AsyncDrawable asyncDrawable = (AsyncDrawable)drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
现在不同的resid会被调用cancle()方法,取消之前的任务,BitmapWorkerTask
还需要进行一些修改,已经cancle的情况下BitmapWorkerTask
将bitmap设置为null。
protected void onPostExecute(Bitmap bitmap) {
if(isCancelled()){
bitmap = null;
}
if(imageViewWeakReference != null && bitmap != null){
ImageView imageView = imageViewWeakReference.get();
BitmapWorkerTask bitmapWorkerTask = Utils.getBitmapWorkerTask(imageView);
if(this == bitmapWorkerTask && imageView != null){
imageView.setImageBitmap(bitmap);
}
}
}
最后修改一下loadBitmap
方法添加cancelPotentialWork
判断取消的操作
public void loadBitmap(ImageView imageview,int resId){
if(Utils.cancelPotentialWork(resId,imageview)) {
BitmapWorkerTask task = new BitmapWorkerTask(imageview, this);
AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(),defaultBitmap,task);
imageview.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
取消操作完成后给imageview绑定AsyncDrawable
最后执行BitmapWorkerTask
加载新图片整个流程就完成了。现在我们BitmapWorkerTask
可以在ListView
或者GridView
中使用了,只需要调用loadBitmap
方法就可以去加载图片。
加载图片的操作就已经完成了,这样当然还不算高效,想想看每次滑动的操作都会判断取消之前的操作,再加载新图片,如果图片数目过多,这种方案肯定不够完美。真正的多图需要一定的缓存,保存最近的图片在缓存中。关于会在下篇文章中讲,敬请期待