友情提示:文章最后附有项目源码
现在,Android有很多优秀的图片加载框架。例如:Picasso,Glide,Fresco。我们几乎只要简单调用几句代码就可以很好的实现图片的加载。很多时候也不需要我们亲自去写图片加载方案。但是,学习图片的三级缓存策略无论是在面试时,还是对于App的其他缓存框架设计都是很有必要的一件事。
今天就从头开始设计一套图片异步加载缓存方案。本方案用到以下技术,想了解更细致的内容可以去以下链接查看,在此不再赘述。
LruCache:LruCache详解 LruCache源码解析
DiskLruCache:Android DiskLruCache完全解析,硬盘缓存的最佳方案
使用Retrofit下载图片:使用Retrofit和Rxjava下载启动图图片
在代码结构的设计上参考了《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 mLruCache;
private static final int MAX_LRU_CACHE_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);
public MemoryCache() {
//初始化LruCache
initLruCache();
}
private void initLruCache() {
mLruCache = new LruCache(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 resultCall = ServiceFactory.getServices().downloadImage(url);
resultCall.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response 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 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一下!):
源码下载