Android中图片的三级缓存策略



在开发过程中,经常会碰到进行请求大量的网络图片的样例。假设处理的不好。非常easy造成oom。对于避免oom的方法,无非就是进行图片的压缩。及时的回收不用的图片。这些看似简单可是处理起来事实上涉及的知识特别广泛。在这里主要解说图片的缓存,通过缓存也是个非常好的避免oom的途径。近期经常使用的到自然是LruCache了,它里面有一个LindedHashMap链式表,并且这个表是按近期最少使用算法排序的,近期使用的往往拍的靠前。最少使用的往往处于队尾,当须要回收利用的时候,最后面的那个元素是会被清除掉的。LruCache主要实现了内存缓存,这里还会解说DiskLruCache,这是GitHub开源库提供的使用文件缓存的一种基于近期最少使用的硬盘缓存类。

同一时候顺带解说图片的简单压缩方法。那么,接下来就须要了解LruCache,DiskLruCache,BitmapFactory。



一、LruCache




LruCache是Android中提供的基于近期最少使用算法的缓存策略,它能够对一定数量的值持有强引用。近期最少使用算法体如今,当有一个值被訪问的时候,这个值就会被移动到队列的对头,而当一个值加入的时候恰好达到LruCache申请的缓存空间。那么处于队尾的值就会被踢出队列。由于该值不再是缓存cache持有的对象,所以一旦垃圾回收器须要回收内存的时候,该值就会由于处于回收机制考虑的对象而可能被回收。


使用Lrucache有非常多细节要注意。你应该重写create方法,这样即使在内存中找不到key相应的值,也能又一次创建一个。应该重写sizeOf方法。由于这种方法默认是返回缓存中实体的数量的。而不是占用空间的大小。这个类是线程安全的。当我们调用get方法的时候,系统会帮我们进行系统同步的。

可是注意。假设你自己重写了create方法的话,create的方法并非线程安全的。可是由于get方法里面进行加入值对象的时候会推断是否发生冲突。所以我们不须要考虑线程安全的问题。



以下具体解说LruCache的各方面。方便后面的使用。



①、LruCache的构造函数解析,首先看源代码:

  public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }


通过构造函数的源代码,这里须要解释两个对象。一个是maxSize,一个是map。


1、maxSize:当我们使用构造函数的时候。假设没有重写sizeOf方法的话,这个值代表在此cache里面能够缓存的实体个数(Bitmap,int,String......)的最大值。假设重写了sizeOf方法的话。这个值代表全部缓存实体所占用的字节大小的总和。此值必须大于0.


2、map:这是LruCache里面用于存放实体对象的链式表,前面说了,Lrucache是基于近期最少使用算法来决定淘汰哪个实体的,这个算法事实上不是LruCache实现的,而是LinkedHashMap实现的。

在这里,map=new LinkedHashMap中的第三个參数。假设传值true的话,那么链式表里面的对象会依据近期最少使用算法来排序,近期最少使用的对象。就越往后。假设传值false的话,就是按插入值得顺序在链式表中排序。


②、又一次设置Lrucache对象缓存大小的最大值:


 public void resize(int maxSize) {}


此方法用于又一次这是缓存大小。当新的maxSize比原来的值要大的时候,仅仅是将新的值赋值给了原来的值,并没有做其它的事情。而当新的值比原来的值小的时候。那么就会把已经缓存的对象从队列里移除。直到缓存的大小小于或者等于新的maxSize的值。


③、依据key来创建一个值:


 protected V create(K key) {}


当依据key从缓存中获取值的时候。假设并没有cache并没有该键值对的存在,那么就会调用此方法。此方法默认实现是返回null对象的,所以假设有须要。我们须要重写这种方法,便于在我们获取某个key相应的值不存在的时候,从create方法创建一个新的值。并加入进链式表里面。注意这种方法不是线程安全的。可能一个线程在调用此方法进行创造值的时候。另一个线程就刚好在读取该key相应的值(读取key相应的值的时候,由于前一个线程的create方法还在执行。所以该线程并没有获得值,因此又会调用create方法去创造一个值)。这样会导致多个值被创建。

所以,假设创建值的时候发生了冲突,那么新创建的值就会被丢弃,否则就会被压进链式表的表头。


④、依据key来获取一个值:


 public final V get(K key) {}



依据key从缓存中获取相应的值,假设缓存中存在该值就返回。假设不存在,就会调用create方法去创建一个值,注意。假设你没有重写这种方法的话。默认返回的都是null值。

假设create了一个新的值(不能为null)且不与原来cache里面的实体产生冲突的话,就会把该值压进cache里面,并返回。假设cache既不存在该值也不能通过create创建一个新值,那么此方法返回null。


⑤、在缓存中加入一个键值对:


 public final V put(K key, V value) {}


将一个键值对加入进缓存,注意key和value均不能为null,此方法假设成功将键值对加入进缓存,那么就会返回null。

假设返回的值不为null。说明缓存里面已经存在了该key相应的值,不能再进行加入。


⑥、将某个对象从缓存区中移除:


public final V remove(K key) {}



将key相应的value从缓存中移除,假设返回null表示移除失败,即缓存中不存在该键值对。假设返回值不为null,说明移除成功。


⑦、计算某个实体占用的内存空间:


protected int sizeOf(K key, V value) {}


此方法非常重要此方法特别重要。这种方法默认的实现是每次有一个实体加入至缓存。就+1,导致兴许计算缓存是否足够容纳实体的时候,是通过推断实体个数来计算。而不是依据实体占用的空间来比較。所以一般来说。这种方法都须要重写,此方法应该返回当前key相应的实体占用的空间大小。



⑧、清除缓存空间占用的全部对象:


public final void evictAll(){}

此方法会依次将全部存储的对象移除。


⑨、返回缓存中的全部对象:


 public synchronized final Map<K, V> snapshot(){}


会返回依照近期最少使用排序的LinkedHashMap对象。此对象保存了全部的缓存对象。样例在后面给出,如今先总的介绍要用到的知识。



二、DiskLruCache



DiskLruCache并非android提供的api,而是一个开源库的代码,这个类受到了Google的强烈推荐。所以。这里将介绍它。它事实上是一个使用文件系统的採用近期最少用算法的缓存类。DiskLruCache是一个拥有一定空间的文件系统方式的缓存对象,当中的每一个缓存实体都包括有一个字符串类型的key,和固定数量的文件(一个key能够相应多个文件。这些文件都保存着缓存数据)。这些文件是按顺序排列的,通常来说是一些文件或者输入流的字节形式的。每一个文件得长度都必须介于0-Integer.MAXVALUE之间。

DiskLruCache是利用文件系统的文件夹来存储数据的,缓存对象会经常对此文件夹进行删除文件获取覆盖文件的操作。因此该文件夹必须是该DiskLruCache专用的。当我们进行DiskLruCache对象创建的时候,应该指定一个缓存大小给它,当缓存数据的大小超过限制的大小的时候。它会在后台将一些缓存实体删除掉,直到缓存的大小达到限制的值。同一时候要注意的一点时。这个限制值不是绝对的,比方DiskLruCache进行文件删除的时候,缓存的容量可能会临时的超越限制的值。

当想要更新或者创建一个缓存实体的话,应该调用DiskLruCache的edit方法获取一个Editor对象。此对象假设是null表示当前的值不可编辑。此方法必须和Editor.commit或者Editor.abort相应。另外就是,使用这个类进行缓存的时候,应该捕获一些常见的IO操作异常,由于这个类在进行写文件的时候假设发现错误。仅仅会改动失败,并不会导致操作上的失败。兴许提到的缓存对象指的是DiskLruCache。缓存实体指的是保存在缓存对象里面的具体对象。


①、DiskLruCache对象的获取:


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

參数解析:

directory:表示存储缓存数据的文件。

appVersion:当前应用的appVersion。主要作用在于书写缓存日志的时候,用来标识缓存数据的版本号号。

valueCount:每一个缓存实体能够相应的文件的个数。key->多个缓存数据

maxSoze:DiskLruCache最大能够缓存的数据容量大小。


②、获取缓存实体。为了获取缓存实体,先要了解一下Snopshot,这是一个DiskLruCache的内部类。它代表了一个缓存实体的值。关于这个类,有两个重要的经常用法:例如以下:


 public InputStream getInputStream(int index)


以及


 public String getString(int index)


前面提到过一个key是能够相应多个缓存文件的,这里的index就是指该key所相应的第index文件。能够通过第一种方法获得该文件的输入流读取内容。假设该文件保存的是字符串的内容,能够通过另外一种方法直接获取到字符串值。

另外,读取完文件的内容后。须要调用close方法将给key相应的缓存文件关闭。

那么怎样获取某个key相应的缓存实体的Snopshot对象内。例如以下:


public synchronized Snapshot get(String key)


调用缓存对象的get方法就可以。



③、改动,加入缓存内容。

相同的。我们须要先了解Editor这个类。这也是DiskLruCache提供的内部类。

这个类代表了某个可编辑的缓存实体。这个类也提供了两个方法进行读取缓存数据的内容。例如以下:


 public InputStream newInputStream(int index)


以及


 public String getString(int index) 

和前面一样。第一个方法获取的是文件的输入流。第二个方法获取的是文件的内容转换为字符串之后的值。上述两个方法用于读取文件内容。假设是要进行编辑内容的话。须要调用例如以下方法:


 public OutputStream newOutputStream(int index)



用于获取某个key相应下标的文件输出流,兴许我们须要把缓存的内容通过它写入缓存。调用这个之后。必须调用commit或者abort方法。来确认是否改动或者覆盖缓存内容。

相同的。要想获取一个缓存实体的可编辑对象,须要通过例如以下方法:


  public Editor edit(String key)



此方法会返回key相应的可编辑对象,注意。这里即使之前没有key相关的缓存对象,通过此方法就会在文件系统里新建一个key相关的缓存对象,因此也会返回一个可编辑对象。假设返回的是null,说明这个key相应的可编辑对象正在被调用,当前进程无法调用。


④、删除某个缓存实体。


public synchronized boolean remove(String key) 



返回true表示成功删除key相应的值。假设该key相应的缓存实体正在被操作或者不存在,就返回false表示无法删除。


⑤、关闭缓存对象:


 public synchronized void close() 


操作完缓存对象须要调用close方法进行关闭。


⑥、删除全部的缓存数据:


 public void delete()


此方法会将缓存对象关闭并删除一切关于此缓存对象的文件系统的缓存数据。


上述两个类可用于存储不论什么类型的数据,在这里主要以缓存图片为样例给大家解说,所以接下来还须要了解一下BitmapFactory这个类。




三、BitmapFactory






这个类可用于针对各种不同数据源来创建Bitmap对象。

这里面最重要的是Options内部类。以及一个将输入流转换成Bitmap的解码方法。以下具体说明:


Options主要有两个属性须要具体了解:

①、inJustDecodeBounds:

假设此值设为true,则用此对象去decode一个bitmap的时候,会返回一个null。可是这并不意味这个inJustDecodeBounds设为true是没有意义的。由于即使没有返回bitmap对象,可是解码后的options对象的其它属性比方outHeight......仍然会拥有值,这对于兴许我们用于推断bitmap对象的大小是否合适非常实用。并且这样子做另一个优点就是,它进行解码图片的时候不须要去申请内存空间。

②、inSampleSize:

此值代表解压后的图片的大小是原来的图片大小的1/inSampleSize倍。这个大小指包括像素的压缩和宽度以及高度的压缩。比方这个值为2,则表示压缩后的图片的宽高各自是原来的1/2,像素大小则为1/4。这个值永远都会是2的倍数,假设赋值了3。那么压缩的时候。会将2赋值给它,即偏小的靠近2的倍数的那个数。

BitmapFactory能够将非常多不同格式存储的数据源解码成Bitmap对象,常见的比方decodeFile,decodeResource,DecodeStream,DecodeByteArrays。此类包括的重载方法太多。就不一一解释了,这里主要解说decodeStream方法,它是最可能用的到,上面提到的前三个方法以及它们的重载方法。最后都是通过调用decodeStream方法进行解码的。

方法原型例如以下:

 public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)

參数解析:

is:用于解析bitmap的原始数据。


ourPadding:解码后的图片距离边界的间隙,假设设为null,表示间隙为0。

opts:解码的解码规则。bitmap解码会依据opts里面的属性进行解码。

此方法会返回一个bitmap对象。当然假设is数据无法进行解码,就会返回null,但假设opts里面的inJustDecodeBounds方法是true的话,依旧会返回null。可是此时的opts的outWidth,outHeight是有值的。


以下通过写一个样例来综合应用如上所说的知识,样例是书写一个利用三级缓存读取网络图片的样例,这个样例对读取的图片未採取压缩,而是应用原来的图片,假设有兴趣研究的话,读者自行加入压缩图片的代码,另外说明,运用LruCache最好使用support.v4支持包的,以便于兼容3.1一下的版本号。

DiskLruCache的源代码例如以下(亦能够依据前面给的链接自行复制):

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cw.cache;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 ******************************************************************************
 * Taken from the JB source code, can be found in:
 * libcore/luni/src/main/java/libcore/io/DiskLruCache.java
 * or direct link:
 * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
 ******************************************************************************
 *
 * A cache that uses a bounded amount of space on a filesystem. Each cache
 * entry has a string key and a fixed number of values. Values are byte
 * sequences, accessible as streams or files. Each value must be between {@code
 * 0} and {@code Integer.MAX_VALUE} bytes in length.
 *
 * <p>The cache stores its data in a directory on the filesystem. This
 * directory must be exclusive to the cache; the cache may delete or overwrite
 * files from its directory. It is an error for multiple processes to use the
 * same cache directory at the same time.
 *
 * <p>This cache limits the number of bytes that it will store on the
 * filesystem. When the number of stored bytes exceeds the limit, the cache will
 * remove entries in the background until the limit is satisfied. The limit is
 * not strict: the cache may temporarily exceed it while waiting for files to be
 * deleted. The limit does not include filesystem overhead or the cache
 * journal so space-sensitive applications should set a conservative limit.
 *
 * <p>Clients call {@link #edit} to create or update the values of an entry. An
 * entry may have only one editor at one time; if a value is not available to be
 * edited then {@link #edit} will return null.
 * <ul>
 *     <li>When an entry is being <strong>created</strong> it is necessary to
 *         supply a full set of values; the empty value should be used as a
 *         placeholder if necessary.
 *     <li>When an entry is being <strong>edited</strong>, it is not necessary
 *         to supply data for every value; values default to their previous
 *         value.
 * </ul>
 * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
 * or {@link Editor#abort}. Committing is atomic: a read observes the full set
 * of values as they were before or after the commit, but never a mix of values.
 *
 * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
 * observe the value at the time that {@link #get} was called. Updates and
 * removals after the call do not impact ongoing reads.
 *
 * <p>This class is tolerant of some I/O errors. If files are missing from the
 * filesystem, the corresponding entries will be dropped from the cache. If
 * an error occurs while writing a cache value, the edit will fail silently.
 * Callers should handle other problems by catching {@code IOException} and
 * responding appropriately.
 */
public final class DiskLruCache implements Closeable {
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TMP = "journal.tmp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    static final String VERSION_1 = "1";
    static final long ANY_SEQUENCE_NUMBER = -1;
    private static final String CLEAN = "CLEAN";
    private static final String DIRTY = "DIRTY";
    private static final String REMOVE = "REMOVE";
    private static final String READ = "READ";

    private static final Charset UTF_8 = Charset.forName("UTF-8");
    private static final int IO_BUFFER_SIZE = 8 * 1024;

    /*
     * This cache uses a journal file named "journal". A typical journal file
     * looks like this:
     *     libcore.io.DiskLruCache
     *     1
     *     100
     *     2
     *
     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
     *     READ 335c4c6028171cfddfbaae1a9c313c52
     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
     *
     * The first five lines of the journal form its header. They are the
     * constant string "libcore.io.DiskLruCache", the disk cache's version,
     * the application's version, the value count, and a blank line.
     *
     * Each of the subsequent lines in the file is a record of the state of a
     * cache entry. Each line contains space-separated values: a state, a key,
     * and optional state-specific values.
     *   o DIRTY lines track that an entry is actively being created or updated.
     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
     *     temporary files may need to be deleted.
     *   o CLEAN lines track a cache entry that has been successfully published
     *     and may be read. A publish line is followed by the lengths of each of
     *     its values.
     *   o READ lines track accesses for LRU.
     *   o REMOVE lines track entries that have been deleted.
     *
     * The journal file is appended to as cache operations occur. The journal may
     * occasionally be compacted by dropping redundant lines. A temporary file named
     * "journal.tmp" will be used during compaction; that file should be deleted if
     * it exists when the cache is opened.
     */

    private final File directory;
    private final File journalFile;
    private final File journalFileTmp;
    private final int appVersion;
    private final long maxSize;
    private final int valueCount;
    private long size = 0;
    private Writer journalWriter;
    private final LinkedHashMap<String, Entry> lruEntries
            = new LinkedHashMap<String, Entry>(0, 0.75f, true);
    private int redundantOpCount;

    /**
     * To differentiate between old and current snapshots, each entry is given
     * a sequence number each time an edit is committed. A snapshot is stale if
     * its sequence number is not equal to its entry's sequence number.
     */
    private long nextSequenceNumber = 0;

    /* From java.util.Arrays */
    @SuppressWarnings("unchecked")
    private static <T> T[] copyOfRange(T[] original, int start, int end) {
        final int originalLength = original.length; // For exception priority compatibility.
        if (start > end) {
            throw new IllegalArgumentException();
        }
        if (start < 0 || start > originalLength) {
            throw new ArrayIndexOutOfBoundsException();
        }
        final int resultLength = end - start;
        final int copyLength = Math.min(resultLength, originalLength - start);
        final T[] result = (T[]) Array
                .newInstance(original.getClass().getComponentType(), resultLength);
        System.arraycopy(original, start, result, 0, copyLength);
        return result;
    }

    /**
     * Returns the remainder of 'reader' as a string, closing it when done.
     */
    public static String readFully(Reader reader) throws IOException {
        try {
            StringWriter writer = new StringWriter();
            char[] buffer = new char[1024];
            int count;
            while ((count = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, count);
            }
            return writer.toString();
        } finally {
            reader.close();
        }
    }

    /**
     * Returns the ASCII characters up to but not including the next "
", or
     * "
".
     *
     * @throws EOFException if the stream is exhausted before the next newline
     *     character.
     */
    public static String readAsciiLine(InputStream in) throws IOException {
        // TODO: support UTF-8 here instead

        StringBuilder result = new StringBuilder(80);
        while (true) {
            int c = in.read();
            if (c == -1) {
                throw new EOFException();
            } else if (c == '
') {
                break;
            }

            result.append((char) c);
        }
        int length = result.length();
        if (length > 0 && result.charAt(length - 1) == '
') {
            result.setLength(length - 1);
        }
        return result.toString();
    }

    /**
     * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
     */
    public static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (RuntimeException rethrown) {
                throw rethrown;
            } catch (Exception ignored) {
            }
        }
    }

    /**
     * Recursively delete everything in {@code dir}.
     */
    // TODO: this should specify paths as Strings rather than as Files
    public static void deleteContents(File dir) throws IOException {
        File[] files = dir.listFiles();
        if (files == null) {
            throw new IllegalArgumentException("not a directory: " + dir);
        }
        for (File file : files) {
            if (file.isDirectory()) {
                deleteContents(file);
            }
            if (!file.delete()) {
                throw new IOException("failed to delete file: " + file);
            }
        }
    }

    /** This cache uses a single background thread to evict entries. */
    private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    private final Callable<Void> cleanupCallable = new Callable<Void>() {
        @Override public Void call() throws Exception {
            synchronized (DiskLruCache.this) {
                if (journalWriter == null) {
                    return null; // closed
                }
                trimToSize();
                if (journalRebuildRequired()) {
                    rebuildJournal();
                    redundantOpCount = 0;
                }
            }
            return null;
        }
    };

    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
        this.directory = directory;
        this.appVersion = appVersion;
        this.journalFile = new File(directory, JOURNAL_FILE);
        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
        this.valueCount = valueCount;
        this.maxSize = maxSize;
    }

    /**
     * Opens the cache in {@code directory}, creating a cache if none exists
     * there.
     *
     * @param directory a writable directory
     * @param appVersion
     * @param valueCount the number of values per cache entry. Must be positive.
     * @param maxSize the maximum number of bytes this cache should use to store
     * @throws IOException if reading or writing the cache directory fails
     */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        // prefer to pick up where we left off
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");
                cache.delete();
            }
        }

        // create a new empty cache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }

    private void readJournal() throws IOException {
        InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
        try {
            String magic = readAsciiLine(in);
            String version = readAsciiLine(in);
            String appVersionString = readAsciiLine(in);
            String valueCountString = readAsciiLine(in);
            String blank = readAsciiLine(in);
            if (!MAGIC.equals(magic)
                    || !VERSION_1.equals(version)
                    || !Integer.toString(appVersion).equals(appVersionString)
                    || !Integer.toString(valueCount).equals(valueCountString)
                    || !"".equals(blank)) {
                throw new IOException("unexpected journal header: ["
                        + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
            }

            while (true) {
                try {
                    readJournalLine(readAsciiLine(in));
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
        } finally {
            closeQuietly(in);
        }
    }

    private void readJournalLine(String line) throws IOException {
        String[] parts = line.split(" ");
        if (parts.length < 2) {
            throw new IOException("unexpected journal line: " + line);
        }

        String key = parts[1];
        if (parts[0].equals(REMOVE) && parts.length == 2) {
            lruEntries.remove(key);
            return;
        }

        Entry entry = lruEntries.get(key);
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        }

        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
            entry.readable = true;
            entry.currentEditor = null;
            entry.setLengths(copyOfRange(parts, 2, parts.length));
        } else if (parts[0].equals(DIRTY) && parts.length == 2) {
            entry.currentEditor = new Editor(entry);
        } else if (parts[0].equals(READ) && parts.length == 2) {
            // this work was already done by calling lruEntries.get()
        } else {
            throw new IOException("unexpected journal line: " + line);
        }
    }

    /**
     * Computes the initial size and collects garbage as a part of opening the
     * cache. Dirty entries are assumed to be inconsistent and will be deleted.
     */
    private void processJournal() throws IOException {
        deleteIfExists(journalFileTmp);
        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
            Entry entry = i.next();
            if (entry.currentEditor == null) {
                for (int t = 0; t < valueCount; t++) {
                    size += entry.lengths[t];
                }
            } else {
                entry.currentEditor = null;
                for (int t = 0; t < valueCount; t++) {
                    deleteIfExists(entry.getCleanFile(t));
                    deleteIfExists(entry.getDirtyFile(t));
                }
                i.remove();
            }
        }
    }

    /**
     * Creates a new journal that omits redundant information. This replaces the
     * current journal if it exists.
     */
    private synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
            journalWriter.close();
        }

        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
        writer.write(MAGIC);
        writer.write("
");
        writer.write(VERSION_1);
        writer.write("
");
        writer.write(Integer.toString(appVersion));
        writer.write("
");
        writer.write(Integer.toString(valueCount));
        writer.write("
");
        writer.write("
");

        for (Entry entry : lruEntries.values()) {
            if (entry.currentEditor != null) {
                writer.write(DIRTY + ' ' + entry.key + '
');
            } else {
                writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '
');
            }
        }

        writer.close();
        journalFileTmp.renameTo(journalFile);
        journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
    }

    private static void deleteIfExists(File file) throws IOException {
//        try {
//            Libcore.os.remove(file.getPath());
//        } catch (ErrnoException errnoException) {
//            if (errnoException.errno != OsConstants.ENOENT) {
//                throw errnoException.rethrowAsIOException();
//            }
//        }
        if (file.exists() && !file.delete()) {
            throw new IOException();
        }
    }

    /**
     * Returns a snapshot of the entry named {@code key}, or null if it doesn't
     * exist is not currently readable. If a value is returned, it is moved to
     * the head of the LRU queue.
     */
    public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }

        if (!entry.readable) {
            return null;
        }

        /*
         * Open all streams eagerly to guarantee that we see a single published
         * snapshot. If we opened streams lazily then the streams could come
         * from different edits.
         */
        InputStream[] ins = new InputStream[valueCount];
        try {
            for (int i = 0; i < valueCount; i++) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));
            }
        } catch (FileNotFoundException e) {
            // a file must have been deleted manually!
            return null;
        }

        redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '
');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return new Snapshot(key, entry.sequenceNumber, ins);
    }

    /**
     * Returns an editor for the entry named {@code key}, or null if another
     * edit is in progress.
     */
    public Editor edit(String key) throws IOException {
        return edit(key, ANY_SEQUENCE_NUMBER);
    }

    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
                && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
            return null; // snapshot is stale
        }
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {
            return null; // another edit is in progress
        }

        Editor editor = new Editor(entry);
        entry.currentEditor = editor;

        // flush the journal before creating files to prevent file leaks
        journalWriter.write(DIRTY + ' ' + key + '
');
        journalWriter.flush();
        return editor;
    }

    /**
     * Returns the directory where this cache stores its data.
     */
    public File getDirectory() {
        return directory;
    }

    /**
     * Returns the maximum number of bytes that this cache should use to store
     * its data.
     */
    public long maxSize() {
        return maxSize;
    }

    /**
     * Returns the number of bytes currently being used to store the values in
     * this cache. This may be greater than the max size if a background
     * deletion is pending.
     */
    public synchronized long size() {
        return size;
    }

    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor) {
            throw new IllegalStateException();
        }

        // if this edit is creating the entry for the first time, every index must have a value
        if (success && !entry.readable) {
            for (int i = 0; i < valueCount; i++) {
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    throw new IllegalStateException("edit didn't create file " + i);
                }
            }
        }

        for (int i = 0; i < valueCount; i++) {
            File dirty = entry.getDirtyFile(i);
            if (success) {
                if (dirty.exists()) {
                    File clean = entry.getCleanFile(i);
                    dirty.renameTo(clean);
                    long oldLength = entry.lengths[i];
                    long newLength = clean.length();
                    entry.lengths[i] = newLength;
                    size = size - oldLength + newLength;
                }
            } else {
                deleteIfExists(dirty);
            }
        }

        redundantOpCount++;
        entry.currentEditor = null;
        if (entry.readable | success) {
            entry.readable = true;
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '
');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '
');
        }

        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }

    /**
     * We only rebuild the journal when it will halve the size of the journal
     * and eliminate at least 2000 ops.
     */
    private boolean journalRebuildRequired() {
        final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
        return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
                && redundantOpCount >= lruEntries.size();
    }

    /**
     * Drops the entry for {@code key} if it exists and can be removed. Entries
     * actively being edited cannot be removed.
     *
     * @return true if an entry was removed.
     */
    public synchronized boolean remove(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null || entry.currentEditor != null) {
            return false;
        }

        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (!file.delete()) {
                throw new IOException("failed to delete " + file);
            }
            size -= entry.lengths[i];
            entry.lengths[i] = 0;
        }

        redundantOpCount++;
        journalWriter.append(REMOVE + ' ' + key + '
');
        lruEntries.remove(key);

        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return true;
    }

    /**
     * Returns true if this cache has been closed.
     */
    public boolean isClosed() {
        return journalWriter == null;
    }

    private void checkNotClosed() {
        if (journalWriter == null) {
            throw new IllegalStateException("cache is closed");
        }
    }

    /**
     * Force buffered operations to the filesystem.
     */
    public synchronized void flush() throws IOException {
        checkNotClosed();
        trimToSize();
        journalWriter.flush();
    }

    /**
     * Closes this cache. Stored values will remain on the filesystem.
     */
    public synchronized void close() throws IOException {
        if (journalWriter == null) {
            return; // already closed
        }
        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
            if (entry.currentEditor != null) {
                entry.currentEditor.abort();
            }
        }
        trimToSize();
        journalWriter.close();
        journalWriter = null;
    }

    private void trimToSize() throws IOException {
        while (size > maxSize) {
//            Map.Entry<String, Entry> toEvict = lruEntries.eldest();
            final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
            remove(toEvict.getKey());
        }
    }

    /**
     * Closes the cache and deletes all of its stored values. This will delete
     * all files in the cache directory including files that weren't created by
     * the cache.
     */
    public void delete() throws IOException {
        close();
        deleteContents(directory);
    }

    private void validateKey(String key) {
        if (key.contains(" ") || key.contains("
") || key.contains("
")) {
            throw new IllegalArgumentException(
                    "keys must not contain spaces or newlines: "" + key + """);
        }
    }

    private static String inputStreamToString(InputStream in) throws IOException {
        return readFully(new InputStreamReader(in, UTF_8));
    }

    /**
     * A snapshot of the values for an entry.
     */
    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private final InputStream[] ins;

        private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
            this.key = key;
            this.sequenceNumber = sequenceNumber;
            this.ins = ins;
        }

        /**
         * Returns an editor for this snapshot's entry, or null if either the
         * entry has changed since this snapshot was created or if another edit
         * is in progress.
         */
        public Editor edit() throws IOException {
            return DiskLruCache.this.edit(key, sequenceNumber);
        }

        /**
         * Returns the unbuffered stream with the value for {@code index}.
         */
        public InputStream getInputStream(int index) {
            return ins[index];
        }

        /**
         * Returns the string value for {@code index}.
         */
        public String getString(int index) throws IOException {
            return inputStreamToString(getInputStream(index));
        }

        @Override public void close() {
            for (InputStream in : ins) {
                closeQuietly(in);
            }
        }
    }

    /**
     * Edits the values for an entry.
     */
    public final class Editor {
        private final Entry entry;
        private boolean hasErrors;

        private Editor(Entry entry) {
            this.entry = entry;
        }

        /**
         * Returns an unbuffered input stream to read the last committed value,
         * or null if no value has been committed.
         */
        public InputStream newInputStream(int index) throws IOException {
            synchronized (DiskLruCache.this) {
                if (entry.currentEditor != this) {
                    throw new IllegalStateException();
                }
                if (!entry.readable) {
                    return null;
                }
                return new FileInputStream(entry.getCleanFile(index));
            }
        }

        /**
         * Returns the last committed value as a string, or null if no value
         * has been committed.
         */
        public String getString(int index) throws IOException {
            InputStream in = newInputStream(index);
            return in != null ?

inputStreamToString(in) : null; } /** * Returns a new unbuffered output stream to write the value at * {@code index}. If the underlying output stream encounters errors * when writing to the filesystem, this edit will be aborted when * {@link #commit} is called. The returned output stream does not throw * IOExceptions. */ public OutputStream newOutputStream(int index) throws IOException { synchronized (DiskLruCache.this) { if (entry.currentEditor != this) { throw new IllegalStateException(); } return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); } } /** * Sets the value at {@code index} to {@code value}. */ public void set(int index, String value) throws IOException { Writer writer = null; try { writer = new OutputStreamWriter(newOutputStream(index), UTF_8); writer.write(value); } finally { closeQuietly(writer); } } /** * Commits this edit so it is visible to readers. This releases the * edit lock so another edit may be started on the same key. */ public void commit() throws IOException { if (hasErrors) { completeEdit(this, false); remove(entry.key); // the previous entry is stale } else { completeEdit(this, true); } } /** * Aborts this edit. This releases the edit lock so another edit may be * started on the same key. */ public void abort() throws IOException { completeEdit(this, false); } private class FaultHidingOutputStream extends FilterOutputStream { private FaultHidingOutputStream(OutputStream out) { super(out); } @Override public void write(int oneByte) { try { out.write(oneByte); } catch (IOException e) { hasErrors = true; } } @Override public void write(byte[] buffer, int offset, int length) { try { out.write(buffer, offset, length); } catch (IOException e) { hasErrors = true; } } @Override public void close() { try { out.close(); } catch (IOException e) { hasErrors = true; } } @Override public void flush() { try { out.flush(); } catch (IOException e) { hasErrors = true; } } } } private final class Entry { private final String key; /** Lengths of this entry's files. */ private final long[] lengths; /** True if this entry has ever been published */ private boolean readable; /** The ongoing edit or null if this entry is not being edited. */ private Editor currentEditor; /** The sequence number of the most recently committed edit to this entry. */ private long sequenceNumber; private Entry(String key) { this.key = key; this.lengths = new long[valueCount]; } public String getLengths() throws IOException { StringBuilder result = new StringBuilder(); for (long size : lengths) { result.append(' ').append(size); } return result.toString(); } /** * Set lengths using decimal numbers like "10123". */ private void setLengths(String[] strings) throws IOException { if (strings.length != valueCount) { throw invalidLengths(strings); } try { for (int i = 0; i < strings.length; i++) { lengths[i] = Long.parseLong(strings[i]); } } catch (NumberFormatException e) { throw invalidLengths(strings); } } private IOException invalidLengths(String[] strings) throws IOException { throw new IOException("unexpected journal line: " + Arrays.toString(strings)); } public File getCleanFile(int i) { return new File(directory, key + "." + i); } public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); } } }




首先是image_layout,这是listView的每一个项的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:scaleType="fitXY"
        android:background="#efefef" />
</LinearLayout>

然后是内存缓存的实现类。MemoryCache:

package com.cw.cache;

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;//为了兼容3.1之前的版本号。请使用支持包中的LruCache
import android.util.Log;

/**
 * Created by Myy on 2016/7/26.
 * 内存缓存
 */
public class MemoryCache {


    private static LruCache<String, Bitmap> cache = null;

    private MemoryCache() {

    }

    private static class MemoryCacheHolder {
        private static MemoryCache cache = new MemoryCache();
    }

    public static MemoryCache getInstance() {
        if (cache == null) {
            initCache();
        }
        return MemoryCacheHolder.cache;
    }

    private static void initCache() {
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        Log.i("最大内存", maxMemory + "");
        cache = new LruCache<String, Bitmap>(maxMemory / 4) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                Log.i("图片内存", value.getByteCount() + "");
                return value.getByteCount();
            }

        };
    }

    public Bitmap get(String url) {
        if (url == null || url.length() == 0)
            throw new NullPointerException("url不可为空");
        String key = StringUtils.urlToKey(url);
        if (cache == null)
            throw new RuntimeException("cache初始化失败");
        return cache.get(key);

    }

    /**
     * 缓存一个Bitmap,返回null表示该url相应的值已存在。加入失败。
     *
     * @param url
     * @param bitmap
     * @return
     */
    public Bitmap put(String url, Bitmap bitmap) {
        if (url == null || url.length() == 0 || bitmap == null)
            throw new NullPointerException("url或者图像不可为空");
        String key = StringUtils.urlToKey(url);
        if (cache == null)
            throw new RuntimeException("cache初始化失败");
        return cache.put(key, bitmap);
    }

    /**
     * 清除内存中的全部缓存数据
     */
    public void clearCache() {
        if (cache == null)
            throw new RuntimeException("cache初始化失败");
        cache.evictAll();
    }
}


接着是文件缓存的实现类:


package com.cw.cache;

import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Created by Myy on 2016/7/26.
 * 文件缓存
 */
public class DiskCache {

    private static DiskLruCache cache = null;
    private static Context c = null;

    private DiskCache() {

    }

    private static class DiskCacheHolder {
        private static DiskCache diskCache = new DiskCache();
    }

    public static DiskCache getInstance(Context context) {
        if (cache == null) {
            c = context.getApplicationContext();
            initCache();
        }
        return DiskCacheHolder.diskCache;
    }

    private static void initCache() {
        File fileDirectory = getFileDirectory();//保存缓存文件的文件夹
        int appVersion = getAppVersion();
        int valueCount = 1;//这里设置每一个key仅仅相应一个缓存实体
        int maxSize = 100 * 1024 * 1024;//100MB的缓存空间
        try {
            //假设缓存文件夹存在缓存文件则直接使用,否则会在缓存文件夹新建缓存相关的文件
            cache = DiskLruCache.open(fileDirectory, appVersion, valueCount, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static int getAppVersion() {
        try {
            return c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    private static File getFileDirectory() {
        String path = null;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            path = Environment.getExternalStorageDirectory().getPath() + File.separator + "bitmap";
        } else {
            path = Environment.getDataDirectory().getPath() + File.separator + "bitmap";
        }
        File file = new File(path);
        if (!file.exists())
            file.mkdir();
        return file;
    }


    /**
     * 清除缓存数据
     */
    public void clearCache() {
        if (cache == null)
            throw new RuntimeException("初始化失败");
        try {
            cache.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 缓存图片
     *
     * @param url
     * @param bitmap
     */
    public void put(String url, Bitmap bitmap) {
        if (url == null || url.length() == 0 || bitmap == null)
            throw new NullPointerException("url或者图像不可为空");
        String key = StringUtils.urlToKey(url);
        if (cache == null)
            throw new RuntimeException("cache初始化失败");
        DiskLruCache.Editor editor = null;
        OutputStream os = null;
        try {
            editor = cache.edit(key);//注意后面调用的abort或者commit方法
            if (editor != null) {
                os = editor.newOutputStream(0);
                if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)) {
                    throw new RuntimeException("图片压缩失败");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            try {
                os.close();
                editor.abort();
            } catch (IOException e1) {
                e1.printStackTrace();
            }

        } finally {
            try {
                os.close();
                editor.commit();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取缓存图片
     *
     * @param url
     * @return
     */
    public Bitmap get(String url) {
        if (url == null || url.length() == 0)
            throw new NullPointerException("url不可为空");
        String key = StringUtils.urlToKey(url);
        if (cache == null)
            throw new RuntimeException("cache初始化失败");
        DiskLruCache.Snapshot snapshot = null;
        InputStream is = null;
        Bitmap bitmap = null;
        try {
            snapshot = cache.get(key);//返回null表示该值不存在或当前正处于不可读状态。
            if (snapshot != null) {
                is = snapshot.getInputStream(0);
                bitmap = BitmapFactory.decodeStream(is);
            } else
                return null;
        } catch (IOException e) {
            e.printStackTrace();
            try {
                snapshot.close();//这里不须要手动关闭is,由于此方法会将刚刚获取的文件流全部关闭
            } catch (Exception e1) {
                e1.printStackTrace();
            } finally {
                snapshot.close();
            }

        }
        return bitmap;
    }

}


然后是StringUtils,定义了图片路径和图片路径转换成string字符串的方法:

package com.cw.cache;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Created by Myy on 2016/7/26.
 */
public class StringUtils {

    public static String urlToKey(String url) {
        StringBuilder sb = new StringBuilder();
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(url.getBytes());
            byte[] bytes = digest.digest();
            for (int i = 0; i < bytes.length; i++)
                sb.append(Integer.toHexString(0xff & bytes[i]));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return sb.toString().length() == 0 ? null : sb.toString();
    }

    public static String[] urlS = new String[]
            {
                    "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fimage53.360doc.com%2FDownloadImg%2F2012%2F07%2F2317%2F25701259_6.jpg",
                    "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fwallpaper%2F1308%2F16%2Fc2%2F24549817_1376646910888.jpg",
                    "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fwww.deskcar.com%2Fdesktop%2Ffengjing%2F2013312114415%2F3.jpg",
                    "http://image.baidu.com/search/down?

tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fimage.tianjimedia.com%2FuploadImages%2F2012%2F011%2FR5J8A0HYL5YV.jpg", "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fwww.bz55.com%2Fuploads%2Fallimg%2F111017%2F13264160c-25.jpg", "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fwww.bz55.com%2Fuploads1%2Fallimg%2F120130%2F1_120130225951_1.jpg", "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fwww.bz55.com%2Fuploads%2Fallimg%2F130618%2F1-13061PU440.jpg", "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fsoftbbs%2F1008%2F26%2Fc0%2F4984165_1282800005719_1024x1024soft.jpg", "http://image.baidu.com/search/down?tn=download&ipn=dwnl&word=download&ie=utf8&fr=result&url=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fwallpaper%2F1307%2F10%2Fc3%2F23153824_1373426670894.jpg", "http://img2.imgtn.bdimg.com/it/u=331221080,82593678&fm=206&gp=0.jpg", "http://img4.imgtn.bdimg.com/it/u=562407178,1662987234&fm=206&gp=0.jpg", "http://img3.imgtn.bdimg.com/it/u=2324814778,3433509063&fm=206&gp=0.jpg", "http://img4.imgtn.bdimg.com/it/u=3570507366,2497738850&fm=206&gp=0.jpg", "http://img4.imgtn.bdimg.com/it/u=846199408,2794756692&fm=206&gp=0.jpg", "http://img5.imgtn.bdimg.com/it/u=221456928,362190599&fm=206&gp=0.jpg", "http://img4.imgtn.bdimg.com/it/u=1969253456,2193232238&fm=206&gp=0.jpg", "http://img3.imgtn.bdimg.com/it/u=2695781595,4041188434&fm=206&gp=0.jpg", "http://img0.imgtn.bdimg.com/it/u=3662196526,1418421672&fm=206&gp=0.jpg", "http://img3.imgtn.bdimg.com/it/u=664997347,4191517248&fm=206&gp=0.jpg", "http://img5.imgtn.bdimg.com/it/u=3641843242,2739246521&fm=206&gp=0.jpg", "http://img3.imgtn.bdimg.com/it/u=2686058747,1067524060&fm=206&gp=0.jpg", "http://img2.imgtn.bdimg.com/it/u=84383536,2556612772&fm=206&gp=0.jpg", "http://img3.imgtn.bdimg.com/it/u=224917259,3388622236&fm=206&gp=0.jpg" }; }



接着是图片载入器。ImageLoader:





package com.cw.cache;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.widget.ImageView;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by Myy on 2016/7/26.
 */
public class ImageLoader {

    private static DiskCache diskCache;
    private static MemoryCache memoryCache;
    private static ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    private static Handler handler = null;

    private ImageLoader() {

    }

    private static class ImageLoaderHolder {
        private static ImageLoader imageLoader = new ImageLoader();
    }

    public static ImageLoader getInstance(Context context) {
        if (diskCache == null || memoryCache == null) {
            diskCache = DiskCache.getInstance(context);
            memoryCache = MemoryCache.getInstance();
            handler = new Handler(Looper.getMainLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    ViewHolder holder = (ViewHolder) msg.obj;
                    Bitmap bitmap = holder.bitmap;
                    ImageView imageView = holder.imageView;
                    if (imageView.getTag().equals(holder.url))
                        imageView.setImageBitmap(bitmap);

                }
            };
        }
        return ImageLoaderHolder.imageLoader;
    }

    /**
     * 读取图片
     *
     * @param url
     * @param imageView
     */
    public void getBitmap(String url, ImageView imageView) {
        Bitmap bitmap = null;
        imageView.setTag(url);
        if (diskCache == null || memoryCache == null) {
            throw new RuntimeException("初始化失败");
        } else {
            //先从内存中读取。然后文件读取。最后网络读取
            bitmap = memoryCache.get(url);
            if (bitmap == null) {
                bitmap = diskCache.get(url);
                if (bitmap == null) {
                    getBitmapFromNet(url, imageView);
                } else {
                    displayBitmap(imageView, bitmap, url);
                }
            } else {
                displayBitmap(imageView, bitmap, url);
            }
        }
    }

    /**
     * 从网络获取图片
     *
     * @param urls
     */
    private  synchronized void getBitmapFromNet(final String urls, final ImageView imageView) {

        pool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    URL url = new URL(urls);
                    HttpURLConnection con = (HttpURLConnection) url.openConnection();
                    InputStream is = con.getInputStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(is);
                    if (bitmap != null) {
                        memoryCache.put(urls, bitmap);
                        diskCache.put(urls, bitmap);
                        displayBitmap(imageView, bitmap, urls);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }


    /**
     * 显示图片
     *
     * @param imageView
     * @param bitmap
     */
    private void displayBitmap(ImageView imageView, Bitmap bitmap, String url) {
        ViewHolder holder = new ViewHolder();
        holder.bitmap = bitmap;
        holder.imageView = imageView;
        holder.url = url;
        Message msg = Message.obtain(handler, 0);
        msg.obj = holder;
        msg.sendToTarget();
    }

    private class ViewHolder {
        Bitmap bitmap;
        ImageView imageView;
        String url;
    }

}


然后是图片适配器,ImageAdapter:


package com.cw.cache;

import android.content.Context;
import android.os.Parcelable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by Myy on 2016/7/26.
 */
public abstract class ImageAdapter extends ArrayAdapter<String> {

    private LayoutInflater inflater = null;
    private int resource = 0;
    private Context context;

    public ImageAdapter(Context context, int resource) {
        super(context, resource, 0, StringUtils.urlS);
        inflater = LayoutInflater.from(context);
        this.resource = resource;
        this.context = context;
    }


    /**
     * 用于推断是否处于空暇状态
     *
     * @return
     */
    public abstract boolean getIdle();

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.i("sss", getIdle() + "");

        if (convertView == null) {
            convertView = inflater.inflate(resource, null);
            ImageView imageView = (ImageView) convertView.findViewById(R.id.imageView);
            ViewHolder viewHolder = new ViewHolder();
            viewHolder.imageView = imageView;
            convertView.setTag(viewHolder);
        }
        View view = convertView;
        ImageView imageView = ((ViewHolder) view.getTag()).imageView;
        if (getIdle()) {
            ImageLoader.getInstance(context).getBitmap((String) getItem(position), imageView);
        }
        return view;

    }

    private class ViewHolder {
        ImageView imageView;
    }
}



最后是mainActivity:


package com.cw.cache;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.AbsListView;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity implements AbsListView.OnScrollListener {

    private boolean isIdle = true;
    private ListView listView;
    private ImageAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (ListView) findViewById(R.id.listView);
        listView.setOnScrollListener(this);
        adapter = new ImageAdapter(this, R.layout.image_layout) {
            @Override
            public boolean getIdle() {
                return isIdle;
            }
        };
        listView.setAdapter(adapter);
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        Log.i("xxx",scrollState+"");
        if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
            isIdle = true;
            adapter.notifyDataSetChanged();//注意,由于当处于闲置状态是,getView方法不会被调用。此时须要手动刷新listView
        }
        else
            isIdle = false;
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

    }


}


AndroidManifest.xml文件例如以下:


<?

xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cw.cache"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!--开启硬件加速有助于渲染图片--> <application android:allowBackup="true" android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>



执行效果例如以下:





当我们执行过一遍之后。关掉网络,再次打开(注意彻底清除任务)假设还能显示图片,说明我们的缓存目的达到了。


---------文章写自:HyHarden---------

--------博客地址:http://blog.csdn.net/qq_25722767-----------



【推广】 免费学中医,健康全家人
原文地址:https://www.cnblogs.com/llguanli/p/8663776.html