圖片的高效加載(三)緩存


轉自:https://my.oschina.net/rengwuxian/blog/184650

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

 

提示:在以上代碼中,我們使用了可用內存的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的方法就介紹完了。把這套思路使用在你的項目中,用戶體驗會馬上大大增強的。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM