Android性能——处理Bitmap
大多情况下,一般推荐使用Glide来获取,解码,显示位图。Glide抽象了Android上图片处理这些和其他与位图和其他图像相关的任务的复杂性。
下边整理的几点是Bitmap加载过程中的基础知识点,也会是Glide等图片库中需要解决的问题。
高效加载大图
图片以各种形状,大小展示。多数情况下,图片比用户UI更大。例如,相机拍着的照片像素数一般都比屏幕的分辨率更高。
内存有限的情况下,理想情况只能是在内存中加载加载低分辨率的图片。低分辨率图片应该适合UI。
读取位图大小及类型
BitmapFactory类提供了decodeByteArray(),decodeFile(),decodeResource()等方法来处理来源不同的Bitmap。根据数据源选择合适的decode方法。这些decode方法尝试给结构化Bitmap分配内存,因此容易导致OutOfMemory异常。每个类型的decode方法签名可以通过BitmapFactory.Options来明确解码参数。例如,设置 inJustDecodeBounds 属性值为true来避免deocde时分配内存,方法返回的Bitmap为null,只设置了 outWidth, outHeight 及 outMimeType。这样的方式使得可以通过在不构造Bitmap(不分配内存)前提下读取Bitmap的大小及类型。
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType
例:一张 2560*1440,位深度32的图片,加载进入内存需要14.0625MB。
加载比例缩小版
大小知道后,就可以确定应该直接加载图片进入内存或应该二次抽样。需要考虑的因素:
- 评估加载整张图片需要的内存的大小;
- 考虑到应用的其他内存需求,愿意为加载此次图片所承担的内存大小;
- 加载Image的目标组件大小;
- 设备的屏幕大小及密度;
例如:要在一个120*120的ImageView显示上面加载的2560×1440的图片是是合适的。
这样就需要使用 BitmapFactory.Options 中的 inSampleSize 设置项,设置 inSampleSize 为int的k值,产生原来k分之一大小的图片加载到内存。
例如:2560*1440(ARGB_8888方式存储)的图片加载,设置了 inSampleSize=4,这样加载到内存的大小就是原来的1/4,即 640×360 的图片,内存的占用也变为了0.879MB,与原图加载占用的 14.0625MB 减少了不止一点。
尝试如下代码,考虑到目标大小计算inSampleSize值:
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var sampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while (halfHeight / sampleSize >= reqHeight && halfWidth / sampleSize >= reqWidth) {
sampleSize *= 2
}
}
return sampleSize;
}
还是使用2560*1440(ARGB_8888方式存储) 的目标大小是 113×113 的图片计算 inSampleSize 值为8,在使用 calculateInSampleSize() 方法计算时,inJustDecodeBounds 设置值是true。
在实际使用 inSampleSize 进行图片加载时,需要将 inJustDecodeBounds 设置false。
缓存位图
开发中,加载当单张图片可以直截了当。但很多情况下是一次需要加载多张图片,甚至可能是不限制数量的加载,如:ListView等组件中加载图片。
考虑到屏幕滑动,图片被移出屏幕后会被回收。这里需要考虑缓存。
内存缓存
内存缓存是占用应用内存来快速访问图片。LruCache 类很适合这种缓存场景。
在缓存场景中没有明确的缓存大小标准,根据使用场景及来选择使用合适的大小。若缓存太小,加载大图时可能导致OOM错误。
如下代码是可以用以建立一个内存缓存:
private lateinit var mMemoryCache: LruCache<String, Bitmap>
override fun onCreate(savedInstanceState: Bundle?) {
// memory返回的是字节单位,转换为KB单位
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
// 使用可用内存的1/8作用图片缓存
val cacheSize = maxMemory / 8
mMemoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
// 返回实体数据Bitmap大小,KB单位,与cacheSize一直。
return bitmap.byteCount / 1024
}
}
}
磁盘缓存
内存缓存可以加速对访问过的Bitmap的再次访问,但在需要展示众多图片的场景下,内存缓存是不可靠的,因为垃圾回收会不定时进行回收内存资源。例如: GridView中展示Bitmap,众多图片很快就会占据内存。
因此这种情况下,可以使用磁盘缓存,将处理的图片进行缓存。
如下的代码是从Android Source中提取的。
private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB
private const val DISK_CACHE_SUBDIR = "thumbnails"
...
private var diskLruCache: DiskLruCache? = null
private val diskCacheLock = ReentrantLock()
private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
private var diskCacheStarting = true
override fun onCreate(savedInstanceState: Bundle?) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
InitDiskCacheTask().execute(cacheDir)
...
}
internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
override fun doInBackground(vararg params: File): Void? {
diskCacheLock.withLock {
val cacheDir = params[0]
diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
diskCacheStarting = false // Finished initialization
diskCacheLockCondition.signalAll() // Wake any waiting threads
}
return null
}
}
internal inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
...
// Decode image in background.
override fun doInBackground(vararg params: Int?): Bitmap? {
val imageKey = params[0].toString()
// Check disk cache in background thread
return getBitmapFromDiskCache(imageKey) ?:
// Not found in disk cache
decodeSampledBitmapFromResource(resources, params[0], 100, 100)
?.also {
// Add final bitmap to caches
addBitmapToCache(imageKey, it)
}
}
}
fun addBitmapToCache(key: String, bitmap: Bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap)
}
// Also add to disk cache
synchronized(diskCacheLock) {
diskLruCache?.apply {
if (!containsKey(key)) {
put(key, bitmap)
}
}
}
}
fun getBitmapFromDiskCache(key: String): Bitmap? =
diskCacheLock.withLock {
// Wait while disk cache is started from background thread
while (diskCacheStarting) {
try {
diskCacheLockCondition.await()
} catch (e: InterruptedException) {
}
}
return diskLruCache?.get(key)
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
fun getDiskCacheDir(context: Context, uniqueName: String): File {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
val cachePath =
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
|| !isExternalStorageRemovable()) {
context.externalCacheDir.path
} else {
context.cacheDir.path
}
return File(cachePath + File.separator + uniqueName)
}
管理图片内存
不同Android版本上对bitmap的内存管理是不同的。
- 在Android 2.2(API 8)或更低版本上,垃圾回收开始时,线程会被停止。这导致性能的降低。在Android 2.3中加入了并发垃圾回收机制,这就意味着bitmap不再被引用时内存就会被回收。
- 在Android 2.3.3(API 10)或更低版本上,bitmap的像素数据存储在native内存中。数据与bitmap本身是分离的,bitmap本身存储在Dalvik堆中。native内存中的像素数据是可预见的不会被释放,潜在的问题即是引起应用内存溢出而crash。
- 在Android 3.0(API 11)后至Android 7.1(API 25),像素数据与bitmap一同存储于堆内存中。
- 在Android 8(API 26)及更高版本,bitmap像素数据存储在native堆中。
对sample BitmapFun有兴趣可以阅读BitmapFun源码。