添加新的缓存管理

This commit is contained in:
seven332 committed 2014-05-30 12:29:25 +08:00
1 parent 2c58f6c9ae
commit 12412d67fb
4 files changed
+2105

No files matched your search

+969
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+25
View File
@@ -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);
}
}
}