在开发过程中,经常会碰到进行请求大量的网络图片的样例。假设处理的不好。非常easy造成oom。对于避免oom的方法,无非就是进行图片的压缩。及时的回收不用的图片。这些看似简单可是处理起来事实上涉及的知识特别广泛。在这里主要解说图片的缓存,通过缓存也是个非常好的避免oom的途径。近期经常使用的到自然是LruCache了,它里面有一个LindedHashMap链式表,并且这个表是按近期最少使用算法排序的,近期使用的往往拍的靠前。最少使用的往往处于队尾,当须要回收利用的时候,最后面的那个元素是会被清除掉的。LruCache主要实现了内存缓存,这里还会解说DiskLruCache,这是GitHub开源库提供的使用文件缓存的一种基于近期最少使用的硬盘缓存类。
同一时候顺带解说图片的简单压缩方法。那么,接下来就须要了解LruCache,DiskLruCache,BitmapFactory。
一、LruCache
可是注意。假设你自己重写了create方法的话,create的方法并非线程安全的。可是由于get方法里面进行加入值对象的时候会推断是否发生冲突。所以我们不须要考虑线程安全的问题。
①、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); }
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来获取一个值:
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) {}
⑦、计算某个实体占用的内存空间:
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)
③、改动,加入缓存内容。
相同的。我们须要先了解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)
⑤、关闭缓存对象:
public synchronized void close()
⑥、删除全部的缓存数据:
public void delete()
上述两个类可用于存储不论什么类型的数据,在这里主要以缓存图片为样例给大家解说,所以接下来还须要了解一下BitmapFactory这个类。
三、BitmapFactory
这里面最重要的是Options内部类。以及一个将输入流转换成Bitmap的解码方法。以下具体说明:
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
參数解析:
/* * 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"); } } }
<?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>
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; } }
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" }; }
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; } }
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) { } }
<?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-----------