Bitmap在不同系统版本上的内存管理
在Android 2.2 (API级别8)或更低版本上,当垃圾收集发生时,应用程序的线程将停止。这会导致延迟,从而降低性能。Android 2.3增加了并发垃圾收集,这意味着在不再引用位图之后,内存很快就会被回收。
在Android 2.3.3 (API级别10)或更低版本中,位图的支持像素数据存储在本机内存中。它与位图本身是分开的,位图本身存储在Dalvik堆中。本机内存中的像素数据不会以可预测的方式释放,这可能会导致应用程序短暂地超出其内存限制并崩溃。从Android 3.0 (API level 11)到Android 7.1 (API level 25),像素数据连同相关的位图一起存储在Dalvik堆中。在Android 8.0 (API级别26)及更高版本中,位图像素数据存储在本机堆中。
在Android2.3.3或更低的系统版本上管理内存
在Android 2.3.3 (API级别10)或更低的版本中,建议使用recycle()。如果你在你的应用程序中显示大量的位图数据,你很可能会遇到OutOfMemoryError错误。recycle()方法允许应用程序尽快回收内存。
注意:只有在确定不再使用位图时,才应该使用recycle()。如果您调用recycle()并稍后尝试绘制位图,您将得到错误:“Canvas:试图使用回收的位图”。
下面的代码片段给出了调用recycle()的示例。它使用引用计数(在变量mDisplayRefCount和mCacheRefCount中)来跟踪位图是当前显示还是在缓存中。当满足以下条件时,代码回收位图:
- mDisplayRefCount和mCacheRefCount的引用计数都是0。
- 位图不是空的,它还没有被回收。
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
在Android3.3及以上版本中管理内存
Android 3.0 (API级别11)引入了BitmapFactory.options.inBitmap字段。如果设置了此选项,接受此Options对象的decode方法将尝试在加载内容时重用现有位图。这意味着位图的内存将被重用,从而提高性能,并同时删除内存分配和反分配。然而,如何使用inBitmap有一定的限制。特别是在Android 4.4 (API级别19)之前,只支持相同大小的位图。有关详细信息,请参阅inBitmap文档。
保存位图供以后使用
下面的代码片段演示了现有的位图如何存储可能以后使用的示例应用程序。当一个应用程序运行在Android 3.0或更高版本并且位图被从LruCache中删除,一个位图的软引用就被放在HashSet中,以便以后在inBitmap中重用。
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))
}
}
}
}
使用已存在的Bitmap
在正在运行的应用程序中,解码器方法检查是否存在可用的位图。例如:
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)
}
下一个代码片段显示了在上面代码片段中调用的addInBitmapOptions()方法。它查找要设置为inBitmap值的现有位图。注意,这个方法只有在找到合适的匹配项时才会为inBitmap设置一个值(你的代码永远不要假设会找到匹配项):
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
}
最后,该方法确定候选位图是否满足inBitmap使用的尺寸标准:
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: Int = targetOptions.outWidth / targetOptions.inSampleSize
val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
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
}
}