添加新的缓存管理
This commit is contained in:
1 parent
2c58f6c9ae
commit
12412d67fb
4 files changed
+2105
No files matched your search
+969
@@ -0,0 +1,969 @@
|
||||
/*
|
||||
* 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.hippo.ehviewer.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(final T[] original, final int start, final 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(final Reader reader) throws IOException {
|
||||
try {
|
||||
final StringWriter writer = new StringWriter();
|
||||
final 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 java.io.EOFException if the stream is exhausted before the next
|
||||
* newline character.
|
||||
*/
|
||||
public static String readAsciiLine(final InputStream in) throws IOException {
|
||||
// TODO: support UTF-8 here instead
|
||||
|
||||
final StringBuilder result = new StringBuilder(80);
|
||||
while (true) {
|
||||
final int c = in.read();
|
||||
if (c == -1) {
|
||||
throw new EOFException();
|
||||
} else if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
|
||||
result.append((char)c);
|
||||
}
|
||||
final 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(final Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (final RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete everything in {@code dir}.
|
||||
*/
|
||||
// TODO: this should specify paths as Strings rather than as Files
|
||||
public static void deleteContents(final File dir) throws IOException {
|
||||
final File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IllegalArgumentException("not a directory: " + dir);
|
||||
}
|
||||
for (final 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(final File directory, final int appVersion, final int valueCount,
|
||||
final long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
journalFile = new File(directory, JOURNAL_FILE);
|
||||
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(final File directory, final int appVersion,
|
||||
final int valueCount, final 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 (final 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 {
|
||||
final InputStream in = new BufferedInputStream(new FileInputStream(journalFile),
|
||||
IO_BUFFER_SIZE);
|
||||
try {
|
||||
final String magic = readAsciiLine(in);
|
||||
final String version = readAsciiLine(in);
|
||||
final String appVersionString = readAsciiLine(in);
|
||||
final String valueCountString = readAsciiLine(in);
|
||||
final 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 (final EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closeQuietly(in);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(final String line) throws IOException {
|
||||
final String[] parts = line.split(" ");
|
||||
if (parts.length < 2) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
final 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 (final Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
|
||||
final 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();
|
||||
}
|
||||
|
||||
final 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 (final 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(final 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(final String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
final 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.
|
||||
*/
|
||||
final InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (final 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(final String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(final String key, final 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
|
||||
}
|
||||
|
||||
final 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(final Editor editor, final boolean success)
|
||||
throws IOException {
|
||||
final 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++) {
|
||||
final File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
final File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
final long oldLength = entry.lengths[i];
|
||||
final 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(final String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
final Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
final 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.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // already closed
|
||||
}
|
||||
for (final 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(final 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(final 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(final String key, final long sequenceNumber, final 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(final int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(final int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (final InputStream in : ins) {
|
||||
closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
|
||||
private boolean hasErrors;
|
||||
|
||||
private Editor(final 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(final 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(final int index) throws IOException {
|
||||
final 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(final 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(final int index, final 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(final OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (final IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] buffer, final int offset, final int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (final IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (final IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (final 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(final String key) {
|
||||
this.key = key;
|
||||
lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() throws IOException {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
for (final long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(final 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 (final NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(final String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(final int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(final int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
}
|
||||
+778
@@ -0,0 +1,778 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Andrew Neal 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.hippo.ehviewer.cache;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.content.ComponentCallbacks2;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Looper;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import com.hippo.ehviewer.util.Util;
|
||||
|
||||
/**
|
||||
* This class holds the memory and disk bitmap caches.
|
||||
*/
|
||||
public final class ImageCache {
|
||||
|
||||
private static final String TAG = ImageCache.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The {@link Uri} used to retrieve album art
|
||||
*/
|
||||
private static final Uri mArtworkUri;
|
||||
|
||||
/**
|
||||
* Default memory cache size as a percent of device memory class
|
||||
*/
|
||||
private static final float MEM_CACHE_DIVIDER = 0.25f;
|
||||
|
||||
/**
|
||||
* Default disk cache size 10MB
|
||||
*/
|
||||
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;
|
||||
|
||||
/**
|
||||
* Compression settings when writing images to disk cache
|
||||
*/
|
||||
private static final CompressFormat COMPRESS_FORMAT = CompressFormat.JPEG;
|
||||
|
||||
/**
|
||||
* Disk cache index to read from
|
||||
*/
|
||||
private static final int DISK_CACHE_INDEX = 0;
|
||||
|
||||
/**
|
||||
* Image compression quality
|
||||
*/
|
||||
private static final int COMPRESS_QUALITY = 98;
|
||||
|
||||
/**
|
||||
* LRU cache
|
||||
*/
|
||||
private MemoryCache mLruCache;
|
||||
|
||||
/**
|
||||
* Disk LRU cache
|
||||
*/
|
||||
private DiskLruCache mDiskCache;
|
||||
|
||||
private static ImageCache sInstance;
|
||||
|
||||
/**
|
||||
* Used to temporarily pause the disk cache while scrolling
|
||||
*/
|
||||
public boolean mPauseDiskAccess = false;
|
||||
private Object mPauseLock = new Object();
|
||||
|
||||
static {
|
||||
mArtworkUri = Uri.parse("content://media/external/audio/albumart");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of <code>ImageCache</code>
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
*/
|
||||
public ImageCache(final Context context) {
|
||||
init(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create a singleton of {@link ImageCache}
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @return A new instance of this class.
|
||||
*/
|
||||
public final static ImageCache getInstance(final Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ImageCache(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache, providing all parameters.
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @param cacheParams The cache parameters to initialize the cache
|
||||
*/
|
||||
private void init(final Context context) {
|
||||
Util.execute(false, new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(final Void... unused) {
|
||||
// Initialize the disk cahe in a background thread
|
||||
initDiskCache(context);
|
||||
return null;
|
||||
}
|
||||
}, (Void[])null);
|
||||
// Set up the memory cache
|
||||
initLruCache(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the disk cache. Note that this includes disk access so this
|
||||
* should not be executed on the main/UI thread. By default an ImageCache
|
||||
* does not initialize the disk cache when it is created, instead you should
|
||||
* call initDiskCache() to initialize it on a background thread.
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
*/
|
||||
private synchronized void initDiskCache(final Context context) {
|
||||
// Set up disk cache
|
||||
if (mDiskCache == null || mDiskCache.isClosed()) {
|
||||
File diskCacheDir = getDiskCacheDir(context, TAG);
|
||||
if (diskCacheDir != null) {
|
||||
if (!diskCacheDir.exists()) {
|
||||
diskCacheDir.mkdirs();
|
||||
}
|
||||
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
|
||||
try {
|
||||
mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
|
||||
} catch (final IOException e) {
|
||||
diskCacheDir = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the Lru cache
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public void initLruCache(final Context context) {
|
||||
final ActivityManager activityManager = (ActivityManager)context
|
||||
.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
final int lruCacheSize = Math.round(MEM_CACHE_DIVIDER * activityManager.getMemoryClass()
|
||||
* 1024 * 1024);
|
||||
mLruCache = new MemoryCache(lruCacheSize);
|
||||
|
||||
// Release some memory as needed
|
||||
context.registerComponentCallbacks(new ComponentCallbacks2() {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onTrimMemory(final int level) {
|
||||
if (level >= TRIM_MEMORY_MODERATE) {
|
||||
evictAll();
|
||||
} else if (level >= TRIM_MEMORY_BACKGROUND) {
|
||||
mLruCache.trimToSize(mLruCache.size() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(final Configuration newConfig) {
|
||||
// Nothing to do
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return an existing ImageCache stored in a {@link RetainFragment}
|
||||
* , if not found a new one is created using the supplied params and saved
|
||||
* to a {@link RetainFragment}
|
||||
*
|
||||
* @param activity The calling {@link FragmentActivity}
|
||||
* @return An existing retained ImageCache object or a new one if one did
|
||||
* not exist
|
||||
*/
|
||||
public static final ImageCache findOrCreateCache(final Activity activity) {
|
||||
|
||||
// Search for, or create an instance of the non-UI RetainFragment
|
||||
final RetainFragment retainFragment = findOrCreateRetainFragment(
|
||||
activity.getFragmentManager());
|
||||
|
||||
// See if we already have an ImageCache stored in RetainFragment
|
||||
ImageCache cache = (ImageCache)retainFragment.getObject();
|
||||
|
||||
// No existing ImageCache, create one and store it in RetainFragment
|
||||
if (cache == null) {
|
||||
cache = getInstance(activity);
|
||||
retainFragment.setObject(cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate an existing instance of this {@link Fragment} or if not found,
|
||||
* create and add it using {@link FragmentManager}
|
||||
*
|
||||
* @param fm The {@link FragmentManager} to use
|
||||
* @return The existing instance of the {@link Fragment} or the new instance
|
||||
* if just created
|
||||
*/
|
||||
public static final RetainFragment findOrCreateRetainFragment(final FragmentManager fm) {
|
||||
// Check to see if we have retained the worker fragment
|
||||
RetainFragment retainFragment = (RetainFragment)fm.findFragmentByTag(TAG);
|
||||
|
||||
// If not retained, we need to create and add it
|
||||
if (retainFragment == null) {
|
||||
retainFragment = new RetainFragment();
|
||||
fm.beginTransaction().add(retainFragment, TAG).commit();
|
||||
}
|
||||
return retainFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new image to the memory and disk caches
|
||||
*
|
||||
* @param data The key used to store the image
|
||||
* @param bitmap The {@link Bitmap} to cache
|
||||
*/
|
||||
public void addBitmapToCache(final String data, final Bitmap bitmap) {
|
||||
if (data == null || bitmap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to memory cache
|
||||
addBitmapToMemCache(data, bitmap);
|
||||
|
||||
// Add to disk cache
|
||||
if (mDiskCache != null) {
|
||||
final String key = hashKeyForDisk(data);
|
||||
OutputStream out = null;
|
||||
try {
|
||||
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
|
||||
if (snapshot == null) {
|
||||
final DiskLruCache.Editor editor = mDiskCache.edit(key);
|
||||
if (editor != null) {
|
||||
out = editor.newOutputStream(DISK_CACHE_INDEX);
|
||||
bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, out);
|
||||
editor.commit();
|
||||
out.close();
|
||||
flush();
|
||||
}
|
||||
} else {
|
||||
snapshot.getInputStream(DISK_CACHE_INDEX).close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "addBitmapToCache - " + e);
|
||||
} finally {
|
||||
try {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
out = null;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "addBitmapToCache - " + e);
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.e(TAG, "addBitmapToCache - " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to add a new image to the memory cache
|
||||
*
|
||||
* @param data The key identifier
|
||||
* @param bitmap The {@link Bitmap} to cache
|
||||
*/
|
||||
public void addBitmapToMemCache(final String data, final Bitmap bitmap) {
|
||||
if (data == null || bitmap == null) {
|
||||
return;
|
||||
}
|
||||
// Add to memory cache
|
||||
if (getBitmapFromMemCache(data) == null) {
|
||||
mLruCache.put(data, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a cached image from the memory cache
|
||||
*
|
||||
* @param data Unique identifier for which item to get
|
||||
* @return The {@link Bitmap} if found in cache, null otherwise
|
||||
*/
|
||||
public final Bitmap getBitmapFromMemCache(final String data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (mLruCache != null) {
|
||||
final Bitmap lruBitmap = mLruCache.get(data);
|
||||
if (lruBitmap != null) {
|
||||
return lruBitmap;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a cached image from the disk cache
|
||||
*
|
||||
* @param data Unique identifier for which item to get
|
||||
* @return The {@link Bitmap} if found in cache, null otherwise
|
||||
*/
|
||||
public final Bitmap getBitmapFromDiskCache(final String data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check in the memory cache here to avoid going to the disk cache less
|
||||
// often
|
||||
if (getBitmapFromMemCache(data) != null) {
|
||||
return getBitmapFromMemCache(data);
|
||||
}
|
||||
|
||||
waitUntilUnpaused();
|
||||
final String key = hashKeyForDisk(data);
|
||||
if (mDiskCache != null) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
final DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
|
||||
if (snapshot != null) {
|
||||
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
|
||||
if (inputStream != null) {
|
||||
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
|
||||
if (bitmap != null) {
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "getBitmapFromDiskCache - " + e);
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to return a cached image from memory cache before fetching from the
|
||||
* disk cache
|
||||
*
|
||||
* @param data Unique identifier for which item to get
|
||||
* @return The {@link Bitmap} if found in cache, null otherwise
|
||||
*/
|
||||
public final Bitmap getCachedBitmap(final String data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
Bitmap cachedImage = getBitmapFromMemCache(data);
|
||||
if (cachedImage == null) {
|
||||
cachedImage = getBitmapFromDiskCache(data);
|
||||
}
|
||||
if (cachedImage != null) {
|
||||
addBitmapToMemCache(data, cachedImage);
|
||||
return cachedImage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to return the album art from memory cache and disk cache, before
|
||||
* calling {@code #getArtworkFromFile(Context, String)} again
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @param data The name of the album art
|
||||
* @param id The ID of the album to find artwork for
|
||||
* @return The artwork for an album
|
||||
*/
|
||||
public final Bitmap getCachedArtwork(final Context context, final String data, final long id) {
|
||||
if (context == null || data == null) {
|
||||
return null;
|
||||
}
|
||||
Bitmap cachedImage = getCachedBitmap(data);
|
||||
if (cachedImage == null && id >= 0) {
|
||||
cachedImage = getArtworkFromFile(context, id);
|
||||
}
|
||||
if (cachedImage != null) {
|
||||
addBitmapToMemCache(data, cachedImage);
|
||||
return cachedImage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch the artwork for an album locally from the user's device
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @param albumID The ID of the album to find artwork for
|
||||
* @return The artwork for an album
|
||||
*/
|
||||
public final Bitmap getArtworkFromFile(final Context context, final long albumId) {
|
||||
if (albumId < 0) {
|
||||
return null;
|
||||
}
|
||||
Bitmap artwork = null;
|
||||
waitUntilUnpaused();
|
||||
try {
|
||||
final Uri uri = ContentUris.withAppendedId(mArtworkUri, albumId);
|
||||
final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver()
|
||||
.openFileDescriptor(uri, "r");
|
||||
if (parcelFileDescriptor != null) {
|
||||
final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
|
||||
artwork = BitmapFactory.decodeFileDescriptor(fileDescriptor);
|
||||
}
|
||||
} catch (final IllegalStateException e) {
|
||||
// Log.e(TAG, "IllegalStateExcetpion - getArtworkFromFile - ", e);
|
||||
} catch (final FileNotFoundException e) {
|
||||
// Log.e(TAG, "FileNotFoundException - getArtworkFromFile - ", e);
|
||||
} catch (final OutOfMemoryError evict) {
|
||||
// Log.e(TAG, "OutOfMemoryError - getArtworkFromFile - ", evict);
|
||||
evictAll();
|
||||
}
|
||||
return artwork;
|
||||
}
|
||||
|
||||
/**
|
||||
* flush() is called to synchronize up other methods that are accessing the
|
||||
* cache first
|
||||
*/
|
||||
public void flush() {
|
||||
Util.execute(false, new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(final Void... unused) {
|
||||
if (mDiskCache != null) {
|
||||
try {
|
||||
if (!mDiskCache.isClosed()) {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "flush - " + e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}, (Void[])null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the disk and memory caches
|
||||
*/
|
||||
public void clearCaches() {
|
||||
Util.execute(false, new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(final Void... unused) {
|
||||
// Clear the disk cache
|
||||
try {
|
||||
if (mDiskCache != null) {
|
||||
mDiskCache.delete();
|
||||
mDiskCache = null;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "clearCaches - " + e);
|
||||
}
|
||||
// Clear the memory cache
|
||||
evictAll();
|
||||
return null;
|
||||
}
|
||||
}, (Void[])null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the disk cache associated with this ImageCache object. Note that
|
||||
* this includes disk access so this should not be executed on the main/UI
|
||||
* thread.
|
||||
*/
|
||||
public void close() {
|
||||
Util.execute(false, new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(final Void... unused) {
|
||||
if (mDiskCache != null) {
|
||||
try {
|
||||
if (!mDiskCache.isClosed()) {
|
||||
mDiskCache.close();
|
||||
mDiskCache = null;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "close - " + e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}, (Void[])null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache and lets the system know
|
||||
* now would be a good time to garbage collect
|
||||
*/
|
||||
public void evictAll() {
|
||||
if (mLruCache != null) {
|
||||
mLruCache.evictAll();
|
||||
}
|
||||
System.gc();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key The key used to identify which cache entries to delete.
|
||||
*/
|
||||
public void removeFromCache(final String key) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
// Remove the Lru entry
|
||||
if (mLruCache != null) {
|
||||
mLruCache.remove(key);
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove the disk entry
|
||||
if (mDiskCache != null) {
|
||||
mDiskCache.remove(hashKeyForDisk(key));
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "remove - " + e);
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to temporarily pause the disk cache while the user is scrolling to
|
||||
* improve scrolling.
|
||||
*
|
||||
* @param pause True to temporarily pause the disk cache, false otherwise.
|
||||
*/
|
||||
public void setPauseDiskCache(final boolean pause) {
|
||||
synchronized (mPauseLock) {
|
||||
if (mPauseDiskAccess != pause) {
|
||||
mPauseDiskAccess = pause;
|
||||
if (!pause) {
|
||||
mPauseLock.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void waitUntilUnpaused() {
|
||||
synchronized (mPauseLock) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
while (mPauseDiskAccess) {
|
||||
try {
|
||||
mPauseLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// ignored, we'll start waiting again
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the user is scrolling, false otherwise.
|
||||
*/
|
||||
public boolean isDiskCachePaused() {
|
||||
return mPauseDiskAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a usable cache directory (external if available, internal otherwise)
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @param uniqueName A unique directory name to append to the cache
|
||||
* directory
|
||||
* @return The cache directory
|
||||
*/
|
||||
public static final File getDiskCacheDir(final Context context, final String uniqueName) {
|
||||
// getExternalCacheDir(context) returns null if external storage is not ready
|
||||
final String cachePath = getExternalCacheDir(context) != null
|
||||
? getExternalCacheDir(context).getPath()
|
||||
: context.getCacheDir().getPath();
|
||||
return new File(cachePath, uniqueName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if external storage is built-in or removable
|
||||
*
|
||||
* @return True if external storage is removable (like an SD card), false
|
||||
* otherwise
|
||||
*/
|
||||
public static final boolean isExternalStorageRemovable() {
|
||||
return Environment.isExternalStorageRemovable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the external app cache directory
|
||||
*
|
||||
* @param context The {@link Context} to use
|
||||
* @return The external cache directory
|
||||
*/
|
||||
public static final File getExternalCacheDir(final Context context) {
|
||||
return context.getExternalCacheDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check how much usable space is available at a given path.
|
||||
*
|
||||
* @param path The path to check
|
||||
* @return The space available in bytes
|
||||
*/
|
||||
public static final long getUsableSpace(final File path) {
|
||||
return path.getUsableSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable
|
||||
* for using as a disk filename.
|
||||
*
|
||||
* @param key The key used to store the file
|
||||
*/
|
||||
public static final String hashKeyForDisk(final String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/332079
|
||||
*
|
||||
* @param bytes The bytes to convert.
|
||||
* @return A {@link String} converted from the bytes of a hashable key used
|
||||
* to store a filename on the disk, to hex digits.
|
||||
*/
|
||||
private static final String bytesToHexString(final byte[] bytes) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (final byte b : bytes) {
|
||||
final String hex = Integer.toHexString(0xFF & b);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple non-UI Fragment that stores a single Object and is retained over
|
||||
* configuration changes. In this sample it will be used to retain an
|
||||
* {@link ImageCache} object.
|
||||
*/
|
||||
public static final class RetainFragment extends Fragment {
|
||||
|
||||
/**
|
||||
* The object to be stored
|
||||
*/
|
||||
private Object mObject;
|
||||
|
||||
/**
|
||||
* Empty constructor as per the {@link Fragment} documentation
|
||||
*/
|
||||
public RetainFragment() {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Make sure this Fragment is retained over a configuration change
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a single object in this {@link Fragment}
|
||||
*
|
||||
* @param object The object to store
|
||||
*/
|
||||
public void setObject(final Object object) {
|
||||
mObject = object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored object
|
||||
*
|
||||
* @return The stored object
|
||||
*/
|
||||
public Object getObject() {
|
||||
return mObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cache images via {@link LruCache}.
|
||||
*/
|
||||
public static final class MemoryCache extends LruCache<String, Bitmap> {
|
||||
|
||||
/**
|
||||
* Constructor of <code>MemoryCache</code>
|
||||
*
|
||||
* @param maxSize The allowed size of the {@link LruCache}
|
||||
*/
|
||||
public MemoryCache(final int maxSize) {
|
||||
super(maxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size in bytes of a bitmap.
|
||||
*/
|
||||
public static final int getBitmapSize(final Bitmap bitmap) {
|
||||
return bitmap.getByteCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected int sizeOf(final String paramString, final Bitmap paramBitmap) {
|
||||
return getBitmapSize(paramBitmap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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.hippo.ehviewer.cache;
|
||||
|
||||
// NOTE: upstream of this class is android.util.LruCache, changes below
|
||||
// expose trimToSize() to be called externally.
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Static library version of {@link android.util.LruCache}. Used to write apps
|
||||
* that run on API levels prior to 12. When running on API level 12 or above,
|
||||
* this implementation is still used; it does not try to switch to the
|
||||
* framework's implementation. See the framework SDK documentation for a class
|
||||
* overview.
|
||||
*/
|
||||
public class LruCache<K, V> {
|
||||
|
||||
private final LinkedHashMap<K, V> map;
|
||||
|
||||
private final int maxSize;
|
||||
|
||||
/** Size of this cache in units. Not necessarily the number of elements. */
|
||||
private int size;
|
||||
|
||||
private int putCount;
|
||||
|
||||
private int createCount;
|
||||
|
||||
private int evictionCount;
|
||||
|
||||
private int hitCount;
|
||||
|
||||
private int missCount;
|
||||
|
||||
/**
|
||||
* @param maxSize for caches that do not override {@link #sizeOf}, this is
|
||||
* the maximum number of entries in the cache. For all other
|
||||
* caches, this is the maximum sum of the sizes of the entries in
|
||||
* this cache.
|
||||
*/
|
||||
public LruCache(final int maxSize) {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
this.maxSize = maxSize;
|
||||
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for {@code key} if it exists in the cache or can be
|
||||
* created by {@code #create}. If a value was returned, it is moved to the
|
||||
* head of the queue. This returns null if a value is not cached and cannot
|
||||
* be created.
|
||||
*/
|
||||
public final V get(final K key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException("key == null");
|
||||
}
|
||||
|
||||
V mapValue;
|
||||
synchronized (this) {
|
||||
mapValue = map.get(key);
|
||||
if (mapValue != null) {
|
||||
this.hitCount++;
|
||||
return mapValue;
|
||||
}
|
||||
this.missCount++;
|
||||
}
|
||||
|
||||
/*
|
||||
* Attempt to create a value. This may take a long time, and the map may
|
||||
* be different when create() returns. If a conflicting value was added
|
||||
* to the map while create() was working, we leave that value in the map
|
||||
* and release the created value.
|
||||
*/
|
||||
|
||||
final V createdValue = create(key);
|
||||
if (createdValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
this.createCount++;
|
||||
mapValue = map.put(key, createdValue);
|
||||
|
||||
if (mapValue != null) {
|
||||
/* There was a conflict so undo that last put */
|
||||
this.map.put(key, mapValue);
|
||||
} else {
|
||||
this.size += safeSizeOf(key, createdValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (mapValue != null) {
|
||||
entryRemoved(false, key, createdValue, mapValue);
|
||||
return mapValue;
|
||||
} else {
|
||||
trimToSize(maxSize);
|
||||
return createdValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches {@code value} for {@code key}. The value is moved to the head of
|
||||
* the queue.
|
||||
*
|
||||
* @return the previous value mapped by {@code key}.
|
||||
*/
|
||||
public final V put(final K key, final V value) {
|
||||
if (key == null || value == null) {
|
||||
throw new NullPointerException("key == null || value == null");
|
||||
}
|
||||
|
||||
V previous;
|
||||
synchronized (this) {
|
||||
this.putCount++;
|
||||
this.size += safeSizeOf(key, value);
|
||||
previous = this.map.put(key, value);
|
||||
if (previous != null) {
|
||||
this.size -= safeSizeOf(key, previous);
|
||||
}
|
||||
}
|
||||
|
||||
if (previous != null) {
|
||||
entryRemoved(false, key, previous, value);
|
||||
}
|
||||
|
||||
trimToSize(maxSize);
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxSize the maximum size of the cache before returning. May be -1
|
||||
* to evict even 0-sized elements.
|
||||
*/
|
||||
public void trimToSize(final int maxSize) {
|
||||
while (true) {
|
||||
K key;
|
||||
V value;
|
||||
synchronized (this) {
|
||||
if (this.size < 0 || this.map.isEmpty() && size != 0) {
|
||||
throw new IllegalStateException(getClass().getName()
|
||||
+ ".sizeOf() is reporting inconsistent results!");
|
||||
}
|
||||
|
||||
if (this.size <= maxSize || this.map.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
final Map.Entry<K, V> toEvict = this.map.entrySet().iterator().next();
|
||||
key = toEvict.getKey();
|
||||
value = toEvict.getValue();
|
||||
this.map.remove(key);
|
||||
this.size -= safeSizeOf(key, value);
|
||||
this.evictionCount++;
|
||||
}
|
||||
|
||||
entryRemoved(true, key, value, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the entry for {@code key} if it exists.
|
||||
*
|
||||
* @return the previous value mapped by {@code key}.
|
||||
*/
|
||||
public final V remove(final K key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException("key == null");
|
||||
}
|
||||
|
||||
V previous;
|
||||
synchronized (this) {
|
||||
previous = this.map.remove(key);
|
||||
if (previous != null) {
|
||||
this.size -= safeSizeOf(key, previous);
|
||||
}
|
||||
}
|
||||
|
||||
if (previous != null) {
|
||||
entryRemoved(false, key, previous, null);
|
||||
}
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for entries that have been evicted or removed. This method is
|
||||
* invoked when a value is evicted to make space, removed by a call to
|
||||
* {@link #remove}, or replaced by a call to {@link #put}. The default
|
||||
* implementation does nothing.
|
||||
* <p>
|
||||
* The method is called without synchronization: other threads may access
|
||||
* the cache while this method is executing.
|
||||
*
|
||||
* @param evicted true if the entry is being removed to make space, false if
|
||||
* the removal was caused by a {@link #put} or {@link #remove}.
|
||||
* @param newValue the new value for {@code key}, if it exists. If non-null,
|
||||
* this removal was caused by a {@link #put}. Otherwise it was
|
||||
* caused by an eviction or a {@link #remove}.
|
||||
*/
|
||||
protected void entryRemoved(final boolean evicted, final K key, final V oldValue,
|
||||
final V newValue) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a cache miss to compute a value for the corresponding key.
|
||||
* Returns the computed value or null if no value can be computed. The
|
||||
* default implementation returns null.
|
||||
* <p>
|
||||
* The method is called without synchronization: other threads may access
|
||||
* the cache while this method is executing.
|
||||
* <p>
|
||||
* If a value for {@code key} exists in the cache when this method returns,
|
||||
* the created value will be released with {@link #entryRemoved} and
|
||||
* discarded. This can occur when multiple threads request the same key at
|
||||
* the same time (causing multiple values to be created), or when one thread
|
||||
* calls {@link #put} while another is creating a value for the same key.
|
||||
*/
|
||||
protected V create(final K key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private int safeSizeOf(final K key, final V value) {
|
||||
final int result = sizeOf(key, value);
|
||||
if (result < 0) {
|
||||
throw new IllegalStateException("Negative size: " + key + "=" + value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the entry for {@code key} and {@code value} in
|
||||
* user-defined units. The default implementation returns 1 so that size is
|
||||
* the number of entries and max size is the maximum number of entries.
|
||||
* <p>
|
||||
* An entry's size must not change while it is in the cache.
|
||||
*/
|
||||
protected int sizeOf(final K key, final V value) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
|
||||
*/
|
||||
public final void evictAll() {
|
||||
trimToSize(-1); // -1 will evict 0-sized elements
|
||||
}
|
||||
|
||||
/**
|
||||
* For caches that do not override {@link #sizeOf}, this returns the number
|
||||
* of entries in the cache. For all other caches, this returns the sum of
|
||||
* the sizes of the entries in this cache.
|
||||
*/
|
||||
public synchronized final int size() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* For caches that do not override {@link #sizeOf}, this returns the maximum
|
||||
* number of entries in the cache. For all other caches, this returns the
|
||||
* maximum sum of the sizes of the entries in this cache.
|
||||
*/
|
||||
public synchronized final int maxSize() {
|
||||
return this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times {@link #get} returned a value.
|
||||
*/
|
||||
public synchronized final int hitCount() {
|
||||
return this.hitCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times {@link #get} returned null or required a new
|
||||
* value to be created.
|
||||
*/
|
||||
public synchronized final int missCount() {
|
||||
return this.missCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times {@link #create(Object)} returned a value.
|
||||
*/
|
||||
public synchronized final int createCount() {
|
||||
return this.createCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times {@link #put} was called.
|
||||
*/
|
||||
public synchronized final int putCount() {
|
||||
return this.putCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of values that have been evicted.
|
||||
*/
|
||||
public synchronized final int evictionCount() {
|
||||
return this.evictionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the current contents of the cache, ordered from least
|
||||
* recently accessed to most recently accessed.
|
||||
*/
|
||||
public synchronized final Map<K, V> snapshot() {
|
||||
return new LinkedHashMap<K, V>(this.map);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public synchronized final String toString() {
|
||||
final int accesses = this.hitCount + this.missCount;
|
||||
final int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
|
||||
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize,
|
||||
this.hitCount, this.missCount, hitPercent);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ import java.util.Locale;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
||||
public class Util {
|
||||
@SuppressWarnings("unused")
|
||||
@@ -223,4 +225,27 @@ public class Util {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an {@link AsyncTask} on a thread pool
|
||||
*
|
||||
* @param forceSerial True to force the task to run in serial order
|
||||
* @param task Task to execute
|
||||
* @param args Optional arguments to pass to
|
||||
* {@link AsyncTask#execute(Object[])}
|
||||
* @param <T> Task argument type
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public static <T> void execute(final boolean forceSerial, final AsyncTask<T, ?, ?> task,
|
||||
final T... args) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.DONUT) {
|
||||
throw new UnsupportedOperationException(
|
||||
"This class can only be used on API 4 and newer.");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || forceSerial) {
|
||||
task.execute(args);
|
||||
} else {
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user