|
|
51CTO旗下网站
|
|
移步端
创造专栏

收拢图片,可以优化内存避免 OOM,但是收拢不是说说而已!(以Glide比方)

图表一直是 App 官方吃内存的富裕户,顶我们做内存优化的时节,永恒也绕不开对图片内存的僵化。可能你很多其他方案一起上,说到底还不如对 Bitmap 拓展例行优化来之得力。

笔者:张�D| 2020-01-18 12:40

 

一. 先后

图表一直是 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() 办法。

      
  1. val bitmapPool = Glide.get(this).bitmapPool 
  2. val bitmap = bitmapPool.get(100,100,Bitmap.Config.ARGB_8888) 
  3. // 拍卖 → 采用 bitmap 
  4. // ...... 
  5.  
  6. // 用完回收 bitmap 
  7. bitmapPool.put(bitmap) 

举重若轻特殊的借鉴,只是将 Bitmap.createBitmap() 办法,替换成 bitmapPool.get() 办法,在采取完成后,再调用 put() 办法回收图片。

2. bitmapPool.get() 都做了什么?

为什么使用 bitmapPool.get() 替换掉 createBitmap() 就足以达到对图片内存的僵化呢?

要掌握所有的池化艺术,都是基于享元模式,名将部分比较重要的风源,最大限度的开展缓存,并以期待下一次之采取时可以直接复用。

故此实际上,bitmapPool.get() 并没有那么神奇之,他只是先下缓存池中找是否有回报可用之 Bitmap 能源,有就重用,没有时依然需要征用 Bitmap.createBitmap() 扮演创建一个图片。

      
  1. // LruBitmapPool.java 
  2. public Bitmap get(int width, int height, Bitmap.Config config) { 
  3.   Bitmap result = getDirtyOrNull(width, height, config); 
  4.   if (result != null) { 
  5.     // 擦除"脏像素" 
  6.     result.eraseColor(Color.TRANSPARENT); 
  7.   } else { 
  8.     // 穿过 Bitmap.createBitmap() 创造图片 
  9.     result = createBitmap(width, height, config); 
  10.   } 
  11.   return result; 

此地会先尝试通过 getDirtyOrNull() 获取缓存池的 Bitmap 能源,如果没有可用之风源,依然是适用 createBitmap() 扮演构造一个新的 Bitmap 目标。

如果 getDirtyOrNull() 找到了可复用的 Bitmap 能源,则会调用 eraseColor() 办法,名将 Bitmap 的"脏像素"拓展擦除,以避免旧图对新图的影响。

3. BitmapPool 如何缓存 Bitmap?

一度图片资源,加载到内存中的后,其实是包含两部分内存占用的,一度是 Bitmap 目标引用,还有一些是图片的像素数据,在 Android 不同版本的迭代过程中,图表的像素数据存放的岗位是挪了又挪。

但是无论像素数据最终放在哪里,其实占用内存的花边一直的都是图片的像素数据,而像素数据占用的蓝天,又受到图片资源之像素尺寸以及单像素的占用的内存尺寸。

例如一个 ARGB_8888 的图形,他像素数据占用内存的算计方法:

      
  1. BitmapRam = BitmapWidth * BitmapHeight * 4 bytes 

其中 4 Bytes 就是 ARGB_8888 另一方面像素占用的内存。

在 BitmapPool 官方,也是基于这 3 个条件来唯一定位一个可用之图形资源,举报到代码中,就是图片的 width、height 以及 Bitmap.Config。

在 BitmapPool 官方有一度 strategy 目标,他是一番 LruPoolStrategy 品种,这是一番接口,咱们常见会用到他的贯彻类 SizeConfigStrategy。

      
  1. // LruBitmapPool.java 
  2. private synchronized Bitmap getDirtyOrNull( 
  3.   int width, int height, @Nullable Bitmap.Config config) { 
  4.   final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG); 
  5.   // ... 
  6.   return result; 

继承看看 SizeConfigStrategy,在其中维护了一番 groupedMap 布局,他的项目是 GroupedLinkedMap,咱们可以将他简单的了解为一个 Key-Value 的键值对,同时他也实现了 LRU 书法。

      
  1. // GroupedLinkedMap.java 
  2. public Bitmap get(int width, int height, Bitmap.Config config) { 
  3.   int size = Util.getBitmapByteSize(width, height, config); 
  4.   Key bestKey = findBestKey(size, config); 
  5.  
  6.   Bitmap result = groupedMap.get(bestKey); 
  7.   // ... 
  8.   return result; 

此地的 get() 办法,就是通过 width、height、config 找到一个对应的 Key,再下groupMap 官方基于此 Key 获取到缓存池中的图片。

4. BitmapPool 如何回收资源

在 Bitmap 采用完后,咱们还要求将他进行回收,回收资源就是适用 LruBitmapPool 的 put() 办法。

      
  1. public synchronized void put(Bitmap bitmap) { 
  2.     // 检验 Bitmap 有效代码,简言之 
  3.   if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize 
  4.       || !allowedConfigs.contains(bitmap.getConfig())) { 
  5.     // 不符合缓存条件,直接 recycle() 
  6.     bitmap.recycle(); 
  7.     return
  8.   } 
  9.  
  10.   final int size = strategy.getSize(bitmap); 
  11.   strategy.put(bitmap); 
  12.   // Other code ... 

在 put() 办法中,会对待回收的 Bitmap 做一个主导的校验,例如是一番可变的 Bitmap;尺寸必须不能大于 maxSize 等。如果条件不满足,直接将图片回收(recycle)。

满足这些前置条件之后,会将他放入 strategy 拓展缓存,这就是前面 get() 办法从缓存池中获取图片操作的多寡结构,就不再赘述了。

5. 查缺补漏

眼前也提出 BitmapPool 并没有什么神奇之,如果资源池中没有需要的 Bitmap,他依然会通过 createBitmap() 结构一个新的 Bitmap 目标。

但是在 Glide 的总体逻辑中,汪洋之行李用到了 BitmapPool,故此可能你需要的 Bitmap 目标,先前被其他逻辑使用并回收。例如在 Glide 的 BitmapResource 官方,recycle() 回收的逻辑,就是直接将图片尝试放入 BitmapPool 官方。

      
  1. // BitmapResource.java 
  2. public class BitmapResource implements Resource<Bitmap>, 
  3.     Initializable { 
  4.   @Override 
  5.   public void recycle() { 
  6.     bitmapPool.put(bitmap); 
  7.   } 

退一地说,就算我们采用的 Bitmap 不在资源池中,咱们只要求采取后,穿过 put()办法将他回收到资源池中,下次依然可以复用。

图表是一番占内存的花边,频繁之结构小尺寸 Bitmap,大多数情况下是不会直接造成 OOM,但是可能会造成频繁之 GC,表现出来就是内存的颠簸,这在 Dalvik 编造机上尤其明显。虽然在 ART 编造机上,对 GC 已经做了部分优化,但是资源之复用依然是一种提高效率的一手。

同时 BitmapPool 自己也会根据 onTrimMemory() 回调,来处理缓存的 Bitmap 的查处逻辑,这无需我们开发者再关心他回收的平整。

      
  1. public void trimMemory(int level) { 
  2.   if (Log.isLoggable(TAG, Log.DEBUG)) { 
  3.     Log.d(TAG, "trimMemory, level=" + level); 
  4.   } 
  5.   if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { 
  6.     clearMemory(); 
  7.   } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN 
  8.              || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { 
  9.     trimToSize(getMaxSize() / 2); 
  10.   } 

此外,对 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 上。

      
  1. Glide.with(fragment) 
  2.     .load(url) 
  3.     .into(imageView); 

如果想通过 Glide 直接加载图片,并拥有 Bitmap 目标,要求用到 asXxx() 的主意和 Target,咱们接下来就来看望,副不同之源加载 Bitmap 的状况,以及同步和异步的分别。

1. 同步加载 Bitmap 目标

有时我们需要在子线程中获取 Bitmap 目标,就要求同步获取的措施。

      
  1. val bitmap = Glide.with(activity) 
  2.     .asBitmap() 
  3.     .load(imageUrl) 
  4.     .submit().get() 

凭借 asBitmap() 和 submit().get() 就足以下某个源中,直接获得 Bitmap 目标。

submit() 还可以约束加载图片的尺寸,富有我们处理。

      
  1. FutureTarget<TranscodeType> submit() 
  2. FutureTarget<TranscodeType> submit(int width, int height) 

2. 异步加载 Bitmap 目标

Glide 也支持异步加载 Bitmap,异步加载,就涉及到线程的改制问题。

      
  1. Glide.with(activity) 
  2.     .asBitmap() 
  3.     .load(imageUrl) 
  4.     .into(object:CustomTarget<Bitmap>(){ 
  5.       override fun onLoadCleared(placeholder: Drawable?) { 
  6.       } 
  7.  
  8.       override fun onResourceReady(resource: Bitmap,  
  9.                                    transition: Transition<in Bitmap>?) { 
  10.         val loadBitmap = resource 
  11.       } 
  12.     }) 

异步加载,要求用到 Target,此地直接行使 Glide 提供的 CustomTarget。

3. 加载图片的 File

咱们掌握用 Glide 加载的图形,在缓存容量允许的范围内,Glide 都市帮咱将图片文件缓存到地方磁盘。

这就是说我们如何通过 Glide 加载一个图片资源,下一场获得缓存的图形文件呢?

其实只要求将上面的 asBitmap() 换成 asFile() 即可。

      
  1. // sync 
  2. val bitmapFile = Glide.with(this) 
  3.     .asFile() 
  4.     .load(imageUrl) 
  5.     .submit().get() 
  6.  
  7. // async 
  8. Glide.with(this) 
  9.     .asFile() 
  10.     .load(imageUrl) 
  11.     .into(object:CustomTarget<File>(){ 
  12.       override fun onLoadCleared(placeholder: Drawable?) { 
  13.       } 
  14.  
  15.       override fun onResourceReady(resource: File, transition:  
  16.                                    Transition<in File>?) { 
  17.           val bitmapFile = resource 
  18.       } 
  19.     }) 

除了 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专栏作者“张�D”的原创稿件,转载请通过微信公众号联系作者获取授权】

戳这里,瞧该作者更多好文

【编纂推荐】

  1. 内存KV缓存/必发娱乐登录,可以选择它? | 1分钟系列
  2. Java劳务,内存OOM题材如何快速稳定?
  3. 惊魂48小时,阿里工程师如何紧急定位线上内存泄露?
  4. 硬盘太慢!内存太慢!网络太慢!全靠我来拯救!
【义务编辑: 武晓燕 TEL:(010)68476606】

点赞 0
  • 内存  OOM  图表
  • 分享:
    大家都在看
    猜你喜欢
    1.