这是解决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.