本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发
一. 前言
我们为什么要学习图片缓存机制?简单来说,就是帮助用户省时省流量。当用户使用RecyclerView
或者ListView
的时候,频繁的发起网络请求不仅会消耗大量的流量,还会消耗大量的时间,毫无疑问,这会让用户的体验相当糟糕。虽然Glide等图片加载框架已经替我们处理好了图片缓存的问题,但是我们仍然有必要去了解和学习图片缓存机制,知其然才能知其所以然。
二. 思路
三. 简单了解Android图片缓存机制
在这里,我们只要了解Android图片的三级缓存机制
就行了,何为三级缓存机制?
-
内存缓存
,读取速度最快。 -
硬盘缓存(文件缓存)
,读取速度比内存缓存稍慢。 -
网络缓存
,读取速度最慢。
所以,我们正确的图片的读取顺序应该是 内存缓存 > 硬盘缓存 > 网络缓存,讲到这里,我们是不是要手动开始撸码了?看官别急,我们先讲解一下基本的API。
四. 了解一下常用的API
缓存机制的通用调度算法是LRU(最近最久未使用),不熟悉的同学,可以自行谷歌,本篇不做介绍。与内存缓存和硬盘缓存对应的类分别是LruCache
和DiskLruCache
,Android在Android 3.1加入了LruCache
缓存类,而DiskLruCache
并非谷歌官方编写,所以我们在写程序的时候不能直接调用,好在Jake Wharton大神集成了库,我们直接用就好了,只要在build.gradle添加如下语句:
implementation 'com.jakewharton:disklrucache:2.0.2'
1.LruCache常用API介绍
方法 | 简介 |
---|---|
LruCache(int maxSize) | 构造方法,maxSize 是缓存大小 |
put(@NonNull K key, @NonNull V value) | 以键值对的方式存入内存缓存 |
get(@NonNull K key) | 使用键取出存入的值 |
remove(@NonNull K key) | 从内存缓存中移除指定键的值 |
2.DiskLruCache常用API介绍
介绍之前,我们需要了解DiskLruCache
使用比LruCache
复杂,我们不能直接使用构造方法直接创建一个DiskLruCache
,而是使用open(File directory, int appVersion, int valueCount, long maxSize)
这个静态方法创建。如果想要将数据存入缓存,需要通过一个key
获取到DiskLruCache.Editor
对象,然后使用Editor
对象获取输出流将我们的数据存入硬盘缓存
,最后使用flush
更新journal文件。对于想要深入探究的同学,请移步郭神的Android DiskLruCache完全解析,硬盘缓存的最佳方案
方法 | 简介 |
---|---|
open(File directory, int appVersion, int valueCount, long maxSize) |
directory 是缓存目录,appVersion 是版本号,valueCount 是指定key可以对应多个缓存数量, |
get(String key) | 返回Snapshot 对象,通过调用该对象的getInputStream(int index) 方法可以获取输入流 |
edit(String key) | 返回DiskLruCache.Editor 对象 |
DiskLruCache.Editor 的newOutputStream(int index) |
创建一个输出流,可以用来存入数据 |
DiskLruCache.Editor 的commit() |
在使用输出流缓存数据后,使用commit() 才会生效 |
DiskLruCache.Editor 的abort() |
与commit() 方法相反,使用abort() 终止缓存生效 |
flush() | 同步缓存日志到journal文件 |
这些是我们常用的方法,当然还有计算当前缓存数据字节的size()
方法、关闭DiskLruCache的close()
方法和清空缓存的delete()
方法等。
五. 手撸代码
网络请求这里我们使用Okhttp,同样需要在build.gradle中添加一行代码,如下:
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
1. 布局
布局这里挺简单,就是一个线性布局里面放一个RecyclerView
,RecyclerView
子布局里面就是一个ImageView,具体的可以看代码。
2. GridPhotoAdapter
这个适配器可以说是本文里面最重要的一个类了(需要继承自RecyclerView.Adapter
),我们慢慢往下看。
// 照片的网络路径
private String[] urls;
// 内存缓存
private LruCache<String,Bitmap> mMemoryCache;
// 硬盘缓存
private DiskLruCache mDisLruCache;
// OkhttpClient
private OkHttpClient okHttpClient;
// 线程池 用来请求下载图片
private ExecutorService service;
// 主线程Handler 用来图片下载完成后更新ImageView
private Handler mHandler;
private Context mContext;
上面是我们需要用到的实例,作用已经在注释中标注出来了。接下来我们来介绍我们的构造函数和一些初始化工作:
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();
// 构建一定数量的线程池
service = Executors.newFixedThreadPool(6);
// 构建内存缓存
// 取最大的1/8内存作为内存缓存
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();
}
};
// 构建硬盘缓存实例
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();
}
}
/*
根据传入的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);
}
/*
获取当前程序的应用版本号
*/
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(内存缓存)
和mDisLruCache(硬盘缓存)
。需要注意的是,我们在构建硬盘缓存路径的时候调用了getDiskCacheDir(Context context, String uniqueName)
函数,这个函数给我们的程序提供了一个缓存地址。介绍完了构造函数,我们再来看一下继承自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);
}
在上面的函数中,我们先找到控件ImageView
和路径url
,在真正的搜索图片缓存之前先设置一个占位图,然后到了我们真正进行图片请求的函数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到内存缓存中
private void addBitmapToMemoryCache(Bitmap bitmap, String url) {
if (getBitmapFromMemoryCache(url) == null)
mMemoryCache.put(url, bitmap);
}
在loadBitmaps()
函数中,我们先从内存缓存中查找是否有该路径的缓存,有的话就直接放到我们的ImageView
中,没有就利用我们的线程池执行一个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对应的硬盘缓存
try {
snapshot = mDisLruCache.get(key);
if (snapshot == null) {
// 如果对应的硬盘缓存没找到,就开始网络请求,并且写入缓存
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();
}
// 将缓存数据解析成Bitmap对象
if (fileDescriptor != null)
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
if (bitmap != null) {
// 将图片添加到内存缓存中
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去取我们的硬盘缓存,取不到的情况下进行网络请求,利用输出流存到我们的硬盘路径下面,最后取到我们的硬盘缓存,在主线程中更新我们的ImageView
。不要以为我们到此就结束了,我们的下载图片downloadImage(final String url, OutputStream outputStream)
还没有看,哈哈~
/*
下载图片
*/
private boolean downloadImage(final String url, OutputStream outputStream) {
Request request = new Request.Builder()
.url(url)
.build();
// 执行操作
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();
// 将日志同步到journal文件中
mAdapter.flushCache();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 退出程序时结束所有的下载任务
mAdapter.cancelDownloadImage();
}
}
这里省略了相关urls
,代码就是写RecyclerView的通用代码,同学们可以自行查看。写完效果就出来了,如图:
六. 总结
通过以上的学习,相信同学们可以对Android图片缓存机制有了更深入的了解,本人水平有限,如有错误,欢迎指出,Over~
地址:
Demo地址
引用:
Android照片墙完整版,完美结合LruCache和DiskLruCache