1.LinkedHashMap
圖片緩存技術(shù)一般使用Lru车摄,其實(shí)Lru就是使用了LinkedHashMap的按訪問順序遍歷;
LinkedHashMap是通過雙向鏈表實(shí)現(xiàn)hashMap遍歷有序静盅,其遍歷方式有2種,一種是按插入順序遍歷,默認(rèn)無參構(gòu)造方法就是按插入順序遍歷,比如:
linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.put("a","a");
此時(shí)如果按照遍歷順序獲取,獲取結(jié)果會(huì)是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");
其遍歷順序都會(huì)按照b->c->a輸出录煤,其實(shí)這就是LRU的最核心思想鳄厌,最近使用過的放在最后;
2.LRU原理圖解
3.LRU源碼解析
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
//存儲(chǔ)的最大容量
this.maxSize = maxSize;
//可以看出Lru就是利用LinkedHashMap按訪問順序遍歷特性實(shí)現(xiàn)的
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++;
//統(tǒng)計(jì)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) {
//注意這個(gè)方法entryRemoved
entryRemoved(false, key, previous, value);
}
//這個(gè)方法就是容量超過閾值時(shí)候妈踊,移除最老數(shù)據(jù)了嚎,重新將縮容到閾值之內(nèi)
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!");
}
//容量小于閾值時(shí)候才退出循環(huán)
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;
}
//依次遍歷移除老數(shù)據(jù)
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
//移除老數(shù)據(jù)回調(diào)方法,此方法可以重寫,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++;
//正常情況下歪泳,這里就已經(jīng)退出了
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.
*/
//下面的方法都是一些異常情況處理萝勤,先不關(guān)注
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統(tǒng)計(jì)的2個(gè)方法,其中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默認(rèn)是返回1呐伞,但是LRU里面的Value一般存放的是些大內(nèi)存東西敌卓,因此需要重寫sizeOf統(tǒng)計(jì)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.實(shí)現(xiàn)圖片LRU的3級(jí)緩存
3級(jí)緩存分別是內(nèi)存的LRU,文件的DiskLRU鞍历,網(wǎng)絡(luò)訪問緩存舵抹;
如下先實(shí)現(xiàn)Memory Lru:
public class LoadFromMemory {
private LruCache<String,Bitmap> mBitmapLru;
public LoadFromMemory(){
//初始化容量設(shè)置為最大內(nèi)存的1/8
mBitmapLru = new LruCache<String, Bitmap>((int) (Runtime.getRuntime().maxMemory()/8)){
//重寫size統(tǒng)計(jì)大小
@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源碼后,實(shí)現(xiàn)一個(gè)內(nèi)存的LRU就是小case了劣砍,沒有特別的惧蛹;
之后考慮實(shí)現(xiàn)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){
//統(tǒng)計(jì)size改為文件的大小
@Override
protected int sizeOf(String key, File value) {
return (int) value.length();
}
//內(nèi)存不夠時(shí)候,移除時(shí)候刑枝,回調(diào)此方法香嗓,可以刪除文件
@Override
protected void entryRemoved(boolean evicted, String key, File oldValue, File newValue) {
if (evicted && oldValue.exists()){
oldValue.delete();
}
}
};
loadAllFile();
}
//初始化時(shí)候,從磁盤文件中加載文件到LRU中去
private void loadAllFile() {
if (mCacheDir.isDirectory()){
File[] files = mCacheDir.listFiles();
if (files == null){
return;
}
//按時(shí)間文件最后一次修改的時(shí)間順序排序
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.lastModified() < o2.lastModified() ? -1 : 1;
}
});
//排序后装畅,在往LRU中添加靠娱,這樣最近修改過的文件肯定是最后添加進(jìn)去的;
for (File file:files){
mLruCache.put(file.getName(),file);
}
}
}
//添加文件緩存
public void addBitmap(String key, Bitmap bitmap){
//將url地址MD5編碼
String fileName = encode(key);
//如果已經(jīng)存在了掠兄,不要在寫文件操作
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;
}
//使用過了像云,重新設(shè)置下文件的最近更新時(shí)間
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就需要注意幾點(diǎn)了,
1.文件緩存LRU在每次重啟時(shí)候蚂夕,需要重寫將文件loadAllFile到LRU中迅诬,注意這個(gè)過程需要將文件排序,這里我們定一個(gè)規(guī)則婿牍,就是文件的最新修改時(shí)間侈贷,如果最近使用過文件,哪怕文件沒有被重寫過等脂,也可以手動(dòng)給其setLastModified一個(gè)文件修改的時(shí)間俏蛮,表示最近使用過;
2.文件存儲(chǔ)時(shí)候上遥,需要采用加密搏屑,一般使用MD5編解碼;
3.重寫LRU的sizeOf/entryRemoved方法粉楚;
tips:這里addBitmap其實(shí)是有點(diǎn)問題辣恋,并不是真正意義上的LRU,put相同元素時(shí)候,先get看看有沒有元素在其中,如果有就不寫文件了抑党,避免無效的IO操作包警;
最后在實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)緩存,網(wǎng)絡(luò)緩存一般就是使用Expires和Cache-Control底靠,具體一般使用Last-Modified / If-Modified-Since害晦,Etag / If-None-Match,2個(gè)對(duì)比緩存暑中,這里也不啰嗦了壹瘟,有興趣自己百度,如果訪問緩存數(shù)據(jù)結(jié)果狀態(tài)碼一般是304的重定向鳄逾,如果不是緩存訪問網(wǎng)絡(luò)狀態(tài)碼就是200稻轨;這里直接使用OKHttp自帶支持網(wǎng)絡(luò)緩存實(shí)現(xiàn)吧:
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);
//內(nèi)存加載
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;
}
//網(wǎng)絡(luò)加載
loadFromNetwork.getBitmap(key, new QueryBitmapListener() {
@Override
public void success(Bitmap bitmap) {
//網(wǎng)絡(luò)加載成功時(shí)候,寫入到磁盤和內(nèi)存
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();
}
//申請(qǐng)動(dòng)態(tài)權(quán)限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("請(qǐng)前往設(shè)置->應(yīng)用->PermissionDemo->權(quán)限中打開相關(guān)權(quán)限殴俱,否則功能無法正常運(yùn)行!")
.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;
}
}
//回調(diào)結(jié)果枚抵,注意线欲,回調(diào)過程可能是從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原理和自己動(dòng)手實(shí)現(xiàn)一個(gè)LRU小Demo實(shí)現(xiàn)了汽摹,其實(shí)緩存不光圖片可以這么干李丰,其他任何大對(duì)象都可以如此實(shí)現(xiàn),知其原理方能運(yùn)用自如逼泣;