Android經典面試問題:請你設計一套圖片非同步載入快取方案——圖片的三級快取
友情提示:文章最後附有專案原始碼
現在,Android有很多優秀的圖片載入框架。例如:Picasso,Glide,Fresco。我們幾乎只要簡單呼叫幾句程式碼就可以很好的實現圖片的載入。很多時候也不需要我們親自去寫圖片載入方案。但是,學習圖片的三級快取策略無論是在面試時,還是對於App的其他快取框架設計都是很有必要的一件事。
今天就從頭開始設計一套圖片非同步載入快取方案。本方案用到以下技術,想了解更細緻的內容可以去以下連結檢視,在此不再贅述。
在程式碼結構的設計上參考了《Android原始碼設計模式解析與實戰》
1、何為三級快取
所謂三級快取,指的是:記憶體快取,本地快取(或者叫檔案快取),網路快取(我個人認為把網路算在快取裡其實是不太合適的)。
(1)記憶體快取:只有當APP執行時才會涉及到。記憶體雖然有容量限制,但是從記憶體讀取資訊是速度最快的。
(2)本地快取:資訊以檔案的形式儲存在本地。只要不清除這些檔案,那麼資訊就一直持久化的儲存著。需要時可以通過流的方式進行讀取。本地容量大,速度次於記憶體。
(3)網路:資訊儲存在遠端Server。通過網路獲取資訊。完全依賴網路情況,速度相對上面兩者來說要慢。
2、為什麼要用三級快取
(1)為使用者節省流量,對相同資源減少多次重複的網路請求。
(2)部分業務需要。例如有些業務需要在使用者斷網時也可以進行一些瀏覽或操作。
(3)各快取讀取速度不相同,結合使用提高效率。
3、圖片非同步載入快取方案的工作流程
4、技術選型
如開頭所提到的幾個技術點。這裡,記憶體快取我們選用LruCache實現。本地快取選用DiskLruCache實現。網路我們通過Retrofit進行圖片檔案的下載。當然,實現方式有很多種,可根據需要自己選擇。
5、方案實現
(1)定義快取介面
首先我們可以確認,無論是記憶體快取,本地快取,還是兩者的結合。都需要獲取圖片的方法和插入圖片的方法。因此我們直接定義一個快取介面,面向介面編寫快取的程式碼。
介面如下:
public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); }
(2)實現記憶體快取
記憶體快取的實現很簡單,把LruCache當成一個Map來用就好了的。程式碼如下:
public class MemoryCache implements ImageCache {
private LruCache<String, Bitmap> mLruCache;
private static final int MAX_LRU_CACHE_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);
public MemoryCache() {
//初始化LruCache
initLruCache();
}
private void initLruCache() {
mLruCache = new LruCache<String, Bitmap>(MAX_LRU_CACHE_SIZE) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return mLruCache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
mLruCache.put(url, bitmap);
}
}
(3)實現本地快取
DiskLruCache是Google自己寫的一個類,用來做本地快取方案十分方便。這個類的具體用法可以參看開頭的相關文章連結。
程式碼如下:
public class DiskCache implements ImageCache {
private DiskLruCache mDiskLruCache;
private static final String DISK_LRU_CACHE_UNIQUE = "Image";
private static final int MAX_DISK_LRU_CACHE_SIZE = 10 * 1024 * 1024;
ExecutorService mExecutorsService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
public DiskCache(Context context) {
//初始化DiskLruCache
initDiskLruCache(context);
}
private void initDiskLruCache(Context context) {
try {
File cacheDir = getDiskCacheDir(
context,
DISK_LRU_CACHE_UNIQUE
);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(
cacheDir,
getAppVersion(context),
1,
MAX_DISK_LRU_CACHE_SIZE
);
} catch (IOException e) {
e.printStackTrace();
}
}
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);
}
private int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(),
0
);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
@Override
public Bitmap getBitmap(String url) {
String bitmapUrlMD5 = Md5Util.getMD5String(url);
Bitmap bitmap = null;
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = mDiskLruCache.get(bitmapUrlMD5);
} catch (IOException e) {
e.printStackTrace();
}
if (snapshot != null) {
InputStream inputStream = snapshot.getInputStream(0);
bitmap = BitmapFactory.decodeStream(inputStream);
}
return bitmap;
}
@Override
public void putBitmap(String url, final Bitmap bitmap) {
final String bitmapUrlMD5 = Md5Util.getMD5String(url);
mExecutorsService.submit(
new Runnable() {
@Override
public void run() {
writeFileToDisk(mDiskLruCache, bitmap, bitmapUrlMD5);
}
}
);
}
private static void writeFileToDisk(
DiskLruCache diskLruCache,
Bitmap bitmap,
String bitmapUrlMD5
) {
DiskLruCache.Editor editor = null;
OutputStream outputStream = null;
try {
editor = diskLruCache.edit(bitmapUrlMD5);
if (editor != null) {
outputStream = editor.newOutputStream(0);
if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
editor.commit();
}
}
} catch (Exception e) {
try {
if (editor != null) {
editor.abort();
}
} catch (Exception e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
try {
diskLruCache.flush();
} catch (Exception e) {
}
}
}
}
可以看到本地快取的時候對url做了一次MD5加密。這是為了從安全考慮。畢竟直接把url暴露在檔案上實在不太雅觀。
(4)完成記憶體快取加本地快取的雙快取邏輯實現
這一塊很簡單。參看之前的三級快取工作流程圖。
對於圖片的獲取:先從記憶體快取獲取圖片。如果不為空直接返回。如果為空,再從本地快取獲取圖片。
對於圖片的儲存:就是往記憶體快取和本地快取分別新增圖片。
程式碼如下:
public class MemoryAndDiskCache implements ImageCache {
private MemoryCache mMemoryCache;
private DiskCache mDiskCache;
public MemoryAndDiskCache(Context context) {
mMemoryCache = new MemoryCache();
mDiskCache = new DiskCache(context);
}
@Override
public Bitmap getBitmap(String url) {
Bitmap bitmap = mMemoryCache.getBitmap(url);
if (bitmap != null) {
return bitmap;
} else {
bitmap = mDiskCache.getBitmap(url);
return bitmap;
}
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
mMemoryCache.putBitmap(url, bitmap);
mDiskCache.putBitmap(url, bitmap);
}
}
(5)實現ImageLoader類
這個類中我們會在建構函式中傳入ImageCache的例項。那麼在獲取和儲存圖片時,只需要呼叫介面中定義的兩個方法即可,無需關注細節。實現細節完全交由建構函式中傳入的ImageCache例項。當要獲取圖片時,先呼叫ImageCache介面例項的getBitmap方法,如果為空。那麼需要我們從網路下載圖片。下載完成後我們只要呼叫ImageCache介面示例的putBitmap方法,即可完成整個圖片快取方案。
程式碼如下:
public class ImageLoader {
private ImageCache mImageCache;
public ImageLoader(ImageCache imageCache) {
mImageCache = imageCache;
}
public void displayImage(String url, ImageView imageView, int defaultImageRes) {
imageView.setImageResource(defaultImageRes);
imageView.setTag(url);
Bitmap bitmap = mImageCache.getBitmap(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
downloadImage(imageView, url);
}
}
private void downloadImage(final ImageView imageView, final String url) {
Call<ResponseBody> resultCall = ServiceFactory.getServices().downloadImage(url);
resultCall.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response != null && response.body() != null) {
InputStream inputStream = response.body().byteStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (TextUtils.equals((String) imageView.getTag(), url)) {
imageView.setImageBitmap(bitmap);
}
mImageCache.putBitmap(url, bitmap);
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
}
}
(6)實際使用
這塊只需要new一個ImageLoader物件。並在建構函式中傳入你希望使用的快取策略。之後呼叫它的displayImage方法即可。
程式碼如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = (ImageView) findViewById(R.id.iv);
String url = "圖片的url地址";
ImageLoader imageLoader = new ImageLoader(
new MemoryAndDiskCache(getApplicationContext())
);
imageLoader.displayImage(url, iv, R.mipmap.ic_launcher);
}
}
6、演示(動圖較大,載入略慢,有興趣的同學請直接跳到7,去下載原始碼吧)
好了,折騰這麼一通後我們來找個圖片試一下吧。
首先看一下,在有網的時候,載入一張網路圖片:
之後,我們殺掉程式,並且關閉網路。再將程式開啟,可以看到之前的圖片仍然能正常顯示:
7、原始碼下載(覺得這篇文章對你有幫助的同學們,歡迎Star一下!):