Android中图片的三级缓存策略



在开发过程中,经常会碰到进行请求大量的网络图片的例子,如果处理的不好,很容易造成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(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 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对应。另外就是,使用这个类进行缓存的时候,应该捕获一些常见的I\O操作异常,因为这个类在进行写文件的时候如果发现错误,只会修改失败,并不会导致操作上的失败。后续提到的缓存对象指的是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.
 *
 * 

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. * *

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. * *

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. *

    *
  • When an entry is being created it is necessary to * supply a full set of values; the empty value should be used as a * placeholder if necessary. *
  • When an entry is being edited, it is not necessary * to supply data for every value; values default to their previous * value. *
* 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. * *

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. * *

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 lruEntries = new LinkedHashMap(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[] 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 "\r\n", or * "\n". * * @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 == '\n') { break; } result.append((char) c); } int length = result.length(); if (length > 0 && result.charAt(length - 1) == '\r') { 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()); private final Callable cleanupCallable = new Callable() { @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 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("\n"); writer.write(VERSION_1); writer.write("\n"); writer.write(Integer.toString(appVersion)); writer.write("\n"); writer.write(Integer.toString(valueCount)); writer.write("\n"); writer.write("\n"); for (Entry entry : lruEntries.values()) { if (entry.currentEditor != null) { writer.write(DIRTY + ' ' + entry.key + '\n'); } else { writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); } } 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 + '\n'); 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 + '\n'); 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() + '\n'); if (success) { entry.sequenceNumber = nextSequenceNumber++; } } else { lruEntries.remove(entry.key); journalWriter.write(REMOVE + ' ' + entry.key + '\n'); } 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 + '\n'); 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(lruEntries.values())) { if (entry.currentEditor != null) { entry.currentEditor.abort(); } } trimToSize(); journalWriter.close(); journalWriter = null; } private void trimToSize() throws IOException { while (size > maxSize) { // Map.Entry toEvict = lruEntries.eldest(); final Map.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("\n") || key.contains("\r")) { 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的每个项的布局:




    

然后是内存缓存的实现类,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 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(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 {

    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文件如下:





    
    
    
    
    
        
            
                

                
            
        
    



运行效果如下:


Android中图片的三级缓存策略_第1张图片


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


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

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



你可能感兴趣的:(android)