LruCache和DiskCache總結

日常我們寫程序的時候經常會使用到網絡的圖片,如果我們每次都去網上加載,那麼性能難免會差一些,並且網絡情況並不是總是 那麼好,那麼這時候我們就需要使用緩存了,我們學習android都知道圖片的三級緩存,分別是內存緩存,硬盤緩存,網絡緩存。

它的大體流程是這樣的,給定一個網址,加載一張圖片

  1. 如果內存緩存中存在,那就取出來,放上去,如果沒有就找硬盤緩存
  2. 如果硬盤緩存中存在,那就取出來,放上去,並添加到內存緩存中,如果沒有就請求網絡
  3. 請求網絡,把圖片放上去,存一份到內存緩存和硬盤緩存中

接下來我們來分析一下到底是怎麼實現的

一、LruCache

LruCache使用的是LRU算法,也叫最近最少使用算法,就是不斷往裏面存東西,超過上限,把最近最少的對象先淘汰,DiskCache使用的也是該算法。

LruCache的使用很簡單

int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }
};

首先得到程序最大可使用的內存空間,然後計算出內存緩存使用的空間,通常設置爲最大可用內存的八分之一,然後實例化一個LruCache對象,和HashMap一樣,因爲裏面就是用LinkHashMap實現的(之後會講),需要指定鍵值對類型,傳入緩存可用空間大小,並實現sizeof方法,對存入的對象的大小進行計算。

源碼分析

1.構造函數

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

實例化的時候傳入的可用空間大小在構造函數中被賦值給成員變量,並示例化了一個LinkHashMap,起始容量爲0,負載因子爲0.75,並將accessOrder設置爲了true

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

LinkHashMap默認是插入順序的,當把accessOrder設置爲true的時候就變成了訪問順序。

public V get(Object key) {
    LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
    if (e == null)
        return null;
    e.recordAccess(this);
    return e.value;
}

當LinkHashMap調用get方法時會調用recordAccess方法 

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {
        lm.modCount++;
        remove();
        addBefore(lm.header);
    }
}

該方法會將該元素刪除並添加到隊列頭部

2.put方法

public final V put(K key, V value) {
    //不可爲空,否則拋出異常
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }
    V previous;
    synchronized (this) {
        //插入的緩存對象值加1
        putCount++;
        //增加已有緩存的大小
        size += safeSizeOf(key, value);
        //向map中加入緩存對象
        previous = map.put(key, value);
        //如果已有緩存對象,則緩存大小恢復到之前
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    //entryRemoved()是個空方法,可以自行實現
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    //調整緩存大小(關鍵方法)
    trimToSize(maxSize);
    return previous;
}

put方法主要做了三件事,第一計算當前已用空間,第二講對象存入,第三調整緩存

3.trimToSize方法

public void trimToSize(int maxSize) {
    //死循環
    while (true) {
        K key;
        V value;
        synchronized (this) {
            //如果map爲空並且緩存size不等於0或者緩存size小於0,拋出異常
            if (size < 0 || (map.isEmpty() && size != 0)) {
                throw new IllegalStateException(getClass().getName()+ ".sizeOf() is reporting inconsistent results!");
            }
            //如果緩存大小size小於最大緩存,或者map爲空,不需要再刪除緩存對象,跳出循環
            if (size <= maxSize || map.isEmpty()) {
            break;
            }
            //迭代器獲取第一個對象,即隊尾的元素,近期最少訪問的元素
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            //刪除該對象,並更新緩存大小
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }
        entryRemoved(true, key, value, null);
    }
}

在該方法中會不斷取隊尾的元素進行移除操作,直到當前緩存大小小於最大緩存空間大小。

4.get方法

public final V get(K key) {
    //key爲空拋出異常
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        //獲取對應的緩存對象
        //get()方法會實現將訪問的元素更新到隊列頭部的功能
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
    missCount++;
}

get方法很簡單,只是調用LinkHashMap的get方法

二、DiskCache

DIskLruCache類並不在Android SDK中,它並不是由Google官方編寫的,但官方對其進行了推薦

要想使用該類需要進行下載,點擊下載

或者使用Maven下載

<dependency>
  <groupId>com.jakewharton</groupId>
  <artifactId>disklrucache</artifactId>
  <version>2.0.2</version>
</dependency>

或者gradle

compile 'com.jakewharton:disklrucache:2.0.2'

1.創建硬盤緩存對象

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

該方法接收四個參數,第一個參數指定的是數據的緩存地址,第二個參數指定當前應用程序的版本號,第三個參數指定同一個key可以對應多少個緩存文件,基本都是傳1,第四個參數指定最多可以緩存多少字節的數據。

對於不同版本的程序,硬盤緩存會清空之前的存儲,存儲新的緩存

緩存的地址可以使用context.getCacheDir().getPath()得到,當然緩存的地址可以自己隨意指定,但是建議使用該方法獲取,便於程序管理

版本號可以使用context.getPackageManager().getPackageInfo(context.getPackageName(), 0)來獲取

2.寫入緩存

寫入緩存要使用DiskLruCache.Editor這個類

public Editor edit(String key) throws IOException

它需要傳入一個字符串參數,該參數會成爲緩存文件的文件名,並且必須是和圖片的URL的對應的

考慮到有些圖片的URL可能存在一些特殊符號,這樣創建文件的時候可能會出問題,所以你需要一種獨一無二的不會出錯的命名方法,你可以使用MD5編碼

private String hashKeyFromUrl(String url){
    String cacheKey;
    try {
        final MessageDigest  mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = byteToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(url.hashCode());
    }
     return  cacheKey;
}

private String byteToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i ++){
          String hex = Integer.toHexString(0xFF & bytes[i]);//得到十六進制字符串
          if (hex.length() == 1){
             sb.append('0');
          }
          sb.append(hex);
    }
    return  sb.toString();
}

有了editor對象之後便可以調用它的newOutputStream來獲取一個輸出流

OutputStream outputStream = editor.newOutputStream(0);

傳入的0代表的是下標,因爲通常一個key對應着一個文件

得到輸出流之後我們便可以寫入緩存了,最後調用一下commit使寫入生效

private boolean downLoadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        try {
            final URL url   = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream(),8 * 1024);
            bos = new BufferedOutputStream(outputStream,8 * 1024);
            int b ;
            while((b = bis.read())!= -1){
                bos.write(b);
            }
            return  true;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (urlConnection != null){
                urlConnection.disconnect();
            }
            closeIn(bis) ;
            closeOut(bos);
        }
        return   false;
}
if (downLoadUrlToStream(url,outputStream)){
    editor.commit();//提交
}else {
    editor.abort();//取消操作
}

就這樣把緩存寫入了

3.查找緩存

查找緩存同樣也需要把url轉換成md5碼,然後使用DiskLruCache.get方法,得到一個DiskLruCache.Snapshot對象,調用它的getInputStream方法便可以獲取輸入流了,之後可以使用BitmapFactory.decodeStream()來獲取bitmap對象,也可對圖像進行縮放BitmapFactory.decodeFileDescriptor(),最後加入到內存緩存中

Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null){
    FileInputStream fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
    FileDescriptor fileDescriptor = fis.getFD();
    bitmap = imageResizer.decodeBitmapFromFileDescriptor(fileDescriptor,targetWidth,targetHeight);
    if (bitmap != null){
        addBitmapToMemoryCache(key,bitmap);
    }
}

4.移除緩存

同樣非常簡單,傳入url轉換後的key,調用remove方法

public synchronized boolean remove(String key) throws IOException

這裏就不演示了

我們也許會給程序添加一個清楚緩存的功能,那麼就需要使用size方法和delete方法了

size方法可以獲得緩存文件總大小,delete方法可以刪除所有緩存文件

5.journal文件

如果你打開你的緩存路徑,你會發現這個文件,該文件記錄了你對緩存空間的添加刪除讀取操作

libcore.io.DiskLruCache
1
1
1

DIRTY eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa
CLEAN eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa 90642
READ eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa
REMOVE eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa

最上面的是固定的,標示着它使用了DiskLruCache硬盤緩存,下面的第一個1是DisLruCache的版本號,第二個1是應用程序的版本號,第三個1是一個key對應一個文件。

在下面的DIRTY表示正在寫入,後面的操作會阻塞,避免產生髒數據,後面跟的一大串是key,也就是之前用url生成了md5

CLEAN代表幹完了一個,後面可以繼續幹了,後面的數字代表緩存文件的大小,會根據這個調整和獲取緩存大小

READ代表正在讀,Remove代表刪除。

該文件並不會一直變大,在程序中有控制,只要到達2000條記錄就清理一次記錄

三、一個完整的ImageLoader

public class ImageLoader {
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;// 50MB
    private Context mContext;
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private boolean mIsDiskLruCacheCreated = false;//用來標記mDiskLruCache是否創建成功
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT+ 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10;
    private final int DISK_CACHE_INDEX = 0;

    private static final int MESSAGE_POST_RESULT = 101;

    private ImageResizer imageResizer = new ImageResizer();

    private static final ThreadFactory mThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());
        }
    };
    /**
     * 創建線程池
     */
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(),mThreadFactory
    );

    /**
     * 創建Handler
     */
    private Handler mHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == MESSAGE_POST_RESULT){
                LoaderResult loadResult = (LoaderResult) msg.obj;
                ImageView iv = loadResult.iv;
                String url = (String) iv.getTag();
                if (url.equals(loadResult.uri)){//防止加載列表形式時,滑動複用的錯位
                    iv.setImageBitmap(loadResult.bitmap);
                }
            }
        }
    };

    private ImageLoader(Context mContext) {
        this.mContext = mContext.getApplicationContext();
        init();
    }
   /**
     * 創建一個ImageLoader
     */
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }

    
   /**
     * 初始化
     * LruCache<String,Bitmap> mMemoryCache
     * DiskLruCache mDiskLruCache
     */
    private void init() {
        // LruCache<String,Bitmap> mMemoryCache
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                 //return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                return bitmap.getByteCount() / 1024;
            }
        };
        // DiskLruCache mDiskLruCache
        File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

   
    /**
     *  加載原始大小的圖
     */
    public  void bindBitmap(String uri,ImageView iv){
        bindBitmap(uri,iv,0,0);
    }

    /**
     * 異步加載網絡圖片 指定大小
     */
    public void bindBitmap(final String uri, final ImageView iv, final int targetWidth, final int targetHeight){
         iv.setTag(uri);
         Bitmap bitmap = loadBitmapFormMemCache(uri);
        if (bitmap != null){
            iv.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri,targetWidth,targetHeight);
                if (bitmap != null){
                    LoaderResult result = new LoaderResult(iv,uri,bitmap);
                    Message message = mHandler.obtainMessage();
                    message.obj = result;
                    message.what = MESSAGE_POST_RESULT;
                    mHandler.sendMessage(message);
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    /**
     * 同步加載網絡圖片
     */
    private Bitmap loadBitmap(String url, int targetWidth, int targetHeight) {
        Bitmap bitmap = loadBitmapFormMemCache(url);
        if (bitmap != null) {
            return bitmap;
        }
        try {
            bitmap = loadBitmapFromDiskCache(url, targetWidth, targetHeight);
            if (bitmap != null) {
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url, targetWidth, targetHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap == null && !mIsDiskLruCacheCreated) {//緩存文件夾創建失敗
            bitmap = downLoadFromUrl(url);
        }
        return bitmap;
    }
    
    /**
     * 向緩存中添加Bitmap
     */
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 通過key拿到bitmap
     */
    private Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }

    private Bitmap loadBitmapFormMemCache(String url) {
        final String key = hashKeyFromUrl(url);
        return getBitmapFromMemoryCache(key);
    }

    /**
     * 從網絡進行請求
     */
    private Bitmap loadBitmapFromHttp(String url, int targetWidth, int targetHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("UI 線程不能進行網絡訪問");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        String key = hashKeyFromUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downLoadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();//重複操作
            }
            mDiskLruCache.flush();//刷新
        }
        return loadBitmapFromDiskCache(url, targetWidth, targetHeight);
    }

    /**
     * 從硬盤緩存中讀取Bitmap
     */
    private Bitmap loadBitmapFromDiskCache(String url, int targetWidth, int targetHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("硬盤讀取Bitmap在UI線程,UI 線程不進行耗時操作");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFromUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if (snapshot != null) {
            FileInputStream fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fis.getFD();
            bitmap = imageResizer.decodeBitmapFromFileDescriptor(fileDescriptor, targetWidth, targetHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

    /**
     * 將數據請求到流之中
     */
    private boolean downLoadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
            bos = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            closeIn(bis);
            closeOut(bos);
        }
        return false;
    }

    /**
     * 直接通過網絡請求圖片 也不做任何的縮放處理
     */
    private Bitmap downLoadFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream bis = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream());
            bitmap = BitmapFactory.decodeStream(bis);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            closeIn(bis);
        }
        return bitmap;
    }


    /**
     * 得到MD5值key
     */
    private String hashKeyFromUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = byteToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    /**
     * 將byte轉換成16進制字符串
     */
    private String byteToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);//得到十六進制字符串
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }


    /**
     * 得到緩存文件夾
     */
    private File getDiskCacheDir(Context mContext, String uniqueName) {
        //判斷儲存卡是否可以用
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = mContext.getExternalCacheDir().getPath();//儲存卡
        } else {
            cachePath = mContext.getCacheDir().getPath();//手機自身內存
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 得到可用空間大小
     */
    private long getUsableSpace(File file) {
        return file.getUsableSpace();
    }

    /**
     * 關閉輸入流
     */
    private void closeIn(BufferedInputStream in) {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                in = null;
            }
        }
    }

    /**
     * 關閉輸輸出流
     */
    private void closeOut(BufferedOutputStream out) {
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                out = null;
            }
        }
    }

    private static class LoaderResult {
        public ImageView iv ;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView iv, String uri, Bitmap bitmap) {
            this.iv = iv;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章