相對(duì)于文字來(lái)說(shuō),圖片的表達(dá)更直接、更有沖擊力、更容易吸引用戶的眼球。設(shè)計(jì)師們也理所當(dāng)然的喜歡用圖片來(lái)傳達(dá)信息。但是對(duì)于開發(fā)者來(lái)說(shuō),圖片就意味著大量的內(nèi)存開銷。要想APP在性能上有更好的表現(xiàn),我們必須處理好顯示圖片所需要的每個(gè)環(huán)節(jié)。
(本文出處:http://www.itdecent.cn/p/eadb0ef271b0)
Android中高效的顯示圖片 - 總結(jié)
前面幾篇關(guān)于高效顯示圖片的文章已經(jīng)實(shí)現(xiàn)了一個(gè)三級(jí)緩存、后臺(tái)加載、裁剪大圖的圖片加載框架??蚣艽笾氯鐖D所示,還有部分知識(shí)點(diǎn)圖里沒(méi)有體現(xiàn)(如Activity重建時(shí)利用Fragment保存數(shù)據(jù)),詳細(xì)情況可以查看之前的文章。完整代碼可以點(diǎn)擊代碼下載。
- 計(jì)算合適的加載尺寸,避免內(nèi)存浪費(fèi) (加載大圖);
- 使用后臺(tái)線程將圖片數(shù)據(jù)加載到內(nèi)存中 (非UI線程加載);
- 通過(guò)緩存提高加載后的圖片數(shù)據(jù)的使用率 (圖片緩存);
- 確認(rèn)圖片不再使用后應(yīng)盡快釋放其所占用的內(nèi)存空間。

管理bitmap內(nèi)存
上面第4條之所以沒(méi)有鏈接,是因?yàn)樗褪潜竟?jié)要講述的內(nèi)容。加載圖片時(shí)所申請(qǐng)的內(nèi)存位于哪里,當(dāng)圖片不再使用時(shí)這部分已經(jīng)申請(qǐng)的內(nèi)存能否被其他需要加載的圖片直接復(fù)用,當(dāng)內(nèi)存確實(shí)需要釋放時(shí)又是如何回收的?這些疑問(wèn)都會(huì)在本節(jié)內(nèi)容中找到答案。
隨著Android系統(tǒng)版本的不斷的更新,Android團(tuán)隊(duì)在圖片內(nèi)存管理方面也做了一些優(yōu)化。
- 在Android 2.2 (API level 8)及其以下版本上,垃圾回收線程工作時(shí),APP線程就得暫停,這一特性無(wú)疑會(huì)降低APP的性能。 Android 2.3開始實(shí)現(xiàn)了并發(fā)垃圾回收,這意味著一個(gè)bitmap對(duì)象不再任何被引用持有時(shí),它所占有的內(nèi)存空間會(huì)很快的被回收。
- 在Android 2.3.3 (API level 10)及其以下版本上,bitmap的ARGB數(shù)據(jù)(backing pixel data)是存在native內(nèi)存里的,而bitmap對(duì)象本身是存在Dalvik的堆里的。當(dāng)bitmap對(duì)象不再被引用時(shí),Dalvik的堆里的內(nèi)存可以被垃圾回收期回收,但是native部分的內(nèi)存卻不會(huì)同步被回收。如果需要頻繁的加載很多bitmap到內(nèi)存中,即使Java層已經(jīng)及時(shí)的釋放掉不用bitmap,依舊有可能引起OOM。幸運(yùn)的是從Android 3.0 (API level 11)開始,bitmap的ARGB數(shù)據(jù)和bitmap對(duì)象一起存在Dalvik的堆里了。這樣bitmap對(duì)象和它的ARGB數(shù)據(jù)就可以同步回收了。


不同Android版本對(duì)bitmap內(nèi)存管理方式不同,我們應(yīng)對(duì)癥下藥的來(lá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)會(huì)拋出
"Canvas: trying to use a recycled bitmap"的錯(cuò)誤。所以調(diào)用recycle()方法之前一定要確認(rèn)bitmap不會(huì)再使用了。
下面提供了一個(gè)使用recycle()的代碼示例。我們使用了引用計(jì)數(shù)來(lái)判斷bitmap是否是被顯示或者被緩存。當(dāng)一個(gè)bitmap不再被顯示也沒(méi)有被緩存時(shí)我們就調(diào)用bitmap的recycle()方法來(lái)釋放內(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è)置了這個(gè)字段,bitmap在加載數(shù)據(jù)時(shí)可以復(fù)用這個(gè)字段所指向的bitmap的內(nèi)存空間。新增的這種內(nèi)存復(fù)用的特性,可以優(yōu)化掉因舊bitmap內(nèi)存釋放和新bitmap內(nèi)存申請(qǐng)所帶來(lái)的性能損耗。但是,內(nèi)存能夠復(fù)用也是有條件的。比如,在Android 4.4(API level 19)之前,只有新舊兩個(gè)bitmap的尺寸一樣才能復(fù)用內(nèi)存空間。Android 4.4開始只要舊bitmap的尺寸大于等于新的bitmap就可以復(fù)用了。
下面是bitmap內(nèi)存復(fù)用的代碼示例。大致分兩步:1、不用的bitmap用軟引用保存起來(lái),以備復(fù)用;2、使用前面保存的bitmap來(lái)創(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()會(huì)去廢棄的bitmap中找一個(gè)能夠被復(fù)用的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()方法用來(lái)判斷bitmap是否能夠被復(fù)用。
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;
}
完整代碼可以點(diǎn)擊代碼下載。