一. 序
图片一直是 App 中吃内存的大户,当我们做内存优化的时候,永远也绕不开对图片内存的优化。可能你很多其他方案一起上,最后还不如对 Bitmap 进行常规优化来的有效。
对图片的优化前提是对图片操作的收拢,这样我们才可以做整体的策略控制。例如对于一些低端设备,我们可以将图片格式从 ARGB_8888 变为 RGB_565,这样一个简单的调整,可以让图片内存的占用减少一半;又例如在适当的时机,主动回收掉一些图片缓存,避免被 Low Memory Kiiler 盯上。
但是这一切的前提,就是我们要收拢对图片的操作。通常我们会使用一些开源的图片库,来简化对图片的操作,例如 Glide、Fresco 或者其他一些自研的图片加载库。
我们当然不会在一个项目中重复集成多个图片加载库,但是很多时候我们会忽略掉一些 Android 下原生操作 Bitmap 的 API,例如 Bitmap.createBitmap()、BitmapFactory 等。
这些系统提供的 API,也是我们收拢图片操作时需要注意的,否者必然有一些图片不是受约束的。那么接下来,我们就以最常用的 Glide 来举例,看看如何替换掉Bitmap.createBitmap() 和 BitmapFactory 的相关操作,来收紧对这些 API 的操作。
ps:本文内容,以 Glide v4.11.0 举例。
二. 收拢哪些 Bitmap 操作
2.1 替换 createBitmap()
Bitmap.createBitmap() 方法,从名字上就可以看出,它是为了创建一个 Bitmap 对象,这在我们做一些图片变换绘制时,经常会用到。而想要利用 Glide 的来优化此步骤,就需要用到 BitmapPool。
BitmapPool 本身是一个接口,我们通常会使用到它的实现类 LruBitmapPool,从名称就可以看出,它基于 LRU 的规则,在一定的内存限制下,缓存和管理一些可供重用的 Bitmap 对象。
接下来我们看看具体的使用。
1. 使用 BitmapPool
在 Glide 中,BitmapPool 渗透到逻辑代码的方方面面。我们想要拿到 BitmapPool 对象也非常的简单,只需要使用 getBitmapPool() 方法即可。
既然是一个池化的方案,那么肯定会有对应的 get() 和 put() 方法。
- val bitmapPool = Glide.get(this).bitmapPool
- val bitmap = bitmapPool.get(100,100,Bitmap.Config.ARGB_8888)
- // 处理 → 使用 bitmap
- // ......
- // 用完回收 bitmap
- bitmapPool.put(bitmap)
没什么特殊的操作,只是将 Bitmap.createBitmap() 方法,替换成 bitmapPool.get() 方法,在使用完成后,再调用 put() 方法回收图片。
2. bitmapPool.get() 都做了什么?
为什么使用 bitmapPool.get() 替换掉 createBitmap() 就可以达到对图片内存的优化呢?
要知道所有的池化技术,都是基于享元模式,将一些比较重要的资源,最大限度的进行缓存,并以期待下一次的使用时可以直接复用。
所以实际上,bitmapPool.get() 并没有那么神奇的,它只是先从缓存池中找是否有对应可用的 Bitmap 资源,有就重用,没有时依然需要调用 Bitmap.createBitmap() 去创建一个图片。
- // LruBitmapPool.java
- public Bitmap get(int width, int height, Bitmap.Config config) {
- Bitmap result = getDirtyOrNull(width, height, config);
- if (result != null) {
- // 擦除"脏像素"
- result.eraseColor(Color.TRANSPARENT);
- } else {
- // 通过 Bitmap.createBitmap() 创建图片
- result = createBitmap(width, height, config);
- }
- return result;
- }
这里会先尝试通过 getDirtyOrNull() 获取缓存池的 Bitmap 资源,如果没有可用的资源,依然是调用 createBitmap() 去构造一个新的 Bitmap 对象。
如果 getDirtyOrNull() 找到了可复用的 Bitmap 资源,则会调用 eraseColor() 方法,将 Bitmap 的"脏像素"进行擦除,以避免旧图对新图的影响。
3. BitmapPool 如何缓存 Bitmap?
一个图片资源,加载到内存中之后,其实是包含两部分内存占用的,一个是 Bitmap 对象引用,还有一部分是图片的像素数据,在 Android 不同版本的迭代过程中,图片的像素数据存放的位置是挪了又挪。
但是不管像素数据最终放在哪里,其实占用内存的大头一直的都是图片的像素数据,而像素数据占用的空间,又受到图片资源的像素尺寸以及单像素的占用的内存尺寸。
例如一个 ARGB_8888 的图片,它像素数据占用内存的计算方式:
- BitmapRam = BitmapWidth * BitmapHeight * 4 bytes
其中 4 Bytes 就是 ARGB_8888 单像素占用的内存。
在 BitmapPool 中,也是基于这 3 个条件来唯一定位一个可用的图片资源,反映到代码中,就是图片的 width、height 以及 Bitmap.Config。
在 BitmapPool 中有一个 strategy 对象,它是一个 LruPoolStrategy 类型,这是一个接口,我们通常会用到它的实现类 SizeConfigStrategy。
- // LruBitmapPool.java
- private synchronized Bitmap getDirtyOrNull(
- int width, int height, @Nullable Bitmap.Config config) {
- final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
- // ...
- return result;
- }
继续看看 SizeConfigStrategy,在其中维护了一个 groupedMap 结构,它的类型是 GroupedLinkedMap,我们可以将它简单的理解为一个 Key-Value 的键值对,同时它也实现了 LRU 算法。
- // GroupedLinkedMap.java
- public Bitmap get(int width, int height, Bitmap.Config config) {
- int size = Util.getBitmapByteSize(width, height, config);
- Key bestKey = findBestKey(size, config);
- Bitmap result = groupedMap.get(bestKey);
- // ...
- return result;
- }
这里的 get() 方式,就是通过 width、height、config 找到一个对应的 Key,再从groupMap 中基于此 Key 获取到缓存池中的图片。
4. BitmapPool 如何回收资源
在 Bitmap 使用完之后,我们还需要将其进行回收,回收资源就是调用 LruBitmapPool 的 put() 方法。
- public synchronized void put(Bitmap bitmap) {
- // 验证 Bitmap 有效性代码,省略
- if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize
- || !allowedConfigs.contains(bitmap.getConfig())) {
- // 不符合缓存条件,直接 recycle()
- bitmap.recycle();
- return;
- }
- final int size = strategy.getSize(bitmap);
- strategy.put(bitmap);
- // Other code ...
- }
在 put() 方法中,会对待回收的 Bitmap 做一个基本的校验,例如是一个可变的 Bitmap;尺寸必须不能大于 maxSize 等。如果条件不满足,直接将图片回收(recycle)。
满足这些前置条件之后,会将其放入 strategy 进行缓存,这就是前面 get() 方法从缓存池中获取图片操作的数据结构,就不再赘述了。
5. 查缺补漏
前面也提到 BitmapPool 并没有什么神奇的,如果资源池中没有需要的 Bitmap,它依然会通过 createBitmap() 构造一个新的 Bitmap 对象。
但是在 Glide 的整个逻辑中,大量的使用到了 BitmapPool,所以可能你需要的 Bitmap 对象,之前被其他逻辑使用并回收。例如在 Glide 的 BitmapResource 中,recycle() 回收的逻辑,就是直接将图片尝试放入 BitmapPool 中。
- // BitmapResource.java
- public class BitmapResource implements Resource<Bitmap>,
- Initializable {
- @Override
- public void recycle() {
- bitmapPool.put(bitmap);
- }
- }
退一步说,就算我们使用的 Bitmap 不在资源池中,我们只需要使用后,通过 put()方法将其回收到资源池中,下次依然可以复用。
图片是一个占内存的大头,频繁的构造小尺寸 Bitmap,多数情况下是不会直接造成 OOM,但是可能会造成频繁的 GC,表现出来就是内存的抖动,这在 Dalvik 虚拟机上尤其明显。虽然在 ART 虚拟机上,对 GC 已经做了一些优化,但是资源的复用依然是一种提高效率的手段。
同时 BitmapPool 本身也会根据 onTrimMemory() 回调,来处理缓存的 Bitmap 的清理逻辑,这无需我们开发者再关心其回收的规则。
- public void trimMemory(int level) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "trimMemory, level=" + level);
- }
- if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
- clearMemory();
- } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
- || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
- trimToSize(getMaxSize() / 2);
- }
- }
另外,对 Bitmap 资源需要谨慎回收,一定要确保这个图片资源不被使用,再将其进行回收,否则会出现一些异常情况。
其实很好理解,BitmapPool 的 put() 回收资源时,可能两种操作,bitmap.recycle()或者将其放入 BitmapPool 以待后续使用。
那么如果一个外部的 View 还在使用的 Bitmap 被 BitmapPool 回收,可能会出现Cannot draw a recycled Bitmap 错误;还有个场景是 BitmapPool 持有了一个外部被回收的 Bitmap 后,下次使用时,会出现 Can't call reconfigure() on a recycled bitmap 错误。
这些都是直接的错误,如果图片没有被回收(recycle),而是被重用了,也可能会导致外部某个 View 展示的图像,被刷新了,这虽然不会直接抛异常,但是依然是一个逻辑错误。
所以谨记,在通过 BitmapPool 回收图片资源时,一定要确保外部没有使用此 Bitmap 的地方,最好立即切断其引用,避免不必要的错误。
2.2 替换 BitmapFactory
说完 Bitmap.createBitmap() 再就是到了 BitmapFactory 了,它的替换比较简单。
BitmapFactory 最主要的功能,就是利用 decodeXxx() 方法,通过不同的源来加载 Bitmap 资源。
而在 Glide 中,我们使用最多的就是从某个源中加载图片,并直接显示在 ImageView 上。
- Glide.with(fragment)
- .load(url)
- .into(imageView);
如果想通过 Glide 直接加载图片,并获得 Bitmap 对象,需要用到 asXxx() 的方法和 Target,我们接下来就来看看,从不同的源加载 Bitmap 的情况,以及同步和异步的区别。
1. 同步加载 Bitmap 对象
有时我们需要在子线程中获取 Bitmap 对象,就需要同步获取的方式。
- val bitmap = Glide.with(activity)
- .asBitmap()
- .load(imageUrl)
- .submit().get()
借助 asBitmap() 和 submit().get() 就可以从某个源中,直接获得 Bitmap 对象。
submit() 还可以约束加载图片的尺寸,方便我们处理。
- FutureTarget<TranscodeType> submit()
- FutureTarget<TranscodeType> submit(int width, int height)
2. 异步加载 Bitmap 对象
Glide 也支持异步加载 Bitmap,异步加载,就涉及到线程的切换问题。
- Glide.with(activity)
- .asBitmap()
- .load(imageUrl)
- .into(object:CustomTarget<Bitmap>(){
- override fun onLoadCleared(placeholder: Drawable?) {
- }
- override fun onResourceReady(resource: Bitmap,
- transition: Transition<in Bitmap>?) {
- val loadBitmap = resource
- }
- })
异步加载,需要用到 Target,这里直接使用 Glide 提供的 CustomTarget。
3. 加载图片的 File
我们知道用 Glide 加载的图片,在缓存容量允许的范围内,Glide 都会帮我们将图片文件缓存到本地磁盘。
那么我们如何通过 Glide 加载一个图片资源,然后获得缓存的图片文件呢?
其实只需要将上面的 asBitmap() 换成 asFile() 即可。
- // sync
- val bitmapFile = Glide.with(this)
- .asFile()
- .load(imageUrl)
- .submit().get()
- // async
- Glide.with(this)
- .asFile()
- .load(imageUrl)
- .into(object:CustomTarget<File>(){
- override fun onLoadCleared(placeholder: Drawable?) {
- }
- override fun onResourceReady(resource: File, transition:
- Transition<in File>?) {
- val bitmapFile = resource
- }
- })
除了 asBitmap() 和 asFile(),还有一些其他的方法,例如 asDrawable() 等,有兴趣可以自行了解。
4. 查缺补漏
Glide 的 load() 方法,本身就支持很多图片资源的加载,我们只需要使用标准的 API 即可。相比于 BitmapFacory 的源来说,还有 InputStream 这个是 Glide 没有支持的。
这也很好理解,既然是一个 Stream,那么它前身肯定是一个本地的文件或者一个网络数据流,最终体现出来就是一个 File 或者一个 Uri,这些都是 Glide 支持的。
如果实在对 InputStream 的输入有要求,可以自行实现 Glide 的 ModelLoader。
参考:https://muyangmin.github.io/glide-docs-cn/tut/custom-modelloader.html
三. 小结
今天我们强化了在 Android 中,收拢图片调用的概念,不仅仅是限制一个项目中只使用一个图片加载库,而是要对一些系统 Api 进行收拢,例如 BitmapFactory 和Bitmap.createBitmap() 等。
对于 BitmapFactory,我们只需要利用 asBitmap()/asFile() 配合 submit()/CustomTarget 就可以替换。
对于 Bitmap.createBitmap() 则需要用到 Glide 的 BitmapPool 即可,用bitmapPool.get() 替换 createBitmap(),使用完成后通过 bitmap.put() 将图片回收。另外我们还聊了 BitmapPool 的部分逻辑,让我们使用的更放心。
【本文为51CTO专栏作者“张旸”的原创稿件,转载请通过微信公众号联系作者获取授权】