如何高效的展示Bitmap

任何应用都逃脱不了图片展示,所以任何应用都逃脱不了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,界面的响应以及滑动也会有或多或少的影响。

Bitmap如何吃掉我们的内存

  • 虽然现在大部分手机都有1G甚至2G的内存,但是千万不要拿手机的内存去和PC的内存比较,2者的性能不是一个级别的。所以我们的应用的可用资源还是比较有限的,Android设备为一个应用分配的存储空间可以少到只有16MB。这里官方说应用应该在这个最低内存限制下进行优化(汗),好消息是大部分的手机限制都要比这个要高。
  • Bitmap是吃内存的第一大户,举个简单的例子,一张高清图片,2592x1936的分辨率,使用最坑的ARGB_8888 方案,这个bitmap会占用的内存为 : 2592*1936*4byte = 20,072,448byte,换算一下就是20MB,直接超过了16MB的最小内存的情况。
  • 面对越来越丰富的内容,我们要加载的图片往往还不止一张,ListView,GridView,viewPager齐刷刷上阵。而在用户手指轻轻的一滑之间,又是一大波图片来袭。真是日了狗!

如何加载Bitmap

记住图片高清的有很多,但是高清的手机屏幕并没有多少,你的手机摄像头的分辨率往往比你的手机屏幕分辨率还要高。既然这样,上面例子中我们的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 就可以满足我们的要求

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);
    }

处理多图并发

到这里为止一整套的处理图片的并加载的流程就完成了,不过这才是第一步。以上讨论的都是单个图片的情况,实习开发往往是ListViewGridView 这种多图并发的情况,我们的BitmapWorkerTask 还不能满足这种要求,因为当ListView或者GridView滚动的时候,他们会去回收子View。子View触发的AsyncTask无法确定是否完成,与AsyncTask所关联的View是否已经回收给其他的子View使用(listview的复用机制),更严重的问题是众多的AsyncTask们开始和结束的顺序是无法保证一致的,所以还会导致我们常常遇见的listviewt图片显示乱序的问题。

AsyncTask与ImageView进行绑定

为了解决上面提到的问题,让每个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 方法就可以去加载图片。

缓存

加载图片的操作就已经完成了,这样当然还不算高效,想想看每次滑动的操作都会判断取消之前的操作,再加载新图片,如果图片数目过多,这种方案肯定不够完美。真正的多图需要一定的缓存,保存最近的图片在缓存中。关于会在下篇文章中讲,敬请期待

你可能感兴趣的:(android,bitmap,图片)