【Android】图片的异步加载

这几天做项目,遇到要从一个相册里面加载数百张图片到GridView的问题,一开始将图片读取为bitmap,由于图片数量过多,发生OOM异常,导致程序崩溃。解决的方案网上也有很多,大致就是将图缩略之后再显示。具体见另外一篇博客(~~)。下面要讲的是另外一个问题。

将图缩略之后,因为要读取数百张图片进行缩略,耗时非常长久。但是事实上GridView(ListView也一样)在一个屏幕上显示的图片数量是有限的,如果首先显示一屏幕,后台再慢慢的加载其余的图片,无疑比让用户黑着屏幕长时间的等待这样的体验要来的好的多。

根据网上所查的资料,目前有两套解决方案:

1)根据博客[源码、文档、分享] 【开发共享】获取Android手机上的图片和视频缩略图,我们可以知道其实Android是自带缩略图的,缩略图在一个叫做.thumbnails的隐藏文件夹中(我写的读取SD卡所有图片格式的文件的程序是可以读取这个隐藏文件的),所以,如果直接读取这个文件夹中的文件,就可以省去图的缩略过程。(这个我没有实验,感觉比较繁琐,而且也不符合我所做项目的需要);

2)异步加载。思路和上面提到的差不多,先用一张默认空白图片先占位,之后慢慢的一张张加载,加载完毕就刷新。下面主要探讨的就是如何异步加载。

 

根据异步加载的思想,我们必须要刷新主界面,Android Handler机制中提到,主界面中主线程是负责管理界面的,要刷新我们就得通知主界面,所以很容易就想到Handler机制。额,所以目前可以这么考虑,假设我么要实现异步加载一张图片,我们可以先用一张默认图片替这个要加载的图片占位,然后将图片的加载放到一个线程里面(主要是不去影响主线程)加载完毕之后,我们可以让这个线程发消息给主线程,之后就可以由主线程更新了。(下面的代码很多是转自:Android异步加载图像小结)代码类似如下:

 1  private void loadImage(final String url, final int id) {
 2          handler.post(new Runnable() {
 3                 public void run() {
 4                     Drawable drawable = null;
 5                     try {
 6                         drawable = Drawable.createFromStream(new URL(url).openStream(), "image.png");
                //A
7 } catch (IOException e) { 8 } 9 ((ImageView) LazyLoadImageActivity.this.findViewById(id)).setImageDrawable(drawable);
                //B
10 } 11 }); 12 }

这里使用的是post,post的具体使用在Android Handler机制中讲过,将线程排进主线程的线程队列,然后等待执行,执行的结果就是ImageView更新Drawable.根据原博客的说法,这种方法的缺陷是如果要加载多个图片,这并不能实现异步加载,而是等到所有的图片都加载完才一起显示,因为它们都运行在一个线程中。我想它的写法是,在A处和B处重复同样的代码,这个样子的确是无法做到几张图片异步加载,因为更新是在一起进行的。但是换个写法差不多就可以了,我们连续使用多个post,每个post里面为一个组件加载一个图片,这样就可以实现异步加载了,慢慢来(对吧?)。

 

好吧,我们来考虑原博客里面的第一次改进,它将Handler+Runnable模式改为Handler+Thread+Message模式。我们先来看一下代码:

第一段:

 1 private void loadImage2(final String url, final int id) {
 2          Thread thread = new Thread(){
 3              @Override
 4              public void run() {
 5                Drawable drawable = null;
 6                     try {
 7                         drawable = Drawable.createFromStream(new URL(url).openStream(), "image.png");
 8                     } catch (IOException e) {
 9                     }
10 
11                 Message message= handler2.obtainMessage() ;
12                  message.arg1 = id;
13                  message.obj = drawable;
14                  handler2.sendMessage(message);
15              }
16          };
17          thread.start();
18          thread = null;
19     }

第二段:

1 final Handler handler2=new Handler(){
2           @Override
3           public void handleMessage(Message msg) {
4              ((ImageView) LazyLoadImageActivity.this.findViewById(msg.arg1)).setImageDrawable((Drawable)msg.obj);
5           }
6       };

这段代码里面新建一个线程,一旦加载完毕就通过Message通知主线程。在主线程的handler中处理图片的更新。其实没什么改进,就是将线程提出来,通知的方式由Message实现。博客后面提出的引入线程池则是主要是管理多线程(因为是在线程里面发送message,所以多线程很好实现),引入缓存的比较值得看一下:做法是建立一个HashMap,其键(key)为加载图像url,其值(value)是图像对象Drawable(原理就是保存其引用,避免重复加载同一张图片)。

 1 public class AsyncImageLoader3 {
 2    //为了加快速度,在内存中开启缓存(主要应用于重复图片较多时,或者同一个图片要多次被访问,比如在ListView时来回滚动)
 3     public Map<String, SoftReference<Drawable>> imageCache = new HashMap<String, SoftReference<Drawable>>();
 4     private ExecutorService executorService = Executors.newFixedThreadPool(5);    //固定五个线程来执行任务
 5     private final Handler handler=new Handler();
 6 
 7      /**
 8      *
 9      * @param imageUrl     图像url地址
10      * @param callback     回调接口
11      * @return     返回内存中缓存的图像,第一次加载返回null
12      */
13     public Drawable loadDrawable(final String imageUrl, final ImageCallback callback) {
14         //如果缓存过就从缓存中取出数据
15         if (imageCache.containsKey(imageUrl)) {
16             SoftReference<Drawable> softReference = imageCache.get(imageUrl);
17             if (softReference.get() != null) {
18                 return softReference.get();
19             }
20         }
21         //缓存中没有图像,则从网络上取出数据,并将取出的数据缓存到内存中
22          executorService.submit(new Runnable() {
23             public void run() {
24                 try {
25                     final Drawable drawable = Drawable.createFromStream(new URL(imageUrl).openStream(), "image.png");
26 
27                     imageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
28 
29                     handler.post(new Runnable() {
30                         public void run() {
31                            callback.imageLoaded(drawable);
32                         }
33                     });
34                 } catch (Exception e) {
35                     throw new RuntimeException(e);
36                 }
37             }
38         });
39         return null;
40     }

这里出现两个新概念SoftwareReference和ImageCallback,我们先不管,之后再解释。先看代码,我们维系了一个map,每次加载一张图片就以url为key,将图片保存在map中,下次加载的时候先检测map中是否可包含url的key,不包含的再去下载。

ImageCallback其实是个接口,这个接口的存在非常关键,因为已经把加载的方法封装在类里面,在这个类里面要获取祝Activity的handler已经非常困难了(没有细想,传递参数或许可能实现),我们需要一种将图片跟新到界面上的有效方法:

1 //对外界开放的回调接口
2     public interface ImageCallback {
3         //注意 此方法是用来设置目标对象的图像资源
4         public void imageLoaded(Drawable imageDrawable);
5     }

在主线程中首先要引入AsyncImageLoader3 对象,然后直接调用其loadDrawable方法即可,需要注意的是ImageCallback接口的imageLoaded方法是唯一可以把加载的图像设置到目标ImageView或其相关的组件上。具体代码如下:

 1 //引入线程池,并引入内存缓存功能,并对外部调用封装了接口,简化调用过程
 2     private void loadImage4(final String url, final int id) {
 3           //如果缓存过就会从缓存中取出图像,ImageCallback接口中方法也不会被执行
 4          Drawable cacheImage = asyncImageLoader.loadDrawable(url,new AsyncImageLoader.ImageCallback() {
 5              //请参见实现:如果第一次加载url时下面方法会执行
 6              public void imageLoaded(Drawable imageDrawable) {
 7                ((ImageView) findViewById(id)).setImageDrawable(imageDrawable);
 8              }
 9          });
10         if(cacheImage!=null){
11           ((ImageView) findViewById(id)).setImageDrawable(cacheImage);
12         }
13     }

注意注意!这里实现了接口当中的imageLoaded方法,就是更新界面!(嗯?所以假设我要在GridView里面显示一组图片的话,就是将u一组rl传入就行?)

原博客中还提到了使用handler的方法(下面也有解释),读者有兴趣可以去好好看看。接下来就给出一段具体的GridView异步加载图片的例子。之前推荐两篇博客:

1)演化理解 Android 异步加载图片(对前面例子代码的总结)

2)Android GridView 异步加载图片(例子就是转自第二篇博客)

 1 import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 import android.app.Activity;
 5 import android.os.Bundle;
 6 import android.widget.GridView;
 7 
 8 public class MainActivity extends Activity {
 9     /** Called when the activity is first created. */
10     @Override
11     public void onCreate(Bundle savedInstanceState) {
12         super.onCreate(savedInstanceState);
13         setContentView(R.layout.main);
14         GridView gridView=(GridView)findViewById(R.id.gridview);
15         List<ImageAndText> list = new ArrayList<ImageAndText>();
16         String[] paths=new String[15];
17         for(int i=0;i<15;i++){
18             int index=i;
19             paths[i]="/sdcard/"+String.valueOf(index+1)+".jpg";//自己动手向SD卡添加15张图片,如:1.jpg
20         }
21         for(int i=0;i<15;i++){
22             list.add(new ImageAndText(paths[i], String.valueOf(i)));
23         }
24         gridView.setAdapter(new ImageAndTextListAdapter(this, list, gridView));
25     }
26 }

这个类没什么,就是Android的主界面,声明了一个GridView,然后加载了几个SD卡上图片的地址,ImageAndText是一个辅助类,代码如下:

 1 public class ImageAndText {
 2         private String imageUrl;
 3         private String text;
 4 
 5         public ImageAndText(String imageUrl, String text) {
 6             this.imageUrl = imageUrl;
 7             this.text = text;
 8         }
 9         public String getImageUrl() {
10             return imageUrl;
11         }
12         public String getText() {
13             return text;
14         }
15 }

记载了图片的具体地址以及要显示的文字。

以下代码是实现异步获取图片的主方法,SoftReference是软引用,是为了更好的为了系统回收变量,重复的URL直接返回已有的资源,实现回调函数,让数据成功后,更新到UI线程。

 1 import java.io.IOException;
 2 import java.io.InputStream;
 3 import java.lang.ref.SoftReference;
 4 import java.net.MalformedURLException;
 5 import java.net.URL;
 6 import java.util.HashMap;
 7 
 8 import android.R.drawable;
 9 import android.graphics.Bitmap;
10 import android.graphics.BitmapFactory;
11 import android.graphics.BitmapFactory.Options;
12 import android.graphics.drawable.BitmapDrawable;
13 import android.graphics.drawable.Drawable;
14 import android.os.Handler;
15 import android.os.Message;
16 import android.util.Log;
17 import android.widget.ImageView;
18 
19 public class AsyncImageLoader {
20 
21      private HashMap<String, SoftReference<Drawable>> imageCache;
22      public AsyncImageLoader() {
23              imageCache = new HashMap<String, SoftReference<Drawable>>();
24          }
25       
26      public Drawable loadDrawable(final String imageUrl, final ImageCallback imageCallback) {
27              if (imageCache.containsKey(imageUrl)) {
28                  SoftReference<Drawable> softReference = imageCache.get(imageUrl);
29                  Drawable drawable = softReference.get();
30                  if (drawable != null) {
31                      return drawable;
32                  }
33              }
34              final Handler handler = new Handler() {
35                  public void handleMessage(Message message) {
36                      imageCallback.imageLoaded((Drawable) message.obj, imageUrl);
37                  }
38              };
39              new Thread() {
40                  @Override
41                  public void run() {
42                      Drawable drawable = loadImageFromUrl(imageUrl);
43                      imageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
44                      Message message = handler.obtainMessage(0, drawable);
45                      handler.sendMessage(message);
46                  }
47              }.start();
48              return null;
49          }
50        
51     public static Drawable loadImageFromUrl(String url) {
52 //        /**
53 //         * 加载网络图片
54 //         */
55 //            URL m;
56 //            InputStream i = null;
57 //            try {
58 //                m = new URL(url);
59 //                i = (InputStream) m.getContent();
60 //            } catch (MalformedURLException e1) {
61 //                e1.printStackTrace();
62 //            } catch (IOException e) {
63 //                e.printStackTrace();
64 //            }
65 //            Drawable d = Drawable.createFromStream(i, "src");
66         
67         /**
68          * 加载内存卡图片
69          */
70             Options options=new Options();
71             options.inSampleSize=2;
72             Bitmap bitmap=BitmapFactory.decodeFile(url, options);
73             Drawable drawable=new BitmapDrawable(bitmap);
74             return drawable;
75         }
76       
77        public interface ImageCallback {
78              public void imageLoaded(Drawable imageDrawable, String imageUrl);
79              
80          }
81 }

这段代码和前面展示的非常相似,但是有些地方是不一样的,比如使用了handler,图片家在完毕后会调用handler进行消息发送,而handler则调用imageCallback接口中的imageLoad回调函数。loadImageFromUrl里面则展示了如何将图片缩小的方法。看到这里是有一个问题的,我们知道handler只会将消息发送给对应的线程,如果我们要将图片更新到主线程,换句话说,handler必须来自主线程?(额,从整个代码来看,因为这个类的实例是在Activity里面使用的,所以这个handler很自然是来自主线程的)

 1 import java.util.List;
 2 
 3 import cn.wangmeng.test.AsyncImageLoader.ImageCallback;
 4 
 5 import android.app.Activity;
 6 import android.graphics.drawable.Drawable;
 7 import android.util.Log;
 8 import android.view.LayoutInflater;
 9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.widget.ArrayAdapter;
12 import android.widget.GridView;
13 import android.widget.ImageView;
14 import android.widget.ListView;
15 import android.widget.TextView;
16 
17 public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {
18 
19         private GridView gridView;
20         private AsyncImageLoader asyncImageLoader;
21         public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, GridView gridView1) {
22             super(activity, 0, imageAndTexts);
23             this.gridView = gridView1;
24             asyncImageLoader = new AsyncImageLoader();
25         }
26 
27         public View getView(int position, View convertView, ViewGroup parent) {
28             Activity activity = (Activity) getContext();
29 
30             // Inflate the views from XML
31             View rowView = convertView;
32             ViewCache viewCache;
33             if (rowView == null) {
34                 LayoutInflater inflater = activity.getLayoutInflater();
35                 rowView = inflater.inflate(R.layout.griditem, null);
36                 viewCache = new ViewCache(rowView);//将Item的布局View传入构造函数
37                 rowView.setTag(viewCache);
38             } else {
39                 viewCache = (ViewCache) rowView.getTag();
40             }
41             ImageAndText imageAndText = getItem(position);//Adapter自带的方法
42 
43             // Load the image and set it on the ImageView
44             String imageUrl = imageAndText.getImageUrl();
45             ImageView imageView = viewCache.getImageView();
46             imageView.setTag(imageUrl);
47             Drawable cachedImage = asyncImageLoader.loadDrawable(imageUrl, new ImageCallback() {
48                 public void imageLoaded(Drawable imageDrawable, String imageUrl) {
49                     ImageView imageViewByTag = (ImageView) gridView.findViewWithTag(imageUrl);
50                     if (imageViewByTag != null) {
51                         imageViewByTag.setImageDrawable(imageDrawable);
52                     }
53                 }
54             });
55             if (cachedImage == null) {
56                 imageView.setImageResource(R.drawable.icon);
57             }else{
58                 imageView.setImageDrawable(cachedImage);
59             }
60             // Set the text on the TextView
61             TextView textView = viewCache.getTextView();
62             textView.setText(imageAndText.getText());
63             return rowView;
64         }
65 
66 }

这段代码值得好好研究一下了。

这个Adapter的类构造函数与我们一般看到的不一样,它将GridView显示所在的Activity,以及GridView显示所要的数据和GridView本身全部作为参数传了进来,声明了图片异步加载类的实例,重点就在getView函数。在此之前先介绍另外一个辅助类:

 1 public class ViewCache {
 2 
 3         private View baseView;
 4         private TextView textView;
 5         private ImageView imageView;
 6 
 7         public ViewCache(View baseView) {
 8             this.baseView = baseView;
 9         }
10 
11         public TextView getTextView() {
12             if (textView == null) {
13                 textView = (TextView) baseView.findViewById(R.id.text);
14             }
15             return textView;
16         }
17 
18         public ImageView getImageView() {
19             if (imageView == null) {
20                 imageView = (ImageView) baseView.findViewById(R.id.image);
21             }
22             return imageView;
23         }
24 
25 }

ViewCache通过传入View参数,可以保存相关View里面组件的引用。

接下来要解释的一个很重要的点就是setTag的作用:

public void setTag (Object tag)

Since: API Level 1
Sets the tag associated with this view. A tag can be used to mark a view in its hierarchy and does not have to be unique within the hierarchy. Tags can also be used to store data within a view without resorting to another data structure.
Parameters:an Object to tag the view with

Tag是用来标记一个组件的,它不一定是唯一的,传入的参数是一个Object。Tags可以被用来存储数据并且不需要考虑将数据转化成另外的数据结构(换句话说,你想存什么就存什么)。

在前面的代码中,每个rowView都设置了一个Tag,这个tag有很妙的用处,rowView是每个Item的显示布局对象,viewCache里面则保存了rowView组件里面相应的组件实例的引用。为什么这样子?先推荐另外一个博客:Android杂谈--内存泄露(1)--contentView缓存使用与ListView优化。读这个博客的目的是去了解contentView存在的意义,通过缓存convertView,可以缓存可视范围内的listItem,当再次向下滑动时又开始更 新View,这种利用缓存convertView的方式可以判断如果缓存中不存在View才创建View,如果已经存在可以利用缓存中的View,这样会 减少很多View的创建,提升了性能。再加上前面提到的缓存图片的例子,应该很容易理解这里的机制,contentView(rowView)里面记录缓存了曾经显示的Item,并且一旦显示过,它就保存了与之相关的ViewCache,并且可以通过getTag得到!!!

到这里不知道大家有没有发现这几段程序的微妙之处,首先是缓存了view,其次当我们取出view,根据url去获取图片的时候,图片也是缓存好的,所以整个过程就会非常迅速!!

然后,就是代码中红色部分是对应的,我想理解起来是非常简单的,同时这里也展示了Tag的另外一种作用:标志组件(前提是tag不重复)

后面一段是精华~在异步加载数据asyncImageLoader.loadDrawable函数返回数据的时候,cacheImage是null的,此时直接设置默认图片(等待图片加载完毕就通过回调函数设置图片),否则,就将加载的图片显示出来。(这就是一开始阐述的思想!)

 

最后,补充一下前面遗留下来的一个问题:SoftReference(内存优化的两个类:SoftReference 和 WeakReference)

如果你想写一个 Java 程序,观察某对象什么时候会被垃圾收集的执行绪清除,你必须要用一个 reference 记住此对象,以便随时观察,但是却因此造成此对象的reference 数目一直无法为零, 使得对象无法被清除。不过,现在有了 Weak Reference 之后,这就可以迎刃而解了。如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 WeakReference 来记住此对象,而不是用一般的 reference。(我的理解是,它指向一个对象,但是引用计数不会有所改变)

 

 1 A obj = new A();
 2 WeakReference wr = new WeakReference(obj);
 3 obj = null;
 4 //等待一段时间,obj对象就会被垃圾回收
 5 ...
 6 if (wr.get()==null) { 
 7   System.out.println("obj 已经被清除了 "); 
 8 } else { 
 9   System.out.println("obj 尚未被清除,其信息是 "+obj.toString());
10 }

 

在此例中,透过 get() 可以取得此 Reference 的所指到的对象,如果传出值为 null 的话,代表此对象已经被清除。这类的技巧,在设计 Optimizer 或 Debugger 这类的程序时常会用到,因为这类程序需要取得某对象的信息,但是不可以 影响此对象的垃圾收集。

Soft Reference 虽然和 Weak Reference 很类似,但是用途却不同。 被 Soft Reference 指到的对象,即使没有任何 Direct Reference,也不会被清除。一直要到 JVM 内存不足时且 没有 Direct Reference 时才会清除,SoftReference 是用来设计 object-cache 之用的。如此一来 SoftReference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。我觉得 Soft Reference 也适合拿来实作 pooling 的技巧。

 

posted @ 2012-09-21 22:35  大脚印  阅读(2428)  评论(0编辑  收藏  举报