转载:http://my.oschina.net/rengwuxian/blog/183802

为什么要在后台加载Bitmap?

有没有过这种体验:你在Android手机上打开了一个带有含图片的ListView的页面,用手猛地一划,就见那ListView嘎嘎地卡,仿佛每一个新的Item都是顶着阻力蹦出来的一样?看完这篇文章,你将学会怎样避免这种情况的发生。

在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法可以把图片加载到Bitmap中。但由于这些方法是耗时的,所以多数情况下,这些方法应该放在非UI线程中,否则将有可能导致界面的卡顿,甚至是触发ANR。

一般情况下,网络图片的加载必须放在后台线程中;而本地图片就可以根据实际情况自行决定了,如果图片不多不大的话,也可以在UI线程中操作来图个方便。至于谷歌官方的说法,是只要是从硬盘或者从网络加载Bitmap,统统不应该在主线程中进行。

基础操作:使用AsyncTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;
 
    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }
 
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }
 
    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

以上代码摘自Android官方文档,是一个后台加载Bitmap并在加载完成后自动将Bitmap设置到ImageView的AsyncTask的实现。有了这个AsyncTask之后,异步加载Bitmap只需要下面的简单代码:

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

然后,一句loadBitmap(R.id.my_image, mImageView) 就能实现本地图片的异步加载了。

并发操作:在ListView和GridView中进行后台加载

在实际中,影响性能的往往是ListView和GridView这种包含大量图片的控件。在滑动过程中,大量的新图片在短时间内一起被加载,对于没有进行任何优化的程序,卡顿现象必然会随之而来。通过使用后台加载Bitmap的方式,这种问题将被有效解决。具体怎么做,我们来看看谷歌推荐的方法。

首先创建一个实现了Drawable接口的类,用来存储AsyncTask的引用。在本例中,选择了继承BitmapDrawable,用来给ImageView设置一个预留的占位图,这个占位图用于在AsyncTask执行完毕之前的显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
    }
}

接下来,和上面类似,依然是使用一个loadBitmap()方法来实现对图片的异步加载。不同的是,要在启动AsyncTask之前,把AsyncTask传给AsyncDrawable,并且使用AsyncDrawable为ImageView设置占位图:

1
2
3
4
5
6
7
8
9
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);
    }
}

然后在Adapter的getView()方法中调用loadBitmap()方法,就可以为每个Item中的ImageView进行图片的动态加载了。

loadBitmap()方法的代码中有两个地方需要注意:第一,cancelPotentialWork()这个方法,它的作用是进行两项检查:首先检查当前是否已经有一个AsyncTask正在为这个ImageView加载图片,如果没有就直接返回true。如果有,再检查这个Task正在加载的资源是否与自己正要进行加载的资源相同,如果相同,那就没有必要再进行多一次的加载了,直接返回false;而如果不同(为什么会不同?文章最后会有解释),就取消掉这个正在进行的任务,并返回true。第二个需要注意的是,本例中的 BitmapWorkerTask 实际上和上例是有所不同的。这两点我们分开说,首先我们看cancelPotentialWork()方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
 
    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // 取消之前的任务
            bitmapWorkerTask.cancel(true);
        } else {
            // 相同任务已经存在,直接返回false,不再进行重复的加载
            return false;
        }
    }
    // 没有Task和ImageView进行绑定,或者Task由于加载资源不同而被取消,返回true
    return true;
}

在cancelPotentialWork()的代码中,首先使用getBitmapWorkerTask()方法获取到与ImageView相关联的Task,然后进行上面所说的判断。好,我们接着来看这个getBitmapWorkerTask()是怎么写的:

1
2
3
4
5
6
7
8
9
10
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获取到它内部的Drawable对象,如果获取到了并且该对象为AsyncDrawable的实例,就调用这个AsyncDrawable的getBitmapWorkerTask()方法来获取到它对应的Task,也就是通过一个ImageView->Drawable->AsyncTask的链来获取到ImageView所对应的AsyncTask。

好的,cancelPotentialWork()方法分析完了,我们回到刚才提到的第二个点:BitmapWorkerTask类的不同。这个类的改动在于onPostExecute()方法,具体请看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
            }
        }
    }
}

从代码中可以看出,在后台加载完Bitmap之后,它 并不是直接把Bitmap设置给ImageView,而是先判断这个ImageView对应的Task是不是自己 (为什么会不同?文章最后会有解释)。如果是自己,才会执行ImageView的setImageBitmap()方法。到此,一个并发的异步加载ListView(或GridView)中图片的实现全部完成。

延伸:文中两个“为什么会不同”的解答

首先,简单说一下ListView中Item和Item对应的View的关系(GridView中同理)。假设一个ListView含有100项,那么它的100个Item应该分别对应一个View用于显示,这样一共是100个View。但Android实际上并没有这样做。出于内存考虑,Android只会为屏幕上可见的每个Item分配一个View。用户滑动ListView,当第一个Item移动到可视范围外后,他所对应的View将会被系统分配给下一个即将出现的Item。

回到问题。

我们不妨假设屏幕上显示了一个ListView,并且它最多能显示10个Item,而用户在最顶部的Item(不妨称他为第1个Item)使用Task加载Bitmap的时候进行了滑动,并且直到第1个Item消失而第11个Item已经在屏幕底部出现的时候,这个Task还没有加载完成。那么此时,原先与第1个Item绑定的ImageView已经被重新绑定到了第11个Item上,并且第11个Item触发了getItem()方法。在getItem()方法中,ImageView第二次使用Task为自己加载Bitmap,但这时它需要加载的图片资源已经变了(由第1个Item对应的资源变成了第11个Item对应的资源),因此在cancelPotentialWork()方法执行时会判断两个资源不一致。这就是为什么相同ImageView却对应了不同的资源。

同理,一个Task持有了一个ImageView,但由于这个Task有可能已经过时,因此这个ImageView所对应的Task未必就是这个Task本身,也有可能是另一个更年轻的Task。

高效使用Bitmaps(二) 后台加载Bitmap的更多相关文章

  1. Android开发之高效加载Bitmap

    一.概述 在Android开发中,我们经常与Bitmap打交道,而对Bitmap的不恰当的操作经常会导致OOM(Out of Memory).这篇文章我们会介绍如何高效地在Android开发中使用Bi ...

  2. thinkphp 的两种建构模式 第一种一个单入口里面定义两个模块,前台和后台,函数控制模块必须function.php前台加载前台模块的汉书配置文件,后台加载后台模块的汉书配置文件,公共文件共用。第二种架构模式两个单入口文件,分别生成两个应用定义define。。。函数可以定义配置文件。。。。

    thinkphp 的两种建构模式  第一种一个单入口里面定义两个模块,前台和后台,函数控制模块必须function.php前台加载前台模块的汉书配置文件,后台加载后台模块的汉书配置文件,公共文件共用. ...

  3. 轻松搞定 easyui datagrid 二次加载的问题(转)

    对于使用url方式的初学者,经常碰到重复请求的问题,这个问题的根源是因为一旦设置了url参数,Datagrid组件在实例化的时候就会做请求,如何避免二次加载这样问题呢,个人觉得注意以下两点基本就可以防 ...

  4. WPF笔记 ( xmlns引用,Resource、Binding 前/后台加载,重新绑定) 2013.6.7更新

    原文:WPF笔记 ( xmlns引用,Resource.Binding 前/后台加载,重新绑定) 2013.6.7更新 1.xmlns Mapping URI的格式是 clr-namespace:&l ...

  5. Emgu.CV怎么加载Bitmap

    EmguCV 在4.0.1版本之后没办法用Bitmap创建Image了. 我给大家说下 EmguCV怎么加载Bitmap 下边是 EmguCV 官方文档写的,意思是从4.0.1以后的版本不能直接Bit ...

  6. 高效使用Bitmaps(一) 大Bitmap的加载

    转载:http://my.oschina.net/rengwuxian/blog/182885 高效使用Bitmaps有什么好处? 我们常常提到的“Android程序优化”,通常指的是性能和内存的优化 ...

  7. Android应用程序后台加载数据

    从ContentProvider查询你需要显示的数据是比较耗时的.如果你在Activity中直接执行查询的操作,那么有可能导致Activity出现ANR的错误.即使没有发生ANR,用户也容易感知到一个 ...

  8. Android中高效的显示图片之一 ——加载大图

    在网上看了不少文章,发现还是官方文档介绍最详细,把重要的东西简单摘要出来.详细可看官方文档地址 ( http://www.bangchui.org/read.php?tid=9 ) . 在应用中显示图 ...

  9. PHP+MySQL+Easyui tree菜单从后台加载json数据(一)

    实现功能:从数据库加载出所有的数据库名,相应的数据库加载对应的数据库表名 原理:(首先看一下参考手册的内容) 异步加载Tree tree 支持内置的异步加载模式,用户创建一个空的tree,然后定义一个 ...

随机推荐

  1. Android:自定义适配器

    无论是ArrayAdapter还是SimpleAdapter都继承了BaseAdapter,自定义适配器同样继承BaseAdapter 实例:Gallery实现图片浏览器 <?xml versi ...

  2. the service mysql56 was not found in the Windows services的解决办法

    mysql无法启动,无法改变状态-CSDN论坛-CSDN.NET-中国最大的IT技术社区 http://bbs.csdn.net/topics/390943788   具体描述: 关闭,重启mysql ...

  3. 最短路径算法之三——Bellman-Ford算法

    Bellman-Ford算法 Dijkstra算法无法判断含负权边的图的最短路. 如果遇到负权,在没有负权回路存在时,即便有负权的边,也可以采用Bellman-Ford算法正确求出最短路径. PS:负 ...

  4. python学习笔记三--字典的使用

    一.基本使用: 1. 赋值:{key:value} 1.1 与列表相同处:会改变索引(键)相关联的值的改变 1.2 与列表不同处:不用考虑值的长度,而列表是有序的需要考虑末尾偏移量,超过末尾偏移量的会 ...

  5. mysql 幻读

    幻读(Phantom Read) 是指当用户读取某一范围的数据行时,B事务在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行.InnoDB和Falcon存储引擎通 过多版本并发 ...

  6. java jvm学习笔记二(类装载器的体系结构)

    欢迎装载请说明出处:http://blog.csdn.net/yfqnihao                 在了解java虚拟机的类装载器之前,有一个概念我们是必须先知道的,就是java的沙箱,什 ...

  7. C# 两个ListBox 数据互传-基础操作

    先看效果图: 两个服务设施列,左边:lbFacility1,右边:lbFacility2,中间向左向右箭头. 如果只是单纯的向左向右移动,那很简单. 因为项目遇到要获取选中项的ID,通过给ListBo ...

  8. 【Python】不定期更新学习小问题整理

    1 ctrl+D 退出python 2 from __future__ import division  小数除法  1/2    整除符号   //  比如4//2 3 为什么要写这个? reloa ...

  9. HTTP DNS小结

    https://www.dnspod.cn/httpdns/guide HttpDNS是使用HTTP协议向DNS服务器的80端口进行请求,代替传统的DNS协议向DNS服务器的53端口进行请求,绕开了运 ...

  10. 《C++ Primer Plus 第六版》读书笔记

    CH1-3:处理数据 1 列表初始化 char c={31325}:不允许缩窄 char c={66}; char c={x}:不能为变量 2 强制类型转换 (typename) value type ...