相信同伴们在日常的开发中,一定对图片加载有所涉猎,并且关于图片加载现有的第三方库也许多,例如Glide、coil等,运用这些三方库咱们好像就没有啥忧虑的,他们内部的内存办理和缓存战略做的很好,可是一旦在某些场景中无法运用图片加载库,或许项目中没有运用图片加载库并且重构难度大的情况下,关于Bitmap内存的办理就显得尤为重要了,一旦运用呈现问题,那么OOM是常有的事。

在Android 8.0之后,Bitmap的内存分配从Java堆搬运到了Native堆中,所以咱们能够经过Android profiler性能检测工具查看内存运用情况。

未经过内存办理,列表滑动前内存状况:

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?

列表滑动时,内存状况:

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?

经过上面两张图咱们能够发现,Java堆区的内存没有改变,可是Native的内存发生了剧烈的颤动,并且伴随着频频的GC,假如有了解JVM的同伴,这种情况下必定伴随着应用的卡顿,所以关于Bitmap加载,就要防止频频地创立和收回,因而本章将会着重介绍Bitmap的内存办理。

1 Bitmap“整容”

首要咱们需求清晰一点,既然是内存办理,莫非只是对图片紧缩确保不会OOM吗?其实不是的,内存办理一定是多面多点的,紧缩是一方面,为什么起标题为“整容”,是由于终究加载到内存的Bitmap一定不是单纯地经过decodeFile就能完成的。

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)
}

假如熟悉RecyclerView的缓存机制应该了解,当RecyclerView的Item移出页面之后,会放在缓存池傍边;当下面的item显现的时分,首要会从缓存池中取出缓存,直接调用onBindViewHolder办法,所以依然会从头创立一个Bitmap,因而针对列表的缓存特性能够选择Bitmap内存复用机制。

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?

看上面这张图,由于顶部的Item在新建的时分,已经在native堆区中分配了一块内存,所以当这块区域被移出屏幕的时分,下面显现的Item不需求再次分配内存空间,而是复用移出屏幕的Item的内存区域,然后防止了频频地创立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)
}

那么怎么完成内存复用,在BitmapFactory中供给了Options选项,当设置inMutable特点为true之后,就代表敞开了内存复用,此刻假如新建了一个Bitmap,并将其添加到inBitmap中,那么后续所有Bitmap的创立,只要比这块内存小,那么都会放在这块内存中,防止重复创立。

滑动前:

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?
滑动时:

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?

经过上图咱们发现,即便是在滑动的时分,Native内存都没有明显的改变。

1.2 Bitmap紧缩

像1.1中这种加载方式,其实都是会直接将Bitmap加载到native内存中,例如咱们设置的ImageView只要100*100,那么图片的巨细为1000 * 800,其实是不需求将这么大体量的图片直接加载到内存中,那么有没有一种方式,在图片加载到内存之前就能拿到这些根底信息呢?

当然有了,这儿仍是要搬出BitmapFactory.Option这个类,其间inJustDecodeBounds这个特点的含义,从字面意思上就能够看出,只解码鸿沟,也便是意味着在加载内存之前,是会拿到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
}

然后会需求核算一个紧缩的系数,给BitmapFactory.Option类的inSampleSize赋值,这样Bitmap就完成了缩放,咱们再次看运行时的内存状况。

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?

Native内存简直下降了一半。

2 手写图片缓存结构

在第一节中,咱们关于Bitmap本身做了一些处理,例如紧缩、内存复用。虽然做了这些处理,可是不足以作为一个优秀的结构对外输出。

为什么呢?像1.2节中,咱们虽然做了内存复用以及紧缩,可是每次加载图片都需求从头调用decodeFile拿到一个bitmap目标,其实这都是同一张图片,即便是在项目中,必定也存在相同的图片,那么咱们必定不能重复加载,因而关于加载过的图片咱们想缓存起来,比及下次加载的时分,直接拿缓存中的Bitmap,其实也是加快了响应时刻。

2.1 内存缓存

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?
首要一个老练的图片加载结构,三级缓存是必须的,像Glide、coil的缓存战略,假如能把这篇文章搞懂了,那么就全通了。

在Android中,供给了LruCache这个类,也是内存缓存的首选,假如熟悉LruCache的同伴,应该明白其间的原理。它其实是一个双向链表,以最近少用原则,当缓存中的数据长时刻不必,并且有新的成员参加进来之后,就会移除尾部的成员,那么咱们首要搞定内存缓存。

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)
        }
    }
}

根底结构选用制作者规划形式,基本都是一些开关,操控是否敞开内存缓存,或许磁盘缓存,接下来进行一些初始化操作。

首要关于内存缓存,咱们运用LruCache,其间有两个中心的办法:sizeOf和entryRemoved,办法的作用已经在注释里了。

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)
    }
}

当LruCache中元素被移除之后,咱们想是不是就需求收回了,那这样的话其实就错了。记不记得咱们前面做的内存复用战略,假如当时Bitmap内存是能够被复用的,直接收回掉,那内存复用就没有意义了,所以针对可复用的Bitmap,能够放到一个复用池中,确保其在内存中

/**
 * 当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()
    }
}

所以这儿加了一个判断,当这个Bitmap是支撑内存复用的话,就加到复用池中,确保其他Item在复用内存的时分不至于找不到内存地址,条件是还没有被收回;那么这儿就有一个问题,当复用池中的目标(弱引用)被释放之后,Bitmap怎么收回呢?与弱引用配套的有一个引用行列,当弱引用被GC收回之后,会被加到引用行列中。

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()
}

初始化的操作,咱们把它放在Application中进行初始化操作

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

其实加了内存缓存之后,跟inBitmap的价值基本便是等价的了,也是为了防止频频地请求内存,能够认为是一个双保险,加上对图片紧缩以及LruCache的缓存战略,真实内存打满的场景仍是比较少的。

2.2 复用池的处理

在前面咱们提到了,为了确保可复用的Bitmap不被收回,然后加到了一个复用池中,那么当从缓存中没有取到数据的时分,就会从复用池中取,相当所以在内存缓存中加了一个二级缓存。

Android进阶宝典 -- 学会Bitmap内存管理,你的App内存还会暴增吗?

针对上述图中的流程,能够对复用池进行处理。

/**
 * 从复用池中取数据
 */
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
    }
}

这儿需求留意一点便是,假如想要复用内存,那么请求的内存一定要比复用的这块内存小,不然就不能匹配上。

所以终究的一个流程便是(这儿没考虑磁盘缓存,假如用过Glide就会知道,磁盘缓存会有问题),首要从内存中取,假如取到了,那么就直接烘托展示;假如没有取到,那么就从复用池中取出一块内存,然后让新创立的Bitmap复用这块内存。

//从内存中取
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: 从内存加载了数据

其实真实要确保咱们的内存稳定,便是尽量防止重复创立目标,特别是大图片,在加载的时分特别需求留意,在项目中呈现内存一直不降的首要原因也是对Bitmap的内存办理不当,所以把握了上面的内容,就能够针对这些问题进行优化。总之万变不离其宗,内存是App的生命线,假如在面试的时分问你怎么规划一个图片加载结构,内存办理是中心,当呈现文章一开头那样的内存曲线的时分,就需求重点关注你的Bitmap是不是又“乱飙”了。

附录 – 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
    }
}