相對于文字來說殉摔,圖片的表達更直接、更有沖擊力舵变、更容易吸引用戶的眼球酣溃。設(shè)計師們也理所當然的喜歡用圖片來傳達信息。但是對于開發(fā)者來說纪隙,圖片就意味著大量的內(nèi)存開銷赊豌。要想APP在性能上有更好的表現(xiàn),我們必須處理好顯示圖片所需要的每個環(huán)節(jié)绵咱。
(本文出處:http://www.reibang.com/p/eadb0ef271b0)
Android中高效的顯示圖片 - 總結(jié)
前面幾篇關(guān)于高效顯示圖片的文章已經(jīng)實現(xiàn)了一個三級緩存碘饼、后臺加載、裁剪大圖的圖片加載框架悲伶“眨框架大致如圖所示,還有部分知識點圖里沒有體現(xiàn)(如Activity重建時利用Fragment保存數(shù)據(jù))拢切,詳細情況可以查看之前的文章蒂萎。完整代碼可以點擊代碼下載。
- 計算合適的加載尺寸淮椰,避免內(nèi)存浪費 (加載大圖)五慈;
- 使用后臺線程將圖片數(shù)據(jù)加載到內(nèi)存中 (非UI線程加載)纳寂;
- 通過緩存提高加載后的圖片數(shù)據(jù)的使用率 (圖片緩存);
- 確認圖片不再使用后應盡快釋放其所占用的內(nèi)存空間泻拦。
管理bitmap內(nèi)存
上面第4條之所以沒有鏈接毙芜,是因為它就是本節(jié)要講述的內(nèi)容。加載圖片時所申請的內(nèi)存位于哪里争拐,當圖片不再使用時這部分已經(jīng)申請的內(nèi)存能否被其他需要加載的圖片直接復用腋粥,當內(nèi)存確實需要釋放時又是如何回收的?這些疑問都會在本節(jié)內(nèi)容中找到答案架曹。
隨著Android系統(tǒng)版本的不斷的更新隘冲,Android團隊在圖片內(nèi)存管理方面也做了一些優(yōu)化。
- 在Android 2.2 (API level 8)及其以下版本上绑雄,垃圾回收線程工作時展辞,APP線程就得暫停,這一特性無疑會降低APP的性能万牺。 Android 2.3開始實現(xiàn)了并發(fā)垃圾回收罗珍,這意味著一個bitmap對象不再任何被引用持有時,它所占有的內(nèi)存空間會很快的被回收脚粟。
- 在Android 2.3.3 (API level 10)及其以下版本上覆旱,bitmap的ARGB數(shù)據(jù)(backing pixel data)是存在native內(nèi)存里的,而bitmap對象本身是存在Dalvik的堆里的核无。當bitmap對象不再被引用時扣唱,Dalvik的堆里的內(nèi)存可以被垃圾回收期回收,但是native部分的內(nèi)存卻不會同步被回收团南。如果需要頻繁的加載很多bitmap到內(nèi)存中画舌,即使Java層已經(jīng)及時的釋放掉不用bitmap,依舊有可能引起OOM已慢。幸運的是從Android 3.0 (API level 11)開始,bitmap的ARGB數(shù)據(jù)和bitmap對象一起存在Dalvik的堆里了霹购。這樣bitmap對象和它的ARGB數(shù)據(jù)就可以同步回收了佑惠。
不同Android版本對bitmap內(nèi)存管理方式不同,我們應對癥下藥的來優(yōu)化不同版本上bitmap的內(nèi)存使用齐疙。
Android 2.3.3 (API level 10)及其以下版本
在Android 2.3.3 (API level 10)及其以下版本上膜楷,Android開發(fā)文檔推薦我們使用 recycle()
方法。recycle()方法可以使APP盡可能快的回收bitmap所使用的native內(nèi)存贞奋。
注意:recycle()方法是不可逆的赌厅,bitmap調(diào)用了recycle()之后就不能再使用了。使用recycle()之后的bitmap系統(tǒng)會拋出
"Canvas: trying to use a recycled bitmap"
的錯誤轿塔。所以調(diào)用recycle()方法之前一定要確認bitmap不會再使用了特愿。
下面提供了一個使用recycle()的代碼示例仲墨。我們使用了引用計數(shù)來判斷bitmap是否是被顯示或者被緩存。當一個bitmap不再被顯示也沒有被緩存時我們就調(diào)用bitmap的recycle()方法來釋放內(nèi)存揍障。
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable has been displayed, then recycle.
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) {
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
Android 3.0 (API level 11)及其以上版本
Android 3.0 開始引入了BitmapFactory.Options.inBitmap
字段目养。如果設(shè)置了這個字段,bitmap在加載數(shù)據(jù)時可以復用這個字段所指向的bitmap的內(nèi)存空間毒嫡。新增的這種內(nèi)存復用的特性癌蚁,可以優(yōu)化掉因舊bitmap內(nèi)存釋放和新bitmap內(nèi)存申請所帶來的性能損耗。但是兜畸,內(nèi)存能夠復用也是有條件的努释。比如,在Android 4.4(API level 19)之前咬摇,只有新舊兩個bitmap的尺寸一樣才能復用內(nèi)存空間伐蒂。Android 4.4開始只要舊bitmap的尺寸大于等于新的bitmap就可以復用了。
下面是bitmap內(nèi)存復用的代碼示例菲嘴。大致分兩步:1饿自、不用的bitmap用軟引用保存起來,以備復用龄坪;2昭雌、使用前面保存的bitmap來創(chuàng)建新的bitmap。
- 保存廢棄的bitmap
Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
mReusableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
@Override
protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable.
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later.
mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}
- 使用現(xiàn)有的廢棄bitmap創(chuàng)建新的bitmap
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// If we're running on Honeycomb or newer, try to use inBitmap.
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
上面代碼片段中使用的addInBitmapOptions()
會去廢棄的bitmap中找一個能夠被復用的bitmap設(shè)置到inBitmap字段健田。
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
// inBitmap only works with mutable bitmaps, so force the decoder to return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found, set it as the value of inBitmap.
options.inBitmap = inBitmap;
}
}
}
// This method iterates through the reusable bitmaps, looking for one to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
synchronized (mReusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again.
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
}
return bitmap;
}
canUseForInBitmap()
方法用來判斷bitmap是否能夠被復用烛卧。
static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
// the new bitmap is smaller than the reusable bitmap candidate allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1;
}
/**
* A helper function to return the byte usage per pixel of a bitmap based on its configuration.
*/
static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}
完整代碼可以點擊代碼下載。