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

  • 相关阅读:
    《人月神话》读后感*part1
    《程序员修炼之道——从小工到专家》阅读笔记*part6
    Java课06
    《程序员修炼之道——从小工到专家》阅读笔记*part5
    《程序员修炼之道——从小工到专家》阅读笔记*part4
    Java课堂测试——输出单个文件中的前N个最常出现的英语单词
    四则运算自动出题系统——网页版
    关于JAVA项目中的常用的异常处理情况
    《程序员修炼之道——从小工到专家》阅读笔记*part3
    《程序员修炼之道——从小工到专家》阅读笔记*part2
  • 原文地址:https://www.cnblogs.com/lqminn/p/2697562.html
Copyright © 2020-2023  润新知