Android的缓存技术:LruCache和DiskLruCache

在Android中当加载大量图片时首先需要考虑的一个问题是如何避免OOM。为了保证内存的使用始终维持在一个合理的范围,通常会把移出屏幕的图片进行回收处理,此时垃圾回收器会认为你不再持有这些图片的引用,从而对这些图片进行GC。然而当某些图片被回收之后用户又将它重新滑入屏幕时,这时又会去重新加载一遍刚刚加载过的图片。这样频繁地处理图片的加载和回收不利于操作的流畅性,而内存和硬盘的Cache就会帮助解决这个问题,实现快速加载已加载过的图片。
在缓存上,主要有两种级别的Cache:LruCache和DiskLruCache。 前者是基于内存的,后者是基于硬盘的。

LruCache(内存缓存技术)

内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。以前常用的内存缓存是通过SoftReference或WeakReference来实现的,但现在不推荐使用这种方式了,从Android2.3(API 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。
现在非常流行的内存缓存技术是LruCache(LRU是Least Recently Used 近期最少使用算法),在android-support-v4包中提供。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
下面是一个使用 LruCache 来缓存图片的例子:

public class BitmapCache {
    private LruCache mMemoryCache;

    public BitmapCache() {
     // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常
       int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
       // 使用最大可用内存值的1/8作为缓存的大小。  
       int cacheSize = maxMemory / 8;  
       mMemoryCache = new LruCache(cacheSize) {  
           @Override  
           protected int sizeOf(String key, Bitmap bitmap) {  
               // 重写此方法来衡量每张图片的大小,默认返回图片数量。  
               return bitmap.getByteCount() / 1024;  
           }  
       };
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
       if (getBitmapFromMemCache(key) == null) {  
          mMemoryCache.put(key, bitmap);  
      }  
  }  

   public Bitmap getBitmapFromMemCache(String key) {  
       return mMemoryCache.get(key);  
   } 
}

这里使用了系统分配给应用程序的八分之一内存来作为缓存大小。那么缓存大小有什么限制吗?其实并没有一个指定大小的缓存可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 Java.lang.OutOfMemory 的异常。

DiskLruCache(硬盘缓存技术)

传统的硬盘缓存一般是通过流的形式将图片存储成文件,譬如如下代码:

public String ImagePath = Environment.getExternalStorageDirectory().getAbsolutePath().toString() + "/Cache";

private void downloadImage(String imageUrl) {
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                Log.d("TAG", "monted sdcard");
            } else {
                Log.d("TAG", "has no sdcard");
            }
            HttpURLConnection con = null;
            FileOutputStream fos = null;
            BufferedOutputStream bos = null;
            BufferedInputStream bis = null;
            File imageFile = null;
            try {
                URL url = new URL(imageUrl);
                con = (HttpURLConnection) url.openConnection();
                con.setConnectTimeout(5 * 1000);
                con.setReadTimeout(15 * 1000);
                con.setDoInput(true);
                con.setDoOutput(true);
                bis = new BufferedInputStream(con.getInputStream());
                imageFile = new File(ImagePath + + "/" + imageUrl);
                fos = new FileOutputStream(imageFile);
                bos = new BufferedOutputStream(fos);
                byte[] b = new byte[1024];
                int length;
                while ((length = bis.read(b)) != -1) {
                    bos.write(b, 0, length);
                    bos.flush();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (bis != null) {
                        bis.close();
                    }
                    if (bos != null) {
                        bos.close();
                    }
                    if (con != null) {
                        con.disconnect();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

但是我们接下来讲的是另外一种硬盘缓存技术:DiskLruCache。LruCache只是管理了内存中图片的存储与释放,其实Google还提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证)。

  • 使用

由于DiskLruCache并不是由Google官方编写的,所以这个类并没有被包含在Android API当中,我们需要将这个类从网上下载下来,然后手动添加到项目当中。DiskLruCache源码见后面Demo。下载好源码之后,只需要在项目中新建一个libcore.io包,然后将DiskLruCache.Java文件复制到这个包中即可。

  • 打开缓存

DiskLruCache是不能new出实例的,如果我们要创建一个DiskLruCache的实例,则需要调用它的open()方法,接口如下所示:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。
DiskLruCache并没有限制数据的缓存位置,可以自由地进行设定,但是通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/Application package/cache这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。但同时我们又需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况,因此比较优秀的程序都会专门写一个方法来获取缓存地址,如下所示:

public File getDiskCacheDir(Context context, String uniqueName) {  
    String cachePath;  
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {  
        cachePath = context.getExternalCacheDir().getPath();  
    } else {  
        cachePath = context.getCacheDir().getPath();  
    }  
    return new File(cachePath + File.separator + uniqueName);  
} 

可以看到,当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,否则就调用getCacheDir()方法来获取缓存路径。前者获取到的就是 /sdcard/Android/data/application package/cache 这个路径,而后者获取到的是 /data/data/application package/cache 这个路径。
接着将获取到的路径和一个uniqueName进行拼接,作为最终的缓存路径返回。那么这个uniqueName又是什么呢?其实这就是为了对不同类型的数据进行区分而设定的一个唯一值,比如说bitmap、object等文件夹。
接着是应用程序版本号,我们可以使用如下代码简单地获取到当前应用程序的版本号:

public int getAppVersion(Context context) {  
    try {  
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);  
        return info.versionCode;  
    } catch (NameNotFoundException e) {  
        e.printStackTrace();  
    }  
    return 1;  
}

需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。
后面两个参数就没什么需要解释的了,第三个参数传1,第四个参数通常传入10M的大小就够了,这个可以根据自身的情况进行调节。
因此,一个非常标准的open()方法就可以这样写:

DiskLruCache mDiskLruCache = null;  
try {  
    File cacheDir = getDiskCacheDir(context, "bitmap");  
    if (!cacheDir.exists()) {  
        cacheDir.mkdirs();  
    }  
    mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);  
} catch (IOException e) {  
    e.printStackTrace();  
}
  • 关闭缓存

这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。

mDiskLruCache.close();
  • 写入缓存

来看写入,比如说现在有一张图片,地址是https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg,那么为了将这张图片下载下来,可以这样写:

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
    HttpURLConnection urlConnection = null;  
    BufferedOutputStream out = null;  
    BufferedInputStream in = null;  
    try {  
        final URL url = new URL(urlString);  
        urlConnection = (HttpURLConnection) url.openConnection();  
        in = new BufferedInputStream(urlConnection.getInputStream());  
        out = new BufferedOutputStream(outputStream);  
        int b;  
        while ((b = in.read()) != -1) {  
            out.write(b);  
        }  
        return true;  
    } catch (final IOException e) {  
        e.printStackTrace();  
    } finally {  
        if (urlConnection != null) {  
            urlConnection.disconnect();  
        }  
        try {  
            if (out != null) {  
                out.close();  
            }  
            if (in != null) {  
                in.close();  
            }  
        } catch (final IOException e) {  
            e.printStackTrace();  
        }  
    }  
    return false;  
} 

图片的传统硬盘缓存就可以通过outputStream写入到本地文件。有了这个方法之后,下面我们就可以使用DiskLruCache来进行写入了,写入的操作是借助DiskLruCache.Editor这个类完成的。类似地,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,接口如下所示:

public Editor edit(String key) throws IOException 

edit()方法接收一个参数key,这个key将会成为缓存文件的文件名,并且必须要和图片的URL是一一对应的。那么怎样才能让key和图片的URL能够一一对应呢?直接使用URL来作为key?不太合适,因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。
我们可以写一个字符串MD5编码的工具类:

public String getMD5String(String key) {  
    String cacheKey;  
    try {  
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");  
        mDigest.update(key.getBytes());  
        cacheKey = bytesToHexString(mDigest.digest());  
    } catch (NoSuchAlgorithmException e) {  
        cacheKey = String.valueOf(key.hashCode());  
    }  
    return cacheKey;  
}  

private String bytesToHexString(byte[] bytes) {  
    StringBuilder sb = new StringBuilder();  
    for (int i = 0; i < bytes.length; i++) {  
        String hex = Integer.toHexString(0xFF & bytes[i]);  
        if (hex.length() == 1) {  
            sb.append('0');  
        }  
        sb.append(hex);  
    }  
    return sb.toString();  
}

现在就可以这样写来得到一个DiskLruCache.Editor的实例:

String imageUrl = "https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg";  
String key = getMD5String(imageUrl);  
DiskLruCache.Editor editor = mDiskLruCache.edit(key);

有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能了。注意newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。
因此,一次完整写入操作的代码如下所示:

new Thread(new Runnable() {  
    @Override  
    public void run() {  
        try {  
            String imageUrl = "https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg";  
            String key = getMD5String(imageUrl);  
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
            if (editor != null) {  
                OutputStream outputStream = editor.newOutputStream(0);
                if (downloadUrlToStream(imageUrl, outputStream)) {  
                    editor.commit();  
                } else {
                    editor.abort();  
                }  
            }  
            mDiskLruCache.flush();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}).start(); 

由于这里调用了downloadUrlToStream()方法来从网络上下载图片,所以一定要确保这段代码是在子线程当中执行的。注意在代码的最后我还调用了一下flush()方法,这个方法并不是每次写入都必须要调用的,但在这里却不可缺少,我会在后面说明它的作用。

  • 读取缓存

读取的方法要比写入简单一些,主要是借助DiskLruCache的get()方法实现的,接口如下所示:

public synchronized Snapshot get(String key) throws IOException

get()方法要求传入一个key来获取到相应的缓存数据,而这个key毫无疑问就是将图片URL进行MD5编码后的值了,因此读取缓存数据的代码就可以这样写:

String imageUrl = "https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg";  
String key = getMD5String(imageUrl);  
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);

这里获取到的是一个DiskLruCache.Snapshot对象,这个对象我们该怎么利用呢?很简单,只需要调用它的getInputStream()方法就可以得到缓存文件的输入流了。同样地,getInputStream()方法也需要传一个index参数,这里传入0就好。有了文件的输入流之后,想要把缓存图片显示到界面上就轻而易举了。所以,一段完整的读取缓存,并将图片加载到界面上的代码如下所示:

try {  
    String imageUrl = "https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg";  
    String key = getMD5String(imageUrl); 
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
    if (snapShot != null) {  
        InputStream is = snapShot.getInputStream(0);  
        Bitmap bitmap = BitmapFactory.decodeStream(is);  
        mImage.setImageBitmap(bitmap);  
    }  
} catch (IOException e) {  
    e.printStackTrace();  
}
  • 移除缓存

学习完了写入缓存和读取缓存的方法之后,最难的两个操作你就都已经掌握了,那么接下来要学习的移除缓存对你来说也一定非常轻松了。移除缓存主要是借助DiskLruCache的remove()方法实现的,接口如下所示:

public synchronized boolean remove(String key) throws IOException

remove()方法中要求传入一个key,然后会删除这个key对应的缓存图片,示例代码如下:

try {  
    String imageUrl = "https://img-my.csdn.net/uploads/201308/31/1377949454_6367.jpg";  
    String key = getMD5String(imageUrl);   
    mDiskLruCache.remove(key);  
} catch (IOException e) {  
    e.printStackTrace();  
}

用法虽然简单,但是你要知道,这个方法我们并不应该经常去调用它。因为你完全不需要担心缓存的数据过多从而占用SD卡太多空间的问题,DiskLruCache会根据我们在调用open()方法时设定的缓存最大值来自动删除多余的缓存。只有你确定某个key对应的缓存内容已经过期,需要从网络获取最新数据的时候才应该调用remove()方法来移除缓存。

  • 清空缓存

移除缓存只能根据key移除掉某一个缓存,如果我们需要清空DiskLruCache的所有缓存呢?delete()方法用于将所有的缓存数据全部删除,比如说手动清理缓存功能就可以用它来做。

mDiskLruCache.delete();
  • 其它API

除了打开、关闭、写入、读取、移除、清空缓存之外,DiskLruCache还提供了另外一些比较常用的API,我们简单学习一下。

  1. size()
    这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。
  2. flush()
    这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件,后面讲)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。

解读journal

我们打开DiskLruCache的缓存目录,如下:
Android的缓存技术:LruCache和DiskLruCache_第1张图片
那么这些文件到底都是些什么呢?其实那些文件名很长的文件就是一张张缓存的图片,每个文件都对应着一张图片,文件名就是我们刚刚将它们的url进行MD5编码后得到的key。而journal文件是DiskLruCache的一个日志文件,程序对每张图片的操作记录都存放在这个文件中,基本上看到journal这个文件就标志着该程序使用DiskLruCache技术了。
再来打开journal文件看看到底记录了哪些日志吧:

libcore.io.DiskLruCache
1
1
1

DIRTY 52fd265dbb4811d091f0b7dce3a04991
CLEAN 52fd265dbb4811d091f0b7dce3a04991 82558
READ 52fd265dbb4811d091f0b7dce3a04991
DIRTY ac8524f67e76ebdbbcea215247c5ed52
REMOVE ac8524f67e76ebdbbcea215247c5ed52

这里选取一张图片缓存的操作日志说明一下:
前五行被称为journal文件的头,第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着我们使用的是DiskLruCache技术。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行是一个空行。
第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。DiskLruCache会在每一行CLEAN记录的最后加上该条缓存数据的大小,以字节为单位。82558也就是我们缓存的那张图片的字节数了,换算出来大概是80.62K。前面我们所学的size()方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把journal文件中所有CLEAN记录的字节数相加,求出的总合再把它返回而已。
除了DIRTY、CLEAN、REMOVE之外,还有一种前缀是READ的记录,这个就非常简单了,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。对于图片和数据量都非常大的程序,可能你还会担心,如果我不停频繁操作的话,就会不断地向journal文件中写入数据,那这样journal文件岂不是会越来越大?这倒不必担心,DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内。

实例

下面我通过一个例子来演示一下LruCache和DiskLruCache的使用:
主要功能就是用一个GridView来展示大量的网络图片,因为GridView有复用机制,就是将滑出屏幕的ImageView持有的图片进行回收,再将ImageView指向新的图片,从而避免了OOM。同时在ImageView加载图片时我们使用了内存和硬盘进行双缓存。
注:如果是自定义的View来实现照片墙,没有复用机制的话,那么需要在ImageView滑出屏幕后手动回收图片,即将ImageView加载一张默认空图片,从而切断与之前图片的引用,让系统可以回收之前图片,避免OOM。

项目流程概述:每一个图片的加载和显示任务都运行在独立的线程(异步任务)中,除非这个图片缓存在LruCache中,这种情况下图片会立即显示。如果需要的图片缓存在DiskLruCache,也会开启一个独立的线程队列。如果在缓存中都没有正确的图片,任务线程会从网络下载。
注:我之前尝试过把获取DiskLruCache中图片任务放在主线程中,和获取LruCache一样,发现卡顿严重,说明获取硬盘缓存还是需要时间的,最后放到异步任务中去获取,效果好多了。

看一下ImageView是怎么进行图片加载的吧:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
      ViewHolder viewHolder = null;
      if(convertView == null) {
            viewHolder = new ViewHolder();
            convertView = LayoutInflater.from(MainActivity.this).inflate(R.layout.image_item, null);
            viewHolder.image = (ImageView) convertView.findViewById(R.id.img);      
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder)convertView.getTag();
        }

        RelativeLayout.LayoutParams pra = (RelativeLayout.LayoutParams) viewHolder.image.getLayoutParams();
        pra.height = (gridview.getWidth() - 2*dip2px(2)) / 3;
        viewHolder.image.setLayoutParams(pra);

        String imageUrl = getItem(position);
        //给每个ImageView设置唯一的tag,防止图片加载错乱
        viewHolder.image.setTag(imageUrl);
        viewHolder.image.setImageResource(R.drawable.error);            
        loadBitmaps(viewHolder.image, imageUrl);
        return convertView;
}

ListView、GridView都有两个方法setTag/findViewWithTag,简单的说就是给Item中的View设置一个唯一的Tag值,之后可以通过这个Tag值找到与其对应的View,因为ListView和GridView都有复用机制,所以当Item被滑出屏幕后,有可能会被复用,Item中的View也就被设置了另外一个Tag值,所以这时通过之前的Tag寻找View返回的是null。
这里给ImageView设置了一个唯一Tag值,就是待加载图片的url,之后图片下载完成,再通过这个Tag找到ImageView,如果ImageView为null,说明图片已经滑出了屏幕,此时不再加载,否则ImageView加载刚刚下载完成的图片,这样防止了下载好的图片加载到别的ImageView而形成错乱。

public void loadBitmaps(ImageView imageView, String imageUrl) {
    try {
        Bitmap bitmap = bitmapCache.getBitmapFromMemCache(imageUrl);
        if (bitmap == null) {
            BitmapDownloadTask task = new BitmapDownloadTask();
            taskCollection.add(task);
            task.execute(imageUrl);
        } else {
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
} 

图片加载过程是先从内存中取图片,如果内存中有图片,直接加载。如果内存中没有图片,则开启一个异步任务去加载图片。

class BitmapDownloadTask extends AsyncTask {
    private String imageUrl;

    @Override
    protected Bitmap doInBackground(String... params) {
        imageUrl = params[0];
        Bitmap bitmap = bitmapDiskCache.getBitmapFromDiskCache(imageUrl);
        if (bitmap != null) {
            bitmapCache.addBitmapToMemoryCache(imageUrl, bitmap);
            return bitmap;
        } else {
                bitmapDiskCache.downloadBitmapToDiskCache(imageUrl, new DownloadListener() {
                @Override
                public void downloadSuccess(Bitmap bitmap) {
                        bitmapCache.addBitmapToMemoryCache(imageUrl, bitmap);                       
                }
                @Override
                public void downloadFail() {
                }       
            });
            return bitmapDiskCache.getBitmapFromDiskCache(imageUrl);
        }
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        ImageView imageView = (ImageView) gridview.findViewWithTag(imageUrl);
        //通过Tag找到对应的ImageView,如果ImageView滑出屏幕,则返回null
        if (imageView != null && bitmap != null) {
            imageView.setImageBitmap(bitmap);
        }
        taskCollection.remove(this);
    }
}   

异步任务中先去硬盘缓存取图片,如果硬盘缓存中存在图片,则将图片先缓存到内存,然后加载到ImageView(这里的ImageView是通过Tag来寻找的,如果ImageView已经滑出屏幕,则返回null,也就不需要加载图片了)。如果硬盘缓存中没有图片,则通过HttpURLConnection下载图片,并通过流的形式存储到硬盘缓存,如果存储到硬盘成功,在回调接口中再将图片存储到内存中,然后加载刚刚下载成功的图片到ImageView(同理滑出屏幕时不加载)。

public void downloadBitmapToDiskCache(String imageUrl, DownloadListener mDownloadListener) {      
    try {
        downloadListener = mDownloadListener;
        String key = getMD5String(imageUrl);  
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
        if (editor != null) {  
            OutputStream outputStream = editor.newOutputStream(0);
            if (downloadUrlToStream(imageUrl, outputStream)) {  
                editor.commit();
                if (downloadListener != null) {
                 Bitmap bmp =  getBitmapFromDiskCache(imageUrl);
                 downloadListener.downloadSuccess(bmp);
                }
            } else {  
                editor.abort();
                if (downloadListener != null) {
                 downloadListener.downloadFail();
                }
            }  
        }  
//      mDiskLruCache.flush(); 
    } catch (IOException e) {
        e.printStackTrace();  
    }
}

以下是通过HttpURLConnection下载图片,并转换成流。

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
        HttpURLConnection urlConnection = null;  
        BufferedOutputStream out = null;  
        BufferedInputStream in = null;  
        try {  
            final URL url = new URL(urlString);  
            urlConnection = (HttpURLConnection) url.openConnection();  
            in = new BufferedInputStream(urlConnection.getInputStream());  
            out = new BufferedOutputStream(outputStream);  
            int b;  
            while ((b = in.read()) != -1) {  
                out.write(b);  
            }  
            return true;  
        } catch (final IOException e) {  
            e.printStackTrace();  
        } finally {  
            if (urlConnection != null) {  
                urlConnection.disconnect();  
            }  
            try {  
                if (out != null) {  
                    out.close();  
                }  
                if (in != null) {  
                    in.close();  
                }  
            } catch (final IOException e) {  
                e.printStackTrace();  
            }  
        }  
        return false;  
    } 

看一下运行效果吧:

Demo下载地址

上面例子中是在子View绘制的时候(getView函数中)加载图片的,但是如果图片还没来得及下载完成,ImageView就滑出屏幕了呢?这种情况下异步任务任然保存在任务队列,之后还会去下载,顶多下载完成后找不到之前的ImageView而不去加载显示。其次在滑动过程中还有子线程在后台下载,容易造成卡顿。下面我们对之前的代码稍作改进:

  • 滑动过程中停止所有下载,增加滑动流畅度
  • 滑动停止后只加载当前显示页图片,对滑出屏幕的ImageView不做处理
    @Override  
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {  
        mFirstVisibleItem = firstVisibleItem;  
        mVisibleItemCount = visibleItemCount;  
        // 下载的任务应该由onScrollStateChanged里调用,但首次进入程序时onScrollStateChanged并不会调用,  
        // 因此在这里为首次进入程序开启下载任务。  
        if (isFirstEnter && visibleItemCount > 0) {  
            loadBitmaps(firstVisibleItem, visibleItemCount);  
            isFirstEnter = false;  
        }  
    } 

    @Override  
    public void onScrollStateChanged(AbsListView view, int scrollState) {  
        // 仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务  
        if (scrollState == SCROLL_STATE_IDLE) {  
            loadBitmaps(mFirstVisibleItem, mVisibleItemCount);  
        } else {  
            cancelAllTasks();  
        }  
    } 

    private void loadBitmaps(int firstVisibleItem, int visibleItemCount) {  
        try {  
            for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {  
                String imageUrl = Images.imageUrls[i];
                BitmapDownloadTask task = new BitmapDownloadTask();
                taskCollection.add(task);
                task.execute(imageUrl);
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    } 

改进后Demo下载地址

你可能感兴趣的:(数据存储)