日期:
来源:鸿洋收集编辑:layz4android
本文作者
作者:layz4android
链接:
https://juejin.cn/post/7201444843138580517
本文由作者授权发布。
相信伙伴们在日常的开发中,一定对图片加载有所涉猎,而且对于图片加载现有的第三方库也很多,例如Glide、coil等,使用这些三方库我们好像就没有啥担忧的,他们内部的内存管理和缓存策略做的很好,但是一旦在某些场景中无法使用图片加载库,或者项目中没有使用图片加载库而且重构难度大的情况下,对于Bitmap内存的管理就显得尤为重要了,一旦使用出现问题,那么OOM是常有的事。
列表滑动时,内存状态:
通过上面两张图我们可以发现,Java堆区的内存没有变化,但是Native的内存发生了剧烈的抖动,而且伴随着频繁的GC,如果有了解JVM的伙伴,这种情况下必定伴随着应用的卡顿,所以对于Bitmap加载,就要避免频繁地创建和回收,因此本章将会着重介绍Bitmap的内存管理。
1.1 Bitmap内存复用
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
bindBitmap(holder)
}
///sdcard/img.png
private fun bindBitmap(holder: ViewHolder) {
val bitmap = BitmapFactory.decodeFile("/sdcard/img.png")
holder.binding.ivImg.setImageBitmap(bitmap)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
bindBitmap(holder)
}
///sdcard/img.png
private fun bindBitmap(holder: ViewHolder) {
if (option == null) {
option = BitmapFactory.Options()
//开启内存复用
option?.inMutable = true
}
val bitmap = BitmapFactory.decodeFile("/sdcard/img.png", option)
option?.inBitmap = bitmap
holder.binding.ivImg.setImageBitmap(bitmap)
}
1.2 Bitmap压缩
private fun bindBitmap(holder: ViewHolder) {
if (option == null) {
option = BitmapFactory.Options()
//开启内存复用
option?.inMutable = true
}
//在加载到内存之前,获取图片的基础信息
option?.inJustDecodeBounds = true
BitmapFactory.decodeFile("/sdcard/img.png",option)
//获取宽高
val outWidth = option?.outWidth ?: 100
val outHeight = option?.outHeight ?: 100
//计算缩放系数
option?.inSampleSize = calculateSampleSize(outWidth, outHeight, 100, 100)
option?.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile("/sdcard/img.png", option)
option?.inBitmap = bitmap
holder.binding.ivImg.setImageBitmap(bitmap)
}
private fun calculateSampleSize(
outWidth: Int,
outHeight: Int,
maxWidth: Int,
maxHeight: Int
): Int? {
var sampleSize = 1
Log.e("TAG","outWidth $outWidth outHeight $outHeight")
if (outWidth > maxWidth && outHeight > maxHeight) {
sampleSize = 2
while (outWidth / sampleSize > maxWidth && outHeight / sampleSize > maxHeight) {
sampleSize *= 2
}
}
return sampleSize
}
Native内存几乎下降了一半。
2.1 内存缓存
class BitmapImageCache {
private var context: Context? = null
//默认关闭
private var isEnableMemoryCache: Boolean = false
private var isEnableDiskCache: Boolean = false
constructor(builder: Builder) {
this.context = context
this.isEnableMemoryCache = builder.isEnableMemoryCache
this.isEnableDiskCache = builder.isEnableDiskCache
}
class Builder {
var context: Context? = null
//是否开启内存缓存
var isEnableMemoryCache: Boolean = false
//是否开启磁盘缓存
var isEnableDiskCache: Boolean = false
fun with(context: Context): Builder {
this.context = context
return this
}
fun enableMemoryCache(isEnable: Boolean): Builder {
this.isEnableMemoryCache = isEnable
return this
}
fun enableDiskCache(isEnable: Boolean): Builder {
this.isEnableDiskCache = isEnable
return this
}
fun build(): BitmapImageCache {
return BitmapImageCache(this)
}
}
}
class BitmapLruCache(
val size: Int
) : LruCache<String, Bitmap>(size) {
/**
* 告诉系统Bitmap内存的大小
*/
override fun sizeOf(key: String, value: Bitmap): Int {
return value.allocationByteCount
}
/**
* 当Lru中的成员被移除之后,会走到这个回调
* @param oldValue 被移除的Bitmap
*/
override fun entryRemoved(evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap?) {
super.entryRemoved(evicted, key, oldValue, newValue)
}
}
/**
* 当Lru中的成员被移除之后,会走到这个回调
* @param oldValue 被移除的Bitmap
*/
override fun entryRemoved(evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap?) {
if (oldValue.isMutable) {
//放入复用池
reusePool?.add(WeakReference(oldValue))
} else {
//回收即可
oldValue.recycle()
}
}
class BitmapLruCache(
val size: Int,
val reusePool: MutableSet<WeakReference<Bitmap>>?,
val referenceQueue: ReferenceQueue<Bitmap>?
) : LruCache<String, Bitmap>(size) {
/**
* 告诉系统Bitmap内存的大小
*/
override fun sizeOf(key: String, value: Bitmap): Int {
return value.allocationByteCount
}
/**
* 当Lru中的成员被移除之后,会走到这个回调
* @param oldValue 被移除的Bitmap
*/
override fun entryRemoved(evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap?) {
if (oldValue.isMutable) {
//放入复用池
reusePool?.add(WeakReference(oldValue, referenceQueue))
} else {
//回收即可
oldValue.recycle()
}
}
}
/**
* 开启弱引用回收检测,目的为了回收Bitmap
*/
fun startWeakReferenceCheck() {
//开启一个线程
Thread {
try {
while (!shotDown) {
val reference = referenceQueue?.remove()
val bitmap = reference?.get()
if (bitmap != null && !bitmap.isRecycled) {
bitmap.recycle()
}
}
} catch (e: Exception) {
}
}.start()
}
fun putCache(key: String, bitmap: Bitmap) {
lruCache?.put(key, bitmap)
}
fun getCache(key: String): Bitmap? {
return lruCache?.get(key)
}
fun clearCache() {
lruCache?.evictAll()
}
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
bitmapImageCache = BitmapImageCache.Builder()
.enableMemoryCache(true)
.with(this)
.build()
//开启内存检测
bitmapImageCache?.startWeakReferenceCheck()
}
companion object {
@SuppressLint("StaticFieldLeak")
@JvmStatic
var bitmapImageCache: BitmapImageCache? = null
}
}
2023-02-18 17:54:10.154 32517-32517/com.lay.nowinandroid E/TAG: outWidth 800 outHeight 560
2023-02-18 17:54:10.154 32517-32517/com.lay.nowinandroid E/TAG: 没有从缓存中获取
2023-02-18 17:54:10.169 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:10.187 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:16.740 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:16.756 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:16.926 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2023-02-18 17:54:17.102 32517-32517/com.lay.nowinandroid E/TAG: 从缓存中获取 Bitmap
2.2 复用池的处理
/**
* 从复用池中取数据
*/
fun getBitmapFromReusePool(width: Int, height: Int, sampleSize: Int): Bitmap? {
var bitmap: Bitmap? = null
//遍历缓存池
val iterator = reusePool?.iterator() ?: return null
while (iterator.hasNext()) {
val checkedBitmap = iterator.next().get()
if (checkBitmapIsAvailable(width, height, sampleSize, bitmap)) {
bitmap = checkedBitmap
iterator.remove()
//放在
break
}
}
return bitmap
}
/**
* 检查当前Bitmap内存是否可复用
*/
private fun checkBitmapIsAvailable(
width: Int,
height: Int,
sampleSize: Int,
bitmap: Bitmap?
): Boolean {
if (bitmap == null) {
return false
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return width < bitmap.width && height < bitmap.height && sampleSize == 1
}
var realWidth = 0
var realHeight = 0
//支持缩放
if (sampleSize > 1) {
realWidth = width / sampleSize
realHeight = height / sampleSize
}
val allocationSize = realHeight * realWidth * getBitmapPixel(bitmap.config)
return allocationSize <= bitmap.allocationByteCount
}
/**
* 获取Bitmap的像素点位数
*/
private fun getBitmapPixel(config: Bitmap.Config): Int {
return if (config == Bitmap.Config.ARGB_8888) {
4
} else {
2
}
}
//从内存中取
var bitmap = BitmapImageCache.getCache(position.toString())
if (bitmap == null) {
//从复用池池中取
val reuse = BitmapImageCache.getBitmapFromReusePool(100, 100, 1)
Log.e("TAG", "从网络加载了数据")
bitmap = ImageUtils.load(imagePath, reuse)
//放入内存缓存
BitmapImageCache.putCache(position.toString(), bitmap)
} else {
Log.e("TAG", "从内存加载了数据")
}
2023-02-18 21:31:57.805 29198-29198/com.lay.nowinandroid E/TAG: 从网络加载了数据
2023-02-18 21:31:57.819 29198-29198/com.lay.nowinandroid E/TAG: outWidth 800 outHeight 560
2023-02-18 21:31:57.830 29198-29198/com.lay.nowinandroid E/TAG: 加入复用池 android.graphics.Bitmap@6c19c7b
2023-02-18 21:31:57.830 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@473ed07
2023-02-18 21:31:57.849 29198-29198/com.lay.nowinandroid E/TAG: 从网络加载了数据
2023-02-18 21:31:57.857 29198-29198/com.lay.nowinandroid E/TAG: outWidth 788 outHeight 514
2023-02-18 21:31:57.871 29198-29198/com.lay.nowinandroid E/TAG: 加入复用池 android.graphics.Bitmap@2a7844
2023-02-18 21:31:57.872 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@4d852a3
2023-02-18 21:31:57.917 29198-29198/com.lay.nowinandroid E/TAG: 从网络加载了数据
2023-02-18 21:31:57.943 29198-29198/com.lay.nowinandroid E/TAG: outWidth 34 outHeight 8
2023-02-18 21:31:57.958 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@a3d491e
2023-02-18 21:31:58.651 29198-29198/com.lay.nowinandroid E/TAG: 从内存加载了数据
2023-02-18 21:31:58.651 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@62fcf27
2023-02-18 21:31:58.706 29198-29198/com.lay.nowinandroid E/TAG: 从内存加载了数据
2023-02-18 21:31:58.707 29198-29198/com.lay.nowinandroid E/TAG: setImageBitmap android.graphics.Bitmap@e2f8a1a
2023-02-18 21:31:58.766 29198-29198/com.lay.nowinandroid E/TAG: 从内存加载了数据
附录 - ImageUtils
object ImageUtils {
private val MAX_WIDTH = 100
private val MAX_HEIGHT = 100
/**
* 加载本地图片
* @param reuse 可以复用的Bitmap内存
*/
fun load(imagePath: String, reuse: Bitmap?): Bitmap {
val option = BitmapFactory.Options()
option.inMutable = true
option.inJustDecodeBounds = true
BitmapFactory.decodeFile(imagePath, option)
val outHeight = option.outHeight
val outWidth = option.outWidth
option.inSampleSize = calculateSampleSize(outWidth, outHeight, MAX_WIDTH, MAX_HEIGHT)
option.inJustDecodeBounds = false
option.inBitmap = reuse
//新创建的Bitmap复用这块内存
return BitmapFactory.decodeFile(imagePath, option)
}
private fun calculateSampleSize(
outWidth: Int,
outHeight: Int,
maxWidth: Int,
maxHeight: Int
): Int {
var sampleSize = 1
Log.e("TAG", "outWidth $outWidth outHeight $outHeight")
if (outWidth > maxWidth && outHeight > maxHeight) {
sampleSize = 2
while (outWidth / sampleSize > maxWidth && outHeight / sampleSize > maxHeight) {
sampleSize *= 2
}
}
return sampleSize
}
}
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!