Android Bitmap大量使用不产生OOM之多线程并发加载Bitmap的处理方式

本文介绍如何使用AsyncTask在Android中实现图片的异步加载,避免UI线程阻塞,并解决ListView和GridView中的图片加载并发问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载请注明 http://blog.youkuaiyun.com/sinat_30276961/article/details/47314787

上一篇讲了Android Bitmap大量使用不产生OOM之“加载大图片资源优化”,用到了BitmapFactory.decode* 这些解析方法,如果涉及到的图片源来自sd卡或者来自网络,就绝对不要在ui主线程执行。这些数据的加载所耗的时间是不确定的,有很多因素会影响到它:读取sd卡的速度,网络的响应速度,图片的尺寸,还有CPU的强弱。如果其中一种情况导致ui线程阻塞了,就直接ANR了,然后应用跟你说bye bye了。

这一篇讲述在后台处理bitmap的方式,用到了AsyncTask,然后给你展示怎么处理并发。

使用AsyncTask

可以通过AsyncTask这个类很方便的实现后台线程执行一些工作,然后把结果传到ui线程。要使用它,就先继承复写它。下面用代码展现了加载大图片时用到的decodeSampledBitmapFromResource()在AsyncTask执行的过程。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // 使用WeakReference来确保ImageView可以被垃圾回收机制回收
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // 在后台解析图片.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // 一旦解析完成,先查看imageview是否已经被回收,没回收就设置bitmap上去.
    @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被回收。

这里插一句,解释下官方的这句话。AsyncTask在执行任务时,没执行完是不会被销毁的。但是和它关联的一个activity已经被finish()了,然后这个activity布局里有ImageView。理论上说这个Imageview已经没用了,应该被销毁,但如果AsyncTask引用ImageView时不是用弱引用,用一般的写法:private Imageview imageview;那此时是强引用,这个不会被垃圾回收器回收。
我们无法保证在AsyncTask执行完,这个ImageView是否还在。所以在onPostExecute()那里,我们要判断一下imageview的存在情况。如果用户已经操作退出了这个activity或者configuration改变了(比方说横竖屏切换),但是task还没完成,那么imageview就可能已经不存在了,被回收了。
补充一句,上面这些情况下,被回收是合理的,因为界面已经不存在了,就没必要再赋值了,也浪费资源,所以,最好用弱引用。

那么怎么异步加载图片资源呢?很简单,创建一个task,并执行它。代码:

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

处理并发性

常规的view控件如listview和gridview在像上面阐述的那样使用AsyncTask时,会出现其他问题。系统为了有效的管理内存,这些控件会在用户滑动时回收子view。如果每个子view都触发一个AsyncTask,我们无法保证当它执行完任务,与其关联的view是否已经被回收并被其他子view重复利用。

这里插一句,listview和gridview都有个特性,被滑出的子view会被其回收,然后给新滑入的子view使用。这里的回收,不是指垃圾回收,而是listview或gridview回收并重复使用。
此外,也无法保证,异步task启动的顺序和task完成的顺序一致。

“Multithreading for Performance”这篇博客更详细的阐述了并发的情况,并且给出了一个方案,那就是ImageView储存一个它触发的AsyncTask,然后在task执行完后再检查一下当前的ImageView储存的是之前的AsyncTask还是新的。
下面就阐述这个方案,创建一个继承Drawable的子类专门用来储存一个task的引用。在这里,BitmapDrawable相当于是image,当任务完成了,可以直接显示到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();
    }
}

此时,在执行BitmapWorkerTask之前,你要先创建AsyncDrawable,并把它赋值给ImageView。

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);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

这里的cancelPotentialWork方法是用来检测是否已经有其他task已经和这个imageview关联了。如果是,它会试图取消先去的任务,通过调用cancel()。有一些小概率情况是,新的任务数据和已经存在的task一致,所以这种情况下,就可以啥都不用做。下面就是cancelPotentialWork的详细代码:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // 如果bitmap的数据还没设置,或者bitmap已有的数据和新的数据请求不同。
        if (bitmapData == 0 || bitmapData != data) {
            // 取消先前的任务  
            bitmapWorkerTask.cancel(true);
        } else {
            // 如果新的请求数据和已有的数据一致,那就不用再重新获取了。
            return false;
        }
    }
    // 当前imageview没有关联的task,或者关联的task已经被cancel了。
    return true;
}

getBitmapWorkerTask(),是用来获取到与此ImageView关联的task。

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

最后一步是更新ui,通过BitmapWorkerTask的onPostExecute(),在里面,先判断这个任务是否已经被取消了。如果没有取消,则根据imageView获取到先前与其绑定的task,看这两个任务是否一致,并且还要判断下imageview是否有被垃圾回收器回收。如果task一样,并且imageview还在,那就赋值。

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

好了,现在这个方案已经很适合用在listview和gridview了,也可以用在类似的会回收重复利用子view的控件上。代码不多,使用起来也很简单,只要在要设置imageview图片的地方,调用一下loadBitmap()就行。比方说,gridview控件的话,就在其Adapter的getView()那里调用loadBitmap()就行。

最后,是我补充部分。我们来理理它的流程:
1.每次下载或者加载图片时,我们用到的方法是loadBitmap()

     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);
            imageView.setImageDrawable(asyncDrawable);
            task.execute(resId);
        }
     }

第2行这里会先去判断这次下载对应的显示控件是否已经有task在执行,如果有task了,就判断下数据源是否一样,不一样的话,就把已经有的task直接cancel了。
这种情况一般是在滑动listview时最上面几个items被滑出了,然后这些items会被下面滑进来的items复用,此时,按道理下面新滑进来的items应该是要发出新的数据源请求了,所以原先创出来的task就可以直接cancel了。这样可以减少不必要的操作,省下内存开销。
第3行开始,说明要么这个imageview没有老的task存在,要么就是有老task存在,但这个task的请求和新的请求不一样,也就是说这个task已经在cancel()的路上了,那么就新建task。
AsyncDrawable的好处是关联了BitmapWorkerTask,并且可以直接给ImageView使用。
第7行,是开始执行task了。
BitmapWorkerTask关联了一个ImageView的弱引用。然后在后台doInBackground可以下载解析图片或者只是解析图片,这个看你需求,再调整下代码。
后台处理完,就是更新ui了onPostExecute:

    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }
        // 一旦解析完成,先查看imageview是否已经被回收,再看task是否一样.
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }

先是判断该task是否已经被cancel了,这在上面分析过了,无非就是请求的数据不一样了。如果被cancel了,把bitmap置为null,方便垃圾回收器回收已经解析出来的bitmap。
然后是判断ImageView里的AsyncDrawable关联的task是否和现在的task一样,一样的话,并且imageview没被回收,再赋值上去。
这里判断task是否一样就是为了防止那些在loadBitmap的cancelPotentialWork里已经被cancel了task,这些就不用再赋值了。

好了,关于“多线程并发加载图片的处理方式”讲完了。下一篇Android Bitmap大量使用不产生OOM之使用缓存机制,感兴趣,可以继续点进去看。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值