Android实现Glide/Coil样式图/视频加载框架,Kotlin
Android实现Glide/Coil样式图/视频加载框架,Kotlin
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /><uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@android:color/white"android:padding="1px"><ImageViewandroid:id="@+id/image"android:layout_width="match_parent"android:layout_height="180px"android:scaleType="centerCrop" /></LinearLayout>
/** 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.*/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.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** 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. Each key must match* the regex <strong>[a-z0-9_-]{1,120}</strong>. 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_TEMP = "journal.tmp";static final String JOURNAL_FILE_BACKUP = "journal.bkp";static final String MAGIC = "libcore.io.DiskLruCache";static final String VERSION_1 = "1";static final long ANY_SEQUENCE_NUMBER = -1;static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);private static final String CLEAN = "CLEAN";private static final String DIRTY = "DIRTY";private static final String REMOVE = "REMOVE";private static final String READ = "READ";/** 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 File journalFileBackup;private final int appVersion;private 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;/** This cache uses a single background thread to evict entries. */final ThreadPoolExecutor executorService =new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());private final Callable<Void> cleanupCallable = new Callable<Void>() {public Void call() throws Exception {synchronized (DiskLruCache.this) {if (journalWriter == null) {return null; // Closed.}trimToSize();if (journalRebuildRequired()) {rebuildJournal();redundantOpCount = 0;}}return null;}};private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {this.directory = directory;this.appVersion = appVersion;this.journalFile = new File(directory, JOURNAL_FILE);this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);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 valueCount the number of values per cache entry. Must be positive.* @param maxSize the maximum number of bytes this cache should use to store* @throws IOException if reading or writing the cache directory fails*/public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)throws IOException {if (maxSize <= 0) {throw new IllegalArgumentException("maxSize <= 0");}if (valueCount <= 0) {throw new IllegalArgumentException("valueCount <= 0");}// If a bkp file exists, use it instead.File backupFile = new File(directory, JOURNAL_FILE_BACKUP);if (backupFile.exists()) {File journalFile = new File(directory, JOURNAL_FILE);// If journal file also exists just delete backup file.if (journalFile.exists()) {backupFile.delete();} else {renameTo(backupFile, journalFile, false);}}// 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();return cache;} catch (IOException journalIsCorrupt) {System.out.println("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 {StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);try {String magic = reader.readLine();String version = reader.readLine();String appVersionString = reader.readLine();String valueCountString = reader.readLine();String blank = reader.readLine();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 + "]");}int lineCount = 0;while (true) {try {readJournalLine(reader.readLine());lineCount++;} catch (EOFException endOfJournal) {break;}}redundantOpCount = lineCount - lruEntries.size();// If we ended on a truncated line, rebuild the journal before appending to it.if (reader.hasUnterminatedLine()) {rebuildJournal();} else {journalWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));}} finally {Util.closeQuietly(reader);}}private void readJournalLine(String line) throws IOException {int firstSpace = line.indexOf(' ');if (firstSpace == -1) {throw new IOException("unexpected journal line: " + line);}int keyBegin = firstSpace + 1;int secondSpace = line.indexOf(' ', keyBegin);final String key;if (secondSpace == -1) {key = line.substring(keyBegin);if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {lruEntries.remove(key);return;}} else {key = line.substring(keyBegin, secondSpace);}Entry entry = lruEntries.get(key);if (entry == null) {entry = new Entry(key);lruEntries.put(key, entry);}if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {String[] parts = line.substring(secondSpace + 1).split(" ");entry.readable = true;entry.currentEditor = null;entry.setLengths(parts);} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {entry.currentEditor = new Editor(entry);} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {// This work was already done by calling lruEntries.get().} else {throw new IOException("unexpected journal line: " + line);}}/*** Computes the initial size and collects garbage as a part of opening the* cache. Dirty entries are assumed to be inconsistent and will be deleted.*/private void processJournal() throws IOException {deleteIfExists(journalFileTmp);for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {Entry entry = i.next();if (entry.currentEditor == null) {for (int t = 0; t < valueCount; t++) {size += entry.lengths[t];}} else {entry.currentEditor = null;for (int t = 0; t < valueCount; t++) {deleteIfExists(entry.getCleanFile(t));deleteIfExists(entry.getDirtyFile(t));}i.remove();}}}/*** Creates a new journal that omits redundant information. This replaces the* current journal if it exists.*/private synchronized void rebuildJournal() throws IOException {if (journalWriter != null) {journalWriter.close();}Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));try {writer.write(MAGIC);writer.write("\n");writer.write(VERSION_1);writer.write("\n");writer.write(Integer.toString(appVersion));writer.write("\n");writer.write(Integer.toString(valueCount));writer.write("\n");writer.write("\n");for (Entry entry : lruEntries.values()) {if (entry.currentEditor != null) {writer.write(DIRTY + ' ' + entry.key + '\n');} else {writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');}}} finally {writer.close();}if (journalFile.exists()) {renameTo(journalFile, journalFileBackup, true);}renameTo(journalFileTmp, journalFile, false);journalFileBackup.delete();journalWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));}private static void deleteIfExists(File file) throws IOException {if (file.exists() && !file.delete()) {throw new IOException();}}private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {if (deleteDestination) {deleteIfExists(to);}if (!from.renameTo(to)) {throw new IOException();}}/*** Returns a snapshot of the entry named {@code key}, or null if it doesn't* exist is not currently readable. If a value is returned, it is moved to* the head of the LRU queue.*/public synchronized Snapshot get(String key) throws IOException {checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if (entry == null) {return null;}if (!entry.readable) {return null;}// Open all streams eagerly to guarantee that we see a single published// snapshot. If we opened streams lazily then the streams could come// from different edits.InputStream[] ins = new InputStream[valueCount];try {for (int i = 0; i < valueCount; i++) {ins[i] = new FileInputStream(entry.getCleanFile(i));}} catch (FileNotFoundException e) {// A file must have been deleted manually!for (int i = 0; i < valueCount; i++) {if (ins[i] != null) {Util.closeQuietly(ins[i]);} else {break;}}return null;}redundantOpCount++;journalWriter.append(READ + ' ' + key + '\n');if (journalRebuildRequired()) {executorService.submit(cleanupCallable);}return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);}/*** Returns an editor for the entry named {@code key}, or null if another* edit is in progress.*/public Editor edit(String key) throws IOException {return edit(key, ANY_SEQUENCE_NUMBER);}private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null|| entry.sequenceNumber != expectedSequenceNumber)) {return null; // Snapshot is stale.}if (entry == null) {entry = new Entry(key);lruEntries.put(key, entry);} else if (entry.currentEditor != null) {return null; // Another edit is in progress.}Editor editor = new Editor(entry);entry.currentEditor = editor;// Flush the journal before creating files to prevent file leaks.journalWriter.write(DIRTY + ' ' + key + '\n');journalWriter.flush();return editor;}/** Returns the directory where this cache stores its data. */public File getDirectory() {return directory;}/*** Returns the maximum number of bytes that this cache should use to store* its data.*/public synchronized long getMaxSize() {return maxSize;}/*** Changes the maximum number of bytes the cache can store and queues a job* to trim the existing store, if necessary.*/public synchronized void setMaxSize(long maxSize) {this.maxSize = maxSize;executorService.submit(cleanupCallable);}/*** Returns the number of bytes currently being used to store the values in* this cache. This may be greater than the max size if a background* deletion is pending.*/public synchronized long size() {return size;}private synchronized void completeEdit(Editor editor, boolean success) throws IOException {Entry entry = editor.entry;if (entry.currentEditor != editor) {throw new IllegalStateException();}// If this edit is creating the entry for the first time, every index must have a value.if (success && !entry.readable) {for (int i = 0; i < valueCount; i++) {if (!editor.written[i]) {editor.abort();throw new IllegalStateException("Newly created entry didn't create value for index " + i);}if (!entry.getDirtyFile(i).exists()) {editor.abort();return;}}}for (int i = 0; i < valueCount; i++) {File dirty = entry.getDirtyFile(i);if (success) {if (dirty.exists()) {File clean = entry.getCleanFile(i);dirty.renameTo(clean);long oldLength = entry.lengths[i];long newLength = clean.length();entry.lengths[i] = newLength;size = size - oldLength + newLength;}} else {deleteIfExists(dirty);}}redundantOpCount++;entry.currentEditor = null;if (entry.readable | success) {entry.readable = true;journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');if (success) {entry.sequenceNumber = nextSequenceNumber++;}} else {lruEntries.remove(entry.key);journalWriter.write(REMOVE + ' ' + entry.key + '\n');}journalWriter.flush();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 redundantOpCompactThreshold = 2000;return redundantOpCount >= redundantOpCompactThreshold //&& redundantOpCount >= lruEntries.size();}/*** Drops the entry for {@code key} if it exists and can be removed. Entries* actively being edited cannot be removed.** @return true if an entry was removed.*/public synchronized boolean remove(String key) throws IOException {checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if (entry == null || entry.currentEditor != null) {return false;}for (int i = 0; i < valueCount; i++) {File file = entry.getCleanFile(i);if (file.exists() && !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 synchronized boolean isClosed() {return journalWriter == null;}private void checkNotClosed() {if (journalWriter == null) {throw new IllegalStateException("cache is closed");}}/** Force buffered operations to the filesystem. */public synchronized void flush() throws IOException {checkNotClosed();trimToSize();journalWriter.flush();}/** Closes this cache. Stored values will remain on the filesystem. */public synchronized void close() throws IOException {if (journalWriter == null) {return; // Already closed.}for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {if (entry.currentEditor != null) {entry.currentEditor.abort();}}trimToSize();journalWriter.close();journalWriter = null;}private void trimToSize() throws IOException {while (size > maxSize) {Map.Entry<String, Entry> toEvict = lruEntries.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();Util.deleteContents(directory);}private void validateKey(String key) {Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);if (!matcher.matches()) {throw new IllegalArgumentException("keys must match regex "+ STRING_KEY_PATTERN + ": \"" + key + "\"");}}private static String inputStreamToString(InputStream in) throws IOException {return Util.readFully(new InputStreamReader(in, Util.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 final long[] lengths;private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {this.key = key;this.sequenceNumber = sequenceNumber;this.ins = ins;this.lengths = lengths;}/*** Returns an editor for this snapshot's entry, or null if either the* entry has changed since this snapshot was created or if another edit* is in progress.*/public Editor edit() throws IOException {return DiskLruCache.this.edit(key, sequenceNumber);}/** Returns the unbuffered stream with the value for {@code index}. */public InputStream getInputStream(int index) {return ins[index];}/** Returns the string value for {@code index}. */public String getString(int index) throws IOException {return inputStreamToString(getInputStream(index));}/** Returns the byte length of the value for {@code index}. */public long getLength(int index) {return lengths[index];}public void close() {for (InputStream in : ins) {Util.closeQuietly(in);}}}private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {@Overridepublic void write(int b) throws IOException {// Eat all writes silently. Nom nom.}};/** Edits the values for an entry. */public final class Editor {private final Entry entry;private final boolean[] written;private boolean hasErrors;private boolean committed;private Editor(Entry entry) {this.entry = entry;this.written = (entry.readable) ? null : new boolean[valueCount];}/*** Returns an unbuffered input stream to read the last committed value,* or null if no value has been committed.*/public InputStream newInputStream(int index) throws IOException {synchronized (DiskLruCache.this) {if (entry.currentEditor != this) {throw new IllegalStateException();}if (!entry.readable) {return null;}try {return new FileInputStream(entry.getCleanFile(index));} catch (FileNotFoundException e) {return null;}}}/*** Returns the last committed value as a string, or null if no value* has been committed.*/public String getString(int index) throws IOException {InputStream in = newInputStream(index);return in != null ? inputStreamToString(in) : null;}/*** Returns a new unbuffered output stream to write the value at* {@code index}. If the underlying output stream encounters errors* when writing to the filesystem, this edit will be aborted when* {@link #commit} is called. The returned output stream does not throw* IOExceptions.*/public OutputStream newOutputStream(int index) throws IOException {if (index < 0 || index >= valueCount) {throw new IllegalArgumentException("Expected index " + index + " to "+ "be greater than 0 and less than the maximum value count "+ "of " + valueCount);}synchronized (DiskLruCache.this) {if (entry.currentEditor != this) {throw new IllegalStateException();}if (!entry.readable) {written[index] = true;}File dirtyFile = entry.getDirtyFile(index);FileOutputStream outputStream;try {outputStream = new FileOutputStream(dirtyFile);} catch (FileNotFoundException e) {// Attempt to recreate the cache directory.directory.mkdirs();try {outputStream = new FileOutputStream(dirtyFile);} catch (FileNotFoundException e2) {// We are unable to recover. Silently eat the writes.return NULL_OUTPUT_STREAM;}}return new FaultHidingOutputStream(outputStream);}}/** Sets the value at {@code index} to {@code value}. */public void set(int index, String value) throws IOException {Writer writer = null;try {writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);writer.write(value);} finally {Util.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);}committed = 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);}public void abortUnlessCommitted() {if (!committed) {try {abort();} catch (IOException ignored) {}}}private class FaultHidingOutputStream extends FilterOutputStream {private FaultHidingOutputStream(OutputStream out) {super(out);}@Override public void write(int oneByte) {try {out.write(oneByte);} catch (IOException e) {hasErrors = true;}}@Override public void write(byte[] buffer, int offset, int length) {try {out.write(buffer, offset, length);} catch (IOException e) {hasErrors = true;}}@Override public void close() {try {out.close();} catch (IOException e) {hasErrors = true;}}@Override public void flush() {try {out.flush();} catch (IOException e) {hasErrors = true;}}}}private final class Entry {private final String key;/** Lengths of this entry's files. */private final long[] lengths;/** True if this entry has ever been published. */private boolean readable;/** The ongoing edit or null if this entry is not being edited. */private Editor currentEditor;/** The sequence number of the most recently committed edit to this entry. */private long sequenceNumber;private Entry(String key) {this.key = key;this.lengths = new long[valueCount];}public String getLengths() throws IOException {StringBuilder result = new StringBuilder();for (long size : lengths) {result.append(' ').append(size);}return result.toString();}/** Set lengths using decimal numbers like "10123". */private void setLengths(String[] strings) throws IOException {if (strings.length != valueCount) {throw invalidLengths(strings);}try {for (int i = 0; i < strings.length; i++) {lengths[i] = Long.parseLong(strings[i]);}} catch (NumberFormatException e) {throw invalidLengths(strings);}}private IOException invalidLengths(String[] strings) throws IOException {throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));}public File getCleanFile(int i) {return new File(directory, key + "." + i);}public File getDirtyFile(int i) {return new File(directory, key + "." + i + ".tmp");}}
}
/** Copyright (C) 2012 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.*/import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;/*** Buffers input from an {@link InputStream} for reading lines.** <p>This class is used for buffered reading of lines. For purposes of this class, a line ends* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated* line at end of input is invalid and will be ignored, the caller may use {@code* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.** <p>This class is intended for reading input that strictly consists of lines, such as line-based* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different* end-of-input reporting and a more restrictive definition of a line.** <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13* and 10, respectively, and the representation of no other character contains these values.* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.* The default charset is US_ASCII.*/
class StrictLineReader implements Closeable {private static final byte CR = (byte) '\r';private static final byte LF = (byte) '\n';private final InputStream in;private final Charset charset;/** Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end* and the data in the range [pos, end) is buffered for reading. At end of input, if there is* an unterminated line, we set end == -1, otherwise end == pos. If the underlying* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.*/private byte[] buf;private int pos;private int end;/*** Constructs a new {@code LineReader} with the specified charset and the default capacity.** @param in the {@code InputStream} to read data from.* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are* supported.* @throws NullPointerException if {@code in} or {@code charset} is null.* @throws IllegalArgumentException if the specified charset is not supported.*/public StrictLineReader(InputStream in, Charset charset) {this(in, 8192, charset);}/*** Constructs a new {@code LineReader} with the specified capacity and charset.** @param in the {@code InputStream} to read data from.* @param capacity the capacity of the buffer.* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are* supported.* @throws NullPointerException if {@code in} or {@code charset} is null.* @throws IllegalArgumentException if {@code capacity} is negative or zero* or the specified charset is not supported.*/public StrictLineReader(InputStream in, int capacity, Charset charset) {if (in == null || charset == null) {throw new NullPointerException();}if (capacity < 0) {throw new IllegalArgumentException("capacity <= 0");}if (!(charset.equals(Util.US_ASCII))) {throw new IllegalArgumentException("Unsupported encoding");}this.in = in;this.charset = charset;buf = new byte[capacity];}/*** Closes the reader by closing the underlying {@code InputStream} and* marking this reader as closed.** @throws IOException for errors when closing the underlying {@code InputStream}.*/public void close() throws IOException {synchronized (in) {if (buf != null) {buf = null;in.close();}}}/*** Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},* this end of line marker is not included in the result.** @return the next line from the input.* @throws IOException for underlying {@code InputStream} errors.* @throws EOFException for the end of source stream.*/public String readLine() throws IOException {synchronized (in) {if (buf == null) {throw new IOException("LineReader is closed");}// Read more data if we are at the end of the buffered data.// Though it's an error to read after an exception, we will let {@code fillBuf()}// throw again if that happens; thus we need to handle end == -1 as well as end == pos.if (pos >= end) {fillBuf();}// Try to find LF in the buffered data and return the line if successful.for (int i = pos; i != end; ++i) {if (buf[i] == LF) {int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;String res = new String(buf, pos, lineEnd - pos, charset.name());pos = i + 1;return res;}}// Let's anticipate up to 80 characters on top of those already read.ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {@Overridepublic String toString() {int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;try {return new String(buf, 0, length, charset.name());} catch (UnsupportedEncodingException e) {throw new AssertionError(e); // Since we control the charset this will never happen.}}};while (true) {out.write(buf, pos, end - pos);// Mark unterminated line in case fillBuf throws EOFException or IOException.end = -1;fillBuf();// Try to find LF in the buffered data and return the line if successful.for (int i = pos; i != end; ++i) {if (buf[i] == LF) {if (i != pos) {out.write(buf, pos, i - pos);}pos = i + 1;return out.toString();}}}}}public boolean hasUnterminatedLine() {return end == -1;}/*** Reads new input data into the buffer. Call only with pos == end or end == -1,* depending on the desired outcome if the function throws.*/private void fillBuf() throws IOException {int result = in.read(buf, 0, buf.length);if (result == -1) {throw new EOFException();}pos = 0;end = result;}
}
/** Copyright (C) 2010 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.*/import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.Charset;/** Junk drawer of utility methods. */
final class Util {static final Charset US_ASCII = Charset.forName("US-ASCII");static final Charset UTF_8 = Charset.forName("UTF-8");private Util() {}static String readFully(Reader reader) throws IOException {try {StringWriter writer = new StringWriter();char[] buffer = new char[1024];int count;while ((count = reader.read(buffer)) != -1) {writer.write(buffer, 0, count);}return writer.toString();} finally {reader.close();}}/*** Deletes the contents of {@code dir}. Throws an IOException if any file* could not be deleted, or if {@code dir} is not a readable directory.*/static void deleteContents(File dir) throws IOException {File[] files = dir.listFiles();if (files == null) {throw new IOException("not a readable directory: " + dir);}for (File file : files) {if (file.isDirectory()) {deleteContents(file);}if (!file.delete()) {throw new IOException("failed to delete file: " + file);}}}static void closeQuietly(/*Auto*/Closeable closeable) {if (closeable != null) {try {closeable.close();} catch (RuntimeException rethrown) {throw rethrown;} catch (Exception ignored) {}}}
}
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.LruCache
import androidx.lifecycle.lifecycleScope
import com.mediaapp.disk.DiskLruCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.security.MessageDigest
import java.security.NoSuchAlgorithmExceptionclass BitmapCache {companion object {const val TAG = "fly/BitmapCache"const val MAX_DISK_SIZE: Long = 1024 * 1024 * 1024const val CACHE_FILE_PATH = "fly_disk_cache"}private var mCtx: Context? = nullprivate var mDiskCache: DiskLruCache? = nullprivate var mMemoryCache = LruCache<String, Bitmap>(1000)constructor(ctx: Context) {mCtx = ctxval file = File(ctx.cacheDir.path, CACHE_FILE_PATH)if (!file.exists()) {file.mkdirs()}mDiskCache = DiskLruCache.open(file, 1, 1, MAX_DISK_SIZE)}fun getMemoryCache(key: String): Bitmap? {return mMemoryCache.get(key)}@WorkerThreadfun putCache(key: String, bmp: Bitmap) {putMemoryCache(key, bmp)putDiskCache(key, bmp)}@WorkerThreadfun getCache(key: String): Bitmap? {var bmp = getMemoryCache(key)if (bmp != null) {Log.d(TAG, "内存缓存存在 $key")return bmp} else {bmp = getDiskCache(key)if (bmp != null) {Log.d(TAG, "Disk缓存存在 $key")putMemoryCache(key, bmp!!) //刷新内存的缓存return bmp}}return null}fun putMemoryCache(key: String, bmp: Bitmap) {mMemoryCache.put(key, bmp)}@WorkerThreadprivate fun getDiskCache(key: String): Bitmap? {val snapshot = mDiskCache?.get(getMD5(key))val bmp = BitmapFactory.decodeStream(snapshot?.getInputStream(0))//mDiskCache?.close()return bmp}@WorkerThreadprivate fun putDiskCache(key: String, bmp: Bitmap) {(mCtx as AppCompatActivity).lifecycleScope.launch(Dispatchers.IO) {val editor = mDiskCache?.edit(getMD5(key))try {bmp.compress(Bitmap.CompressFormat.PNG, 100, editor?.newOutputStream(0)!!)editor.commit()//mDiskCache?.flush()//mDiskCache?.close()Log.d(TAG, "写入成功 $key")} catch (e: Exception) {Log.e(TAG, "editor $e $key")}}}private fun getMD5(msg: String): String {var md: MessageDigest? = nulltry {md = MessageDigest.getInstance("MD5")} catch (e: NoSuchAlgorithmException) {e.printStackTrace()}md?.reset()md?.update(msg.toByteArray())val bytes = md?.digest()var result = ""if (bytes != null) {for (b in bytes) {// byte转换成16进制result += String.format("%02x", b)}}return result}
}
import android.graphics.Bitmap
import android.util.Log
import wseemann.media.FFmpegMediaMetadataRetrieverclass BitmapDecoder : Loader.Decoder {companion object {const val TAG = "fly/BitmapDecoder"}override fun decode(data: Any?): Bitmap? {val mData = data as MyDataLog.d(TAG, "开始解码 $mData")val ffMMR = FFmpegMediaMetadataRetriever()var bmp: Bitmap? = nulltry {ffMMR.setDataSource(mData.path)bmp = ffMMR.frameAtTime} catch (e: Exception) {Log.e(TAG, "FFmpeg MMR: ${e.message}:$mData")} finally {try {ffMMR.release()} catch (exc: Exception) {Log.e(TAG, "$exc")}}return bmp}
}
import android.content.Context
import android.graphics.Bitmapclass Loader {companion object {const val TAG = "fly/Loader"}private var mCtx: Context? = nullprivate var mListener: Listener? = nullprivate var mBitmapCache: BitmapCache? = nullprivate var mDecoder: Decoder? = nullprivate var mData: MyData? = nullconstructor(ctx: Context) {mCtx = ctx}fun cache(cache: BitmapCache): Loader {mBitmapCache = cachereturn this}fun load(data: MyData): Loader {mData = datareturn this}fun start(): Loader {var bmp: Bitmap?bmp = mBitmapCache?.getCache(mData.toString())if (bmp != null) {mListener?.onSuccess(bmp)} else {bmp = mDecoder?.decode(mData)if (bmp != null) {mListener?.onSuccess(bmp)mBitmapCache?.putCache(mData.toString(), bmp)}}mListener?.onError()return this}fun decoder(decoder: Decoder): Loader {mDecoder = decoderreturn this}fun setListener(l: Listener): Loader {mListener = lreturn this}interface Listener {fun onSuccess(bitmap: Bitmap) {}fun onError() {}}interface Decoder {fun decode(data: Any?): Bitmap? {return null}}
}
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launchclass MainActivity : AppCompatActivity() {companion object {const val TAG = "fly/MainActivity"const val SPAN_COUNT = 6const val VIDEO = 1}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val rv = findViewById<RecyclerView>(R.id.rv)val layoutManager = GridLayoutManager(this, SPAN_COUNT)layoutManager.orientation = GridLayoutManager.VERTICALrv.layoutManager = layoutManagerval adapter = MyAdapter(this)rv.adapter = adapterrv.layoutManager = layoutManagerval ctx = thislifecycleScope.launch(Dispatchers.IO) {val videoList = readAllVideo(ctx)Log.d(TAG, "readAllVideo size=${videoList.size}")val lists = arrayListOf<MyData>()lists.addAll(videoList)lifecycleScope.launch(Dispatchers.Main) {adapter.dataChanged(lists)}}}private fun readAllVideo(ctx: Context): ArrayList<MyData> {val videos = ArrayList<MyData>()//读取视频Videoval cursor = ctx.contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,null,null,null,null)while (cursor!!.moveToNext()) {//路径val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA))val id = cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)val videoUri: Uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getLong(id))//名称//val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME))//大小//val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE))videos.add(MyData(videoUri, path, VIDEO))}cursor.close()return videos}
}
import android.content.Context
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launchclass MyAdapter : RecyclerView.Adapter<MyAdapter.VideoHolder> {companion object {const val TAG = "fly/MyAdapter"}private var mCtx: Context? = nullprivate var mItems = ArrayList<MyData>()private var mBitmapCache: BitmapCache? = nullconstructor(ctx: Context) : super() {mCtx = ctxmBitmapCache = BitmapCache(mCtx!!)}fun dataChanged(items: ArrayList<MyData>) {this.mItems = itemsnotifyDataSetChanged()}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoHolder {val v = LayoutInflater.from(mCtx).inflate(R.layout.image_layout, null)return VideoHolder(v)}override fun onBindViewHolder(holder: VideoHolder, position: Int) {loadVideoCover(mItems[position], holder.image)}override fun getItemCount(): Int {return mItems.size}class VideoHolder : RecyclerView.ViewHolder {var image: ImageView? = nullconstructor(itemView: View) : super(itemView) {image = itemView.findViewById<ImageView>(R.id.image)image?.setImageResource(android.R.drawable.ic_menu_gallery)}}private fun loadVideoCover(data: MyData, image: ImageView?) {val cacheBmp = mBitmapCache?.getMemoryCache(data.toString())if (cacheBmp != null) {image?.setImageBitmap(cacheBmp)} else {(mCtx as AppCompatActivity).lifecycleScope.launch(Dispatchers.IO) {Loader(mCtx!!).cache(mBitmapCache!!).load(data).decoder(BitmapDecoder()).setListener(object : Loader.Listener {override fun onSuccess(bitmap: Bitmap) {(mCtx as AppCompatActivity).lifecycleScope.launch(Dispatchers.Main) {image?.setImageBitmap(bitmap)}}}).start()}}}
}
import android.net.Uri
import android.text.TextUtilsopen class MyData {var uri: Uri? = nullvar path: String? = nullvar lastModified = 0Lvar width = 0var height = 0var position = -1var type = -1 //-1未知。1,普通图。2,视频。constructor(uri: Uri?, path: String?, type: Int = -1) {this.uri = urithis.path = paththis.type = type}override fun equals(other: Any?): Boolean {return TextUtils.equals(this.toString(), other.toString())}override fun toString(): String {return "MyData(uri=$uri, path=$path, lastModified=$lastModified, width=$width, height=$height, position=$position, type=$type)"}
}
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin-CSDN博客文章浏览阅读610次,点赞21次,收藏6次。本文介绍了一个基于Coil3的Android视频封面抽取实现方案。项目通过MediaStore获取设备视频列表,使用MediaMetadataRetriever提取视频首帧作为缩略图,并采用二级缓存(内存+磁盘)优化性能。核心功能包括:1)声明读写存储权限;2)RecyclerView网格布局展示;3)协程处理耗时操作;4)自定义ImageLoader配置缓存策略(最大2GB)。相比原生方案,该实现通过Coil的缓存机制提升了缩略图加载效率,同时处理了视频损坏等异常情况。相关技术细节可参考作者CSDN博客中https://blog.csdn.net/zhangphil/article/details/150224812
Android快速视频解码抽帧FFmpegMediaMetadataRetriever,Kotlin(2)-CSDN博客文章浏览阅读216次。本文介绍了两种Android视频封面提取方案对比:1)原生MediaMetadataRetriever速度较慢;2)第三方FFmpegMediaMetadataRetriever(FFMMR)实现快速抽帧。详细说明了FFMMR的集成方法(添加依赖和权限),并提供了完整的Kotlin实现代码,包括视频列表读取、缓存管理、协程异步处理等核心功能。通过LruCache缓存缩略图提升性能,记录处理耗时和失败情况。相比前文介绍的原生方案,本文重点突出了FFMMR在解码效率和性能上的优势,为需要快速获取视频帧的场景提供https://blog.csdn.net/zhangphil/article/details/150061648