Bitmaps加載之緩存

前文介紹了Bitmaps的異步加載,將單個Bitmap加載到視圖控件是很簡單直接的,但是要同時批量加載的話就會變得復(fù)雜了.對于ListView或GridView或ViewPager來說,由于他們是可以滑動的,所以有多少圖片將要被加載出來就變得不確定.

由于ListView等這種View帶有回收機(jī)制,因此內(nèi)存的使用會保持在較低的水平,但同時由于這個回收機(jī)制,之前已經(jīng)加載的Bitmap會被GC釋放掉,這種方式有個不好的地方是:你滑到一個位置a,然后滑到一個位置b,然后再滑到a,本來a位置的Bitmap已經(jīng)加載完了,但是當(dāng)你滑到b時a位置的Bitmap就被GC回收了,這時a又要重新加載Bitmap.可以想象像ListView這種需要頻繁滑動并且?guī)в泻芏郼hildView的View如果每次都要重新加載的話是多么的耗時耗流量.
因此,需要引入內(nèi)存和磁盤緩存的幫助,才能讓ListView這類型的View快速的加載圖片以支持快速滑動.

使用內(nèi)存緩存

內(nèi)存緩存是以占用app應(yīng)用內(nèi)存為代價來給Bitmap提供快速訪問.LruCache這個非常適用于做內(nèi)存緩存,比如緩存bitmap等,它的內(nèi)部是用一個LinkedHashMap來保存緩存對象的強(qiáng)引用,并使用LRU(最近最少使用)算法來控制隊列的移除以保證不超過設(shè)定的內(nèi)存大小.

注意: 過去LinkedHashMap保存的是引用對象的弱引用或軟引用,但是這現(xiàn)在不推薦,因為Android 2.3(API 9)以后GC變得更加的aggressive,弱引用或軟引用會被回收,導(dǎo)致引用無效.除此之外,在3.0之前有個問題,就是Bitmap的backing date(也是就是真正的字節(jié)數(shù)據(jù),Bitmap只是保存一些信息而已)占據(jù)的內(nèi)存不會被正常釋放,會潛在的導(dǎo)致應(yīng)用因為內(nèi)存溢出而crash.

LruCache使用的時候需要設(shè)置一個大小,如何設(shè)置一個合理的大小呢?可以參考以下幾個因素:

  • 除了你的Activity和application之外,其他的地方使用內(nèi)存情況如何?
  • 一次會有多少張圖片顯示在屏幕牡属?有多少張圖片需要被加載出來等待被顯示?
  • 設(shè)備的屏幕大小和密度是多少仆抵?高密度屏幕的設(shè)備需要更大的cache空間.
  • Bitmaps的尺寸和配置參數(shù)是什么魂莫?它們每個要占多少內(nèi)存?
  • 這些Images放被訪問的頻率如何?有一些訪問比其他頻繁的嗎?可以根據(jù)這個來決定是否將這些對象設(shè)置成常駐內(nèi)存或是使用多個LruCache.
  • 你能平衡好數(shù)量和質(zhì)量嗎?有時只存儲一大推的小圖,然后把大圖放在使用的時候再加載,這種方式在某些情況很有用.

上述提到了這么多的因素,但是實際上并沒有什么具體的最合理的數(shù)值或公式,你需要自己去分析然后確定對于你的app最適合的緩存大小.太小的話不僅無用還帶來而外的計算,太大又會導(dǎo)致app的可用內(nèi)存的減少,容易OOM.

下面看個LruCache的設(shè)置例子:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            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);
}

注意:

  • 上面代碼中給cache分配的大小為app可用總內(nèi)存的1/8,假設(shè)app總內(nèi)存為32M,那么cache的大小為4M,如屏幕分辨率為800x480,滿屏的話需要圖片大小為1.5M左右(8004804 bytes),對于GridView來說,這個cache可以緩存2.5個頁面左右(4/1.5).
  • 注意計算時的單位要統(tǒng)一,即maxMemory的單位和sizeOf()返回的單位要統(tǒng)一,上述的代碼用的是Kbytes.

有了cache之后加載圖片的邏輯變成這樣:
當(dāng)收到圖片加載請求,先去cache中查找是否有緩存,有則直接返回,沒有則返回空接著去請求圖片.

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

    // 1. 去cache中查找
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    
    if (bitmap != null) {
        // 2. cache中有緩存
        mImageView.setImageBitmap(bitmap);
    } else {
        // 3. cache中內(nèi)緩存
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

之前自定義的Task也要更新一下,需要把加載完畢的Bitmap放到cache中

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        // 將加載完畢的bitmap放到cache中
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

使用磁盤緩存

內(nèi)存緩存對于最近使用的Bitmap的快速訪問非常有用,但是這個還是不夠可靠.

  • 像GridView這種帶大量childView的控件很容易就占滿了內(nèi)存緩存.
  • 當(dāng)你的應(yīng)用被其他任務(wù)打斷時,比如來了個電話,你的app就會跑到后臺運行,這樣你的app的進(jìn)程可能會被系統(tǒng)給destroy掉,這樣內(nèi)存緩存也就沒了,而當(dāng)用戶返回時app又要重新加載這些資源.

這個時候磁盤緩存就可以幫忙了,磁盤緩存可以緩存Bitmaps,這樣就可以減少非內(nèi)存緩存圖片的加載速度,當(dāng)然磁盤的訪問速度要比內(nèi)存的來的慢并且加載的時間也不可預(yù)知,因此應(yīng)該使用異步加載.

注意: 如果要緩存經(jīng)常訪問的圖片,可以使用ContentProvider,比如相冊類.

下面的代碼使用了一個DiskLruCache類的引用,來自Android source,具體如下:

// DiskLruCache的引用,算法一樣為LRU
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
// 磁盤緩存初始化標(biāo)志值,true表示還沒初始化
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) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    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];
            // 獲取一個DiskLruCache對象
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // Finished initialization
            mDiskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            // 注意這個方法,與源碼的存入方式不一樣,這里把這個方法當(dāng)作是將bitmap存入磁盤緩存的操作即可
            mDiskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
        // Wait while disk cache is started from background thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

注意:

  • 初始化磁盤緩存要進(jìn)行磁盤操作,因此需要異步.
  • 由于磁盤緩存初始化的異步,從而導(dǎo)致了一種可能的的出現(xiàn): 用戶調(diào)用getBitmapFromDiskCache()來訪問磁盤緩存,然后磁盤緩存還沒創(chuàng)建完畢,因此使用同步鎖來防止這種情況的出現(xiàn).
  • 檢查內(nèi)存緩存是否有請求資源是在UI線程中操作的,而檢查磁盤緩存是在異步操作的.
  • 圖片從網(wǎng)絡(luò)加載完畢之后要放到內(nèi)存緩存和磁盤緩存,以便后續(xù)使用.
  • 上述代碼中有一句: mDiskLruCache.put(key, bitmap);注意這個方法,源碼中沒有這個方法,源碼的存入方式不一樣,這里把這個方法當(dāng)作是將bitmap存入磁盤緩存的操作即可,具體可以參考官方Demo.

處理運行時配置的變更

Handling Runtime Changes,配置的變更如屏幕旋轉(zhuǎn)等,會導(dǎo)致Activity被destroy掉然后restart,所以如果你想要在配置變更的時候還保存內(nèi)存緩存的話,有一個方法可以幫助:
將內(nèi)存緩存引用存到一個設(shè)置了setRetainInstance(true)(該方法會改變Fragment的生命周期)的Fragment里面,在Activity重新創(chuàng)建完畢之后重該fragment里面獲取內(nèi)存緩存的引用,具體看下面代碼:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> mRetainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

要測試的話,可以調(diào)用setRetainInstance()方法,分別傳入true和false,然后旋轉(zhuǎn)屏幕.

Reference:

  1. Caching Bitmaps
  2. 官方DisplayingBitmaps Demo
  3. What's difference between “backing pixel data” and bitmap object?](http://stackoverflow.com/questions/33297471/whats-difference-between-backing-pixel-data-and-bitmap-object)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末慰技,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子抗斤,更是在濱河造成了極大的恐慌投队,老刑警劉巖哆料,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異策添,居然都是意外死亡材部,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門唯竹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乐导,“玉大人,你說我怎么就攤上這事浸颓∥锉郏” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵产上,是天一觀的道長棵磷。 經(jīng)常有香客問我,道長晋涣,這世上最難降的妖魔是什么仪媒? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮谢鹊,結(jié)果婚禮上算吩,老公的妹妹穿的比我還像新娘。我一直安慰自己撇贺,他們只是感情好赌莺,可當(dāng)我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著松嘶,像睡著了一般艘狭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天巢音,我揣著相機(jī)與錄音遵倦,去河邊找鬼。 笑死官撼,一個胖子當(dāng)著我的面吹牛梧躺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播傲绣,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掠哥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秃诵?” 一聲冷哼從身側(cè)響起续搀,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎菠净,沒想到半個月后禁舷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡毅往,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年牵咙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攀唯。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡洁桌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侯嘀,到底是詐尸還是另有隱情战坤,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布残拐,位于F島的核電站途茫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏溪食。R本人自食惡果不足惜囊卜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望错沃。 院中可真熱鬧栅组,春花似錦、人聲如沸枢析。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽醒叁。三九已至司浪,卻和暖如春泊业,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啊易。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工吁伺, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人租谈。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓篮奄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親割去。 傳聞我的和親對象是個殘疾皇子窟却,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容