LRU分析

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原理图解

image.png

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实现了,其实缓存不光图片可以这么干,其他任何大对象都可以如此实现,知其原理方能运用自如;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容