http://www.eoeandroid.com/thread-254866-1-1.html
在APP应用中,listview的异步加载图片方式能够带来很好的用户体验,同时也是考量程序性能的一个重要指标。关于listview的异步加载,网上其实很多示例了,中心思想都差不多,不过很多版本或是有bug,或是有性能问题有待优化。有鉴于此,本人在网上找了个相对理想的版本并在此基础上进行改造,下面就让在下阐述其原理以探索个中奥秘,与诸君共赏…
贴张效果图先:
异步加载图片基本思想:
1. 先从内存缓存中获取图片显示(内存缓冲)
2. 获取不到的话从SD卡里获取(SD卡缓冲)
3. 都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示(视情况看是否要显示)
OK,先上adapter的代码:
01 | public class LoaderAdapter extends BaseAdapter{ |
02 |
03 | private static final String TAG = "LoaderAdapter" ; |
04 | private boolean mBusy = false ; |
05 |
06 | public void setFlagBusy( boolean busy) { |
07 | this .mBusy = busy; |
08 | } |
09 |
10 | |
11 | private ImageLoader mImageLoader; |
12 | private int mCount; |
13 | private Context mContext; |
14 | private String[] urlArrays; |
15 | |
16 | |
17 | public LoaderAdapter( int count, Context context, String []url) { |
18 | this .mCount = count; |
19 | this .mContext = context; |
20 | urlArrays = url; |
21 | mImageLoader = new ImageLoader(context); |
22 | } |
23 | |
24 | public ImageLoader getImageLoader(){ |
25 | return mImageLoader; |
26 | } |
27 |
28 | @Override |
29 | public int getCount() { |
30 | return mCount; |
31 | } |
32 |
33 | @Override |
34 | public Object getItem( int position) { |
35 | return position; |
36 | } |
37 |
38 | @Override |
39 | public long getItemId( int position) { |
40 | return position; |
41 | } |
42 |
43 | @Override |
44 | public View getView( int position, View convertView, ViewGroup parent) { |
45 |
46 | ViewHolder viewHolder = null ; |
47 | if (convertView == null ) { |
48 | convertView = LayoutInflater.from(mContext).inflate( |
49 | R.layout.list_item, null ); |
50 | viewHolder = new ViewHolder(); |
51 | viewHolder.mTextView = (TextView) convertView |
52 | .findViewById(R.id.tv_tips); |
53 | viewHolder.mImageView = (ImageView) convertView |
54 | .findViewById(R.id.iv_image); |
55 | convertView.setTag(viewHolder); |
56 | } else { |
57 | viewHolder = (ViewHolder) convertView.getTag(); |
58 | } |
59 | String url = "" ; |
60 | url = urlArrays[position % urlArrays.length]; |
61 | |
62 | viewHolder.mImageView.setImageResource(R.drawable.ic_launcher); |
63 | |
64 |
65 | if (!mBusy) { |
66 | mImageLoader.DisplayImage(url, viewHolder.mImageView, false ); |
67 | viewHolder.mTextView.setText( "--" + position |
68 | + "--IDLE ||TOUCH_SCROLL" ); |
69 | } else { |
70 | mImageLoader.DisplayImage(url, viewHolder.mImageView, true ); |
71 | viewHolder.mTextView.setText( "--" + position + "--FLING" ); |
72 | } |
73 | return convertView; |
74 | } |
75 |
76 | static class ViewHolder { |
77 | TextView mTextView; |
78 | ImageView mImageView; |
79 | } |
80 | } |
关键代码是ImageLoader的DisplayImage方法,再看ImageLoader的实现
001 | public class ImageLoader { |
002 |
003 | private MemoryCache memoryCache = new MemoryCache(); |
004 | private AbstractFileCache fileCache; |
005 | private Map<ImageView, String> imageViews = Collections |
006 | .synchronizedMap( new WeakHashMap<ImageView, String>()); |
007 | // 线程池 |
008 | private ExecutorService executorService; |
009 |
010 | public ImageLoader(Context context) { |
011 | fileCache = new FileCache(context); |
012 | executorService = Executors.newFixedThreadPool( 5 ); |
013 | } |
014 |
015 | // 最主要的方法 |
016 | public void DisplayImage(String url, ImageView imageView, boolean isLoadOnlyFromCache) { |
017 | imageViews.put(imageView, url); |
018 | // 先从内存缓存中查找 |
019 |
020 | Bitmap bitmap = memoryCache.get(url); |
021 | if (bitmap != null ) |
022 | imageView.setImageBitmap(bitmap); |
023 | else if (!isLoadOnlyFromCache){ |
024 | |
025 | // 若没有的话则开启新线程加载图片 |
026 | queuePhoto(url, imageView); |
027 | } |
028 | } |
029 |
030 | private void queuePhoto(String url, ImageView imageView) { |
031 | PhotoToLoad p = new PhotoToLoad(url, imageView); |
032 | executorService.submit( new PhotosLoader(p)); |
033 | } |
034 |
035 | private Bitmap getBitmap(String url) { |
036 | File f = fileCache.getFile(url); |
037 | |
038 | // 先从文件缓存中查找是否有 |
039 | Bitmap b = null ; |
040 | if (f != null && f.exists()){ |
041 | b = decodeFile(f); |
042 | } |
043 | if (b != null ){ |
044 | return b; |
045 | } |
046 | // 最后从指定的url中下载图片 |
047 | try { |
048 | Bitmap bitmap = null ; |
049 | URL imageUrl = new URL(url); |
050 | HttpURLConnection conn = (HttpURLConnection) imageUrl |
051 | .openConnection(); |
052 | conn.setConnectTimeout( 30000 ); |
053 | conn.setReadTimeout( 30000 ); |
054 | conn.setInstanceFollowRedirects( true ); |
055 | InputStream is = conn.getInputStream(); |
056 | OutputStream os = new FileOutputStream(f); |
057 | CopyStream(is, os); |
058 | os.close(); |
059 | bitmap = decodeFile(f); |
060 | return bitmap; |
061 | } catch (Exception ex) { |
062 | Log.e( "" , "getBitmap catch Exception...\nmessage = " + ex.getMessage()); |
063 | return null ; |
064 | } |
065 | } |
066 |
067 | // decode这个图片并且按比例缩放以减少内存消耗,虚拟机对每张图片的缓存大小也是有限制的 |
068 | private Bitmap decodeFile(File f) { |
069 | try { |
070 | // decode image size |
071 | BitmapFactory.Options o = new BitmapFactory.Options(); |
072 | o.inJustDecodeBounds = true ; |
073 | BitmapFactory.decodeStream( new FileInputStream(f), null , o); |
074 |
075 | // Find the correct scale value. It should be the power of 2. |
076 | final int REQUIRED_SIZE = 100 ; |
077 | int width_tmp = o.outWidth, height_tmp = o.outHeight; |
078 | int scale = 1 ; |
079 | while ( true ) { |
080 | if (width_tmp / 2 < REQUIRED_SIZE |
081 | || height_tmp / 2 < REQUIRED_SIZE) |
082 | break ; |
083 | width_tmp /= 2 ; |
084 | height_tmp /= 2 ; |
085 | scale *= 2 ; |
086 | } |
087 |
088 | // decode with inSampleSize |
089 | BitmapFactory.Options o2 = new BitmapFactory.Options(); |
090 | o2.inSampleSize = scale; |
091 | return BitmapFactory.decodeStream( new FileInputStream(f), null , o2); |
092 | } catch (FileNotFoundException e) { |
093 | } |
094 | return null ; |
095 | } |
096 |
097 | // Task for the queue |
098 | private class PhotoToLoad { |
099 | public String url; |
100 | public ImageView imageView; |
101 |
102 | public PhotoToLoad(String u, ImageView i) { |
103 | url = u; |
104 | imageView = i; |
105 | } |
106 | } |
107 |
108 | class PhotosLoader implements Runnable { |
109 | PhotoToLoad photoToLoad; |
110 |
111 | PhotosLoader(PhotoToLoad photoToLoad) { |
112 | this .photoToLoad = photoToLoad; |
113 | } |
114 |
115 | @Override |
116 | public void run() { |
117 | if (imageViewReused(photoToLoad)) |
118 | return ; |
119 | Bitmap bmp = getBitmap(photoToLoad.url); |
120 | memoryCache.put(photoToLoad.url, bmp); |
121 | if (imageViewReused(photoToLoad)) |
122 | return ; |
123 | BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad); |
124 | // 更新的操作放在UI线程中 |
125 | Activity a = (Activity) photoToLoad.imageView.getContext(); |
126 | a.runOnUiThread(bd); |
127 | } |
128 | } |
129 |
130 | /** |
131 | * 防止图片错位 |
132 | * |
133 | * @param photoToLoad |
134 | * @return |
135 | */ |
136 | boolean imageViewReused(PhotoToLoad photoToLoad) { |
137 | String tag = imageViews.get(photoToLoad.imageView); |
138 | if (tag == null || !tag.equals(photoToLoad.url)) |
139 | return true ; |
140 | return false ; |
141 | } |
142 |
143 | // 用于在UI线程中更新界面 |
144 | class BitmapDisplayer implements Runnable { |
145 | Bitmap bitmap; |
146 | PhotoToLoad photoToLoad; |
147 |
148 | public BitmapDisplayer(Bitmap b, PhotoToLoad p) { |
149 | bitmap = b; |
150 | photoToLoad = p; |
151 | } |
152 |
153 | public void run() { |
154 | if (imageViewReused(photoToLoad)) |
155 | return ; |
156 | if (bitmap != null ) |
157 | photoToLoad.imageView.setImageBitmap(bitmap); |
158 | |
159 | } |
160 | } |
161 |
162 | public void clearCache() { |
163 | memoryCache.clear(); |
164 | fileCache.clear(); |
165 | } |
166 |
167 | public static void CopyStream(InputStream is, OutputStream os) { |
168 | final int buffer_size = 1024 ; |
169 | try { |
170 | byte [] bytes = new byte [buffer_size]; |
171 | for (;;) { |
172 | int count = is.read(bytes, 0 , buffer_size); |
173 | if (count == - 1 ) |
174 | break ; |
175 | os.write(bytes, 0 , count); |
176 | } |
177 | } catch (Exception ex) { |
178 | Log.e( "" , "CopyStream catch Exception..." ); |
179 | } |
180 | } |
181 | } |
先从内存中加载,没有则开启线程从SD卡或网络中获取,这里注意从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅,这是优化一。于此同时,在adapter里有个busy变量,表示listview是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话无需再开启线程去外存或网络获取图片,这是优化二。ImageLoader里的线程使用了线程池,从而避免了过多线程频繁创建和销毁,有的童鞋每次总是new一个线程去执行这是非常不可取的,好一点的用的AsyncTask类,其实内部也是用到了线程池。在从网络获取图片时,先是将其保存到sd卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存,这是优化三。
而图片错位问题的本质源于我们的listview使用了缓存convertView,假设一种场景,一个listview一屏显示九个item,那么在拉出第十个item的时候,事实上该item是重复使用了第一个item,也就是说在第一个item从网络中下载图片并最终要显示的时候其实该item已经不在当前显示区域内了,此时显示的后果将是在可能在第十个item上输出图像,这就导致了图片错位的问题。所以解决之道在于可见则显示,不可见则不显示。在ImageLoader里有个imageViews的map对象,就是用于保存当前显示区域图像对应的url集,在显示前判断处理一下即可。
下面再说下内存缓冲机制,本例采用的是LRU算法,先看看MemoryCache的实现
01 | public class MemoryCache { |
02 |
03 | private static final String TAG = "MemoryCache" ; |
04 | // 放入缓存时是个同步操作 |
05 | // LinkedHashMap构造方法的最后一个参数true代表这个map里的元素将按照最近使用次数由少到多排列,即LRU |
06 | // 这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率 |
07 | private Map<String, Bitmap> cache = Collections |
08 | .synchronizedMap( new LinkedHashMap<String, Bitmap>( 10 , 1 .5f, true )); |
09 | // 缓存中图片所占用的字节,初始0,将通过此变量严格控制缓存所占用的堆内存 |
10 | private long size = 0 ; // current allocated size |
11 | // 缓存只能占用的最大堆内存 |
12 | private long limit = 1000000 ; // max memory in bytes |
13 |
14 | public MemoryCache() { |
15 | // use 25% of available heap size |
16 | setLimit(Runtime.getRuntime().maxMemory() / 10 ); |
17 | } |
18 |
19 | public void setLimit( long new_limit) { |
20 | limit = new_limit; |
21 | Log.i(TAG, "MemoryCache will use up to " + limit / 1024 . / 1024 . + "MB" ); |
22 | } |
23 |
24 | public Bitmap get(String id) { |
25 | try { |
26 | if (!cache.containsKey(id)) |
27 | return null ; |
28 | return cache.get(id); |
29 | } catch (NullPointerException ex) { |
30 | return null ; |
31 | } |
32 | } |
33 |
34 | public void put(String id, Bitmap bitmap) { |
35 | try { |
36 | if (cache.containsKey(id)) |
37 | size -= getSizeInBytes(cache.get(id)); |
38 | cache.put(id, bitmap); |
39 | size += getSizeInBytes(bitmap); |
40 | checkSize(); |
41 | } catch (Throwable th) { |
42 | th.printStackTrace(); |
43 | } |
44 | } |
45 |
46 | /** |
47 | * 严格控制堆内存,如果超过将首先替换最近最少使用的那个图片缓存 |
48 | * |
49 | */ |
50 | private void checkSize() { |
51 | Log.i(TAG, "cache size=" + size + " length=" + cache.size()); |
52 | if (size > limit) { |
53 | // 先遍历最近最少使用的元素 |
54 | Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator(); |
55 | while (iter.hasNext()) { |
56 | Entry<String, Bitmap> entry = iter.next(); |
57 | size -= getSizeInBytes(entry.getValue()); |
58 | iter.remove(); |
59 | if (size <= limit) |
60 | break ; |
61 | } |
62 | Log.i(TAG, "Clean cache. New size " + cache.size()); |
63 | } |
64 | } |
65 |
66 | public void clear() { |
67 | cache.clear(); |
68 | } |
69 |
70 | /** |
71 | * 图片占用的内存 |
72 | * |
73 | * <a href="\"http://www.eoeandroid.com/home.php?mod=space&uid=2768922\"" target="\"_blank\"">@Param</a> bitmap |
74 | * |
75 | * @return |
76 | */ |
77 | long getSizeInBytes(Bitmap bitmap) { |
78 | if (bitmap == null ) |
79 | return 0 ; |
80 | return bitmap.getRowBytes() * bitmap.getHeight(); |
81 | } |
82 | } |
首先限制内存图片缓冲的堆内存大小,每次有图片往缓存里加时判断是否超过限制大小,超过的话就从中取出最少使用的图片并将其移除,当然这里如果不采用这种方式,换做软引用也是可行的,二者目的皆是最大程度的利用已存在于内存中的图片缓存,避免重复制造垃圾增加GC负担,OOM溢出往往皆因内存瞬时大量增加而垃圾回收不及时造成的。只不过二者区别在于LinkedHashMap里的图片缓存在没有移除出去之前是不会被GC回收的,而SoftReference里的图片缓存在没有其他引用保存时随时都会被GC回收。所以在使用LinkedHashMap这种LRU算法缓存更有利于图片的有效命中,当然二者配合使用的话效果更佳,即从LinkedHashMap里移除出的缓存放到SoftReference里,这就是内存的二级缓存,有兴趣的童鞋不凡一试。
下面附上工程链接:
亦可上github下载
github下载地址:https://github.com/geniusgithub/SyncLoaderBitmapDemo
亲,动起尼的手指,让listview飞起来!
more brilliant,Please pay attention to my CSDN blog -->http://blog.csdn.net/geniuseoe2012