注意:在大多數情況下,建議您使用 Glide 程式庫在應用程式中擷取、解碼,並顯示點陣圖。Glide 在處理上述作業,以及其他在 Android 中使用點陣圖和其他圖片的相關任務時,大多能將複雜之處化繁為簡。如要瞭解如何使用和下載 Glide,請造訪 GitHub 的 Glide 存放區。
除了「快取點陣圖」中所述的步驟之外,您還可以採行一些措施進行垃圾收集和點陣圖重複使用。我們會依照您指定的 Android 版本,提供相應的建議策略。此類別包含的 BitmapFun
範例應用程式會說明如何設計應用程式,以在不同的 Android 版本中有效運作。
為輔助說明本課程,以下介紹 Android 強化點陣圖記憶體管理的過程:
- 在 Android 2.2 (API 級別 8) 以下版本中,發生垃圾收集時,應用程式的執行緒就會停止。這會引發延遲現象,進一步也可能造成效能下降。Android 2.3 於是新增了並行垃圾收集;這表示系統不再參照點陣圖後,會立即取回記憶體。
- 在 Android 2.3.3 (API 級別 10) 以下版本中,點陣圖的備份像素資料是儲存在原生記憶體中。這點與點陣圖不同,因為點陣圖是儲存在 Dalvik 堆積中。原生記憶體中的像素資料不會以可預測的方式釋出,而這可能會導致應用程式短暫超出記憶體限制和當機情況。從 Android 3.0 (API 級別 11) 到 Android 7.1 (API 級別 25),像素資料都是與相關點陣圖一起儲存在 Dalvik 堆積中。在 Android 8.0 (API 級別 26) 以上版本中,點陣圖像素資料會儲存在原生堆積中。
以下章節說明如何針對不同的 Android 版本,讓點陣圖記憶體獲得最佳化管理。
管理 Android 2.3.3 以下版本的記憶體
在 Android 2.3.3 (API 級別 10) 以下版本中,建議使用 recycle()
。如果要在應用程式中顯示大量的點陣圖資料,就可能會發生 OutOfMemoryError
錯誤。recycle()
方法可允許應用程式盡快收回記憶體。
注意:只有在確定不再使用點陣圖時,才應使用 recycle()
。如果呼叫 recycle()
之後又嘗試繪製點陣圖,系統就會顯示錯誤訊息:"Canvas: trying to use a recycled bitmap"
。
下列程式碼片段提供呼叫 recycle()
的範例。這會使用參照計數 (在變數 mDisplayRefCount
和 mCacheRefCount
中) 追蹤點陣圖目前正在顯示或是快取中。如果符合以下條件,程式碼就會回收點陣圖:
mDisplayRefCount
和mCacheRefCount
的參考計數都是 0。- 點陣圖不是
null
,且尚未回收。
Kotlin
private var cacheRefCount: Int = 0 private var displayRefCount: Int = 0 ... // Notify the drawable that the displayed state has changed. // Keep a count to determine when the drawable is no longer displayed. fun setIsDisplayed(isDisplayed: Boolean) { synchronized(this) { if (isDisplayed) { displayRefCount++ hasBeenDisplayed = true } else { displayRefCount-- } } // 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. fun setIsCached(isCached: Boolean) { synchronized(this) { if (isCached) { cacheRefCount++ } else { cacheRefCount-- } } // Check to see if recycle() can be called. checkState() } @Synchronized private fun checkState() { // If the drawable cache and display ref counts = 0, and this drawable // has been displayed, then recycle. if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap() ) { getBitmap()?.recycle() } } @Synchronized private fun hasValidBitmap(): Boolean = getBitmap()?.run { !isRecycled } ?: false
Java
private int cacheRefCount = 0; private int displayRefCount = 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) { displayRefCount++; hasBeenDisplayed = true; } else { displayRefCount--; } } // 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) { cacheRefCount++; } else { cacheRefCount--; } } // 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 (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap()) { getBitmap().recycle(); } } private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); }
管理 Android 3.0 以上版本的記憶體
Android 3.0 (API 級別 11) 加入了 BitmapFactory.Options.inBitmap
欄位。如果設定此選項,採用 Options
物件的解碼方法會在載入內容時,嘗試重複使用現有的點陣圖。這表示,系統會重複使用點陣圖的記憶體提升效能,並且移除記憶體配置和取消配置。不過,使用 inBitmap
有一些特定的限制。尤其是在 Android 4.4 (API 級別 19) 之前,系統僅支援大小相同的點陣圖。詳情請參閱 inBitmap
說明文件。
儲存點陣圖以供日後使用
下列程式碼片段說明如何儲存現有點陣圖,以供日後在範例應用程式中使用。如果應用程式是在 Android 3.0 以上版本中執行,而點陣圖從 LruCache
移除,則點陣圖的軟參照會放置在 HashSet
中,以便日後與 inBitmap
一起重複使用:
Kotlin
var reusableBitmaps: MutableSet<SoftReference<Bitmap>>? = null private lateinit var memoryCache: LruCache<String, BitmapDrawable> // If you're running on Honeycomb or newer, create a // synchronized HashSet of references to reusable bitmaps. if (Utils.hasHoneycomb()) { reusableBitmaps = Collections.synchronizedSet(HashSet<SoftReference<Bitmap>>()) } memoryCache = object : LruCache<String, BitmapDrawable>(cacheParams.memCacheSize) { // Notify the removed entry that is no longer being cached. override fun entryRemoved( evicted: Boolean, key: String, oldValue: BitmapDrawable, newValue: BitmapDrawable ) { if (oldValue is RecyclingBitmapDrawable) { // The removed entry is a recycling drawable, so notify it // that it has been removed from the memory cache. 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. reusableBitmaps?.add(SoftReference(oldValue.bitmap)) } } } }
Java
Set<SoftReference<Bitmap>> reusableBitmaps; private LruCache<String, BitmapDrawable> memoryCache; // If you're running on Honeycomb or newer, create a // synchronized HashSet of references to reusable bitmaps. if (Utils.hasHoneycomb()) { reusableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>()); } memoryCache = new LruCache<String, BitmapDrawable>(cacheParams.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. reusableBitmaps.add (new SoftReference<Bitmap>(oldValue.getBitmap())); } } } .... }
使用現有點陣圖
在執行中的應用程式,解碼器方法會檢查是否有可用的現有點陣圖。例如:
Kotlin
fun decodeSampledBitmapFromFile( filename: String, reqWidth: Int, reqHeight: Int, cache: ImageCache ): Bitmap { val options: BitmapFactory.Options = 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) }
Java
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()
方法;這種方法是嘗試尋找現有點陣圖做為 inBitmap
的值。請注意,此方法僅會在找到合適的點陣圖時才設定 inBitmap
值,您的程式碼不應假設為一定會找到合適的點陣圖:
Kotlin
private fun addInBitmapOptions(options: BitmapFactory.Options, cache: ImageCache?) { // inBitmap only works with mutable bitmaps, so force the decoder to // return mutable bitmaps. options.inMutable = true // Try to find a bitmap to use for inBitmap. cache?.getBitmapFromReusableSet(options)?.also { inBitmap -> // 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: fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? { mReusableBitmaps?.takeIf { it.isNotEmpty() }?.let { reusableBitmaps -> synchronized(reusableBitmaps) { val iterator: MutableIterator<SoftReference<Bitmap>> = reusableBitmaps.iterator() while (iterator.hasNext()) { iterator.next().get()?.let { item -> if (item.isMutable) { // Check to see it the item can be used for inBitmap. if (canUseForInBitmap(item, options)) { // Remove from reusable set so it can't be used again. iterator.remove() return item } } else { // Remove from the set if the reference has been cleared. iterator.remove() } } } } } return null }
Java
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 (reusableBitmaps != null && !reusableBitmaps.isEmpty()) { synchronized (reusableBitmaps) { final Iterator<SoftReference<Bitmap>> iterator = reusableBitmaps.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
:
Kotlin
private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean { return 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. val width = ceil((targetOptions.outWidth * 1.0f / targetOptions.inSampleSize).toDouble()).toInt() val height = ceil((targetOptions.outHeight * 1.0f / targetOptions.inSampleSize).toDouble()).toInt() val byteCount: Int = width * height * getBytesPerPixel(candidate.config) byteCount <= candidate.allocationByteCount } else { // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 candidate.width == targetOptions.outWidth && candidate.height == targetOptions.outHeight && targetOptions.inSampleSize == 1 } } /** * A helper function to return the byte usage per pixel of a bitmap based on its configuration. */ private fun getBytesPerPixel(config: Bitmap.Config): Int { return when (config) { Bitmap.Config.ARGB_8888 -> 4 Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2 Bitmap.Config.ALPHA_8 -> 1 else -> 1 } }
Java
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 = (int) Math.ceil(targetOptions.outWidth * 1.0f / targetOptions.inSampleSize); int height = (int) Math.ceil(targetOptions.outHeight * 1.0f / 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; }