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

jopen 11年前發布 | 29K 次閱讀 Cache 緩存組件

應用的場景

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

什么是Cache?

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

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

1/2:Memory Cache內存中的Cache

Memory Cache使用內存來為應用程序提供Cache。由于內存的讀寫速度非常快,所以我們應該優先使用它(相對于下面將介紹的Disk Cache來說)。

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

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

下面我們看一段為Bitmap設置LruCache的代碼

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); }</pre>

提示:在以上代碼中,我們使用了可用內存的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);
}

}</pre>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);

}</pre>提示:由于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的更多性質和使用方法,如果有興趣可以自己查找相關資料。

來自:http://my.oschina.net/rengwuxian/blog/184650

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!