【Android開源專案分析】android輕量級開源快取框架——ASimpleCache(ACache)原始碼分析
ASimpleCache框架原始碼連結
官方介紹
ASimpleCache 是一個為android制定的 輕量級的 開源快取框架。輕量到只有一個java檔案(由十幾個類精簡而來)。
1、它可以快取什麼東西?
普通的字串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java物件,和 byte資料。
2、它有什麼特色?
特色主要是:
1:輕,輕到只有一個JAVA檔案。
2:可配置,可以配置快取路徑,快取大小,快取數量等。
3:可以設定快取超時時間,快取超時自動失效,並被刪除。
4:支援多程序。
3、它在android中可以用在哪些場景?
1、替換SharePreference當做配置檔案
2、可以快取網路請求資料,比如oschina的android客戶端可以快取http請求的新聞內容,快取時間假設為1個小時,超時後自動失效,讓客戶端重新請求新的資料,減少客戶端流量,同時減少伺服器併發量。
3、您來說…
4、如何使用 ASimpleCache?
以下有個小的demo,希望您能喜歡:
ACache mCache = ACache.get(this);
mCache.put("test_key1", "test value");
mCache.put("test_key2", "test value", 10);//儲存10秒,如果超過10秒去獲取這個key,將為null
mCache.put("test_key3", "test value", 2 * ACache.TIME_DAY);//儲存兩天,如果超過兩天去獲取這個key,將為null
獲取資料
ACache mCache = ACache.get(this);
String value = mCache.getAsString("test_key1");
原始碼分析
一、ACache類結構圖
ASimpleCache裡只有一個JAVA檔案——ACache.java
首先我用思維導圖製作了ACache類的詳細結構圖:
二、官方demo分析
通過分析官方給的demo來驅動原始碼分析吧
以字串儲存為例(官方給的demo裡給出了很多種資料讀取的例子,其實方法相似),開啟SaveStringActivity.java:
package com.yangfuhai.asimplecachedemo;
import org.afinal.simplecache.ACache;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
/**
*
* @ClassName: SaveStringActivity
* @Description: 快取string
* @Author Yoson Hao
* @WebSite www.haoyuexing.cn
* @Email [email protected]
* @Date 2013-8-7 下午9:59:43
*
*/
public class SaveStringActivity extends Activity {
private EditText mEt_string_input;
private TextView mTv_string_res;
private ACache mCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_save_string);
// 初始化控制元件
initView();
mCache = ACache.get(this);
}
/**
* 初始化控制元件
*/
private void initView() {
mEt_string_input = (EditText) findViewById(R.id.et_string_input);
mTv_string_res = (TextView) findViewById(R.id.tv_string_res);
}
/**
* 點選save事件
*
* @param v
*/
public void save(View v) {
if (mEt_string_input.getText().toString().trim().length() == 0) {
Toast.makeText(
this,
"Cuz u input is a nullcharacter ... So , when u press \"read\" , if do not show any result , plz don't be surprise",
Toast.LENGTH_SHORT).show();
}
// mCache.put("testString", mEt_string_input.getText().toString());
mCache.put("testString", mEt_string_input.getText().toString(),300);
}
/**
* 點選read事件
*
* @param v
*/
public void read(View v) {
String testString = mCache.getAsString("testString");
if (testString == null) {
Toast.makeText(this, "String cache is null ...", Toast.LENGTH_SHORT)
.show();
mTv_string_res.setText(null);
return;
}
mTv_string_res.setText(testString);
}
/**
* 點選clear事件
*
* @param v
*/
public void clear(View v) {
mCache.remove("testString");
}
}
可以看到快取字串的讀取方法很簡單!!!
- 在onCreate裡通過get方式獲取快取例項
mCache = ACache.get(this); - 在save按鈕的點選事件裡,通過put方法往快取例項裡儲存字串
mCache.put(“testString”, mEt_string_input.getText().toString(),300); - 在read按鈕的點選事件裡,通過getAsString方法從快取例項裡讀取字串
mCache.getAsString(“testString”);
其他資料讀取,方法相似,也是這三個步驟。300為儲存時間300秒。
三、ACache原始碼分析
1、獲取快取例項
那我們就從ACache.get()開始吧,其實檢視上面的思維導圖,ACache類的構造方法為private的,所以新建快取例項只能通過ACache.get方式獲取。
//例項化應用程式場景快取
public static ACache get(Context ctx) {
return get(ctx, "ACache");
}
//新建快取目錄
public static ACache get(Context ctx, String cacheName) {
//新建資料夾,檔案路徑為應用場景快取路徑目錄,資料夾名為ACache(new File()也可新建檔案,帶上字尾即可)
File f = new File(ctx.getCacheDir(), cacheName);
return get(f, MAX_SIZE, MAX_COUNT);
}
//新建快取例項,存入例項map,key為快取目錄+每次應用開啟的程序id
public static ACache get(File cacheDir, long max_zise, int max_count) {
//返回key為快取目錄+每次應用開啟的程序id的map的value值,賦給快取例項manager
ACache manager = mInstanceMap.get(cacheDir.getAbsoluteFile() + myPid());
if (manager == null) { //快取例項為空時,
manager = new ACache(cacheDir, max_zise, max_count);
mInstanceMap.put(cacheDir.getAbsolutePath() + myPid(), manager);//插入map
}
return manager;
}
在呼叫ACache.get(Context)方法過程中,其實執行了三個get方法
(1)get(Context ctx)->(2)get(Context ctx, String cacheName)->(3)get(File cacheDir, long max_zise, int max_count)
在(2)中新建了快取目錄,路徑為:
/data/data/app-package-name/cache/ACache
快取大小MAX_SIZE和數量MAX_COUNT均由final變數控制。
其實最終呼叫(3)獲取例項:
mInstanceMap的key為快取目錄+每次應用開啟的程序id,value為ACache.
初次執行,mInstanceMap沒有任何鍵值對,所以manager == null。故通過ACache構造方法構造新例項,最後將該例項引用存入mInstanceMap。
接下來我們來看看ACache構造方法:
//ACache建構函式 為private私有(所以在其他類裡獲得例項只能通過get()方法)
private ACache(File cacheDir, long max_size, int max_count) {
if (!cacheDir.exists() && !cacheDir.mkdirs()) { //快取目錄不存在並且無法建立時,丟擲異常
throw new RuntimeException("can't make dirs in " + cacheDir.getAbsolutePath());
}
mCache = new ACacheManager(cacheDir, max_size, max_count);//例項化ACacheManager內部類例項
}
快取目錄不存在並且無法建立時,丟擲異常,否則例項化ACacheManager內部類例項(快取管理器)。ACacheManager內部類的建構函式如下:
//內部類ACacheManager的建構函式
private ACacheManager(File cacheDir, long sizeLimit, int countLimit) {
this.cacheDir = cacheDir;
this.sizeLimit = sizeLimit;
this.countLimit = countLimit;
cacheSize = new AtomicLong(); //原子類例項cacheSize,不用加鎖保證執行緒安全
cacheCount = new AtomicInteger(); //原子類例項cacheCount,不用加鎖保證執行緒安全
calculateCacheSizeAndCacheCount();
}
建構函式得到原子類例項cacheSize和cacheCount,通過calculateCacheSizeAndCacheCount();方法計算cacheSize和cacheCount如下:
/**
* 計算 cacheSize和cacheCount
*/
private void calculateCacheSizeAndCacheCount() {
new Thread(new Runnable() {
@Override
public void run() {
//int size = 0;
long size = 0; //這裡long型別才對——by牧之丶
int count = 0;
File[] cachedFiles = cacheDir.listFiles(); //返回快取目錄cacheDir下的檔案陣列
if (cachedFiles != null) {
for (File cachedFile : cachedFiles) { //對檔案陣列遍歷
size += calculateSize(cachedFile);
count += 1;
lastUsageDates.put(cachedFile, cachedFile.lastModified()); //將快取檔案和最後修改時間插入map
}
cacheSize.set(size); //設定為給定值
cacheCount.set(count); //設定為給定值
}
}
}).start();
}
calculateCacheSizeAndCacheCount方法中開啟執行緒進行大小和數量的計算。計算完後存入cacheSize和cacheCount,cacheSize和cacheCount在內部類中定義為AtomicLong和AtomicInteger量子類,也就是執行緒安全的。其基本的特性就是在多執行緒環境下,當有多個執行緒同時執行這些類的例項包含的方法時,具有排他性,即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待佇列中選擇一個另一個執行緒進入。
到這裡獲取快取例項工作完成,主要完成了如下工作:
- 新建了快取目錄
- 通過ACache構造方法構造新例項,並且將該例項引用插入mInstanceMap
- 例項化ACacheManager,計算cacheSize和cacheCount
接下來就是資料存取操作。
2、往快取例項存入資料
從上面的思維導圖public method(各種資料的讀寫方法)中,有各種public的put和get等方法來快取各種資料型別的資料。由上面的demo的put方法
mCache.put(“testString”, mEt_string_input.getText().toString(),300);我們找到原形:
/**
* 儲存 String資料 到 快取中
*
* @param key
* 儲存的key
* @param value
* 儲存的String資料
* @param saveTime
* 儲存的時間,單位:秒
*/
public void put(String key, String value, int saveTime) {
put(key, Utils.newStringWithDateInfo(saveTime, value));
}
這裡的put方法可以指定快取時間。呼叫他自身的另一個put方法:
/**
* 儲存 String資料 到 快取中
*
* @param key
* 儲存的key
* @param value
* 儲存的String資料
*/
public void put(String key, String value) {
File file = mCache.newFile(key); //新建檔案
BufferedWriter out = null; //緩衝字元輸出流,作用是為其他字元輸出流新增一些緩衝功能
try {
out = new BufferedWriter(new FileWriter(file), 1024); //獲取file字元流
out.write(value); // 把value寫入
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
mCache.put(file); //更新cacheCount和cacheSize lastUsageDates插入新建檔案和時間的鍵值對
}
}
在put(String key, String value)方法中首先呼叫mCache.newFile(key)新建一個檔案:
//新建檔案
private File newFile(String key) {
return new File(cacheDir, key.hashCode() + ""); //新建檔案,檔名為key的整型雜湊碼
}
新建的檔名為key的整型雜湊碼。回到put(String key, String value)中,然後通過out.write(value);將資料存入檔案。最後呼叫mCache.put(file);進行ACacheManager例項的更新操作:
//更新cacheCount和cacheSize lastUsageDates插入新建檔案和時間的鍵值對
//檔案放入程式快取後,統計快取總量,總數,檔案存放到檔案map中(value值為檔案最後修改時間,便於根據設定的銷燬時間進行銷燬)
//快取沒有超過限制,則增加快取總量,總數的數值
//快取超過限制,則減少快取總量,總數的數值
//通過removeNext方法找到最老檔案的大小
private void put(File file) {
int curCacheCount = cacheCount.get(); //獲取數量
while (curCacheCount + 1 > countLimit) { //大於上限
long freedSize = removeNext(); //移除舊的檔案,返回檔案大小
cacheSize.addAndGet(-freedSize); //更新cacheSize
curCacheCount = cacheCount.addAndGet(-1);//更新cacheCount
}
cacheCount.addAndGet(1);//更新cacheCount
long valueSize = calculateSize(file); //計算檔案大小
long curCacheSize = cacheSize.get(); //獲取當前快取大小
while (curCacheSize + valueSize > sizeLimit) { //大於上限
long freedSize = removeNext(); //移除舊的檔案,返回檔案大小
curCacheSize = cacheSize.addAndGet(-freedSize); //更新curCacheSize
}
cacheSize.addAndGet(valueSize); //更新cacheSize
Long currentTime = System.currentTimeMillis();
file.setLastModified(currentTime); //設定檔案最後修改時間
lastUsageDates.put(file, currentTime); //插入map
}
分析完ACacheManager的put()後,我們回到put(key, Utils.newStringWithDateInfo(saveTime, value))
其中第二個引數value傳入的是Utils.newStringWithDateInfo(saveTime, value),而newStringWithDateInfo是ACache的內部工具類的一個方法,在value內容前面加上了時間資訊:
//返回時間資訊+value
private static String newStringWithDateInfo(int second, String strInfo) {
return createDateInfo(second) + strInfo;
}
返回拼接createDateInfo(second)和value的字串。createDateInfo()如下:
//時間資訊
private static String createDateInfo(int second) {
String currentTime = System.currentTimeMillis() + "";
while (currentTime.length() < 13) { //小於13,前面補0
currentTime = "0" + currentTime;
}
return currentTime + "-" + second + mSeparator; //當前時間-儲存時間
}
返回字串格式為 當前時間-儲存時間“ ”
3、從快取例項讀取資料
由上面的demo的get方法mCache.getAsString(“testString”);我們找到原形:
/**
* 讀取 String資料
*
* @param key
* @return String 資料
*/
public String getAsString(String key) {
File file = mCache.get(key); //獲取檔案
if (!file.exists())
return null;
boolean removeFile = false;
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
String readString = "";
String currentLine;
while ((currentLine = in.readLine()) != null) { //逐行遍歷
readString += currentLine; //每行字串連線
}
if (!Utils.isDue(readString)) { //String資料未到期
return Utils.clearDateInfo(readString);//去除時間資訊的字串內容
} else {
removeFile = true; //移除檔案標誌位為真
return null;
}
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (removeFile)
remove(key); //移除快取
}
}
getAsString(String key)方法裡首先通過快取管理器的mCache.get(key)方法獲取檔案,然後用Utils.isDue(readString)**判斷是否字串資料到期,未到期返回去除時間資訊的字串內容;到期則移除快取,返回空。**Utils.isDue(readString)呼叫了isDue(byte[] data)判斷:
/**
* 判斷快取的byte資料是否到期
*
* @param data
* @return true:到期了 false:還沒有到期
*/
private static boolean isDue(byte[] data) {
String[] strs = getDateInfoFromDate(data);
if (strs != null && strs.length == 2) {
String saveTimeStr = strs[0];
while (saveTimeStr.startsWith("0")) {
saveTimeStr = saveTimeStr.substring(1, saveTimeStr.length());
}
long saveTime = Long.valueOf(saveTimeStr);
long deleteAfter = Long.valueOf(strs[1]);
if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) {
return true;
}
}
return false;
}
至此整個快取字串讀取過程在ACache的原始碼分析完成,其他快取資料型別讀取方法分析過程一樣。
JsonObject、JsonArray、Bitmap、Drawable、序列化的存入快取都是轉化為字串/byte格式,再呼叫函式put(String key, String value)即可。
比如BitMap的儲存:
/**
* 儲存 bitmap 到 快取中,有儲存時間
*
* @param key
* 儲存的key
* @param value
* 儲存的 bitmap 資料
* @param saveTime
* 儲存的時間,單位:秒
*/
public void put(String key, Bitmap value, int saveTime) {
put(key, Utils.Bitmap2Bytes(value), saveTime);
}
Bitmap的讀取:
/**
* 讀取 bitmap 資料
*
* @param key
* @return bitmap 資料
*/
public Bitmap getAsBitmap(String key) {
if (getAsBinary(key) == null) {
return null;
}
return Utils.Bytes2Bimap(getAsBinary(key));
}
思想就是把bitmap轉化為byte[], 再呼叫快取byte的函式 。通過Utils.Bitmap2Bytes(value)完成Bitmap → byte[] 的轉換。通過Utils.Bytes2Bimap(getAsBinary(key))完成byte[] → Bitmap的轉換。
/*
* Bitmap → byte[]
*/
private static byte[] Bitmap2Bytes(Bitmap bm) {
if (bm == null) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();//byte[]輸出流
bm.compress(Bitmap.CompressFormat.PNG, 100, baos);//按指定的圖片格式以及畫質,將圖片轉換為輸出流。壓縮圖片,不壓縮是100,表示壓縮率為0
return baos.toByteArray();
}
/*
* byte[] → Bitmap
*/
private static Bitmap Bytes2Bimap(byte[] b) {
if (b.length == 0) {
return null;
}
return BitmapFactory.decodeByteArray(b, 0, b.length);//解析
}
很簡單吧。Drawable的快取就是先轉化為Bitmap,之後就是上面的步驟轉換成byte。
總結
該開源庫類簡單,容易理解。
- 可以使用ACache把那些不需要實時更新的資料快取起來,一來減少網路請求,二來本地載入速度也快。
- 可以設定快取時間。
- 可以替換SharePreference當做配置檔案,儲存多種資料型別,比如可以儲存頭像資訊。