1.LinkedHashMap
图片缓存技术一般使用Lru,其实Lru就是使用了LinkedHashMap的按访问顺序遍历;
LinkedHashMap是通过双向链表实现hashMap遍历有序,其遍历方式有2种,一种是按插入顺序遍历,默认无参构造方法就是按插入顺序遍历,比如:
linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.put("a","a");
此时如果按照遍历顺序获取,获取结果会是a->b->c;
如下是LinkedHashMap的源码,可以看出,成员变量accessOrder就是控制遍历顺序,如果为true就是按访问顺序遍历;
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
如果new LinkedHashMap(16,0.75f,true)方式初始化LinkedHashMap
linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.put("a","a");
或者
linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.get("a");
其遍历顺序都会按照b->c->a输出,其实这就是LRU的最核心思想,最近使用过的放在最后;
2.LRU原理图解
3.LRU源码解析
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
//存储的最大容量
this.maxSize = maxSize;
//可以看出Lru就是利用LinkedHashMap按访问顺序遍历特性实现的
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
public final V put(K key, V value) {
// key/value都不能为null
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
//统计size大小
size += safeSizeOf(key, value);
//如果之前put过key/value,那获取到oldValue
previous = map.put(key, value);
//将之前的oldValue的size减去
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//移除之前oldValue
if (previous != null) {
//注意这个方法entryRemoved
entryRemoved(false, key, previous, value);
}
//这个方法就是容量超过阈值时候,移除最老数据,重新将缩容到阈值之内
trimToSize(maxSize);
return previous;
}
private void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
//容量小于阈值时候才退出循环
if (size <= maxSize) {
break;
}
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
// END LAYOUTLIB CHANGE
if (toEvict == null) {
break;
}
//依次遍历移除老数据
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
//移除老数据回调方法,此方法可以重写,DiskLRU就可以利用
entryRemoved(true, key, value, null);
}
}
public final V get(K key) {
//key/value都不可以为null
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
//正常情况下,这里就已经退出了
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
//下面的方法都是一些异常情况处理,先不关注
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
//size统计的2个方法,其中sizeOf一般需重写
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
protected int sizeOf(K key, V value) {
return 1;
}
可以看到sizeOf默认是返回1,但是LRU里面的Value一般存放的是些大内存东西,因此需要重写sizeOf统计size方法,此方法必须重写;
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
evictAll方法看注释也知道是清空缓存方法;
4.实现图片LRU的3级缓存
3级缓存分别是内存的LRU,文件的DiskLRU,网络访问缓存;
如下先实现Memory Lru:
public class LoadFromMemory {
private LruCache<String,Bitmap> mBitmapLru;
public LoadFromMemory(){
//初始化容量设置为最大内存的1/8
mBitmapLru = new LruCache<String, Bitmap>((int) (Runtime.getRuntime().maxMemory()/8)){
//重写size统计大小
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
//往LRU中添加
public void addBitmap(String key,Bitmap bitmap){
mBitmapLru.put(key,bitmap);
}
//从LRU中获取
public Bitmap getBitmap(String key){
return mBitmapLru.get(key);
}
//清空LRU
public void clearMemory(){
mBitmapLru.evictAll();
}
}
了解了LRU源码后,实现一个内存的LRU就是小case了,没有特别的;
之后考虑实现DiskLRU:
public class LoadFromDiskFile {
private File mCacheDir;
private static final String SD_CACHE_DIR = Environment.getExternalStorageDirectory() + "/photo";
private LruCache<String,File> mLruCache;
private static int DISK_CACHE_SIZE = 1024*1024*1024;
public LoadFromDiskFile() {
File cacheFile = new File(SD_CACHE_DIR);
if (!cacheFile.exists()){
cacheFile.mkdir();
}
this.mCacheDir = new File(SD_CACHE_DIR);
mLruCache = new LruCache<String, File>(DISK_CACHE_SIZE){
//统计size改为文件的大小
@Override
protected int sizeOf(String key, File value) {
return (int) value.length();
}
//内存不够时候,移除时候,回调此方法,可以删除文件
@Override
protected void entryRemoved(boolean evicted, String key, File oldValue, File newValue) {
if (evicted && oldValue.exists()){
oldValue.delete();
}
}
};
loadAllFile();
}
//初始化时候,从磁盘文件中加载文件到LRU中去
private void loadAllFile() {
if (mCacheDir.isDirectory()){
File[] files = mCacheDir.listFiles();
if (files == null){
return;
}
//按时间文件最后一次修改的时间顺序排序
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.lastModified() < o2.lastModified() ? -1 : 1;
}
});
//排序后,在往LRU中添加,这样最近修改过的文件肯定是最后添加进去的;
for (File file:files){
mLruCache.put(file.getName(),file);
}
}
}
//添加文件缓存
public void addBitmap(String key, Bitmap bitmap){
//将url地址MD5编码
String fileName = encode(key);
//如果已经存在了,不要在写文件操作
if (mLruCache.get(fileName) != null) return;
//写文件操作
File file = new File(SD_CACHE_DIR, fileName);
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
BufferedOutputStream buffer = new BufferedOutputStream(outputStream);
bitmap.compress(Bitmap.CompressFormat.JPEG,70,buffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
mLruCache.put(key,file);
}
//从文件读取Bitmap
public Bitmap getBitmap(String key){
File file = mLruCache.get(encode(key));
if (file == null){
return null;
}
//使用过了,重新设置下文件的最近更新时间
file.setLastModified(System.currentTimeMillis());
return BitmapFactory.decodeFile(file.getAbsolutePath());
}
//清空缓存
public void clearFileCache(){
mLruCache.evictAll();
}
//MD5文件名
public String encode(String string) {
byte[] hash = new byte[0];
try {
hash = MessageDigest.getInstance("MD5").digest(
string.getBytes("UTF-8"));
} catch (Exception e) {
e.printStackTrace();
}
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
if ((b & 0xFF) < 0x10) {
hex.append("0");
}
hex.append(Integer.toHexString(b & 0xFF));
}
return hex.toString();
}
}
DiskLRU就需要注意几点了,
1.文件缓存LRU在每次重启时候,需要重写将文件loadAllFile到LRU中,注意这个过程需要将文件排序,这里我们定一个规则,就是文件的最新修改时间,如果最近使用过文件,哪怕文件没有被重写过,也可以手动给其setLastModified一个文件修改的时间,表示最近使用过;
2.文件存储时候,需要采用加密,一般使用MD5编解码;
3.重写LRU的sizeOf/entryRemoved方法;
tips:这里addBitmap其实是有点问题,并不是真正意义上的LRU,put相同元素时候,先get看看有没有元素在其中,如果有就不写文件了,避免无效的IO操作;
最后在实现一个网络缓存,网络缓存一般就是使用Expires和Cache-Control,具体一般使用Last-Modified / If-Modified-Since,Etag / If-None-Match,2个对比缓存,这里也不啰嗦了,有兴趣自己百度,如果访问缓存数据结果状态码一般是304的重定向,如果不是缓存访问网络状态码就是200;这里直接使用OKHttp自带支持网络缓存实现吧:
public class LoadFromNetwork {
private OkHttpClient okHttpClient;
public LoadFromNetwork(){
okHttpClient = new OkHttpClient.Builder().retryOnConnectionFailure(true).build();
}
public void getBitmap(final String key,final LruUtils.QueryBitmapListener listener){
final Request request = new Request.Builder().url(key).build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
listener.failure(e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()){
listener.failure("response error"+key);
return;
}
InputStream inputStream = response.body().byteStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap!=null){
listener.success(bitmap);
}else {
listener.failure("bitmap is null" + key);
}
}
});
}
}
将3种缓存封装在一起:
public class LruUtils{
public static final String TAG = "LruUtils";
private LoadFromMemory loadFromMemory;
private LoadFromDiskFile loadFromDiskFile;
private LoadFromNetwork loadFromNetwork;
public LruUtils(){
loadFromMemory = new LoadFromMemory();
loadFromDiskFile = new LoadFromDiskFile();
loadFromNetwork = new LoadFromNetwork();
}
public interface QueryBitmapListener{
void success(Bitmap bitmap);
void failure(String error);
}
public void loadBitmap(final String key, final QueryBitmapListener listener){
Bitmap bitmap = loadFromMemory.getBitmap(key);
//内存加载
if (bitmap!=null){
Log.i(TAG,"loadFromMemory");
listener.success(bitmap);
return;
}
bitmap = loadFromDiskFile.getBitmap(key);
//文件磁盘加载
if (bitmap!=null){
loadFromMemory.addBitmap(key,bitmap);
Log.i(TAG,"loadFromDiskFile");
listener.success(bitmap);
return;
}
//网络加载
loadFromNetwork.getBitmap(key, new QueryBitmapListener() {
@Override
public void success(Bitmap bitmap) {
//网络加载成功时候,写入到磁盘和内存
loadFromMemory.addBitmap(key,bitmap);
loadFromDiskFile.addBitmap(key,bitmap);
Log.i(TAG,"loadFromNetwork");
listener.success(bitmap);
}
@Override
public void failure(String error) {
listener.failure(error);
}
});
}
//清空缓存
public void clearAllCache(){
loadFromDiskFile.clearFileCache();
loadFromMemory.clearMemory();
}
}
public class MainActivity extends AppCompatActivity implements View.OnClickListener,LruUtils.QueryBitmapListener {
private ImageView imageView;
private Button loadNext;
private Button clear;
private LruUtils lruUtils;
private int loadIndex = 0;
private final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 0;
private String[] imageUrl = new String[]{
"http://t-1.tuzhan.com/38c4c77ede92/c-2/l/2013/09/16/12/e6b466e4fc034b50b098535b14ee497d.jpg",
"http://picapi.zhituad.com/photo/89/78/69ADE.jpg",
"http://image.biaobaiju.com/uploads/20181001/22/1538404773-AMGObvJmUQ.jpg",
"http://pic30.nipic.com/20130619/2531170_124430379002_2.jpg",
"http://pic24.nipic.com/20121017/8362416_132430698000_2.jpg",
"http://pic29.nipic.com/20130515/12667289_101713416109_2.jpg"
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.image);
loadNext = findViewById(R.id.next);
clear = findViewById(R.id.clear);
loadNext.setOnClickListener(this);
clear.setOnClickListener(this);
lruUtils = new LruUtils();
requestPermission();
}
//申请动态权限PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE
private void requestPermission() {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CONTACTS)) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
} else {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i(LruUtils.TAG,"onRequestPermissionsResult granted");
} else {
Log.i(LruUtils.TAG,"onRequestPermissionsResult denied");
showWaringDialog();
}
return;
}
}
}
private void showWaringDialog() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("警告!")
.setMessage("请前往设置->应用->PermissionDemo->权限中打开相关权限,否则功能无法正常运行!")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
}).show();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.next:
loadIndex = (++loadIndex%imageUrl.length);
lruUtils.loadBitmap(imageUrl[loadIndex],this);
break;
case R.id.clear:
lruUtils.clearAllCache();
break;
}
}
//回调结果,注意,回调过程可能是从OKHTTP线程中回来,线程切换下
@Override
public void success(final Bitmap bitmap) {
runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
@Override
public void failure(final String error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(),error,Toast.LENGTH_SHORT).show();
}
});
}
}
到此LRU原理和自己动手实现一个LRU小Demo实现了,其实缓存不光图片可以这么干,其他任何大对象都可以如此实现,知其原理方能运用自如;