本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)
一. 前言
我們?yōu)槭裁匆獙W習圖片緩存機制?簡單來說,就是幫助用戶省時省流量。當用戶使用RecyclerView或者ListView的時候,頻繁的發(fā)起網(wǎng)絡(luò)請求不僅會消耗大量的流量,還會消耗大量的時間,毫無疑問,這會讓用戶的體驗相當糟糕。雖然Glide等圖片加載框架已經(jīng)替我們處理好了圖片緩存的問題,但是我們?nèi)匀挥斜匾チ私夂蛯W習圖片緩存機制,知其然才能知其所以然。
二. 思路

三. 簡單了解Android圖片緩存機制
在這里,我們只要了解Android圖片的三級緩存機制就行了,何為三級緩存機制?
-
內(nèi)存緩存,讀取速度最快。 -
硬盤緩存(文件緩存),讀取速度比內(nèi)存緩存稍慢。 -
網(wǎng)絡(luò)緩存,讀取速度最慢。
所以,我們正確的圖片的讀取順序應(yīng)該是 內(nèi)存緩存 > 硬盤緩存 > 網(wǎng)絡(luò)緩存,講到這里,我們是不是要手動開始擼碼了?看官別急,我們先講解一下基本的API。
四. 了解一下常用的API
緩存機制的通用調(diào)度算法是LRU(最近最久未使用),不熟悉的同學,可以自行谷歌,本篇不做介紹。與內(nèi)存緩存和硬盤緩存對應(yīng)的類分別是LruCache和DiskLruCache,Android在Android 3.1加入了LruCache緩存類,而DiskLruCache并非谷歌官方編寫,所以我們在寫程序的時候不能直接調(diào)用,好在Jake Wharton大神集成了庫,我們直接用就好了,只要在build.gradle添加如下語句:
implementation 'com.jakewharton:disklrucache:2.0.2'
1.LruCache常用API介紹
| 方法 | 簡介 |
|---|---|
| LruCache(int maxSize) | 構(gòu)造方法,maxSize是緩存大小 |
| put(@NonNull K key, @NonNull V value) | 以鍵值對的方式存入內(nèi)存緩存 |
| get(@NonNull K key) | 使用鍵取出存入的值 |
| remove(@NonNull K key) | 從內(nèi)存緩存中移除指定鍵的值 |
2.DiskLruCache常用API介紹
介紹之前,我們需要了解DiskLruCache使用比LruCache復雜,我們不能直接使用構(gòu)造方法直接創(chuàng)建一個DiskLruCache,而是使用open(File directory, int appVersion, int valueCount, long maxSize)這個靜態(tài)方法創(chuàng)建。如果想要將數(shù)據(jù)存入緩存,需要通過一個key獲取到DiskLruCache.Editor對象,然后使用Editor對象獲取輸出流將我們的數(shù)據(jù)存入硬盤緩存,最后使用flush更新journal文件。對于想要深入探究的同學,請移步郭神的Android DiskLruCache完全解析,硬盤緩存的最佳方案
| 方法 | 簡介 |
|---|---|
| open(File directory, int appVersion, int valueCount, long maxSize) |
directory是緩存目錄,appVersion是版本號,valueCount是指定key可以對應(yīng)多個緩存數(shù)量, |
| get(String key) | 返回Snapshot對象,通過調(diào)用該對象的getInputStream(int index)方法可以獲取輸入流 |
| edit(String key) | 返回DiskLruCache.Editor對象 |
DiskLruCache.Editor的newOutputStream(int index) |
創(chuàng)建一個輸出流,可以用來存入數(shù)據(jù) |
DiskLruCache.Editor的commit() |
在使用輸出流緩存數(shù)據(jù)后,使用commit()才會生效 |
DiskLruCache.Editor的abort() |
與commit()方法相反,使用abort()終止緩存生效 |
| flush() | 同步緩存日志到j(luò)ournal文件 |
這些是我們常用的方法,當然還有計算當前緩存數(shù)據(jù)字節(jié)的size()方法、關(guān)閉DiskLruCache的close()方法和清空緩存的delete()方法等。
五. 手擼代碼
網(wǎng)絡(luò)請求這里我們使用Okhttp,同樣需要在build.gradle中添加一行代碼,如下:
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
1. 布局
布局這里挺簡單,就是一個線性布局里面放一個RecyclerView,RecyclerView子布局里面就是一個ImageView,具體的可以看代碼。
2. GridPhotoAdapter
這個適配器可以說是本文里面最重要的一個類了(需要繼承自RecyclerView.Adapter),我們慢慢往下看。
// 照片的網(wǎng)絡(luò)路徑
private String[] urls;
// 內(nèi)存緩存
private LruCache<String,Bitmap> mMemoryCache;
// 硬盤緩存
private DiskLruCache mDisLruCache;
// OkhttpClient
private OkHttpClient okHttpClient;
// 線程池 用來請求下載圖片
private ExecutorService service;
// 主線程Handler 用來圖片下載完成后更新ImageView
private Handler mHandler;
private Context mContext;
上面是我們需要用到的實例,作用已經(jīng)在注釋中標注出來了。接下來我們來介紹我們的構(gòu)造函數(shù)和一些初始化工作:
public GridPhotoAdapter(String[] urls, Handler mHandler, Context context) {
this.urls = urls;
this.mHandler = mHandler;
this.mContext = context;
init();
}
/*
一些必要的初始化的工作
*/
private void init() {
okHttpClient = new OkHttpClient.Builder()
.build();
// 構(gòu)建一定數(shù)量的線程池
service = Executors.newFixedThreadPool(6);
// 構(gòu)建內(nèi)存緩存
// 取最大的1/8內(nèi)存作為內(nèi)存緩存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory/8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
return value.getByteCount();
}
};
// 構(gòu)建硬盤緩存實例
File file = getDiskCacheDir(mContext,"photo");
if(!file.exists())
file.mkdirs();
try {
mDisLruCache = DiskLruCache.open(file,getAppInfoVersion(),1,10*1024*1024);
} catch (IOException e) {
e.printStackTrace();
}
}
/*
根據(jù)傳入的uniqueName獲取唯一的硬盤的緩存路徑
*/
private 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);
}
/*
獲取當前程序的應(yīng)用版本號
*/
private int getAppInfoVersion() {
try {
PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
在init()中,我們初始化了okHttpClient、service(線程池)、mMemoryCache(內(nèi)存緩存)和mDisLruCache(硬盤緩存)。需要注意的是,我們在構(gòu)建硬盤緩存路徑的時候調(diào)用了getDiskCacheDir(Context context, String uniqueName)函數(shù),這個函數(shù)給我們的程序提供了一個緩存地址。介紹完了構(gòu)造函數(shù),我們再來看一下繼承自RecyclerView.Adapter必須要復寫的三個方法:
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View root = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycle_item_net_work,viewGroup,false);
ViewHolder viewHolder = new ViewHolder(root);
viewHolder.imageView = root.findViewById(R.id.grid_photo);
// root.setTag(urls[i]);
return viewHolder;
}
public class ViewHolder extends RecyclerView.ViewHolder{
public ImageView imageView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
}
@Override
public int getItemCount() {
// 返回路徑的長度
return urls.length;
}
onCreateViewHolder()和getItemCount()很簡單,這里就不再介紹了。這邊我們著重介紹onBindViewHolder()方法:
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
ImageView imageView = viewHolder.imageView;
String url = urls[i];
// imageView.setTag(url);
imageView.setImageResource(R.drawable.shape_item_empty);
loadBitmaps(imageView, url);
}
在上面的函數(shù)中,我們先找到控件ImageView和路徑url,在真正的搜索圖片緩存之前先設(shè)置一個占位圖,然后到了我們真正進行圖片請求的函數(shù)loadBitmaps(ImageView imageView, String url):
/**
* 加載Bitmap對象,如果Bitmap不在LruCache中,就開啟線程去查詢
*
* @param imageView 圖片
* @param url 地址
*/
private void loadBitmaps(ImageView imageView, String url) {
Bitmap bitmap = getBitmapFromMemoryCache(url);
if (bitmap != null) {
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
} else {
service.execute(new ImageRunnable(url,imageView));
}
}
// 添加Bitmap到內(nèi)存緩存中
private void addBitmapToMemoryCache(Bitmap bitmap, String url) {
if (getBitmapFromMemoryCache(url) == null)
mMemoryCache.put(url, bitmap);
}
在loadBitmaps()函數(shù)中,我們先從內(nèi)存緩存中查找是否有該路徑的緩存,有的話就直接放到我們的ImageView中,沒有就利用我們的線程池執(zhí)行一個ImageRunnable,我們再來看看ImageRunnable的代碼:
public class ImageRunnable implements Runnable {
private String url;
private Bitmap bitmap;
private ImageView mView;
public ImageRunnable(String url,ImageView imageView) {
this.url = url;
this.mView = imageView;
}
@Override
public void run() {
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
DiskLruCache.Snapshot snapshot = null;
// 對url進行加密得到key
final String key = hashKeyForDisk(url);
// 查找key對應(yīng)的硬盤緩存
try {
snapshot = mDisLruCache.get(key);
if (snapshot == null) {
// 如果對應(yīng)的硬盤緩存沒找到,就開始網(wǎng)絡(luò)請求,并且寫入緩存
DiskLruCache.Editor editor = mDisLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadImage(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
snapshot = mDisLruCache.get(key);
}
if (snapshot != null) {
fileInputStream = (FileInputStream) snapshot.getInputStream(0);
fileDescriptor = fileInputStream.getFD();
}
// 將緩存數(shù)據(jù)解析成Bitmap對象
if (fileDescriptor != null)
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
if (bitmap != null) {
// 將圖片添加到內(nèi)存緩存中
addBitmapToMemoryCache(bitmap, url);
}
if (bitmap != null)
// 在主線程中更新
mHandler.post(new Runnable() {
@Override
public void run() {
/* ImageView image = mRecyclerView.findViewWithTag(url);
if (image != null)
image.setImageBitmap(bitmap);*/
mView.setImageBitmap(bitmap);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
這里的邏輯其實也比較簡單,首先通過url得到我們的key,然后利用key去取我們的硬盤緩存,取不到的情況下進行網(wǎng)絡(luò)請求,利用輸出流存到我們的硬盤路徑下面,最后取到我們的硬盤緩存,在主線程中更新我們的ImageView。不要以為我們到此就結(jié)束了,我們的下載圖片downloadImage(final String url, OutputStream outputStream)還沒有看,哈哈~
/*
下載圖片
*/
private boolean downloadImage(final String url, OutputStream outputStream) {
Request request = new Request.Builder()
.url(url)
.build();
// 執(zhí)行操作
Call call = okHttpClient.newCall(request);
Response response = null;
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
response = call.execute();
in = new BufferedInputStream(response.body().byteStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
利用Okhttp寫的同步下載圖片請求,看代碼就ok。我們再來回顧一下邏輯,用一張流程圖概括吧:

3. NetWorkActivity
public class NetWorkActivity extends AppCompatActivity {
public final static String[] imageThumbUrls = new String[]{
// 路徑省略了 具體的可以看代碼
};
private GridPhotoAdapter mAdapter;
public static void show(Context context) {
Intent intent = new Intent(context, NetWorkActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_net_work);
initWidget();
}
private void initWidget() {
RecyclerView mRecyclerView = findViewById(R.id.recycle);
Handler mHandler = new Handler(Looper.getMainLooper());
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
mRecyclerView.setAdapter(mAdapter = new GridPhotoAdapter(imageThumbUrls, mHandler,this));
}
@Override
protected void onPause() {
super.onPause();
// 將日志同步到j(luò)ournal文件中
mAdapter.flushCache();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 退出程序時結(jié)束所有的下載任務(wù)
mAdapter.cancelDownloadImage();
}
}
這里省略了相關(guān)urls,代碼就是寫RecyclerView的通用代碼,同學們可以自行查看。寫完效果就出來了,如圖:

六. 總結(jié)
通過以上的學習,相信同學們可以對Android圖片緩存機制有了更深入的了解,本人水平有限,如有錯誤,歡迎指出,Over~
地址:
Demo地址
引用:
Android照片墻完整版,完美結(jié)合LruCache和DiskLruCache