【安卓中的緩存策略系列】安卓緩存策略之磁盤緩存DiskLruCache

安卓中的緩存包括兩種情況即內存緩存與磁盤緩存,其中內存緩存主要是使用LruCache這個類,其中內存緩存我在【安卓中的緩存策略系列】安卓緩存策略之內存緩存LruCache中已經進行過詳細講解,如看官還沒看過此博客,建議看官先去看一下。

我們知道LruCache可以讓我們快速的從內存中獲取用戶最近使用過的Bitmap,但是我們無法保證最近訪問過的Bitmap都能夠保存在緩存中,像類似GridView等需要大量數據填充的控件很容易就會用完整個內存緩存。另外,我們的應用可能會被類似打電話等行爲而暫停導致退到後臺,因爲後臺應用可能會被殺死,那麼內存緩存就會被銷燬,緩存的Bitmap也就不存在了。一旦用戶恢復應用的狀態,那麼應用就需要重新處理那些圖片,另外某些情況下即使用戶退出整個APP後重新打開該APP其緩存的圖片應該還能被顯示出來,顯然此種情況下使用內存緩存是做不到的。

而磁盤緩存可以用來保存那些已經處理過的Bitmap,它還可以減少那些不在內存緩存中的Bitmap的加載次數。磁盤緩存主要涉及到DiskLruCache這個類。下面從源碼的角度詳細講解DiskLruCache這個類,然後在此基礎上講解如何使用DiskLruCache,讓讀者知其然更知其所以然。


一DiskLruCache類:

首先我們來看一下其構造函數

 private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
        this.directory = directory;
        this.appVersion = appVersion;
        this.journalFile = new File(directory, JOURNAL_FILE);
        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
        this.valueCount = valueCount;
        this.maxSize = maxSize;
    }
可以看到其構造函數被private修飾,也就意味着對外是不可見的,即我們不能通過其構造函數來創建一個DiskLruCache對象,如果要創建一個DiskLruCache實例需要使用open函數,其代碼如下:

 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        // prefer to pick up where we left off
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");
                cache.delete();
            }
        }

        // create a new empty cache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }
可以看到open函數對應的參數與DiskLruCache的參數完全一致,其中第一個參數directory表示磁盤緩存在文件系統中的存儲路徑,一般選擇SD卡上的緩存路徑,默認位置爲/sdcard/Android/data/<application package_name>/cache目錄下,其中<application package_name>表示應用的包名,當應用被卸載後該目錄會被刪除。第二個參數顧名思義爲app版本號,通常將其置爲1,當版本號被改變時會清空之前所有的緩存文件,第三個參數用來指定單個緩存節點可以對應的緩存文件個數,通常爲1,第四個參數maxSize顧名思義表示該磁盤緩存的最大容量。

其中第一個參數maxSize也可以指定選擇data下的當前應用的目錄(此時的緩存路徑爲/data/data/<application package>/cache),所以通常我們先判斷是否存在SD卡,如果存在則使用SD卡緩存,否則選擇data下的當前應用的目錄緩存。具體代碼如下:

public File getDiskCacheDir(Context context, String uniqueName) {
	String cachePath;
	if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
			|| !Environment.isExternalStorageRemovable()) {
		cachePath = context.getExternalCacheDir().getPath();
	} else {
		cachePath = context.getCacheDir().getPath();
	}
	return new File(cachePath + File.separator + uniqueName);
}

在open函數中可以看到首先會調用DiskLruCache的構造函數,在該構造函數中創建了journalFile,journalFileTmp這兩個文件,然後判斷journalFile是否存在,如果存在則
調用cache.readJournal();讀取journal日誌文件,然後調用 cache.processJournal();處理日誌文件,該函數的作用就是計算初始化的大小和收集緩存文件中的垃圾文件( Computes the initial size and collects garbage as a part of opening the cache),刪除Dirty記錄(Dirty entries are assumed to be inconsistent and will be deleted),即垃圾文件.這個概念與數據庫中的讀取髒數據是差不多的,講到這裏就不得不爲讀者講解一下DiskLruCache的日誌文件的格式。格式如下(注:此圖來源於網絡,向貢獻該圖的人表示感謝)


其中的前五行基本上是固定的,表示DiskLruCache日誌文件的頭部數據,第一行是個固定的字符串“libcore.io.DiskLruCache”,意味着我們使用的是DiskLruCache,

第二行是DiskLruCache的版本號,這個值是恆爲1的。第三行是應用程序的版本號,這個值與我們在open()方法裏傳入的版本號是相同的。第四行是valueCount,這個值也是在open()方法中傳入的,通常情況下都爲1。第五行是一個空行。空行過後纔是日誌文件的內容:

接下來是一個以DIRTY開頭的行,其後的一串數字表示的是存入的數據的key,如果讀者瞭解數據庫的話,知道一般DIRTY表示的是髒數據,這是因爲當我們每次向磁盤緩存中寫入一條數據時都會向journal文件中寫入一條DIRTY記錄,表示我們正準備寫入一條緩存數據,但不知結果如何。當調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,意味着這條“髒”數據被“洗乾淨”,它不再是髒數據,當調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,後面都應該有一行對應的CLEAN或者REMOVE的記錄,否則這條數據就是“髒”的,會被自動刪除掉。另外以READ開頭的行表示我們從緩存中讀取了一條數據,這時會向日志文件中添加一個READ記錄。

這樣我們就可以理解上面講述的 cache.processJournal()函數處理日誌文件的過程,即該函數會清除只出現DIRTY但未出現CLEAN或REMOVE的記錄,即出現CLEAN且沒被REMOVE的記錄纔會保存下來,然後通過cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE);將CLEAN的記錄保存到日誌文件中,最後返回該cache。


第二種情況是如果cache.journalFile不存在,相當於初次創建cahce文件,則會創建一個空的cache,代碼如下:

        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
在創建空的Cahce時會調用 cache.rebuildJournal();的方法,該方法的作用是刪除日誌文件中的多餘信息,如果日誌文件已存在,則會替換當前的日誌文件。在該過程中會向日志文件中寫入頭部數據,這也是爲何我們在上面看到的DiskLruCache的日誌文件的格式中會包含前面5行數據的原因,代碼如下:

 private synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
            journalWriter.close();
        }

        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
        writer.write(MAGIC);
        writer.write("\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');
            }
        }

        writer.close();
        journalFileTmp.renameTo(journalFile);
        journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
    }

接下來我們看一下DiskLruCache中的重要方法:

首先來看一下關於添加緩存的edit方法。

 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;
    }
可以看到edit方法是同步的。edit方法中首先會調用validateKey(key);來檢測傳入的key是否合法,不能包含空格,換行,當我們在緩存一張圖片時通常我們拿到的是圖片的Url,而Url中可能會包含上述不合法字符,所以通常我們會將圖片的Url轉換爲key,然後將其作爲參數傳給edit(),然後調用LinkedHashMap的get方法通過key獲取緩存entry,如果該entry爲空(表示我們第一次存入該緩存),則會通過key創建Entry將其賦給entry然後將其put到lruEntries,即entry = new Entry(key);lruEntries.put(key, entry);

如果獲取的entry不爲空,則代表不是初次存入該key的緩存,則判斷entry.currentEditor是否爲空,如果不爲空則表示當前緩存entry正在被edit,此時將直接返回null,即DiskLruCache不允許同時edit一個緩存對象。注意entry.currentEditor不爲空的前提是entry不爲空。

如果如果獲取的entry不爲空同時entry.currentEditor爲空,則會根據entyr構造Editor對象editor,然後將該editor的值賦給entry.currentEditor,然後調用journalWriter.write(DIRTY + ' ' + key + '\n');向日志文件中寫入一個DIRTY行,表示該記錄正在被操作。最後返回該editor。通過該editor的 public OutputStream newOutputStream(int index)方法可以得到緩存文件輸出流。通過該文件輸出流就可以將緩存寫入到磁盤上保存起來,最後必須調用editor的commit()來提交寫入操作,這樣才真真正正的把記錄寫入到磁盤緩存上了。


再來看一下獲取緩存的get方法:

 public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }


        if (!entry.readable) {
            return null;
        }


        /*
         * Open all streams eagerly to guarantee that we see a single published
         * snapshot. If we opened streams lazily then the streams could come
         * from different edits.
         */
        InputStream[] ins = new InputStream[valueCount];
        try {
            for (int i = 0; i < valueCount; i++) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));
            }
        } catch (FileNotFoundException e) {
            // a file must have been deleted manually!
            return null;
        }


        redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }


        return new Snapshot(key, entry.sequenceNumber, ins);
    }

同樣可以看到get方法也是同步的,它的作用就是根據key返回一個Snapshot對象,可以看到在該方法中同樣先調用 validateKey(key);進行合法性檢測,如果合法則通過key獲取緩存entry,如果entry爲空或當前不可讀則返回null,否則根據valueCountd的值創建valueCount個文件輸入流,這些文件輸入流的源即爲entry中CLEAN記錄的緩存,即 ins[i] = new FileInputStream(entry.getCleanFile(i));然後調用journalWriter.append(READ + ' ' + key + '\n');向緩存日誌文件中寫入一個READ記錄行,最後通過key和文件輸入流數組來構造一個Snapshot對象,將其返回。當該值返回後會將其移動到緩存隊列的頭部(If a value is returned, it is moved to the head of the LRU queue)


得到Snapshot對象後,通過該對象的public InputStream getInputStream(int index)方法可以獲取到緩存的文件輸入流,通過該文件輸入流即可將緩存的記錄轉換爲Bitmap對象。



二DiskLruCache的使用

同樣DiskLruCache的使用也主要包括上個模塊,即創建磁盤緩存,向磁盤緩存中添加記錄,從緩存中獲取記錄。下面先簡單介紹這三個模塊的使用,然後結合LruCache和DiskLruCache給出安卓緩存策略的完整代碼。

創建緩存:創建緩存主要使用的是open函數:public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

public File getDiskCacheDir(Context context, String uniqueName) {
	String cachePath;
	if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
			|| !Environment.isExternalStorageRemovable()) {
		cachePath = context.getExternalCacheDir().getPath();
	} else {
		cachePath = context.getCacheDir().getPath();
	}
	return new File(cachePath + File.separator + uniqueName);
}

	public int getAppVersion(Context context) {
		try {
			PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
			return info.versionCode;
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}
		return 1;
	}


     DiskLruCache mDiskLruCache = null;
     try {
	File cacheDir = getDiskCacheDir(context, "bitmap");
	if (!cacheDir.exists()) {
		cacheDir.mkdirs();
	}
	mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
       } catch (IOException e) {
	e.printStackTrace();
      }
其中getDiskCacheDir函數用來獲取緩存的路徑,將其返回值作爲參數傳給open函數的第一個參數,getAppVersion用來獲取App的版本號,將其返回值作爲參數傳給open函數的第二個參數,第三個參數一般置爲1,第四個參數一般指定爲10M即可。

寫入緩存:寫入緩存主要是通過DiskLruCache.Editor類來完成的,該類是通過DiskLruCache的edit()方法來獲取的。通常寫入磁盤緩存是從網絡上獲取然後寫入緩存的,因此我們得定義一個線程從網絡上獲取圖片。

public String hashKeyFromUrl(String key) {
	String cacheKey;
	try {
		final MessageDigest mDigest = MessageDigest.getInstance("MD5");
		mDigest.update(key.getBytes());
		cacheKey = bytesToHexString(mDigest.digest());
	} catch (NoSuchAlgorithmException e) {
		cacheKey = String.valueOf(key.hashCode());
	}
	return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
	StringBuilder sb = new StringBuilder();
	for (int i = 0; i < bytes.length; i++) {
		String hex = Integer.toHexString(0xFF & bytes[i]);
		if (hex.length() == 1) {
			sb.append('0');
		}
		sb.append(hex);
	}
	return sb.toString();
}


new Thread(){
	@Override
	public void run() {
		try {
			String imageUrl = "http://www.baidu.com/logo.jpg";
			String key = hashKeyFromUrl(imageUrl);
			DiskLruCache.Editor editor = mDiskLruCache.edit(key);
			if (editor != null) {
				OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
				if (downloadUrlToStream(imageUrl, outputStream)) {
					editor.commit();
				} else {
					editor.abort();
				}
			}
			mDiskLruCache.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}.start();

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
	HttpURLConnection urlConnection = null;
	BufferedOutputStream out = null;
	BufferedInputStream in = null;
	try {
		final URL url = new URL(urlString);
		urlConnection = (HttpURLConnection) url.openConnection();
		in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
		out = new BufferedOutputStream(outputStream, 8 * 1024);
		int b;
		while ((b = in.read()) != -1) {
			out.write(b);
		}
		return true;
	} catch (final IOException e) {
		e.printStackTrace();
	} finally {
		if (urlConnection != null) {
			urlConnection.disconnect();
		}
		try {
			if (out != null) {
				out.close();
			}
			if (in != null) {
				in.close();
			}
		} catch (final IOException e) {
			e.printStackTrace();
		}
	}
	return false;
}
其中hashKeyFromUrl這個函數用來將網絡上圖片的Url裝換爲key,因爲網絡上的Url可能包含不合法字符,這個在前面的源碼分析中已經講解過。

然後通過mDiskLruCache.edit(key);通過key構造一個Editor對象,然後editor.newOutputStream(DISK_CACHE_INDEX)獲取文件輸出流(DISK_CACHE_INDEX通常指定爲0),然後將該輸出流和網絡上圖片的Url作爲參數傳遞給downloadUrlToStream(String urlString, OutputStream outputStream) 函數,該函數的作用是通過制定的圖片的Url和OutputStream 將網絡上的圖片通過outputStream寫入到本地文件中,這裏傳入的是DiskLruCache的輸出流,所以就將其寫入到了磁盤緩存中。注意該操作要在一個子線程中進行,下載完成之後還用調用editor的commit方法才能將其真真正正寫入緩存。如果下載過程出現錯誤,則會通過Editor的abort()函數來回退整個操作。

獲取緩存:獲取緩存主要是通過public synchronized Snapshot get(String key) 函數來完成的。代碼如下:

try {
	String imageUrl = "http://www.baidu.com/logo.jpg";
	String key = hashKeyFromUrl(imageUrl);
	DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
       
	if (snapShot != null) {
		FileInputStream fis =(FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
		Bitmap bitmap = BitmapFactory.decodeStream(fis);//注意此種方式未對網絡上獲取的圖片進行壓縮處理
		mImage.setImageBitmap(bitmap);
	}
} catch (IOException e) {
	e.printStackTrace();
}
即調用mDiskLruCache.get(key);獲取DiskLruCache.Snapshot對象,通過該對象的snapShot.getInputStream(DISK_CACHE_INDEX);獲取輸入流,獲取到該輸入流後就基本上和本地文件操作是類似的,很容易將其轉換爲一個Bitmap對象,注意上述代碼中未對獲取的圖片進行壓縮處理,直接顯示在ImageView控件上這是不妥的,關於圖片壓縮的內容請參看我的博客:安卓圖片壓縮技術


好了,以上就是本人理解的關於DiskLruCache相關的知識點,看官如果覺得不錯,請記得點擊下方的“頂”或“贊”按鈕給我一點小小的鼓勵哦微笑,看官也可以看看我的其它博客的文章哦!微笑






發佈了98 篇原創文章 · 獲贊 330 · 訪問量 118萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章