高效使用Bitmaps(三) 神奇的Cache

應用的場景 html

假設你開發了一個聊天程序,它的好友列表中顯示從網絡獲取的好友頭像。但是若是用戶發現每次進入好友列表的時候,程序都要從新下載頭像才能進行顯示,甚至當把列表滑動到底部再從新滑動回頂部的時候,剛纔已經加載完成了的頭像居然又變成了空白圖片開始從新加載,這將是一種糟糕的用戶體驗。爲了解決這種問題,你須要使用高速緩存技術——Cache。 java

什麼是Cache? android

Cache,高速緩存,原意是指計算機中一塊比內存更高速容量更小的存儲器。更廣義地說,Cache指對於最近使用過的信息的可高速讀取的存儲塊。而本文要講的Cache技術,指的就是將最近使用過的Bitmap緩存在手機的內存與磁盤中,來實現再次使用Bitmap時的瞬時加載,以節省用戶的時間和手機流量。 緩存

下面將針對Android中的兩種Cache類型Memory Cache和Disk Cache分別進行介紹。樣例代碼取自Android開發者站 網絡

1/2:Memory Cache內存中的Cache 併發

Memory Cache使用內存來爲應用程序提供Cache。因爲內存的讀寫速度很是快,因此咱們應該優先使用它(相對於下面將介紹的Disk Cache來講)。 ide

Android中提供了LruCache類來進行Memory Cache的管理(該類是在Android 3.1時推出的,但咱們可使用android -support-v4.jar的兼容包來對低版本的手機提供支持)。 this

提示:有人習慣使用SoftReference和WeakReference來作Memory Cache,但谷歌官方不建議這麼作。由於自從Android2.3以後,Android中的GC變得更加積極,致使這種作法中緩存的Bitmaps很是容易被回收掉;另外,在Android3.0以前,Bitmap的數據是直接分配在native memory中,它的釋放是不受dalvik控制的,所以更容易致使內存的溢出。若是你喜歡簡單粗暴的總結,那就是:反正不要用這種方法來管理Memory Cache。 google

下面咱們看一段爲Bitmap設置LruCache的代碼 spa

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 獲取虛擬機可用內存(內存佔用超過該值的時候,將報OOM異常致使程序崩潰)。最後除以1024是爲了以kb爲單位
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // 使用可用內存的1/8來做爲Memory Cache
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // 重寫sizeOf()方法,使用Bitmap佔用內存的kb數做爲LruCache的size
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

提示:在以上代碼中,咱們使用了可用內存的1/8來提供給Memory Cache,咱們簡單分析一下這個值。一個普通屏幕尺寸、hdpi的手機的可用內存爲32M,那麼他的Memory Cache爲32M/8=4M。一般hdpi的手機爲480*800像素,它一個全屏Bitmap佔用內存爲480*800*4B=1536400B≈1.5M。那麼4M的內存爲大約2.5個屏幕大小的bitmap提供緩存。同理,一個普通尺寸、xhdpi大小的720*1280的手機能夠爲大約2.2個屏幕大小的bitmap提供緩存。

當一個ImageView須要設置一個bitmap的時候,LruCache會進行檢查,若是它已經緩存了相應的bitmap,它就直接取出來並設置給這個ImageView;不然,他將啓動一個後臺線程加載這個Bitmap

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}
BitmapWorkerTask在加載完成後,經過前面的addBitmapToMemoryCache()方法把這個bitmap進行緩存:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 後臺加載Bitmap
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

2/2:Disk Cache(磁盤中的Cache)

前面已經提到,Memory Cache的優勢是讀寫很是快。但它的缺點就是容量過小了,並且不能持久化,因此在用戶在滑動GridView時它很快會被用完,並且切換多個界面時或者是關閉程序從新打開後,再次進入原來的界面,Memory Cache是無能爲力的。這個時候,咱們就要用到Disk Cache了。

Disk Cache將緩存的數據放在磁盤中,所以不論用戶是頻繁切換界面,仍是關閉程序,Disk Cache是不會消失的。

實際上,Android SDK中並無一個類來實現Disk Cache這樣的功能。但google其實已經提供了實現代碼:DiskLruCache。咱們只要把它搬到本身的項目中就能夠了。

下面請看一段使用DiskLruCache來配合Memory Cache進行圖片緩存的代碼

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 初始化memory cache
    ...
    // 開啓後臺線程初始化disk cache
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (mDiskCacheLock) {
            File cacheDir = params[0];
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // 初始化完成
            mDiskCacheLock.notifyAll(); // 喚醒被hold住的線程
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 在後臺加載圖片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // 經過後臺線程檢查disk cache
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // 若是沒有在disk cache中發現這個bitmap
            // 加載這個bitmap
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // 把這個bitmap加入cache
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // 把bitmap加入memory cache
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // 一樣,也加入disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // 等待disk cache初始化完畢
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// 在自帶的cache目錄下創建一個獨立的子目錄。優先使用外置存儲。但若是外置存儲不存在,使用內置存儲。
public static File getDiskCacheDir(Context context, String uniqueName) {
    // 若是MEDIA目錄已經掛載或者外置存儲是手機自帶的(Nexus設備都這麼幹),使用外置存儲;不然使用內置存儲
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}
提示:因爲disk cache的初始化是耗時操做,因此這個過程被放在了後臺進程。而由此致使的結果是,主線程有可能在它初始化完成以前就嘗試讀取disk cache,這會致使程序出錯。所以以上代碼中使用了synchronized關鍵字和一個lock對象來確保在初始化完成以前disk cache不會被訪問。(什麼是synchronized?文章最後會有介紹)

上面這段代碼看起來比較多,但大體讀一下就會發現,它的思路很是簡單:1.讀取cache的時候,優先讀取memory cache,讀不到的時候再讀取disk cache;2.把bitmap保存到cache中的時候,memory cache和disk cache都要保存。

至此,使用Cache來緩存Bitmap的方法就介紹完了。把這套思路使用在你的項目中,用戶體驗會立刻大大加強的。


延伸:什麼是synchronized?

概念:爲了防止多個後臺併發線程同時對同一個對象進行寫操做時發成錯誤,java使用synchronized關鍵字對一個對象「加鎖」,以保證同時只有一個線程能夠訪問該對象。

舉個例子:快過年了,咱倆去火車站買回家的火車票,我在1號窗口,你在2號窗口,而且咱倆同時排隊到了窗戶跟前。巧的是,咱倆買的是同一趟車,而這趟車如今只剩一張票了。而後咱倆都跟售票員說:就這張了,買!因而兩個售票員同時點擊了電腦上的「出票」按鈕。後臺系統接到兩個請求,兩個線程同時進行處理,執行了這麼兩行代碼:

if (tickedCount > 0) { // 若是還有票
    tickedCount -= 1; // 票數減一
    printTicket(); // 出票
}

線程1和線程2幾乎同時運行,而且幾乎同時執行到第一行代碼,線程1一看,哦還有票,行,出票吧!而後執行了第二行代碼,票數減一。但它不知道,在他執行第二行代碼以前,線程2也執行到了第一行,這線程2也一看,哦還有票,行,出票吧!因而在線程1出票以後,線程2在已經沒票的狀況下依然把票數減到了-1,而且執行printTicket()方法嘗試出票。到了這裏,程序究竟是會報錯仍是會出兩張同樣的票已經不重要,重要的是:系統出問題了,它作了不應作的事。

那麼怎麼解決呢?很簡單,加鎖:

synchronized(this) {
    if (tickedCount > 0) { // 若是還有票
        tickedCount -= 1; // 票數減一
        printTicket(); // 出票
    }
}
上面這段代碼因爲加了鎖,致使同一時間只有一個線程能夠進入這個代碼塊,當一個線程進入後,其餘線程必須等這個線程執行完這段代碼後釋放了鎖,才能進入這個代碼塊。這樣,同時出同一張票的bug就不可能出現了。固然,我只是舉例,上面的代碼只是一個簡化模型。

因爲篇幅限制,沒法詳細地介紹synchronized的更多性質和使用方法,若是有興趣能夠本身查找相關資料。

相關文章
相關標籤/搜索