(译文)解决Android ListView中图片异步加载错位问题

本文介绍了一种解决ListView中图片异步加载错位问题的方法。通过使用BitmapManager管理图片下载及缓存,配合ViewHolder模式和线程池技术,实现了图片的高效加载及避免错位现象。

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

这是解决Listview图片异步加载错位的问题(英文版)的译文。

PS:因为能力有限,所以翻译有错的欢迎提出来。

Lazy loading of images in Listview

常见的在LIstview里面加入ImageView,例如,你想要做一个食谱的app,你想要在紧挨着食物或饮料的地方放一个图片,有时候这些图片是从网上下载并展示出来,不幸的是,出问题了,如果你试过了,你可能知道是怎么回事了,或者一些奇怪的小毛病,在本教程中,我会告诉你怎样下载图片和展示它们,我们也会探讨一些陷阱,比如循环和并发性。



设置列表视图
我猜你已经知道如何定义一行布局列表视图和知道适配器。

下面是一个例子,一个RSS阅读器的适配器。从一篇文章检索文本数据类,它包含文章的标题、作者和出版日期。使本教程更清楚,我有一个显式的url列表,将用于每个条目的图像。

复制代码
public class ListAdapter extends ArrayAdapter<Article> {  
    private List<Article> articles;  
    private Context context;  
    private final LayoutInflater inflator;  
  
    private static final String[] URLS = {  
            "http://lh5.ggpht.com/_mrb7w4gF8Ds/TCpetKSqM1I/AAAAAAAAD2c/Qef6Gsqf12Y/s144-c/_DSC4374%20copy.jpg",  
            "http://lh5.ggpht.com/_Z6tbBnE-swM/TB0CryLkiLI/AAAAAAAAVSo/n6B78hsDUz4/s144-c/_DSC3454.jpg",  
            "http://lh3.ggpht.com/_GEnSvSHk4iE/TDSfmyCfn0I/AAAAAAAAF8Y/cqmhEoxbwys/s144-c/_MG_3675.jpg",  
            "http://lh6.ggpht.com/_Nsxc889y6hY/TBp7jfx-cgI/AAAAAAAAHAg/Rr7jX44r2Gc/s144-c/IMGP9775a.jpg",  
            "http://lh3.ggpht.com/_lLj6go_T1CQ/TCD8PW09KBI/AAAAAAAAQdc/AqmOJ7eg5ig/s144-c/Juvenile%20Gannet%20despute.jpg",  
            "http://lh6.ggpht.com/_ZN5zQnkI67I/TCFFZaJHDnI/AAAAAAAABVk/YoUbDQHJRdo/s144-c/P9250508.JPG",  
            "http://lh4.ggpht.com/_XjNwVI0kmW8/TCOwNtzGheI/AAAAAAAAC84/SxFJhG7Scgo/s144-c/0014.jpg",  
            "http://lh6.ggpht.com/_lnDTHoDrJ_Y/TBvKsJ9qHtI/AAAAAAAAG6g/Zll2zGvrm9c/s144-c/000007.JPG",  
            "http://lh6.ggpht.com/_qvCl2efjxy0/TCIVI-TkuGI/AAAAAAAAOUY/vbk9MURsv48/s144-c/DSC_0844.JPG",  
            "http://lh4.ggpht.com/_4f1e_yo-zMQ/TCe5h9yN-TI/AAAAAAAAXqs/8X2fIjtKjmw/s144-c/IMG_1786.JPG",  
            "http://lh6.ggpht.com/_iFt5VZDjxkY/TB9rQyWnJ4I/AAAAAAAADpU/lP2iStizJz0/s144-c/DSCF1014.JPG",  
            "http://lh5.ggpht.com/_hepKlJWopDg/TB-_WXikaYI/AAAAAAAAElI/715k4NvBM4w/s144-c/IMG_0075.JPG",  
            "http://lh6.ggpht.com/_OfRSx6nn68g/TCzsQic_z3I/AAAAAAABOOI/5G4Kwzb2qhk/s144-c/EASTER%20ISLAND_Hanga%20Roa_31.5.08_46.JPG",  
            "http://lh6.ggpht.com/_ZGv_0FWPbTE/TB-_GLhqYBI/AAAAAAABVxs/cVEvQzt0ke4/s144-c/IMG_1288_hf.jpg",  
            "http://lh6.ggpht.com/_a29lGRJwo0E/TBqOK_tUKmI/AAAAAAAAVbw/UloKpjsKP3c/s144-c/31012332.jpg",  
            "http://lh3.ggpht.com/_55Lla4_ARA4/TB6xbyxxJ9I/AAAAAAABTWo/GKe24SwECns/s144-c/Bluebird%20049.JPG",  
            "http://lh3.ggpht.com/_iVnqmIBYi4Y/TCaOH6rRl1I/AAAAAAAA1qg/qeMerYQ6DYo/s144-c/Kwiat_100626_0016.jpg",  
            "http://lh6.ggpht.com/_QFsB_q7HFlo/TCItd_2oBkI/AAAAAAAAFsk/4lgJWweJ5N8/s144-c/3705226938_d6d66d6068_o.jpg",  
            "http://lh5.ggpht.com/_JTI0xxNrKFA/TBsKQ9uOGNI/AAAAAAAChQg/z8Exh32VVTA/s144-c/CRW_0015-composite.jpg",  
            "http://lh6.ggpht.com/_loGyjar4MMI/S-InVNkTR_I/AAAAAAAADJY/Fb5ifFNGD70/s144-c/Moving%20Rock.jpg",  
            "http://lh4.ggpht.com/_L7i4Tra_XRY/TBtxjScXLqI/AAAAAAAAE5o/ue15HuP8eWw/s144-c/opera%20house%20II.jpg",  
            "http://lh3.ggpht.com/_rfAz5DWHZYs/S9cstBTv1iI/AAAAAAAAeYA/EyZPUeLMQ98/s144-c/DSC_6425.jpg",  
            "http://lh6.ggpht.com/_iGI-XCxGLew/S-iYQWBEG-I/AAAAAAAACB8/JuFti4elptE/s144-c/norvig-polar-bear.jpg",  
            "http://lh3.ggpht.com/_M3slUPpIgmk/SlbnavqG1cI/AAAAAAAACvo/z6-CnXGma7E/s144-c/mf_003.jpg",  
            "http://lh4.ggpht.com/_loGyjar4MMI/S-InQvd_3hI/AAAAAAAADIw/dHvCFWfyHxQ/s144-c/Rainbokeh.jpg",  
            "http://lh4.ggpht.com/_yy6KdedPYp4/SB5rpK3Zv0I/AAAAAAAAOM8/mokl_yo2c9E/s144-c/Point%20Reyes%20road%20.jpg",  
            "http://lh5.ggpht.com/_6_dLVKawGJA/SMwq86HlAqI/AAAAAAAAG5U/q1gDNkmE5hI/s144-c/mobius-glow.jpg",  
            "http://lh3.ggpht.com/_QFsB_q7HFlo/TCItc19Jw3I/AAAAAAAAFs4/nPfiz5VGENk/s144-c/4551649039_852be0a952_o.jpg",  
            "http://lh6.ggpht.com/_TQY-Nm7P7Jc/TBpjA0ks2MI/AAAAAAAABcI/J6ViH98_poM/s144-c/IMG_6517.jpg",  
            "http://lh3.ggpht.com/_rfAz5DWHZYs/S9cLAeKuueI/AAAAAAAAeYU/E19G8DOlJRo/s144-c/DSC_4397_8_9_tonemapped2.jpg",  
            "http://lh4.ggpht.com/_TQY-Nm7P7Jc/TBpi6rKfFII/AAAAAAAABbg/79FOc0Dbq0c/s144-c/david_lee_sakura.jpg",  
            "http://lh3.ggpht.com/_TQY-Nm7P7Jc/TBpi8EJ4eDI/AAAAAAAABb0/AZ8Cw1GCaIs/s144-c/Hokkaido%20Swans.jpg",  
            "http://lh3.ggpht.com/_1aZMSFkxSJI/TCIjB6od89I/AAAAAAAACHM/CLWrkH0ziII/s144-c/079.jpg",  
            "http://lh5.ggpht.com/_loGyjar4MMI/S-InWuHkR9I/AAAAAAAADJE/wD-XdmF7yUQ/s144-c/Colorado%20River%20Sunset.jpg",  
            "http://lh3.ggpht.com/_0YSlK3HfZDQ/TCExCG1Zc3I/AAAAAAAAX1w/9oCH47V6uIQ/s144-c/3138923889_a7fa89cf94_o.jpg",  
            "http://lh6.ggpht.com/_K29ox9DWiaM/TAXe4Fi0xTI/AAAAAAAAVIY/zZA2Qqt2HG0/s144-c/IMG_7100.JPG",  
            "http://lh6.ggpht.com/_0YSlK3HfZDQ/TCEx16nJqpI/AAAAAAAAX1c/R5Vkzb8l7yo/s144-c/4235400281_34d87a1e0a_o.jpg",  
            "http://lh4.ggpht.com/_8zSk3OGcpP4/TBsOVXXnkTI/AAAAAAAAAEo/0AwEmuqvboo/s144-c/yosemite_forrest.jpg",  
            "http://lh4.ggpht.com/_6_dLVKawGJA/SLZToqXXVrI/AAAAAAAAG5k/7fPSz_ldN9w/s144-c/coastal-1.jpg",  
            "http://lh4.ggpht.com/_WW8gsdKXVXI/TBpVr9i6BxI/AAAAAAABhNg/KC8aAJ0wVyk/s144-c/IMG_6233_1_2-2.jpg",  
            "http://lh3.ggpht.com/_loGyjar4MMI/S-InS0tJJSI/AAAAAAAADHU/E8GQJ_qII58/s144-c/Windmills.jpg",  
            "http://lh4.ggpht.com/_loGyjar4MMI/S-InbXaME3I/AAAAAAAADHo/4gNYkbxemFM/s144-c/Frantic.jpg",  
            "http://lh5.ggpht.com/_loGyjar4MMI/S-InKAviXzI/AAAAAAAADHA/NkyP5Gge8eQ/s144-c/Rice%20Fields.jpg",  
            "http://lh3.ggpht.com/_loGyjar4MMI/S-InZA8YsZI/AAAAAAAADH8/csssVxalPcc/s144-c/Seahorse.jpg",  
            "http://lh3.ggpht.com/_syQa1hJRWGY/TBwkCHcq6aI/AAAAAAABBEg/R5KU1WWq59E/s144-c/Antelope.JPG",  
            "http://lh5.ggpht.com/_MoEPoevCLZc/S9fHzNgdKDI/AAAAAAAADwE/UAno6j5StAs/s144-c/c84_7083.jpg",  
            "http://lh4.ggpht.com/_DJGvVWd7IEc/TBpRsGjdAyI/AAAAAAAAFNw/rdvyRDgUD8A/s144-c/Free.jpg",  
            "http://lh6.ggpht.com/_iO97DXC99NY/TBwq3_kmp9I/AAAAAAABcz0/apq1ffo_MZo/s144-c/IMG_0682_cp.jpg",  
            "http://lh4.ggpht.com/_7V85eCJY_fg/TBpXudG4_PI/AAAAAAAAPEE/8cHJ7G84TkM/s144-c/20100530_120257_0273-Edit-2.jpg" };  
  
    private static class ViewHolder {  
        public ImageView iconView;  
        public TextView nameTextView;  
        public TextView bottomText;  
    }  
  
    public ListAdapter(Context context, int textViewResourceId,  
            List<Article> articles) {  
        super(context, textViewResourceId, articles);  
        this.articles = articles;  
        this.context = context;  
        inflator = (LayoutInflater) context  
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  
        BitmapManager.INSTANCE.setPlaceholder(BitmapFactory.decodeResource(  
                context.getResources(), R.drawable.icon));  
    }  
  
    @Override  
    public View getView(int position, View convertView, ViewGroup parent) {  
        ViewHolder holder;  
  
        if (convertView == null) {  
            convertView = inflator.inflate(R.layout.article_row, null);  
  
            TextView nameTextView = (TextView) convertView  
                    .findViewById(R.id.title);  
            TextView bottomText = (TextView) convertView  
                    .findViewById(R.id.bottomtext);  
            ImageView iconView = (ImageView) convertView  
                    .findViewById(R.id.article_icon);  
            holder = new ViewHolder();  
            holder.nameTextView = nameTextView;  
            holder.bottomText = bottomText;  
            holder.iconView = iconView;  
            convertView.setTag(holder);  
        } else {  
            holder = (ViewHolder) convertView.getTag();  
        }  
  
        Article article = articles.get(position);  
        holder.nameTextView.setText(article.getTitle());  
        holder.bottomText.setText(article.getAuthor() + " | "  
                + article.getPubDate());  
  
        holder.iconView.setTag(URLS[position]);  
        BitmapManager.INSTANCE.loadBitmap(URLS[position], holder.iconView, 32,  
                32);  
  
        return convertView;  
    }  
}
复制代码


你看,我循环再用view,我只在 convertView = = null的时候从XML加载xml文件。我也存储所有xml文件里面的子空间的索引,并settag,这样大大提高了性能,详细请见Google Conference video
下载 bitmaps

直接建立一个HTTP连接下载图片并用iconView.setImageBitmap()设置图片进去是一种不好的方法,这将引起非常严重的延迟,因为主线程需要等到图片下载完成,才能做其他的事情,我们需要另起线程,下载并存储图片。
上面所说的可以再用一个BitmapManager解决,见代码:

复制代码
 1 private Bitmap downloadBitmap(String url, int width, int height) {  
 2         try {  
 3             Bitmap bitmap = BitmapFactory.decodeStream((InputStream) new URL(  
 4                     url).getContent());  
 5             bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);  
 6             cache.put(url, new SoftReference<Bitmap>(bitmap));  
 7             return bitmap;  
 8         } catch (MalformedURLException e) {  
 9             e.printStackTrace();  
10         } catch (IOException e) {  
11             e.printStackTrace();  
12         }  
13   
14         return null;  
15     }
复制代码

我不知道这是否是最好的下图片的方法,如果你有更好的,请留言。你看,我把下载的图片用Map<String, SoftReference<Bitmap>> cache存储起来了,并且用了软引用,因为我希望当VM内存低的时候回收垃圾(GC)。

循环再利用的困境
As I've mentioned before, the row view can be recycled. This means that, if you're scrolling, the view of a row that slides off the screen, can be used as the view of a row that comes into the screen. For example, row 1 disappears and row 3 appears during scrolling. When row 1 was on the screen, the download of the image started. Now imagine that the downloaded of the image of row 1 takes a very long time. When row 3 appears, the image of row 3 is put into the queue. It may happen that this download finishes before the one of row 1, which means that the picture of row 3 will be the one of row 1.

正如我上面提到的,Listview 的一列可以循环利用,意思是,当你滑动listview的时候,一列划出了屏幕,可以作为连续的角度进入屏幕(没理解,哎,╮(╯▽╰)╭),例如,第1行消失,第3行出现,当第1行在屏幕的时候,相应的图片1在下载,现在想像一下,图片1的下载需要很长时间,当第3行出现在屏幕的时候,图片3加入了下载队列,也许图片3比图片1要先下载完成,那么图片3可能就显示在第1行了。(这一段有的句子我翻译不清,所以留下吧,自己看下,理解下)

PS:下面这个就很好理解了,

Row 1 visible: start download of image 1
Scrolling
Row 3 visible (recycled view of row 1): start download of image 3
Download of image 3 done: row 3 image set to image 3
Download of image 1 done: row 3 image set to image 1

为了避免上面的问题,我们需要存储imageview相应的URL。如果图片下载完成,但是不是对应的列,(看上面的例子),那么就忽略掉,定义一个map集合:Map<ImageView, String> imageViews = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
检查对比bitmap

复制代码
 1 public void loadBitmap(final String url, final ImageView imageView,  
 2         final int width, final int height) {  
 3     imageViews.put(imageView, url);  
 4     Bitmap bitmap = getBitmapFromCache(url);  
 5   
 6     // check in UI thread, so no concurrency issues  
 7     if (bitmap != null) {  
 8         Log.d(null, "Item loaded from cache: " + url);  
 9         imageView.setImageBitmap(bitmap);  
10     } else {  
11         imageView.setImageBitmap(placeholder);  
12         queueJob(url, imageView, width, height);  
13     }  
14 } 
复制代码

当下载bitmap的时候,我们首先要做的是,把Imagevie和URL联系在一起,当bitmap在缓存里面的话,就从缓存里面取出,否则,下载。

应该指出的是 loadBitmap()应该再主线程里面调用,这意味着我们不需要检查imageView回收,我们可以调用setImageBitmap 函数。
工作线程

剩下的就是在新线程里面调用downloadBitmap()。我在网上看到的大多是开启一个新线程,但是这是非常低效的,试想一下,加入listview有500行,那么就需要开启500个线程,我们可以使用java里面的ExecutorService。我们开启一个线程池(newFixedThreadPool),里面有5个线程(这个是几个可以随意,我没有测试多少个是合适的),这样就会同时下载5个图片,所有其他的下载都存储在一个无界的队列。

复制代码
 1 public void queueJob(final String url, final ImageView imageView,  
 2             final int width, final int height) {  
 3         /* Create handler in UI thread. */  
 4         final Handler handler = new Handler() {  
 5             @Override  
 6             public void handleMessage(Message msg) {  
 7                 String tag = imageViews.get(imageView);  
 8                 if (tag != null && tag.equals(url)) {  
 9                     if (msg.obj != null) {  
10                         imageView.setImageBitmap((Bitmap) msg.obj);  
11                     } else {  
12                         imageView.setImageBitmap(placeholder);  
13                         Log.d(null, "fail " + url);  
14                     }  
15                 }  
16             }  
17         };  
18   
19         pool.submit(new Runnable() {  
20             @Override  
21             public void run() {  
22                 final Bitmap bmp = downloadBitmap(url, width, height);  
23                 Message message = Message.obtain();  
24                 message.obj = bmp;  
25                 Log.d(null, "Item downloaded: " + url);  
26   
27                 handler.sendMessage(message);  
28             }  
29         });  
30     }  
复制代码

新建了一个Runnable加到了队列,它用来下载bitmap,之后,发送一个message给handler。之所以用handler是因为所有UI操作都要在UI线程里,这就意味着我们不能马上设置imagebitmap。handler应该创建在将被传入message的线程里,这就是为什么我们不得不把handler写到Runnable 匿名类的的外面。

在handleMessage()里,bitmap从msg.obj重新取回,如果当前URL是最后一个与ImageView相对应的,bitmap才设置。


总结

总之,我们已经写了一个延迟加载位图与回收listviews的manager。能够提高Listview性能,例如取消下载如果视图被摧毁和防止下载相同的文件在同一时间。总之,如果你发现本教程有帮助,请留言:)


所有代码

复制代码
 1 public enum BitmapManager {  
 2     INSTANCE;  
 3   
 4     private final Map<String, SoftReference<Bitmap>> cache;  
 5     private final ExecutorService pool;  
 6     private Map<ImageView, String> imageViews = Collections  
 7             .synchronizedMap(new WeakHashMap<ImageView, String>());  
 8     private Bitmap placeholder;  
 9   
10     BitmapManager() {  
11         cache = new HashMap<String, SoftReference<Bitmap>>();  
12         pool = Executors.newFixedThreadPool(5);  
13     }  
14   
15     public void setPlaceholder(Bitmap bmp) {  
16         placeholder = bmp;  
17     }  
18   
19     public Bitmap getBitmapFromCache(String url) {  
20         if (cache.containsKey(url)) {  
21             return cache.get(url).get();  
22         }  
23   
24         return null;  
25     }  
26   
27     public void queueJob(final String url, final ImageView imageView,  
28             final int width, final int height) {  
29         /* Create handler in UI thread. */  
30         final Handler handler = new Handler() {  
31             @Override  
32             public void handleMessage(Message msg) {  
33                 String tag = imageViews.get(imageView);  
34                 if (tag != null && tag.equals(url)) {  
35                     if (msg.obj != null) {  
36                         imageView.setImageBitmap((Bitmap) msg.obj);  
37                     } else {  
38                         imageView.setImageBitmap(placeholder);  
39                         Log.d(null, "fail " + url);  
40                     }  
41                 }  
42             }  
43         };  
44   
45         pool.submit(new Runnable() {  
46             @Override  
47             public void run() {  
48                 final Bitmap bmp = downloadBitmap(url, width, height);  
49                 Message message = Message.obtain();  
50                 message.obj = bmp;  
51                 Log.d(null, "Item downloaded: " + url);  
52   
53                 handler.sendMessage(message);  
54             }  
55         });  
56     }  
57   
58     public void loadBitmap(final String url, final ImageView imageView,  
59             final int width, final int height) {  
60         imageViews.put(imageView, url);  
61         Bitmap bitmap = getBitmapFromCache(url);  
62   
63         // check in UI thread, so no concurrency issues  
64         if (bitmap != null) {  
65             Log.d(null, "Item loaded from cache: " + url);  
66             imageView.setImageBitmap(bitmap);  
67         } else {  
68             imageView.setImageBitmap(placeholder);  
69             queueJob(url, imageView, width, height);  
70         }  
71     }  
72   
73     private Bitmap downloadBitmap(String url, int width, int height) {  
74         try {  
75             Bitmap bitmap = BitmapFactory.decodeStream((InputStream) new URL(  
76                     url).getContent());  
77             bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);  
78             cache.put(url, new SoftReference<Bitmap>(bitmap));  
79             return bitmap;  
80         } catch (MalformedURLException e) {  
81             e.printStackTrace();  
82         } catch (IOException e) {  
83             e.printStackTrace();  
84         }  
85   
86         return null;  
87     }  
88 }  
复制代码

In case you're wondering, an enum is the preferred way to create a singleton class.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值