Fresco 源码浅析

 
 


前言
fresco是facebook主导的一个开源图片缓存库,从它提供的示例程序comparison来看,fresco貌似比其他的几个目前android主流的图片缓存库(glide,picasso,uil等)更快更节省内存。接下来就看看它到底是如何做到的。


一、背景:
 1:lru与SoftReference那些年的爱恨情仇:很久很久以前,android的内存缓存还用SoftReference,

    在2.3以后,垃圾回收机制被修改,软引用已不推荐用作缓存技术了,lru等才是正义。既然软引用已经不再推荐使用, 为毛还要提到它呢?因为freso偏偏用到了它!


   2: dalvik  vm heap和native heap:heap即为堆,malloc、new等函数(方法)都是从堆申请内存空间,这点不用多解释。

 两个heap的区别在于,java操作的是dvm的heap,native代码使用malloc操作的是native heap。而dvm对heap的大小是有限制的, 如果超过了这个限制,就会抛出OOM异常。这也是OOM异常的由来。于是fresco就会在native层申请内存来代替部分java层的内存。


   3: ashmem :匿名共享内存,前面提到fresco会用native申请内存空间,ashmem就是其中的一种。ashmem通过pin、unpin操作内存块, 被unpinned的内存会放在一个lru列表,当内核空间不足时调用ashmem注册的回调来清理内存。 具体介绍可参考罗升阳老师的《Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析》。


   4: Bitmap:关于Bitmap的使用技巧很多,官方有详细的文档,这里只提一下fresco用到的部分内容。传入一个Bitmap给BitmapFactory复用,减少申请内存。关于不同版本对inBitmap的大小限制见官方文档,fresco取的是大小一致的,能满足最严格的条件。 </span>

inTempStorage:在BitmapFactory对流进行解码时会分配一段临时的缓冲,如果不传入的话,每次解码都会重新申请,官方建议是16k。
inPurgeable:根据官方文档,如果这个参数被设置为true,Bitmap会创建一段pixels空间,当系统需要回收时,这段空间就会被清除。
在这种情况下,如果需要重新访问这段空间,Bitmap就会重新对未编码的数据进行编码。这未编码数据的保存方式可持
有它的引用或进行一次全拷贝,由inInputShareable参数控制。这两个参数都仅限lollipop以下版本使用。
 这个参数具体是如何影响内存管理的,涉及到skia的具体实现,从BitmapFactory的相关代码来看,貌似也用到了ashmem,
有兴趣的可自行研究。 这个参数应该比较常见了,一般用在对图片压缩。如果直接对一张大尺寸的图片进行解码,会十分占用内存,
  可以要求BitmapFactory按照固定的大小进行编码。这时我们可以通过这个参数先进行一次解码,只获取图片的大小,
 并不解析其中的数据,然后将图片大小按照一定比例进行缩放,再通过BitmapFactory来真正解码。
 
二:设计框架:
Fresco是一个MVC模型,由三大组件构成,它们的对应关系如下所示:
M -> DraweeHierarchy
V -> DraweeView
C -> DraweeController
M 所对应的 DraweeHierarchy是一个有层次结构的数据结构,DraweeView用来显示位于DraweeHierarchy最顶层的图像(DraweeController 则用来控制DraweeHierarchy<的顶层图像是哪一个。FadeDrawable (top level drawable)
 |
 +--o ScaleTypeDrawable
 |  |
 |  +--o BitmapDrawable
 |
 +--o ScaleTypeDrawable
    |
    +--o BitmapDrawable

三者的互动关系很简单,DraweeView 把获得的 Event 转发给 Controller,然后 Controller 根据 Event 来决定是否需要显示和隐藏 (包括动画)图像,而这些图像都存储在 Hierarchy 中,最后 DraweeView 绘制时直接通过 getTopLevelDrawable 就可以获取需要显示的图像。

Fresco 源码浅析_第1张图片

需要注意的是,虽然现在最新的代码中,DraweeView 还是继承自 ImageView,但是以后会直接继承 View,所以我们用 DraweeView 时,尽量不要使用ImageView 的API,例如 setImageXxxsetScaleType,注释里也写得很清楚。

Although ImageView is subclassed instead of subclassing View directly, this class does not support ImageView’s setImageXxx, setScaleType and similar methods. Extending ImageView is a short term solution in order to inherit some of its implementation (padding calculations, etc.). This class is likely to be converted to extend View directly in the future, so avoid using ImageView’s methods and properties (T5856175).

由关系图可以看出,DraweeView 中并没有 DraweeHierarchy 和 DraweeController 类型的成员变量,而只有一个 DrawHolder 类型的 mDrawHolder

public class DraweeHolder<DH extends DraweeHierarchy> implements VisibilityCallback {
  // other properties

  private DH mHierarchy;
  private DraweeController mController = null;

  // methods
}

DraweeHolder 存储了 mHierarchy 和 mController,FB 为什么要这么设计呢?注释里也写得很清楚:

Drawee users, should, as a rule, use DraweeView or its subclasses. There are situations where custom views are required, however, and this class is for those circumstances.

稍微解释一下,这是一个解耦的设计,当我们不想使用 DraweeView,通过 ViewHolder 照样可以使用其他两个组件。比方说,自定义一个View,然后像DraweeView 那样,在 View 中添加一个 DrawHolder 的成员变量。

再来看 DraweeView 的代码:

public class DraweeView<DH extends DraweeHierarchy> extends ImageView {
  // other methods and properties

  /** Sets the hierarchy. */
  public void setHierarchy(DH hierarchy) {
    mDraweeHolder.setHierarchy(hierarchy);
    super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
  }

  /** Sets the controller. */
  public void setController(@Nullable DraweeController draweeController) {
    mDraweeHolder.setController(draweeController);
    super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
  }
}


每次为 DraweeView 设置 hierarchy 或 controller 时,会同时通过 super.setImageDrawable(mDraweeHolder.getTopLevelDrawable()) 更新需要显示的图像。

/**
 * Gets the top-level drawable if hierarchy is set, null otherwise.
 */
public Drawable getTopLevelDrawable() {
  return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable();
}


DraweeHierarchy 只定义了一个方法 - getTopLevelDrawable

public interface DraweeHierarchy {

  /**
   * Returns the top level drawable in the corresponding hierarchy. Hierarchy should always have
   * the same instance of its top level drawable.
   * @return top level drawable
   */
  public Drawable getTopLevelDrawable();
}


DraweeController 也是一个接口,暴露了设置 hierarchy 和接收 Event 的方法。

  • void setHierarchy(@Nullable DraweeHierarchy hierarchy)
  • public boolean onTouchEvent(MotionEvent event)
  • **
     * Interface that represents a Drawee controller used by a DraweeView.
     * <p> The view forwards events to the controller. The controller controls
     * its hierarchy based on those events.
     */
    public interface DraweeController {
    
      /** Gets the hierarchy. */
      @Nullable
      public DraweeHierarchy getHierarchy();
    
      /** Sets a new hierarchy. */
      void setHierarchy(@Nullable DraweeHierarchy hierarchy);
    
      /**
       * Called when the view containing the hierarchy is attached to a window
       * (either temporarily or permanently).
       */
      public void onAttach();
    
      /**
       * Called when the view containing the hierarchy is detached from a window
       * (either temporarily or permanently).
       */
      public void onDetach();
    
      /**
       * Called when the view containing the hierarchy receives a touch event.
       * @return true if the event was handled by the controller, false otherwise
       */
      public boolean onTouchEvent(MotionEvent event);
    
      /**
       * For an animated image, returns an Animatable that lets clients control the animation.
       * @return animatable, or null if the image is not animated or not loaded yet
       */
      public Animatable getAnimatable();
    
    }


三:Fresco中几个重要类

fresco有几类比较核心的内容,包括cahce、pool、decoder以及producer和consumer。下面分别介绍。 
1. cache,这明显就是缓存了,其中包括内存缓存及磁盘缓存,而内存缓存又派生了别的几个子类。 
Fresco 源码浅析_第2张图片
所有的内存缓存都实现了MemoryCache接口。InstrumentedMemoryCache和AnimatedFrameCache的实际操作都由它们的代理CountingMemoryCache来执行。内存缓存还可以分为已解码的缓存和未解码的缓存,如将一张图片读到内存后得到Encoded数据存入未解码缓存,然后对其解码,得到Bitmap存入已解码的缓存。可见fresco的缓存是多种多样的。 
2. pool,其实也可以说是缓存,里面保存的对象基本都是申请的内存空间,将这些对象实例存起来就是为了要复用其申请的内存。cache里的MemoryCache缓存的东西是可以直接拿来用的,当前不用再对里面的数据进行操作,而pool只是复用了对象实例的空间,从pool取出来马上就要对其中的空间进行数据写入等操作。总之,它们的区别在于,MemoryCache复用数据,而pool仅是复用其内存空间。 
Fresco 源码浅析_第3张图片
整个对象池家族中最基础的就是Pool接口,BasePool实现了Pool,然后从它们派生出了各种各样的对象池。BasePool还包含了一个Bucket的SparseArray,所有需要缓存的实体,属性一样的都存在同一个Bucket中。这里所说的属性,对应内存复用而言,自然是大小了。被缓存的对象包括byte数组、NativeMemoryTrunk等。图中以NativeMomoryTrunk为例,画出了它们之间的关系。下面简单介绍一下几个重要的类。 
NativMemoryTrunk,看名字就能猜出它是用到了native的内存空间,这与我们提到的第2点背景知识相符,主要是用来存放Encoded数据。 
OOMSoftReference,背景知识第一点就提到了软引用,fresco到底用了什么黑科技?可以看到,OOMSoftReference有三个SoftReference,其中的注释说到,每次dvm的gc时,每第二个软引用都会被视为弱引用被回收掉。这也就意味着只要我们在(dvm)heap里有两个连续的软引用就能保证其中一个不会被回收,也就意味着它们所引用的对象不会被回收。然而我们无法保证软引用在heap里的位置,为了安全起见,于是就连续new了三个实例出来。关于dvm对软引用的回收可以看MarkSweep的preserveSomeSoftReferences方法:

static void preserveSomeSoftReferences(Object **list)  
{  
    assert(list != NULL);  
    GcMarkContext *ctx = &gDvm.gcHeap->markContext;  
    size_t referentOffset = gDvm.offJavaLangRefReference_referent;  
    Object *clear = NULL;  
    size_t counter = 0;  
    while (*list != NULL) {  
        Object *ref = dequeuePendingReference(list);  
        Object *referent = dvmGetFieldObject(ref, referentOffset);  
        if (referent == NULL) {  
            /* Referent was cleared by the user during marking. */  
            continue;  
        }  
        bool marked = isMarked(referent, ctx);  
        if (!marked && ((++counter) & 1)) {  
            /* Referent is white and biased toward saving, mark it. */  
            markObject(referent, ctx);  
            marked = true;  
        }  
        if (!marked) {  
            /* Referent is white, queue it for clearing. */  
            enqueuePendingReference(ref, &clear);  
        }  
    }  
    *list = clear;  
    /* 
     * Restart the mark with the newly black references added to the 
     * root set. 
     */  
    processMarkStack(ctx);  
}


注意其中的if(!marked && ((++counter) & 1))这个条件,只要当它成立的时候才会标记该引用。没有被标记的软引用就会被回收掉了。 
FlexByteArrayPool,Kitkat版本的decoder使用,具体操作由SoftRefByteArrayPool实现,用来缓存未解码的(Encoded)图片数据。关于deocder见第3点介绍。 
SynchronizedPool,ArtDecoder使用,缓存背景知识第4点提到inTempStorage所使用的一段临时缓冲。它使用Object数组来保存对象实例,并未使用Bucket。 
BitmapPool,同样也是ArtDecoder使用,缓存背景知识第4点inBitmap提到的重用Bitmap。 
NativeMemoryTrunkPool,缓存NativeMemoryTrunk的对象池。 
3. decoder,用来解码的类,根据不同平台有不同的实现,主要分为3种decoder。其中Gingerbread和Kitkat都继承自DalvikPurgeableDecoder,这跟背景知识第四点提到的inPurgeable有关。 
Fresco 源码浅析_第4张图片
GingerbreadPurgeableDecoder,Gingerbread <= sdk < KitKat时使用,Encoded数据保存在native层(NativeMemoryTrunk),并复制到MemoryFile(ashmem),之后交给bitmap解码,现在默认配置不会启用。从github上的某条commit来看,貌似会导致OOM所以不用了。 
KitKatPurgeableDecoder,sdk < Lollipop,Encoded数据保存在native层(NativeMemoryTrunk),从FlexByteArrayPool获取一段java的byte数组缓冲,将Encoded数据复制过去,之后由bitmap进行解码。它使用byte数组代替ashmem,从代码的注释来看,fresco用到的ashmem的技巧貌似从kitkat之后就不好使了,具体是什么原因还没看出来。现在已经代替GingerbreadPurgeableDecoder。 
ArtDecoder,sdk >= Lollipop,直接将Encoded数据封装为流来解码,解码时用到了ByteBuffer缓冲(SynchronizedPool)。 
4. producer和consumer,跟操作系统或者多线程的生产者消费者模式的关系貌似并不是特别大,看起来更像是RxJava这种模式。fresco获取图片就是不断的把自己定义为一个consumer,然后向相应的producer请求数据(produceResults)的过程。以从磁盘缓存获取图片为例,首先向内存缓存的producer请求数据,内存缓存发现找不到,然后自己也定义一个consumer,向磁盘缓存producer请求数据,假设磁盘缓存找到了目标图片,然后回调内存缓存的consumer,此时内存缓存把图片缓存起来,最后回调那个调用内存缓存producer的consumer。当然了,实际的过程会更加复杂,这里已经把解码的过程省略了。

四、异步请求:

Fresco,在业务处理上是复用java5 中的多线程,即:Future,Callable,Excutor

在Fresco中,这个扮演Future来监听请求结果的是"DataSource",即数据源。而DataSource提供了一个订阅的方法,调用者在使用DataSource时必须传入 DataSubscriber和Executor。DataSubscriber可以从DataSource获取到处理中和处理完毕的结果,并且提供了很 简单的方法来区分。因为我们需要非常频繁的处理这些对象,所以必须有一个明确的close调用,幸运的是,DataSource本身就是 Closeable。

在后台,每一个箱子上面都实现了一个叫做“生产者/消费者”的新框架。在这个问题是,我们是从ReactiveX获取的灵感。我们的系统拥有和RxJava相似的接口,但是更加适合移动设备,并且有内置的对Closeables的支持。

保持简单的接口。Producer只有一个叫做produceResults的方法,这个方法需要一个Consumer对象。反过来,Consumer有一个onNewResult方法。

我们使用像这样的系统把Producer联系起来。假设我们有一个producer的工作是把类型I转化为类型O,那么它看起来应该是这个样子:

public class OutputProducer<I, O> implements Producer<O> {
 
  private final Producer<I> mInputProducer;
 
  public OutputProducer(Producer<I> inputProducer) {
    this.mInputProducer = inputProducer;
  }
 
  public void produceResults(Consumer<O> outputConsumer, ProducerContext context) {
    Consumer<I> inputConsumer = new InputConsumer(outputConsumer);
    mInputProducer.produceResults(inputConsumer, context);
  }
 
  private static class InputConsumer implements Consumer<I> {
    private final Consumer<O> mOutputConsumer;
 
    public InputConsumer(Consumer<O> outputConsumer) {
      mOutputConsumer = outputConsumer;
    }
 
    public void onNewResult(I newResult, boolean isLast) {
      O output = doActualWork(newResult);
      mOutputConsumer.onNewResult(output, isLast);      
    }
  }
}
</span>

这可以使我们把非常复杂的步骤串起来,同时也可以保持他们逻辑的独立性。


五、获取图片的过程

上面第四点已经大致提了一下流程,下面我们已时序图的方式来看一下更加详细的调用过程,需要说明的是,下图展示的仍不是完成的流程,为了看起来更加简洁,省略了部分非业务流程。我们假设平台为KitKat,图片要从磁盘缓存中读取,并且该图片是jpg格式的。其他几种情况可自行研究。 
Fresco 源码浅析_第5张图片
(1) 从内存缓存中取图片 
(2)-(3) 内存缓存中没有,再向未编码的内存缓存取。注意这两个缓存使用的都是InstrumentedMemoryCache,实际上它们的对象实例是不一样的,已解码的存的是Bitmap类型的,未解码的存的是EncodedImage类型的。 
(4)-(5) 从磁盘缓存中取数据 
(6) 磁盘缓存中有,把数据读到内存来 
(7) 申请一段NativeMemoryChunk内存,把磁盘的数据读过去 
(8) 回调EncodedMemoryProducer定义的Consumer,这里先会对得到的数据进行缓存,然后复制一个新的EncodedImage继续传递 
(9) 将数据保存到未解码的缓存中 
(10) 回调ResizeAndRotateProducer的Consumer,判断是否需要缩放或旋转,这里假设不需要 
(11) 回调DecodeProducer的Consumer,它会向其工作队列中提交一个解码任务 
(12) 获取解码的一些参数,如解码的质量等 
(13) 根据图片格式执行不同的解码方法,这里假设是jpg 
(14) 直接调用对应平台的解码方法,这里假设是Kitkat 
(15) 从FlexByteArrayPool中获取一段byte数组缓冲,将图片Encoded数据从NativeMemoryChunk中复制过去,解码得到Bitmap 
(16) 回调BitmapMemoryCacheProducer的Consumer 
(17) 将解码后的Bitmap放入缓存中


六、总结:

如果不算ashmem的purgable,fresco还用到了三个缓存,首先是DiskCache,然后还有两个MemoryCache,分别是保存已解码Bitmap的和保存EncodedImage的缓存。除此之外,为了避免频繁的申请内存、回收内存造成内存抖动,fresco还用到了大量的对象池。相关的缓存技术有lru、ashmem、SoftReference等,已经深入到了虚拟机层面。 

参考文献:

1、Fresco使用文档

2、Fresco源码解析 - Hierarachy-View-Controller

3、Fresco介绍 - 一个新的android图片加载库

4、Android系统匿名共享内存Ashmem(Anonymous Shared Memory)在进程间共享的原理分析



你可能感兴趣的:(Fresco 源码浅析)