Glide中的资源复用

为什么 - 资源复用的作用

Glide通过复用资源避免不必要的内存分配。Dalvik虚拟机(在Lollipop之前)有两种基本的垃圾回收方式,GC_CONCURRENTGC_FOR_ALLOC。每次GC_CONCURRENT会阻塞主线程5ms。由于每次操作的时间少于16ms(1帧的时间),GC_CONCURRENT并不会引起掉帧。相反的是GC_FOR_ALLOC,他会停止所有操作,阻塞主线程125+ms,事实上,GC_FOR_ALLOC总是会让你的app掉很多帧。尤其是在滑动时,导致明显的卡顿。 很不幸,即使只是分配适当的空间(比如16kb的buffer)Dalvik表现的也很糟糕。不断的小内存分配,或者一次大的内存分配(比如说bitmap),将会引起GC_FOR_ALLOC。因此,你分配内存的越多,你会遇到垃圾回收器阻塞应用的情况就越多,应用掉帧就越严重。 通过适度复用大块资源,Glide可以避免内存抖动,减少垃圾回收器阻塞app的次数。

怎么做 - Glide是如何复用资源

Glide的资源复用策略比较宽松。这意味着,当Glide认为该资源可以被安全的复用时,才有几率去复用它,并不需要开发者在每个request后面去手动释放资源。

标志-哪些资源可复用

Glide有两个简单的标志来识别可复用的资源。

  1. Glide.clear()

    View或者Target上调用clear()方法都表示,Glide要取消加载,可以安全地把Target占用的所有资源(Bitmap,bytes数组等)放入资源池中(pool)。用户可以在任何时候安全地手动调用clear()方法,但是典型情况下,我们不需要这样做,看第二条。

  2. View或者Target的复用

    当用户把图片加载到一个已经存在的View或者Target上时(注:确切的说是调用into(xxx)方法之后),Glide会先调用clear()清空该View/Target上的加载请求并复用已经显示过的资源。因此,如果你的ListView或者RecyclerView中的view使用了复用机制(注:如ViewHolder),那么Glide会自动为他们缓存资源和管理加载请求。

引用计数

如果两个请求指向同一个资源,为了避免额外的工作,Glide会把单个资源分配给他们。这就导致一个问题,当Glide得知某个资源不被某个调用者使用,这并不表示它不会被其他调用者使用。为了避免回收调依旧被使用的资源,Glide使用引用计数来跟踪资源。 当把资源提供给View/Target时,该资源的引用计数加1,当清空View/Target时,引用计数减1。当引用计数为0时,Glide会回收资源,并把它的内容放回可用内存池中。

放入缓存池

Glide的Resource API有一个recycle()方法,当Glide认为资源不再被引用时,会调用该方法,资源会放入缓存池中。

Glide提供的BitmapPool接口可以让Resource获取Bitmap和复用Bitmap对象。Glide的BitmapPool可以通过Glide单例获得:

Glide.get(context).getBitmapPool();

ResourceDecoder可以返回Resource的任何实现。所有,开发者可以实现他们自己的Resource和ResourceDecoder来自定义地缓存一些特有类型的数据。 同样地,开发者如果想更多地控制Bitmap缓存,可以实现自己的BitmapPool,然后通过GlideModule配置到Glide中。

常见的错误

不幸的是,缓存池设计使我们很难判断开发者是否误用了资源或者bitmap。但是,在Glide中有两个主要的现象会暗示你某些地方可能出了问题。

现象

  1. Cannot draw a recycled Bitmap

    Glide有个固定大小的Bitmap池。当Bitmap不再被复用(注:不是使用,区分使用和复用),会从池中移走。Glide会调用recycle())(注:指的是Bitmap真正的recycle,不是Resource类的recycle)。你告诉Glide放心地回收某个Bitmap,但是,你的应用不小心还持有这个Bitmap的引用,应用程序可能会绘制这个Bitmap,导致崩溃。

  2. View在多张图片之间闪烁,或者同样的图片出现在多个View中

    如果一张图片被放入BitmapPool中多次。或者虽然一张图片被放入了pool中,但是某个View依然持有这个图的引用,与此同时,另一张图片被解析成了Bitmap(注:此Bitmap正好用了刚才那张图片的控件来存放解析后的数据)。如果发生这种事情,Bitmap的内容就会被换成了新的图片内容。此时,View依然尝试着绘制Bitmap,导致原来的View中显示了一张新的图片!

原因

这些问题主要有两个原因:

  1. 尝试加载两个不同的资源到同一Target中

    在Glide中,没有安全的方法来加载多个资源到单一的Target中。开发者可以使用thumbnail()来加载一系列资源到到某个Target中,但是对于每一个加载的资源来说,只有在下一个onResourceReady()被调用前,它的引用才是安全的。 开发者如果想加载多个资源到同一个View中,可以使用两个独立的Target。为了确保加载过程不相互取消,开发者要么不使用ViewTarget的子类,要么在继承ViewTarget时,复写setRequest()getRequest(),不要使用tag来存储Request。(注:需要一个demo)

    译者注:对于同一个view,调用两次Glide.xxx.into(view),第二次调用会先清空第一个加载的图片(出现空白),再去下载新的图片,如果想要在第二张图片下载下来之前依旧显示之前的,需要一些技巧

  2. 加载一个资源放入到Target,然后清空或者复用了Target,但是依然引用这这个资源。

    最简单的避免这个错误的方法是在onLoadCleared()方法中把所有对资源对象的引用置null。一般情况下,加载一个Bitmap,然后引用它的Target是安全的。不安全的是,你清空了这个Target,却依然引用着这个Bitmap。