關(guān)于Android的 DiskLruCache磁盤(pán)緩存機(jī)制原理
一、為什么用DiskLruCache
1、LruCache和DiskLruCache
LruCache和DiskLruCache兩者都是利用到LRU算法,通過(guò)LRU算法對(duì)緩存進(jìn)行管理,以最近最少使用作為管理的依據(jù),刪除最近最少使用的數(shù)據(jù),保留最近最常用的數(shù)據(jù);
LruCache運(yùn)用于內(nèi)存緩存,而DiskLruCache是存儲(chǔ)設(shè)備緩存;
2、為何使用DiskLruCache
離線數(shù)據(jù)存在的意義,當(dāng)無(wú)網(wǎng)絡(luò)或者是網(wǎng)絡(luò)狀況不好時(shí),APP依然具備部分功能是一種很好的用戶體驗(yàn);
假設(shè)網(wǎng)易新聞這類新聞客戶端,數(shù)據(jù)完全存儲(chǔ)在緩存中而不使用DiskLruCache技術(shù)存儲(chǔ),那么當(dāng)客戶端被銷毀,緩存被釋放,意味著再次打開(kāi)APP將是一片空白;
另外DiskLruCache技術(shù)也可為app“離線閱讀”這一功能做技術(shù)支持;
DiskLruCache的存儲(chǔ)路徑是可以自定義的,不過(guò)也可以是默認(rèn)的存儲(chǔ)路徑,而默認(rèn)的存儲(chǔ)路徑一般是這樣的:/sdcard/Android/data/包名/cache,包名是指APP的包名。我們可以在手機(jī)上打開(kāi),瀏覽這一路徑;
二、DiskLruCache使用
1、添加依賴
// add dependence implementation 'com.jakewharton:disklrucache:2.0.2'
2、創(chuàng)建DiskLruCache對(duì)象
/* * directory – 緩存目錄 * appVersion - 緩存版本 * valueCount – 每個(gè)key對(duì)應(yīng)value的個(gè)數(shù) * maxSize – 緩存大小的上限 */ DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 1, 1024 * 1024 * 10);
3、添加 / 獲取 緩存(一對(duì)一)
/**
* 添加一條緩存,一個(gè)key對(duì)應(yīng)一個(gè)value
*/
public void addDiskCache(String key, String value) throws IOException {
File cacheDir = context.getCacheDir();
DiskLruCache diskLruCache = DiskLruCache.open(cacheDir, 1, 1, 1024 * 1024 * 10);
DiskLruCache.Editor editor = diskLruCache.edit(key);
// index與valueCount對(duì)應(yīng),分別為0,1,2...valueCount-1
editor.newOutputStream(0).write(value.getBytes());
editor.commit();
diskLruCache.close();
}
/**
* 獲取一條緩存,一個(gè)key對(duì)應(yīng)一個(gè)value
*/
public void getDiskCache(String key) throws IOException {
File directory = context.getCacheDir();
DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 1, 1024 * 1024 * 10);
String value = diskLruCache.get(key).getString(0);
diskLruCache.close();
}
4、添加 / 獲取 緩存(一對(duì)多)
/**
* 添加一條緩存,1個(gè)key對(duì)應(yīng)2個(gè)value
*/
public void addDiskCache(String key, String value1, String value2) throws IOException {
File directory = context.getCacheDir();
DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 2, 1024 * 1024 * 10);
DiskLruCache.Editor editor = diskLruCache.edit(key);
editor.newOutputStream(0).write(value1.getBytes());
editor.newOutputStream(1).write(value2.getBytes());
editor.commit();
diskLruCache.close();
}
/**
* 添加一條緩存,1個(gè)key對(duì)應(yīng)2個(gè)value
*/
public void getDiskCache(String key) throws IOException {
File directory = context.getCacheDir();
DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 2, 1024);
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
String value1 = snapshot.getString(0);
String value2 = snapshot.getString(1);
diskLruCache.close();
}
三、源碼分析

1、open()
DiskLruCache的構(gòu)造方法是private修飾,這也就是告訴我們,不能通過(guò)new DiskLruCache來(lái)獲取實(shí)例,構(gòu)造方法如下:
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;
}
但是提供了open()方法,供我們獲取DiskLruCache的實(shí)例,open方法如下:
/**
* 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,包括,大小,版本,路徑,key對(duì)應(yīng)多少value
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
//如果日志文件存在,就開(kāi)始賭文件信息,并返回
//主要就是構(gòu)建entry列表
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();
}
}
//不存在就新建一個(gè)
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
open函數(shù):如果日志文件存在,直接去構(gòu)建entry列表;如果不存在,就構(gòu)建日志文件;
2、rebuildJournal()
構(gòu)建文件:
//這個(gè)就是我們可以直接在disk里面看到的journal文件 主要就是對(duì)他的操作
private final File journalFile;
//journal文件的temp 緩存文件,一般都是先構(gòu)建這個(gè)緩存文件,等待構(gòu)建完成以后將這個(gè)緩存文件重新命名為journal
private final File journalFileTmp;
/**
* 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();
}
//指向journalFileTmp這個(gè)日志文件的緩存
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);
}
//所以這個(gè)地方 構(gòu)建日志文件的流程主要就是先構(gòu)建出日志文件的緩存文件,如果緩存構(gòu)建成功 那就直接重命名這個(gè)緩存文件,這樣做好處在哪里?
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
//這里也是把寫(xiě)入日志文件的writer初始化
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
再來(lái)看當(dāng)日志文件存在的時(shí)候,做了什么
3、readJournal()
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 + "]");
}
//這里開(kāi)始,就開(kāi)始讀取日志信息
int lineCount = 0;
while (true) {
try {
//構(gòu)建LruEntries entry列表
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 {
//初始化寫(xiě)入文件的writer
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
然后看下這個(gè)函數(shù)里面的幾個(gè)主要變量:
//每個(gè)entry對(duì)應(yīng)的緩存文件的格式 一般為1,也就是一個(gè)key,對(duì)應(yīng)幾個(gè)緩存,一般設(shè)為1,key-value一一對(duì)應(yīng)的關(guān)系
private final int valueCount;
private long size = 0;
//這個(gè)是專門用于寫(xiě)入日志文件的writer
private Writer journalWriter;
//這個(gè)集合應(yīng)該不陌生了,
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true);
//這個(gè)值大于一定數(shù)目時(shí) 就會(huì)觸發(fā)對(duì)journal文件的清理了
private int redundantOpCount;
下面就看下entry這個(gè)實(shí)體類的內(nèi)部結(jié)構(gòu)
private final class Entry {
private final String key;
/**
* Lengths of this entry's files.
* 這個(gè)entry中 每個(gè)文件的長(zhǎng)度,這個(gè)數(shù)組的長(zhǎng)度為valueCount 一般都是1
*/
private final long[] lengths;
/**
* True if this entry has ever been published.
* 曾經(jīng)被發(fā)布過(guò) 那他的值就是true
*/
private boolean readable;
/**
* The ongoing edit or null if this entry is not being edited.
* 這個(gè)entry對(duì)應(yīng)的editor
*/
private Editor currentEditor;
@Override
public String toString() {
return "Entry{" +
"key='" + key + '\'' +
", lengths=" + Arrays.toString(lengths) +
", readable=" + readable +
", currentEditor=" + currentEditor +
", sequenceNumber=" + sequenceNumber +
'}';
}
/**
* The sequence number of the most recently committed edit to this entry.
* 最近編輯他的序列號(hào)
*/
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));
}
//臨時(shí)文件創(chuàng)建成功了以後 就會(huì)重命名為正式文件了
public File getCleanFile(int i) {
Log.v("getCleanFile","getCleanFile path=="+new File(directory, key + "." + i).getAbsolutePath());
return new File(directory, key + "." + i);
}
//tmp開(kāi)頭的都是臨時(shí)文件
public File getDirtyFile(int i) {
Log.v("getDirtyFile","getDirtyFile path=="+new File(directory, key + "." + i + ".tmp").getAbsolutePath());
return new File(directory, key + "." + i + ".tmp");
}
}
DiskLruCache的open函數(shù)的主要流程就基本走完了;
4、get()
/**
* 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.
* 通過(guò)key獲取對(duì)應(yīng)的snapshot
*/
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++;
//在取得需要的文件以后 記得在日志文件里增加一條記錄 并檢查是否需要重新構(gòu)建日志文件
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
5、validateKey
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 + "\"");
}
}
這里是對(duì)存儲(chǔ)entry的map的key做了正則驗(yàn)證,所以key一定要用md5加密,因?yàn)橛行┨厥庾址?yàn)證不能通過(guò);
然后看這句代碼對(duì)應(yīng)的:
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
對(duì)應(yīng)的回調(diào)函數(shù)是:
/** 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;
}
};
其中再來(lái)看看trimTOSize()的狀態(tài)
6、trimTOSize()
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
就是檢測(cè)總緩存是否超過(guò)了限制數(shù)量,
再來(lái)看journalRebuildRequired函數(shù)
7、journalRebuildRequired()
/**
* 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();
}
就是校驗(yàn)redundantOpCount是否超出了范圍,如果是,就重構(gòu)日志文件;
最后看get函數(shù)的返回值 new Snapshot()
/** A snapshot of the values for an entry. */
//這個(gè)類持有該entry中每個(gè)文件的inputStream 通過(guò)這個(gè)inputStream 可以讀取他的內(nèi)容
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);
}
}
}
到這里就明白了get最終返回的其實(shí)就是entry根據(jù)key 來(lái)取的snapshot對(duì)象,這個(gè)對(duì)象直接把inputStream暴露給外面;
8、save的過(guò)程
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
//根據(jù)傳進(jìn)去的key 創(chuàng)建一個(gè)entry 并且將這個(gè)key加入到entry的那個(gè)map里 然后創(chuàng)建一個(gè)對(duì)應(yīng)的editor
//同時(shí)在日志文件里加入一條對(duì)該key的dirty記錄
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//因?yàn)檫@里涉及到寫(xiě)文件 所以要先校驗(yàn)一下寫(xiě)日志文件的writer 是否被正確的初始化
checkNotClosed();
//這個(gè)地方是校驗(yàn) 我們的key的,通常來(lái)說(shuō) 假設(shè)我們要用這個(gè)緩存來(lái)存一張圖片的話,我們的key 通常是用這個(gè)圖片的
//網(wǎng)絡(luò)地址 進(jìn)行md5加密,而對(duì)這個(gè)key的格式在這里是有要求的 所以這一步就是驗(yàn)證key是否符合規(guī)范
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;
}
然后取得輸出流
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);
}
}
注意這個(gè)index 其實(shí)一般傳0 就可以了,DiskLruCache 認(rèn)為 一個(gè)key 下面可以對(duì)應(yīng)多個(gè)文件,這些文件 用一個(gè)數(shù)組來(lái)存儲(chǔ),所以正常情況下,我們都是
一個(gè)key 對(duì)應(yīng)一個(gè)緩存文件 所以傳0
//tmp開(kāi)頭的都是臨時(shí)文件
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
然后你這邊就能看到,這個(gè)輸出流,實(shí)際上是tmp 也就是緩存文件的 .tmp 也就是緩存文件的 緩存文件 輸出流;
這個(gè)流 我們寫(xiě)完畢以后 就要commit;
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
這個(gè)就是根據(jù)緩存文件的大小 更新disklrucache的總大小 然后再日志文件里對(duì)該key加入clean的log
//最后判斷是否超過(guò)最大的maxSize 以便對(duì)緩存進(jìn)行清理
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);
}
}
commit以后 就會(huì)把tmp文件轉(zhuǎn)正 ,重命名為 真正的緩存文件了;
這個(gè)里面的流程和日志文件的rebuild 是差不多的,都是為了防止寫(xiě)文件的出問(wèn)題。所以做了這樣的冗余處理;
總結(jié):
DiskLruCache,利用一個(gè)journal文件,保證了保證了cache實(shí)體的可用性(只有CLEAN的可用),且獲取文件的長(zhǎng)度的時(shí)候可以通過(guò)在該文件的記錄中讀取。
利用FaultHidingOutputStream對(duì)FileOutPutStream很好的對(duì)寫(xiě)入文件過(guò)程中是否發(fā)生錯(cuò)誤進(jìn)行捕獲,而不是讓用戶手動(dòng)去調(diào)用出錯(cuò)后的處理方法;
到此這篇關(guān)于關(guān)于Android DiskLruCache的磁盤(pán)緩存機(jī)制原理的文章就介紹到這了,更多相關(guān)Android DiskLruCache磁盤(pán)緩存機(jī)制原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 實(shí)現(xiàn)界面刷新的幾種方法
這篇文章主要介紹了Android 實(shí)現(xiàn)界面刷新的相關(guān)資料,這里提供了幾種方法及實(shí)例代碼,具有一定的參考價(jià)值,需要的朋友可以參考下2016-11-11
淺談Android Studio 4.1 更新內(nèi)容
這篇文章主要介紹了淺談Android Studio 4.1 更新內(nèi)容,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
實(shí)例詳解android studio如何導(dǎo)入.so文件的方法
通過(guò)實(shí)例給大家詳細(xì)講解了如何在android studio如何導(dǎo)入.so文件以及中間遇到的問(wèn)題解決辦法,需要的讀者們可以仔細(xì)學(xué)習(xí)一下。2017-12-12
Android開(kāi)發(fā)實(shí)現(xiàn)的Intent跳轉(zhuǎn)工具類實(shí)例
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)的Intent跳轉(zhuǎn)工具類,簡(jiǎn)單描述了Intent組件的功能并結(jié)合實(shí)例形式給出了頁(yè)面跳轉(zhuǎn)、拍照、圖片調(diào)用等相關(guān)操作技巧,需要的朋友可以參考下2017-11-11
Android中Handler實(shí)現(xiàn)倒計(jì)時(shí)的兩種方式
本篇文章主要介紹了Android中Handler實(shí)現(xiàn)倒計(jì)時(shí)的兩種方式,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
Android使用CardView作為RecyclerView的Item并實(shí)現(xiàn)拖拽和左滑刪除
這篇文章主要介紹了Android使用CardView作為RecyclerView的Item并實(shí)現(xiàn)拖拽和左滑刪除,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
Android 自定義TextView去除paddingTop和paddingBottom
這篇文章主要介紹了Android 自定義TextView去除paddingTop和paddingBottom的相關(guān)資料,這里提供實(shí)例來(lái)幫助大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-09-09
android編程實(shí)現(xiàn)的自定義注釋模板實(shí)例
這篇文章主要介紹了android編程實(shí)現(xiàn)的自定義注釋模板,以完整實(shí)例形式分析了Android自定義魔板的定義及具體實(shí)現(xiàn)與使用技巧,需要的朋友可以參考下2015-11-11

