Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> Android ImageLoader 框架之圖片緩存

Android ImageLoader 框架之圖片緩存

編輯:Android資訊

在Android ImageLoader框架系列博文中,我們從基本架構到具體實現已經更新了大部分的內容。今天,我們來講最後一個關鍵點,即圖片的緩存。為了用戶體驗,通常情況下我們都會將已經下載的圖片緩存起來,一般來說內存和本地都會有圖片緩存。那既然是框架,必然需要有很好的定制性,這讓我們又自然而然的想到了抽象。下面我們就一起來看看緩存的實現吧。

緩存接口

在Android ImageLoader框架之圖片加載與加載策略我們聊到了Loader,然後闡述了AbsLoader的基本邏輯,其中就有圖片緩存。因此AbsLoader中必然含有緩存對象的引用。我們看看相關代碼:

/**
 * @author mrsimple
 */
public abstract class AbsLoader implements Loader {

    /**
     * 圖片緩存
     */
    private static BitmapCache mCache = SimpleImageLoader.getInstance().getConfig().bitmapCache;

    // 代碼省略
}

AbsLoader中定義了一個static的BitmapCache對象,這個就是圖片緩存對象。那為什麼是static呢?因為不管Loader有多少個,緩存對象都應該是共享的,也就是緩存只有一份。說了那麼多,那我們先來了解一下BitmapCache吧。

public interface BitmapCache {

    public Bitmap get(BitmapRequest key);

    public void put(BitmapRequest key, Bitmap value);

    public void remove(BitmapRequest key);

}

BitmapCache很簡單,只聲明了獲取、添加、移除三個方法來操作圖片緩存。這裡有依賴了一個BitmapRequest類,這個類代表了一個圖片加載請求,該類中有該請求對應的ImageView、圖片uri、顯示Config等屬性。在緩存這塊我們主要要使用圖片的uri來檢索緩存中是否含有該圖片,緩存以圖片的uri為key,Bitmap為value來關聯存儲。另外需要BitmapRequest的ImageView寬度和高度,以此來按尺寸加載圖片。

定義BitmapCache接口還是為了可擴展性,面向接口的編程的理念又再一次的浮現在你面前。如果是你,你會作何設計呢?自己寫代碼來練習一下吧,看看自己作何考慮,如果實現,這樣你才會從中有更深的領悟。

內存緩存

既然是框架,那就需要接受用戶各種各樣的需求。但通常來說框架會有一些默認的實現,對於圖片緩存來說內存緩存就其中的一個默認實現,它會將已經加載的圖片緩存到內存中,大大地提升圖片重復加載的速度。內存緩存我們的策略是使用LRU算法,直接使用了support.v4中的LruCache類,相關代碼如下。

/**
 * 圖片的內存緩存,key為圖片的uri,值為圖片本身
 * 
 * @author mrsimple
 */
public class MemoryCache implements BitmapCache {

    private LruCache<String, Bitmap> mMemeryCache;

    public MemoryCache() {

        // 計算可使用的最大內存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 取4分之一的可用內存作為緩存
        final int cacheSize = maxMemory / 4;
        mMemeryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };

    }

    @Override
    public Bitmap get(BitmapRequest key) {
        return mMemeryCache.get(key.imageUri);
    }

    @Override
    public void put(BitmapRequest key, Bitmap value) {
        mMemeryCache.put(key.imageUri, value);
    }

    @Override
    public void remove(BitmapRequest key) {
        mMemeryCache.remove(key.imageUri);
    }

}

就是簡單的實現了BitmapCache接口,然後內部使用LruCache類實現內存緩存。比較簡單,就不做說明了。

sd卡緩存

對於圖片緩存,內存緩存是不夠的,更多的需要是將圖片緩存到sd卡中,這樣用戶在下次進入app時可以直接從本地加載圖片,避免重復地從網絡上讀取圖片數據,即耗流量,用戶體驗又不好。sd卡緩存我們使用了Jake Wharton的DiskLruCache類,我們的sd卡緩存類為DiskCache,代碼如下 :

public class DiskCache implements BitmapCache {

    /**
     * 1MB
     */
    private static final int MB = 1024 * 1024;

    /**
     * cache dir
     */
    private static final String IMAGE_DISK_CACHE = "bitmap";
    /**
     * Disk LRU Cache
     */
    private DiskLruCache mDiskLruCache;
    /**
     * Disk Cache Instance
     */
    private static DiskCache mDiskCache;

    /**
     * @param context
     */
    private DiskCache(Context context) {
        initDiskCache(context);
    }

    public static DiskCache getDiskCache(Context context) {
        if (mDiskCache == null) {
            synchronized (DiskCache.class) {
                if (mDiskCache == null) {
                    mDiskCache = new DiskCache(context);
                }
            }

        }
        return mDiskCache;
    }

    /**
     * 初始化sdcard緩存
     */
    private void initDiskCache(Context context) {
        try {
            File cacheDir = getDiskCacheDir(context, IMAGE_DISK_CACHE);
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            mDiskLruCache = DiskLruCache
                    .open(cacheDir, getAppVersion(context), 1, 50 * MB);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取sd緩存的目錄,如果掛載了sd卡則使用sd卡緩存,否則使用應用的緩存目錄。
     * @param context Context
     * @param uniqueName 緩存目錄名,比如bitmap
     * @return
     */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            Log.d("", "### context : " + context + ", dir = " + context.getExternalCacheDir());
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

        @Override
    public synchronized Bitmap get(final BitmapRequest bean) {
        // 圖片解析器
        BitmapDecoder decoder = new BitmapDecoder() {

            @Override
            public Bitmap decodeBitmapWithOption(Options options) {
                final InputStream inputStream = getInputStream(bean.imageUriMd5);
                Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null,
                        options);
                IOUtil.closeQuietly(inputStream);
                return bitmap;
            }
        };

        return decoder.decodeBitmap(bean.getImageViewWidth(),
                bean.getImageViewHeight());
    }

    private InputStream getInputStream(String md5) {
        Snapshot snapshot;
        try {
            snapshot = mDiskLruCache.get(md5);
            if (snapshot != null) {
                return snapshot.getInputStream(0);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void put(BitmapRequest key, Bitmap value) {
        // 代碼省略 
    }

    public void remove(BitmapRequest key) {
        // 代碼省略
    }

}

代碼比較簡單,也就是實現BitmapCache,然後包裝一下DiskLruCache類的方法實現圖片文件的增加、刪除、獲取方法。這裡給大家介紹一個類,是我為了簡化圖片按ImageView尺寸加載的輔助類,即BitmapDecoder。

BitmapDecoder

BitmapDecoder是一個按ImageView尺寸加載圖片的輔助類,一般我加載圖片的過程是這樣的:
1. 創建BitmapFactory.Options options,設置options.inJustDecodeBounds = true,使得只解析圖片尺寸等信息;
2. 根據ImageView的尺寸來檢查是否需要縮小要加載的圖片以及計算縮放比例;
3. 設置options.inJustDecodeBounds = false,然後按照options設置的縮小比例來加載圖片.

BitmapDecoder類使用decodeBitmap方法封裝了這個過程 ( 模板方法噢 ),用戶只需要實現一個子類,並且覆寫BitmapDecoder的decodeBitmapWithOption實現圖片加載即可完成這個過程(參考DiskCache中的get方法)。代碼如下 :

/**
 * 封裝先加載圖片bound,計算出inSmallSize之後再加載圖片的邏輯操作
 * 
 * @author mrsimple
 */
public abstract class BitmapDecoder {

    /**
     * @param options
     * @return
     */
    public abstract Bitmap decodeBitmapWithOption(Options options);

    /**
     * @param width 圖片的目標寬度
     * @param height 圖片的目標高度
     * @return
     */
    public Bitmap decodeBitmap(int width, int height) {
        // 如果請求原圖,則直接加載原圖
        if (width <= 0 || height <= 0) {
            return decodeBitmapWithOption(null);
        }

        // 1、獲取只加載Bitmap寬高等數據的Option, 即設置options.inJustDecodeBounds = true;
        BitmapFactory.Options options = getJustDecodeBoundsOptions();
        // 2、通過options加載bitmap,此時返回的bitmap為空,數據將存儲在options中
        decodeBitmapWithOption(options);
        // 3、計算縮放比例, 並且將options.inJustDecodeBounds設置為false;
        calculateInSmall(options, width, height);
        // 4、通過options設置的縮放比例加載圖片
        return decodeBitmapWithOption(options);
    }

    /**
     * 獲取BitmapFactory.Options,設置為只解析圖片邊界信息
     */
    private Options getJustDecodeBoundsOptions() {
        //
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 設置為true,表示解析Bitmap對象,該對象不占內存
        options.inJustDecodeBounds = true;
        return options;
    }

    protected void calculateInSmall(Options options, int width, int height) {
        // 設置縮放比例
        options.inSampleSize = computeInSmallSize(options, width, height);
        // 圖片質量
        options.inPreferredConfig = Config.RGB_565;
        // 設置為false,解析Bitmap對象加入到內存中
        options.inJustDecodeBounds = false;
        options.inPurgeable = true;
        options.inInputShareable = true;
    }

    private int computeInSmallSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            // Calculate ratios of height and width to requested height and
            // width
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);

            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
            final float totalPixels = width * height;

            // Anything more than 2x the requested pixels we'll sample down
            // further
            final float totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
                inSampleSize++;
            }
        }
        return inSampleSize;
    }

}

在decodeBitmap中,我們首先創建BitmapFactory.Options對象,並且設置options.inJustDecodeBounds = true,然後第一次調用decodeBitmapWithOption(options),使得只解析圖片尺寸等信息;然後調用calculateInSmall方法,該方法會調用computeInSmallSize來根據ImageView的尺寸來檢查是否需要縮小要加載的圖片以及計算縮放比例,在calculateInSmall方法的最後將 options.inJustDecodeBounds = false,使得下次再次decodeBitmapWithOption(options)時會加載圖片;那最後一步必然就是調用decodeBitmapWithOption(options)啦,這樣圖片就會按照按照options設置的縮小比例來加載圖片了。

我們使用這個輔助類封裝了這個麻煩、重復的過程,在一定程度上簡化了代碼,也使得代碼的可復用性更高,也是模板方法模式的一個較好的示例。

二級緩存

有了內存和sd卡緩存,其實這還不夠。我們的需求很可能就是這個緩存會同時有內存和sd卡緩存,這樣上述兩種緩存的優點我們就會具備,這裡我們把它稱為二級緩存。看看代碼吧,也很簡單。

/**
 * 綜合緩存,內存和sd卡雙緩存
 * 
 * @author mrsimple
 */
public class DoubleCache implements BitmapCache {
    DiskCache mDiskCache;
    MemoryCache mMemoryCache = new MemoryCache();

    public DoubleCache(Context context) {
        mDiskCache = DiskCache.getDiskCache(context);
    }

    @Override
    public Bitmap get(BitmapRequest key) {
        Bitmap value = mMemoryCache.get(key);
        if (value == null) {
            value = mDiskCache.get(key);
            saveBitmapIntoMemory(key, value);
        }
        return value;
    }

    private void saveBitmapIntoMemory(BitmapRequest key, Bitmap bitmap) {
        // 如果Value從disk中讀取,那麼存入內存緩存
        if (bitmap != null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    @Override
    public void put(BitmapRequest key, Bitmap value) {
        mDiskCache.put(key, value);
        mMemoryCache.put(key, value);
    }

    @Override
    public void remove(BitmapRequest key) {
        mDiskCache.remove(key);
        mMemoryCache.remove(key);
    }

}

其實就是封裝了內存緩存和sd卡緩存的相關操作嘛~ 那我就不要再費口舌了

自定義緩存

緩存是有很多實現策略的,既然我們要可擴展性,那就要允許用戶注入自己的緩存實現。只要你實現BitmapCache,就可以將它通過ImageLoaderConfig注入到ImageLoader內部。

    private void initImageLoader() {
        ImageLoaderConfig config = new ImageLoaderConfig()
                .setLoadingPlaceholder(R.drawable.loading)
                .setNotFoundPlaceholder(R.drawable.not_found)
                .setCache(new MyCache())
        // 初始化
        SimpleImageLoader.getInstance().init(config);
    }

MyCache.java

// 自定義緩存實現類
public class MyCache implements BitmapCache {

    // 代碼

    @Override
    public Bitmap get(BitmapRequest key) {
        // 你的代碼
    }

    @Override
    public void put(BitmapRequest key, Bitmap value) {
        // 你的代碼  
    }

    @Override
    public void remove(BitmapRequest key) {
        // 你的代碼
    }

}

Github地址

SimpleImageLoader。

總結

ImageLoader系列到這裡就算結束了,我們從基本架構、具體實現、設計上面詳細的闡述了一個簡單、可擴展性較好的ImageLoader實現過程,希望大家看完這個系列之後能夠自己去實現一遍,這樣你會發現一些具體的問題,領悟能夠更加的深刻。如果你在看這系列博客的過程中,真的能夠從中體會到面向對象的基本原則、設計思考等東西,而不是說”我擦,我又找到了一個可以copy來用的ImageLoader”,那我就覺得我做的這些分享到達目的了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved