【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 的技巧。