Android圖片快取之Lru演算法

總李寫程式碼發表於2016-08-02

前言:

     上篇我們總結了Bitmap的處理,同時對比了各種處理的效率以及對記憶體佔用大小。我們得知一個應用如果使用大量圖片就會導致OOM(out of memory),那該如何處理才能近可能的降低oom發生的概率呢?之前我們一直在使用SoftReference軟引用,SoftReference是一種現在已經不再推薦使用的方式,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的物件,這讓軟引用變得不再可靠,所以今天我們來認識一種新的快取處理演算法Lru,然後學習一下基於Lru的Lrucache、DiskLruCache 實現我們的圖片快取。

 圖片快取相關部落格地址:

Lru:

   LRU是Least Recently Used 的縮寫,翻譯過來就是“最近最少使用”,LRU快取就是使用這種原理實現,簡單的說就是快取一定量的資料,當超過設定的閾值時就把一些過期的資料刪除掉,比如我們快取10000條資料,當資料小於10000時可以隨意新增,當超過10000時就需要把新的資料新增進來,同時要把過期資料刪除,以確保我們最大快取10000條,那怎麼確定刪除哪條過期資料呢,採用LRU演算法實現的話就是將最老的資料刪掉。

基於LruCache實現記憶體快取:

 1.)初始化MemoryCache

  這裡記憶體快取的是Drawable 而不是Bitmap 理由是Drawable相對Bitmap來說有很大的記憶體優勢

        int maxMemory = (int) Runtime.getRuntime().maxMemory();//獲取系統分配給應用的總記憶體大小
        int mCacheSize = maxMemory / 8;//設定圖片記憶體快取佔用八分之一
        mMemoryCache = new LruCache<String, Drawable>(mCacheSize) {
            //必須重寫此方法,來測量Bitmap的大小
            @Override
            protected int sizeOf(String key, Drawable value) {
                if (value instanceof BitmapDrawable) {
                    Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
                    return bitmap == null ? 0 : bitmap.getByteCount();
                }
                return super.sizeOf(key, value);
            }
        };

2.)新增一個Drawable到記憶體快取

  /**
     * 新增Drawable到記憶體快取
     *
     * @param key
     * @param drawable
     */
    private void addDrawableToMemoryCache(String key, Drawable drawable) {
        if (getDrawableFromMemCache(key) == null && drawable != null) {
            mMemoryCache.put(key, drawable);
        }
    }

3.)從記憶體快取中獲取一個Drawable

    /**
     * 從記憶體快取中獲取一個Drawable
     *
     * @param key
     * @return
     */
    public Drawable getDrawableFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

4.)從記憶體快取中移除一個Drawable

   /**
     * 從記憶體快取中移除
     *
     * @param key
     */
    public void removeCacheFromMemory(String key) {
        mMemoryCache.remove(key);
    }

5.)清空記憶體快取

    /**
     * 清理記憶體快取
     */
    public void cleanMemoryCCache() {
        mMemoryCache.evictAll();
    }

其實Lru快取機制本質上就是儲存在一個LinkedHashMap儲存,為了保障插入的資料順序,方便清理。

基於DiskLruCache實現磁碟快取:

   DiskLruCache類並不是谷歌官方實現,需要自行下載,下載地址:https://github.com/JakeWharton/DiskLruCache

  1.)初始化DiskLruCache

       File cacheDir = context.getCacheDir();//指定的是資料的快取地址
        long diskCacheSize = 1024 * 1024 * 30;//最多可以快取多少位元組的資料
        int appVersion = DiskLruUtils.getAppVersion(context);//指定當前應用程式的版本號
        int valueCount = 1;//指定同一個key可以對應多少個快取檔案
        try {
            mDiskCache = DiskLruCache.open(cacheDir, appVersion, valueCount, diskCacheSize);
        } catch (Exception ex) {
        }

2.)寫入一個檔案到磁碟快取

    /**
     * 新增Bitmap到磁碟快取
     *
     * @param key
     * @param value
     */
    private void addBitmapToDiskCache(String key, byte[] value) {
        OutputStream out = null;
        try {
            DiskLruCache.Editor editor = mDiskCache.edit(key);
            if (editor != null) {
                out = editor.newOutputStream(0);
                if (value != null && value.length > 0) {
                    out.write(value);
                    out.flush();
                    editor.commit();
                } else {
                    editor.abort();
                }
            }
            mDiskCache.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            DiskLruUtils.closeQuietly(out);
        }
    }

3.)從磁碟快取中讀取Drawable

    /**
     * 從磁碟快取中獲取一個Drawable
     *
     * @param key
     * @return
     */
    public Drawable getDrawableFromDiskCache(String key) {
        try {
            DiskLruCache.Snapshot snapShot = mDiskCache.get(key);
            if (snapShot != null) {
                InputStream is = snapShot.getInputStream(0);
                Bitmap bitmap = BitmapFactory.decodeStream(is);
                Drawable drawable = DiskLruUtils.bitmap2Drawable(bitmap);
                //從磁碟中讀取到之後 加入記憶體快取
                addDrawableToMemoryCache(key, drawable);
                return drawable;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

4.)從磁碟快取中移除

    /**
     * 從磁碟快取中移除
     *
     * @param key
     */
    public void removeCacheFromDisk(String key) {
        try {
            mDiskCache.remove(key);
        } catch (Exception e) {
        }
    }

5.)清空磁碟快取

    /**
     * 清理磁碟快取
     */
    public void cleanDiskCache() {
        try {
            mDiskCache.delete();
        } catch (Exception e) {
        }
    }

圖片下載過程:

   接下來例項中用到了一點RxJava的知識有不瞭解RxJava的請自行了解一下。

  1.)採用非同步方式操作磁碟快取和網路下載, 記憶體快取可以在主執行緒中操作

   public void disPlay(final ImageView imageView, String imageUrl) {
        //生成唯一key
        final String key = DiskLruUtils.hashKeyForDisk(imageUrl);
        //先從記憶體中讀取
        Drawable drawableFromMemCache = getDrawableFromMemCache(key);
        if (drawableFromMemCache != null) {
            imageView.setImageDrawable(drawableFromMemCache);
            return;
        }
        Observable.just(imageUrl)
                .map(new Func1<String, Drawable>() {
                    @Override
                    public Drawable call(String imageUrl) { // 引數型別 String
                        //從磁碟中讀取
                        Drawable drawableFromDiskCache = getDrawableFromDiskCache(key);
                        if (drawableFromDiskCache != null) {
                            return drawableFromDiskCache;
                        }
                        //網路下載
                        return download(imageUrl); // 返回型別 Drawable
                    }
                })
                .subscribeOn(Schedulers.io()) // 指定 subscribe() 發生在 IO 執行緒
                .observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回撥發生在主執行緒
                .subscribe(new Action1<Drawable>() {
                    @Override
                    public void call(Drawable drawable) { // 引數型別 Drawable
                        imageView.setImageDrawable(drawable);
                    }
                });
    }

2.)下載圖片過程以及處理

 private Drawable download(String imageUrl) {
        HttpURLConnection urlConnection = null;
        ByteArrayOutputStream bos = null;
        InputStream ins = null;
        try {
            final URL url = new URL(imageUrl);
            urlConnection = (HttpURLConnection) url.openConnection();
            ins = urlConnection.getInputStream();
            bos = new ByteArrayOutputStream();
            int b;
            while ((b = ins.read()) != -1) {
                bos.write(b);
            }
            bos.flush();
            byte[] bytes = bos.toByteArray();
            Bitmap bitmap = DiskLruUtils.bytes2Bitmap(bytes);
            String key = DiskLruUtils.hashKeyForDisk(imageUrl);
            Drawable drawable = DiskLruUtils.bitmap2Drawable(bitmap);
            //加入記憶體快取
            addDrawableToMemoryCache(key, drawable);
            //加入磁碟快取
            addBitmapToDiskCache(key, bytes);
            return drawable;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            DiskLruUtils.closeQuietly(bos);
            DiskLruUtils.closeQuietly(ins);
        }
        return null;
    }

 

附上最終圖片快取單例簡單實現全部程式碼以及DiskLruUtils工具類程式碼

ImageLoadManager.java
public class ImageLoadManager {
    private LruCache<String, Drawable> mMemoryCache;//記憶體快取
    private DiskLruCache mDiskCache;//磁碟快取
    private static ImageLoadManager mInstance;//獲取圖片下載單例引用

    /**
     * 構造器
     *
     * @param context
     */
    private ImageLoadManager(Context context) {
        int maxMemory = (int) Runtime.getRuntime().maxMemory();//獲取系統分配給應用的總記憶體大小
        int mCacheSize = maxMemory / 8;//設定圖片記憶體快取佔用八分之一
        mMemoryCache = new LruCache<String, Drawable>(mCacheSize) {
            //必須重寫此方法,來測量Bitmap的大小
            @Override
            protected int sizeOf(String key, Drawable value) {
                if (value instanceof BitmapDrawable) {
                    Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
                    return bitmap == null ? 0 : bitmap.getByteCount();
                }
                return super.sizeOf(key, value);
            }
        };

        File cacheDir = context.getCacheDir();//指定的是資料的快取地址
        long diskCacheSize = 1024 * 1024 * 30;//最多可以快取多少位元組的資料
        int appVersion = DiskLruUtils.getAppVersion(context);//指定當前應用程式的版本號
        int valueCount = 1;//指定同一個key可以對應多少個快取檔案
        try {
            mDiskCache = DiskLruCache.open(cacheDir, appVersion, valueCount, diskCacheSize);
        } catch (Exception ex) {
        }
    }

    /**
     * 獲取單例引用
     *
     * @return
     */
    public static ImageLoadManager getInstance(Context context) {
        ImageLoadManager inst = mInstance;
        if (inst == null) {
            synchronized (RequestManager.class) {
                inst = mInstance;
                if (inst == null) {
                    inst = new ImageLoadManager(context.getApplicationContext());
                    mInstance = inst;
                }
            }
        }
        return inst;
    }

    public void disPlay(final ImageView imageView, String imageUrl) {
        //生成唯一key
        final String key = DiskLruUtils.hashKeyForDisk(imageUrl);
        //先從記憶體中讀取
        Drawable drawableFromMemCache = getDrawableFromMemCache(key);
        if (drawableFromMemCache != null) {
            imageView.setImageDrawable(drawableFromMemCache);
            return;
        }
        Observable.just(imageUrl)
                .map(new Func1<String, Drawable>() {
                    @Override
                    public Drawable call(String imageUrl) { // 引數型別 String
                        //從磁碟中讀取
                        Drawable drawableFromDiskCache = getDrawableFromDiskCache(key);
                        if (drawableFromDiskCache != null) {
                            return drawableFromDiskCache;
                        }
                        //網路下載
                        return download(imageUrl); // 返回型別 Drawable
                    }
                })
                .subscribeOn(Schedulers.io()) // 指定 subscribe() 發生在 IO 執行緒
                .observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回撥發生在主執行緒
                .subscribe(new Action1<Drawable>() {
                    @Override
                    public void call(Drawable drawable) { // 引數型別 Drawable
                        imageView.setImageDrawable(drawable);
                    }
                });
    }


    /**
     * 新增Drawable到記憶體快取
     *
     * @param key
     * @param drawable
     */
    private void addDrawableToMemoryCache(String key, Drawable drawable) {
        if (getDrawableFromMemCache(key) == null && drawable != null) {
            mMemoryCache.put(key, drawable);
        }
    }

    /**
     * 從記憶體快取中獲取一個Drawable
     *
     * @param key
     * @return
     */
    public Drawable getDrawableFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    /**
     * 從磁碟快取中獲取一個Drawable
     *
     * @param key
     * @return
     */
    public Drawable getDrawableFromDiskCache(String key) {
        try {
            DiskLruCache.Snapshot snapShot = mDiskCache.get(key);
            if (snapShot != null) {
                InputStream is = snapShot.getInputStream(0);
                Bitmap bitmap = BitmapFactory.decodeStream(is);
                Drawable drawable = DiskLruUtils.bitmap2Drawable(bitmap);
                //從磁碟中讀取到之後 加入記憶體快取
                addDrawableToMemoryCache(key, drawable);
                return drawable;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 新增Bitmap到磁碟快取
     *
     * @param key
     * @param value
     */
    private void addBitmapToDiskCache(String key, byte[] value) {
        OutputStream out = null;
        try {
            DiskLruCache.Editor editor = mDiskCache.edit(key);
            if (editor != null) {
                out = editor.newOutputStream(0);
                if (value != null && value.length > 0) {
                    out.write(value);
                    out.flush();
                    editor.commit();
                } else {
                    editor.abort();
                }
            }
            mDiskCache.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            DiskLruUtils.closeQuietly(out);
        }
    }

    private Drawable download(String imageUrl) {
        HttpURLConnection urlConnection = null;
        ByteArrayOutputStream bos = null;
        InputStream ins = null;
        try {
            final URL url = new URL(imageUrl);
            urlConnection = (HttpURLConnection) url.openConnection();
            ins = urlConnection.getInputStream();
            bos = new ByteArrayOutputStream();
            int b;
            while ((b = ins.read()) != -1) {
                bos.write(b);
            }
            bos.flush();
            byte[] bytes = bos.toByteArray();
            Bitmap bitmap = DiskLruUtils.bytes2Bitmap(bytes);
            String key = DiskLruUtils.hashKeyForDisk(imageUrl);
            Drawable drawable = DiskLruUtils.bitmap2Drawable(bitmap);
            //加入記憶體快取
            // addDrawableToMemoryCache(key, drawable);
            //加入磁碟快取
            addBitmapToDiskCache(key, bytes);
            return drawable;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            DiskLruUtils.closeQuietly(bos);
            DiskLruUtils.closeQuietly(ins);
        }
        return null;
    }

    /**
     * 從快取中移除
     *
     * @param key
     */
    public void removeCache(String key) {
        removeCacheFromMemory(key);
        removeCacheFromDisk(key);
    }

    /**
     * 從記憶體快取中移除
     *
     * @param key
     */
    public void removeCacheFromMemory(String key) {
        mMemoryCache.remove(key);
    }

    /**
     * 從磁碟快取中移除
     *
     * @param key
     */
    public void removeCacheFromDisk(String key) {
        try {
            mDiskCache.remove(key);
        } catch (Exception e) {
        }
    }

    /**
     * 磁碟快取大小
     *
     * @return
     */
    public long diskCacheSize() {

        return mDiskCache.size();
    }

    /**
     * 記憶體快取大小
     *
     * @return
     */
    public long memoryCacheSize() {

        return mMemoryCache.size();
    }

    /**
     * 關閉磁碟快取
     */
    public void closeDiskCache() {
        try {
            mDiskCache.close();
        } catch (Exception e) {
        }
    }

    /**
     * 清理快取
     */
    public void cleanCache() {
        cleanMemoryCCache();
        cleanDiskCache();
    }

    /**
     * 清理磁碟快取
     */
    public void cleanDiskCache() {
        try {
            mDiskCache.delete();
        } catch (Exception e) {
        }
    }

    /**
     * 清理記憶體快取
     */
    public void cleanMemoryCCache() {
        mMemoryCache.evictAll();
    }
}
ImageLoadManager.java
DiskLruUtils.java
final class DiskLruUtils {

    /**
     * 關閉輸入輸出流
     */
    public static void closeQuietly(/*Auto*/Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (RuntimeException rethrown) {
                throw rethrown;
            } catch (Exception ignored) {
            }
        }
    }

    /**
     * 獲取versionCode
     */
    public static int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }


    public static String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    public static String bytesToHexString(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();
    }

    /**
     * Bitmap → bytes
     */
    public static byte[] bitmap2Bytes(Bitmap bm) {
        if (bm == null) {
            return null;
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
        return baos.toByteArray();
    }

    /**
     * bytes → Bitmap
     */
    public static Bitmap bytes2Bitmap(byte[] bytes) {
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    }


    /**
     * Drawable → Bitmap
     */
    public static Bitmap drawable2Bitmap(Drawable drawable) {
        if (drawable == null) {
            return null;
        }
        // 取 drawable 的長寬
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        // 取 drawable 的顏色格式
        Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
        // 建立對應 bitmap
        Bitmap bitmap = Bitmap.createBitmap(w, h, config);
        // 建立對應 bitmap 的畫布
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, w, h);
        // 把 drawable 內容畫到畫布中
        drawable.draw(canvas);
        return bitmap;
    }

    /*
         * Bitmap → Drawable
         */
    public static Drawable bitmap2Drawable(Bitmap bm) {
        if (bm == null) {
            return null;
        }
        BitmapDrawable bd = new BitmapDrawable(bm);
        bd.setTargetDensity(bm.getDensity());
        return new BitmapDrawable(bm);
    }

}
DiskLruUtils.java

總結:

 以上就是基於Lru圖片快取簡單實現

 

相關文章