在 Android 應用中加載位圖很棘手主要有以下幾個原因:
位圖很容易耗盡應用程序的內(nèi)存。例如,Pixel 手機上的相機拍攝的照片最高可達 4048 x 3036 像素(1200 萬像素)。如果使用的位圖配置是ARGB_8888,則默認為 Android 2.3(API 級別 9)及更高版本,加載單張照片進入內(nèi)存需要大約 48 MB 的內(nèi)存(4048 * 3036 * 4 字節(jié))。如此大的內(nèi)存需求可以立即耗盡應用程序可用的所有內(nèi)存。
在 UI 線程上加載位圖會降低應用程序的性能,導致響應速度慢甚至 ANR。因此,在使用位圖時適當?shù)毓芾砭€程非常重要。
如果你的應用程序正在將多個位圖加載到內(nèi)存中,則需要有技巧地管理內(nèi)存和磁盤緩存。否則,應用程序 UI 的響應性和流暢性可能會受到影響。
多數(shù)情況下建議使用 Glide 庫來獲取,解碼和顯示應用中的位圖。其他受歡迎的圖像加載庫還有 Square 的 Picasso 和 Facebook 的 Fresco。這些庫簡化了與 Android 上的位圖和其他類型圖像相關的大多數(shù)復雜任務。你還可以選擇直接使用 Android 框架中內(nèi)置的低級 API。有關執(zhí)行此操作的詳細信息,請參閱 Loading Large Bitmaps Efficiently,Caching Bitmaps,和 Managing Bitmap Memory。
一、高效加載大位圖 — Loading Large Bitmaps Efficiently
下面介紹如何通過在內(nèi)存中加載較小的子采樣版本來解碼大型位圖,而不會超出每個應用程序的內(nèi)存限制。
1.1 讀取位圖尺寸和類型 — Read Bitmap Dimensions and Type
BitmapFactory 類提供了幾種解碼方法(decodeByteArray()
,decodeFile()
,decodeResource()
等),用于從各種源創(chuàng)建位圖。根據(jù)圖像數(shù)據(jù)源選擇最合適的解碼方法。這些方法嘗試為構(gòu)造的位圖分配內(nèi)存,因此很容易導致 OutOfMemory 異常。每種類型的解碼方法都有其他簽名,可讓你通過 BitmapFactory.Options
類指定解碼選項。解碼時將 inJustDecodeBounds
屬性設置為 true 可避免內(nèi)存分配,為位圖對象返回 null 但設置 outWidth,outHeight 和 outMimeType。此技術允許你在構(gòu)造(和內(nèi)存分配)位圖之前讀取圖像數(shù)據(jù)的尺寸和類型。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
要避免 java.lang.OutOfMemory
異常,請在解碼之前檢查位圖的尺寸,除非你完全信任該源為你提供可預測大小的圖像數(shù)據(jù),這些數(shù)據(jù)可以輕松地放入可用內(nèi)存中。
1.2 將縮小版本加載到內(nèi)存中 — Load a Scaled Down Version into Memory
既然圖像尺寸已知,它們可用于決定是否應將完整圖像加載到內(nèi)存中,或者是否應加載子采樣版本。以下是需要考慮的一些因素:
估計在內(nèi)存中加載完整圖像的內(nèi)存使用情況。
根據(jù)應用程序的其他內(nèi)存要求,你愿意加載此圖片的內(nèi)存量。
要加載圖像的目標 ImageView 或 UI 組件的尺寸。
當前設備的屏幕尺寸和密度。
例如,如果最終將在 ImageView 中以 128 x 96 像素的縮略圖顯示,則不值得將 1024 x 768 像素圖像加載到內(nèi)存中。
要告訴解碼器對圖像進行子采樣,將較小的版本加載到內(nèi)存中,請在 BitmapFactory.Options
對象中將 inSampleSize
設置為 true。例如,使用 inSampleSize
為 4 解碼的分辨率為 2048 x 1536 的圖像會產(chǎn)生大約 512 x 384 的位圖。將其加載到內(nèi)存中對于完整圖像使用 0.75 MB 而不是 12 MB(假設 ARGB_8888
的位圖配置)。以下是一種根據(jù)目標寬度和高度計算樣本大小值的方法,該值為 2 的冪:
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
注意:計算 2 的冪是因為解碼器使用最終值,通過向下舍入到最接近的 2 的冪,根據(jù) inSampleSize 文檔。
要使用此方法,首先將 inJustDecodeBounds
設置為 true 進行解碼,傳遞選項然后使用新的 inSampleSize
值再次解碼,并將 inJustDecodeBounds
設置為 false
:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
此方法可以輕松地將任意大尺寸的位圖加載到顯示 100 x 100 像素縮略圖的 ImageView 中,如以下示例代碼所示:
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
你可以按照類似的過程解碼來自其他來源的位圖,方法是根據(jù)需要替換相應的 BitmapFactory.decode*
方法。
二、管理位圖內(nèi)存 — Managing Bitmap Memory
除了緩存位圖中描述的步驟之外,你還可以執(zhí)行一些特定操作來促進垃圾回收和位圖重用。推薦的策略取決于你的目標 Android 版本。以下內(nèi)容將向你展示如何設計應用程序以在不同版本的 Android 中高效工作。
為了之后更好的說明,我們先看看 Android 對位圖內(nèi)存管理的演變過程:
在 Android 2.2(API 級別 8)及更低版本中,當垃圾回收發(fā)生時,你的應用程序的線程會停止。這會導致延遲,從而降低性能。Android 2.3 添加了并發(fā)垃圾回收,這意味著當不再引用位圖時很快就會回收內(nèi)存。
在 Android 2.3.3(API 級別 10)及更低版本中,位圖的像素數(shù)據(jù)存儲在 native 內(nèi)存中。它與位圖本身是分開的,位圖本身存儲在 Dalvik 堆中。native 內(nèi)存中的像素數(shù)據(jù)不會以可預測的方式釋放,這可能導致應用程序短暫超出其內(nèi)存限制并崩潰。從 Android 3.0(API 級別 11)到 Android 7.1(API 級別 25),像素數(shù)據(jù)與相關位圖一起存儲在 Dalvik 堆上。在 Android 8.0(API 級別 26)及更高版本中,位圖像素數(shù)據(jù)存儲在 native 堆中。
以下部分介紹如何針對不同的 Android 版本優(yōu)化位圖內(nèi)存管理。
2.1 在 Android 2.3.3 和更低版本上管理內(nèi)存 — Manage Memory on Android 2.3.3 and Lower
在 Android 2.3.3(API 級別 10)及更低版本上,建議使用 recycle()
。如果你在應用中顯示大量位圖數(shù)據(jù),則可能會遇到 OutOfMemoryError 錯誤。recycle()
方法允許應用程序盡快回收內(nèi)存。
警告:只有在確定不再使用位圖時才應使用
recycle()
。如果你調(diào)用recycle()
并稍后嘗試繪制該位圖,將收到錯誤:"Canvas: trying to use a recycled bitmap"
。
以下代碼片段給出了調(diào)用 recycle()
的示例。它使用引用計數(shù)(在變量 mDisplayRefCount 和 mCacheRefCount 中)來跟蹤當前位圖是正在顯示還是在緩存中。當滿足以下條件時,代碼會回收位圖:
mDisplayRefCount 和 mCacheRefCount 的引用計數(shù)均為 0。
位圖不為 null 且尚未回收。
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();
}
2.2 在 Android 3.0 及更高版本上管理內(nèi)存 — Manage Memory on Android 3.0 and Higher
Android 3.0(API 級別 11)引入了 BitmapFactory.Options.inBitmap
字段。如果設置了此選項,則使用 Options 對象的解碼方法將在加載內(nèi)容時嘗試重用現(xiàn)有位圖。這意味著重用了位圖的內(nèi)存,從而提高了性能,并省去了內(nèi)存分配和回收的步驟。但是,使用 inBitmap 有一些限制。比如說,在 Android 4.4(API 級別 19)之前,僅支持相同大小的位圖。
2.2.1 保存位圖供以后使用 — Save a bitmap for later use
以下代碼段演示了如何存儲現(xiàn)有位圖以便以后使用。當應用程序在 Android 3.0 或更高版本上運行并且位圖從 LruCache 中移出時,可以把位圖的軟引用放置在 HashSet 中,以便稍后可以在 inBitmap 中重用:
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()));
}
}
}
....
}
2.2.2 使用現(xiàn)有位圖 — Use an existing bitmap
在正在運行的應用程序中,decoder 方法檢查是否存在可以使用的現(xiàn)有位圖。例如:
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);
}
以下代碼段顯示了上述代碼段中調(diào)用的 addInBitmapOptions()
方法。它查找現(xiàn)有位圖以設置為 inBitmap 的值。請注意,如果找到合適的匹配項,此方法僅為 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;
}
最后,使用以下方法確定候選位圖是否滿足用于 inBitmap 的大小標準:
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;
}