原文首發於微信公衆號:jzman-blog,歡迎關注交流!java
Android 中緩存的使用比較廣泛,使用相應的緩存策略能夠減小流量的消耗,也能夠在必定程度上提升應用的性能,如加載網絡圖片的狀況,不該該每次都從網絡上加載圖片,應該將其緩存到內存和磁盤中,下次直接從內存或磁盤中獲取,緩存策略通常使用 LRU(Least Recently Used) 算法,即最近最少使用算法,下面將從內存緩存和磁盤緩存兩個方面以圖片爲例 介紹 Android 中如何使用緩存,閱讀本文以前,請先閱讀上篇文章:android
LruCache 是 Android 3.1 提供的一個緩存類,經過該類能夠快速訪問緩存的 Bitmap 對象,內部採用一個 LinkedHashMap 以強引用的方式存儲須要緩存的 Bitmap 對象,當緩存超過指定的大小以前釋放最近不多使用的對象所佔用的內存。git
注意:Android 3.1 以前,一個經常使用的內存緩存是一個 SoftReference 或 WeakReference 的位圖緩存,如今已經不推薦使用了。Android 3.1 以後,垃圾回收器更加註重回收 SoftWeakference/WeakReference,這使得使用該種方式實現緩存很大程度上無效,使用 support-v4 兼容包中的 LruCache 能夠兼容 Android 3.1 以前的版本。github
首先計算須要的緩存大小,具體以下:算法
//第一種方式:
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
//獲取當前硬件條件下應用所佔的大體內存大小,單位爲M
int memorySize = manager.getMemoryClass();//M
int cacheSize = memorySize/ 8;
//第二種方式(比較經常使用)
int memorySize = (int) Runtime.getRuntime().maxMemory();//bytes
int cacheSize = memorySize / 8;
複製代碼
而後,初始化 LruCache ,具體以下:緩存
//初始化 LruCache 且設置了緩存大小
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
//計算每個緩存Bitmap的所佔內存的大小,內存單位應該和 cacheSize 的單位保持一致
return value.getByteCount();
}
};
複製代碼
//參數put(String key,Bitmap bitmap)
lruCache.put(key,bitmap)
複製代碼
//參數get(String key)
Bitmap bitmap = lruCache.get(key);
imageView.setImageBitmap(bitmap);
複製代碼
下面使用 LruCache 加載一張網絡圖片來演示 LruCache 的簡單使用。bash
建立一個簡單的 ImageLoader,裏面封裝獲取緩存 Bitmap 、添加 Bitmap 到緩存中以及從緩存中移出 Bitmap 的方法,具體以下:微信
//ImageLoader
public class ImageLoader {
private LruCache<String , Bitmap> lruCache;
public ImageLoader() {
int memorySize = (int) Runtime.getRuntime().maxMemory() / 1024;
int cacheSize = memorySize / 8;
lruCache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
//計算每個緩存Bitmap的所佔內存的大小
return value.getByteCount()/1024;
}
};
}
/** * 添加Bitmapd到LruCache中 * @param key * @param bitmap */
public void addBitmapToLruCache(String key, Bitmap bitmap){
if (getBitmapFromLruCache(key)==null){
lruCache.put(key,bitmap);
}
}
/** * 獲取緩存的Bitmap * @param key */
public Bitmap getBitmapFromLruCache(String key){
if (key!=null){
return lruCache.get(key);
}
return null;
}
/** * 移出緩存 * @param key */
public void removeBitmapFromLruCache(String key){
if (key!=null){
lruCache.remove(key);
}
}
}
複製代碼
而後建立一個線程類用於加載圖片,具體以下:網絡
//加載圖片的線程
public class LoadImageThread extends Thread {
private Activity mActivity;
private String mImageUrl;
private ImageLoader mImageLoader;
private ImageView mImageView;
public LoadImageThread(Activity activity,ImageLoader imageLoader, ImageView imageView,String imageUrl) {
this.mActivity = activity;
this.mImageLoader = imageLoader;
this.mImageView = imageView;
this.mImageUrl = imageUrl;
}
@Override
public void run() {
HttpURLConnection connection = null;
InputStream is = null;
try {
URL url = new URL(mImageUrl);
connection = (HttpURLConnection) url.openConnection();
is = connection.getInputStream();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK){
final Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageLoader.addBitmapToLruCache("bitmap",bitmap);
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mImageView.setImageBitmap(bitmap);
}
});
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connection!=null){
connection.disconnect();
}
if (is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
複製代碼
而後,在 MainActivity 中使用 ImageLoader 加載並緩存網絡圖片到內存中, 先從內存中獲取,若是緩存中沒有須要的 Bitmap ,則從網絡上獲取圖片並添加到緩存中,使用過程當中一旦退出應用,系統將會釋放內存,關鍵方法以下:app
//獲取圖片
private void loadImage(){
Bitmap bitmap = imageLoader.getBitmapFromLruCache("bitmap");
if (bitmap==null){
Log.i(TAG,"從網絡獲取圖片");
new LoadImageThread(this,imageLoader,imageView,url).start();
}else{
Log.i(TAG,"從緩存中獲取圖片");
imageView.setImageBitmap(bitmap);
}
}
// 移出緩存
private void removeBitmapFromL(String key){
imageLoader.removeBitmapFromLruCache(key);
}
複製代碼
而後在相應的事件裏調用上述獲取圖片、移出緩存的方法,具體以下:
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btnLoadLruCache:
loadImage();
break;
case R.id.btnRemoveBitmapL:
removeBitmapFromL("bitmap");
break;
}
}
複製代碼
下面來一張日誌截圖說明執行狀況:
磁盤緩存就是指將緩存對象寫入文件系統,使用磁盤緩存可有助於在內存緩存不可用時縮短加載時間,從磁盤緩存中獲取圖片相較從緩存中獲取較慢,若是能夠應該在後臺線程中處理;磁盤緩存使用到一個 DiskLruCache 類來實現磁盤緩存,DiskLruCache 收到了 Google 官方的推薦使用,DiskLruCache 不屬於 Android SDK 中的一部分,首先貼一個 DiskLruCache 的源碼連接 DiskLruCache 源碼地址 。
DiskLruCache 的構造方法是私有的,故不能用來建立 DiskLruCache,它提供一個 open 方法用於建立自身,方法以下:
/** * 返回相應目錄中的緩存,若是不存在則建立 * @param directory 緩存目錄 * @param appVersion 表示應用的版本號,通常設爲1 * @param valueCount 每一個Key所對應的Value的數量,通常設爲1 * @param maxSize 緩存大小 * @throws IOException if reading or writing the cache directory fails */
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
...
// 建立DiskLruCache
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
...
return cache;
}
//若是緩存目錄不存在,建立緩存目錄以及DiskLruCache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
...
return cache;
}
複製代碼
注意:緩存目錄能夠選擇 SD 卡上的緩存目錄,及 /sdcard/Android/data/應用包名/cache 目錄,也能夠選擇當前應用程序 data 下的緩存目錄,固然能夠指定其餘目錄,若是應用卸載後但願刪除緩存文件,就選擇 SD 卡上的緩存目錄,若是但願保留數據請選擇其餘目錄,還有一點,若是是內存緩存,退出應用以後緩存將會被清除。
DiskLruCache 緩存的添加是經過 Editor 完成的,Editor 表示一個緩存對象的編輯對象,能夠經過其 edit(String key) 方法來獲取對應的 Editor 對象,若是 Editor 正在使用 edit(String key) 方法將會返回 null,即 DiskLruCache 不容許同時操做同一個緩存對象。固然緩存的添加都是經過惟一的 key 來進行添加操做的,那麼什麼做爲 key 比較方便嗎,以圖片爲例,通常講 url 的 MD5 值做爲 key ,計算方式以下:
//計算url的MD5值做爲key
private String hashKeyForDisk(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.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();
}
複製代碼
經過 url 的 MD5 的值獲取到 key 以後,就能夠經過 DiskLruCache 對象的 edit(String key) 方法獲取 Editor 對象,而後經過 Editor 對象的 commit 方法,大概意思就是釋放 Editir 對象,以後就能夠經過 key 進行其餘操做咯。
固然,獲取到 key 以後就能夠向 DiskLruCache 中添加要緩存的東西咯,要加載一個網絡圖片到緩存中,顯然就是的經過下載的方式將要緩存的東西寫入文件系統中,那麼就須要一個輸出流往裏面寫東西,主要有兩種處理方式:
這裏以第一種方式爲例,將根據 url 將網絡圖片添加到磁盤緩存中,同時也添加到內存緩存中,具體以下:
//添加網絡圖片到內存緩存和磁盤緩存
public void putCache(final String url, final CallBack callBack){
Log.i(TAG,"putCache...");
new AsyncTask<String,Void,Bitmap>(){
@Override
protected Bitmap doInBackground(String... params) {
String key = hashKeyForDisk(params[0]);
DiskLruCache.Editor editor = null;
Bitmap bitmap = null;
try {
URL url = new URL(params[0]);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(1000 * 30);
conn.setConnectTimeout(1000 * 30);
ByteArrayOutputStream baos = null;
if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){
BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int len = -1;
while((len=bis.read(bytes))!=-1){
baos.write(bytes,0,len);
}
bis.close();
baos.close();
conn.disconnect();
}
if (baos!=null){
bitmap = decodeSampledBitmapFromStream(baos.toByteArray(),300,200);
addBitmapToCache(params[0],bitmap);//添加到內存緩存
editor = diskLruCache.edit(key);
//關鍵
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));
editor.commit();//提交
}
} catch (IOException e) {
try {
editor.abort();//放棄寫入
} catch (IOException e1) {
e1.printStackTrace();
}
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
callBack.response(bitmap);
}
}.execute(url);
}
複製代碼
在 DiskLruCache 緩存的添加中瞭解瞭如何獲取 key,獲取到 key 以後,經過 DiskLruCache 對象的 get 方法得到 Snapshot 對象,而後根據 Snapshot 對象得到 InputStream,最後經過 InputStream 就能夠得到 Bitmap ,固然能夠利用 上篇文章 中的對 Bitmap 採樣的方式進行適當的調整,也能夠在緩存以前先壓縮再緩存,獲取 InputStream 的方法具體以下:
//獲取磁盤緩存
public InputStream getDiskCache(String url) {
Log.i(TAG,"getDiskCache...");
String key = hashKeyForDisk(url);
try {
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot!=null){
return snapshot.getInputStream(0);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
複製代碼
DiskLruCache 的主要部分大體如上,下面實現一個簡單的三級緩存來講明 LruCache 和 DiskLruCache 的具體使用,MainActivity 代碼以下:
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "cache_test";
public static String CACHE_DIR = "diskCache"; //緩存目錄
public static int CACHE_SIZE = 1024 * 1024 * 10; //緩存大小
private ImageView imageView;
private LruCache<String, String> lruCache;
private LruCacheUtils cacheUtils;
private String url = "http://img06.tooopen.com/images/20161012/tooopen_sy_181713275376.jpg";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = (ImageView) findViewById(R.id.imageView);
}
@Override
protected void onResume() {
super.onResume();
cacheUtils = LruCacheUtils.getInstance();
//建立內存緩存和磁盤緩存
cacheUtils.createCache(this,CACHE_DIR,CACHE_SIZE);
}
@Override
protected void onPause() {
super.onPause();
cacheUtils.flush();
}
@Override
protected void onStop() {
super.onStop();
cacheUtils.close();
}
public void loadImage(View view){
load(url,imageView);
}
public void removeLruCache(View view){
Log.i(TAG, "移出內存緩存...");
cacheUtils.removeLruCache(url);
}
public void removeDiskLruCache(View view){
Log.i(TAG, "移出磁盤緩存...");
cacheUtils.removeDiskLruCache(url);
}
private void load(String url, final ImageView imageView){
//從內存中獲取圖片
Bitmap bitmap = cacheUtils.getBitmapFromCache(url);
if (bitmap == null){
//從磁盤中獲取圖片
InputStream is = cacheUtils.getDiskCache(url);
if (is == null){
//從網絡上獲取圖片
cacheUtils.putCache(url, new LruCacheUtils.CallBack<Bitmap>() {
@Override
public void response(Bitmap bitmap1) {
Log.i(TAG, "從網絡中獲取圖片...");
Log.i(TAG, "正在從網絡中下載圖片...");
imageView.setImageBitmap(bitmap1);
Log.i(TAG, "從網絡中獲取圖片成功...");
}
});
}else{
Log.i(TAG, "從磁盤中獲取圖片...");
bitmap = BitmapFactory.decodeStream(is);
imageView.setImageBitmap(bitmap);
}
}else{
Log.i(TAG, "從內存中獲取圖片...");
imageView.setImageBitmap(bitmap);
}
}
}
複製代碼
佈局文件比較簡單就不貼代碼了,下面是日誌運行截圖說明執行狀況,以下圖所示:
這篇文章記錄了 LruCache 和 DiskLruCache 的基本使用方式,至少應該對這兩個緩存輔助類有了必定的瞭解,它的具體實現請參考源碼。 【文中代碼】:傳送門
能夠關注公衆號:jzman-blog,一塊兒交流學習。