Android如何高效的加载图片(2)---在ui线程中处理Bitmaps

前面我们总结了Androd中如何去加载尺寸比较大的图片。现在我们需要考虑的问题是当我们从磁盘或者网络中加载图片时,由于磁盘的读取速度或者网速的原因导致话费很长的时间去加载。如果吧这些耗时的代码放在ui线程,会导致ANR异常
所以。这篇文章中,我们将讨论使用AsyncTask在后台线程中去加载图片,并且最后将会教你如果处理并发问题

使用AsyncTask

AsyncTask类提供了一些的方法在后台线程执行一些耗时操作,并且把最终的执行结果发布到ui线程。
AsyncTask使用步骤,首先,创建一个类去继承AsyncTask。然后重写它的一些方法,下面这断代码使用了AsyncTaskdecodeSampledBitmapFromResource()(注:此方法为以合适的缩放比加载bitmap图片,具体请看上一节Android中Bitmaps 处理详解(1)中此方法的创建过程)把一张尺寸较大的图片加载进ImageView。

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

    // 在后台线程中获取图片并转为bitmap.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // 获取到bitmap后,判断如果ImageView没有被回收,则吧图片加载进ImageView
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

在上面的代码中,对ImageView使用了软引用WeakReference,目的是为了确保AsyncTask不会阻止ImageView和ImageView持有的其他引用被及时回收。所以在后台执行完任务后,我们不确定ImageView是否被回收,所以在**onPostExecute()**方法中判断ImageView是否为null, 比如,如果用户在AsyncTask执行结束前关闭了Activity界面。

通过以上封装,我们在后台线程中加载图片可以简答的用下面的代码就可完成:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

处理并发问题

常见的视图组件,如ListView和GridView,在使用前面AsyncTask操作时面对同样的问题就是,为了消耗内存,这些组件在滚动的时候会去循环利用他们的子View,如果每个子View都执行了AsyncTask,所以我们不能确定,某个item执行了AsyncTask,当Async执行完毕后,该item是否还存在,同样,我们也不能保证AsyncTask的启动顺序就是AsyncTask的完成顺序。
有国外大牛给出了解决方法,用ImageView存储最近的AsyncTask引用(如果这个现在不好理解,可窘继续往下看)。
首先,创建一个Drawable的子类来存储对任务(即AsyncTask)的引用,在本例中,使用BitmapDrawable,以便在任务完成时,在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();
    }
}

我们看到上面代码中,先不管它的父类BitmapDrawable和弱引用WeakReference;我们简化上面的代码就是:


    static class AsyncDrawable {
        BitmapWorkerTask bitmapWorkerTaskReference;
        
        public AsyncDrawable( BitmapWorkerTask bitmapWorkerTask) {
            bitmapWorkerTaskReference = bitmapWorkerTask;
        }
        
        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference;
        }
    }

通过简化的代码,我们知道,上步我们只是创建了一个类,然后在类中创建一个成员变量BitmapWorkerTask,然后添加set/get方法来设置获取BitmapWorkerTask。

回到我们的问题,我们前面说过,要把任务与ImageView绑定到一起,即把
BitmapWorkerTask和ImageView绑定到一起,测试我们想到的ImageView有setTag()和getTag()方法,可以完成,当然,这也是一种思路。
现在国外大牛提供了一种更巧妙的解决方法就是把通过ImageView的setImageDrawable()方法和getDrawable()方法来进行绑定,这样绑定的好处因为他的参数是Drawable对象,我们可以通过在Drawable的子类当中来设置任务,然后当任务执行前,ImageView就去加载Drawable子类所对应的图片,任务执行结束后,ImageView已经加载了需要去加载的图片。这样的Drawable就相当于一个placeholder占位图 ,实现了ImageView对任务的绑定。

所以我们上面类 需要去继承BitmapDrawable类。而用软引用的作用前面已经说过了,这里再不做重复说明了。

这会儿我们看看这个并发问题中处理加载图片的最终代码:

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        // 创建AsyncDrawable对象
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        // 绑定ImageView与任务
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

我们看到,在执行任务前,我们首先调用了cancelPotentialWork(resId, imageView)方法,那么这个方法是干什么用的呢?
下面为cancelPotentialWork(resId, imageView)方法的代码:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
	// 获取当前ImageView绑定的任务
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
	    // 获取到任务执行的资源Id
        final int bitmapData = bitmapWorkerTask.data;
        // 判断id是否被设置过,跟当前的id是否相同
        if (bitmapData == 0 || bitmapData != data) {
            // 如果不相同,退出任务
            bitmapWorkerTask.cancel(true);
        } else {
            // 如果相同,则说明当前的任务正在运行。
            return false;
        }
    }
     // 当前的ImageView没有绑定任务,或者任务已经运行完成
    return true;
}

我们看到上面代码中有个方法,getBitmapWorkerTask(imageView),这个方法就是获取传入的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;
}

下面来解释一下代码:
首先,我们会调用loadBitmap(int resId, ImageView imageView)传入要加载的图片的资源ID,和图片显示的控件去加载图片, 先去获取控件ImageView所绑定的加载任务,判断当前ImageView所绑定的任务(如果绑定了任务)中加载的图片资源Id是否与当前即将要被加载的图片资源ID相同,如果不同,则退出当前ImageView所绑定的任务,重新开始新的任务去加载当前传入的资源id。
如果当前的资源id与ImageView所绑定的任务正在加载的资源ID相同,则让它继续执行任务,不去干预。

最后我们重新去修改 BitmapWorkerTask中的**onPostExecute()**里面的代码如下:

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

修改的代码主要作用是检查当前的任务是否被取消,和当前任务是否是ImageView控件所绑定的任务。

经过以上的封装,我们就可以在诸如ListView、GridView和其他任何复用子View的控件去加载图片。

请继续阅读下篇Android如何高效的加载图片(3)— 图片的缓存

你可能感兴趣的:(Java,Android)