Fresco图片框架内部实现原理探索

Posted Sunzxyong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Fresco图片框架内部实现原理探索相关的知识,希望对你有一定的参考价值。

流行的网络框架

目前流行的网络图片框架:
Picasso、Universal Image Loader、Volley的(ImageLoader、NetworkImageView)、Glide和Fresco

简明的介绍下(具体细节和功能可看源码和wiki):
其中Picasso和Universal Image Loader相比其它的算是最轻量级的图片框架了,它们拥有较少的方法数,Universal Image Loader是这五个框架中定制性最强的,它内部实现还是按网络框架的套路走:HttpUrlConnection+线程池+Handler,支持渐显效果。
而Picasso只有一些图片加载框架应有的基本功能,所以因此它是最轻量的,在需求只要基本的图片加载与双缓存功能下,可以选Picasso作为项目的基础库,Picasso它内部默认是使用OkHttpClient作为加载网络图片的下载器,毕竟不用自家用谁的,在OkHttpClient没有的情况下则使用HttpUrlConnection,同上面一样,下载器+线程池+Handler,不过它内部的线程池比较有意思,线程池的线程数量是根据当前的网络环境来动态改变的,wifi网络下为4,4G为3,3G为2,2G为1,其它情况下默认为3,支持渐显效果。
Volley的没什么可说的,基本功能都有,网络框架的附赠功能。

Glide的话,Google官方推荐,支持Gif、图片缩略图、本地视频解码、请求和动画生命周期的自动管理、渐显动画、支持OkHttp和Volley等等,默认是使用HttpUrlConnection加载图片的,源码灰常多,200多个类,不想看

Fresco我认为是这几个框架中性能最佳的一个框架,着重介绍,它内部用了大量的建造者模式、单例模式、静态工厂模式、生产/消费者模式。内部实现比较复杂,就拿图片加载来说,是通过在异步线程中回调图片的输入流,然后通过一系列读取、写入、转化成EncodedImage,然后再Decode成Bitmap,通过Handler转给UI线程显示,通过IO操作存储在硬盘缓存目录下。

Fresco性能上的优点

优一:

1、支持webp格式的图片,是Google官方推行的,它的大小比其它格式图片的大小要小一半左右,目前各个大公司都渐入的使用这种图片格式了,比如:Youtube、Gmail、淘宝、QQ空间等都已尝鲜,使用该格式最大的优点就是轻量、省流量、图片加载迅速。而Fresco是通过jni来实现支持WebP格式图片。

优二:

2、5.0以下系统:使用”ashmem”(匿名共享内存)区域存储Bitmap缓存,这样Bitmap对象的创建、释放将永远不会触发GC,关于”ashmem”存储区域,它是一个不在Java堆区的一片存储内存空间,它的管理由Linux内核驱动管理,不必深究,只要知道这块存储区域是别于堆内存之外的一块空间就行了,且这块空间是可以多进程共享的,GC的活动不会影响到它。5.0以上系统,由于内存管理的优化,所以对于5.0以上的系统Fresco将Bitmap缓存直接放到了堆内存中。

关于”ashmem”的存储区域,我们的应用程序并不能像访问堆内存一样直接访问这块内存块,但是也有一些例外,对于Bitmap而言,有一种为”Purgeable Bitmap”可擦除的Bitmap位图是存储在这块内存区域中的,BitmapFactory.Options中有这么一个属性inPurgeable

BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

所以通过配置inPurgeable = true这个属性,这样解码出来的Bitmap位图就存储在”ashmem”区域中,之后用到”ashmem”中得图片时,则把这个图片从这个区域中取出来,渲染完毕后则放回这个位置。

既然Fresco中Bitmap缓存在5.0以前是放在”ashmem”中,GC并不会回收它们,且也不会被”ashmeme”内置的清除机制回收它们,所以这样虽然使得在堆中不会造成内存泄露,而在这块区域可能造成内存泄露,Fresco中采取的办法则是使用引用计数的方式,其中有一个SharedReference这个类,这个类中有这么两个方法:addReference()和deleteReference(),通过这两个基本方法来对引用进行计数,一旦计数为零时,则对应的资源将会清除(如:Bitmap.recycle()等),而Fresco为了考虑更容易被我们使用,又提供了一个CloseableReference类,该类可以说是SharedReference类上功能的封装,CloseableReference同时也实现了Cloneable、Closeable接口,它在调用.clone()方法时同时会调用addReference()来增加一个引用计数,在调用.close()方法时同时会调用deleteReference()来删除一个引用计数,所以在使用Fresco的使用,我们都是与CloseableReference类打交道,使用CloseableReference必须遵循以下两条规则:
1、在赋值CloseableReference给新对象的时候,调用.clone()进行赋值
2、在超出作用域范围的时候,必须调用.close(),通常会在finally代码块中调用

void gee() 
  CloseableReference<Val> ref = foo();
  try 
    haa(ref);
   finally 
    ref.close();
  

遵循这些规则可以有效地防止内存泄漏。

优三:

3、使用了三级缓存:Bitmap缓存+未解码图片缓存+硬盘缓存。
其中前两个就是内存缓存,Bitmap缓存根据系统版本不同放在了不同内存区域中,而未解码图片的缓存只在堆内存中,Fresco分了两步做内存缓存,这样做有什么好处呢?第一个好处就如上的第二条,第二个好处是加快图片的加载速度,Fresco的加载图片的流程为:查找Bitmap缓存中是否存在,存在则直接返回Bitmap直接使用,不存在则查找未解码图片的缓存,如果存在则进行Decode成Bitmap然后直接使用并加入Bitmap缓存中,如果未解码图片缓存中查找不到,则进行硬盘缓存的检查,如有,则进行IO、转化、解码等一系列操作,最后成Bitmap供我们直接使用,并把未解码(Encode)的图片加入未解码图片缓存,把Bitmap加入Bitmap缓存中,如硬盘缓存中没有,则进行Network操作下载图片,然后加入到各个缓存中。

既然Fresco使用了三级缓存,而有两级是内存缓存,所以当我们的App在后台时或者在内存低的情况下在onLowMemory()方法中,我们应该手动清除应用的内存缓存,我们可以使用下面的方式:

        ImagePipeline imagePipeline = Fresco.getImagePipeline();
        //清空内存缓存(包括Bitmap缓存和未解码图片的缓存)
        imagePipeline.clearMemoryCaches();
        //清空硬盘缓存,一般在设置界面供用户手动清理
        imagePipeline.clearDiskCaches();

        //同时清理内存缓存和硬盘缓存
        imagePipeline.clearCaches();

优四:

4、Fresco框架的ImagePipeline设计图

从设计图中可以看出,UIThread只做图片的显示和从内存缓存中加载图片这两件事,而其它事情如:图片的Decode、内存缓存的写、硬盘缓存的IO操作、网络操作等都用非UIThread来处理了,这使得UIThread专注界面的显示,而其它工作由其它线程完成,使UI更加流畅。

Fresco中的MVC模式

Fresco框架整体是一个MVC模式

DraweeView——View
DraweeController——Control
DraweeHierarchy——Model

它们之间的关系大致如下:
DraweeHierarchy意为视图的层次结构,用来存储和描述图片的信息,同时也封装了一些图片的显示和视图层级的方法。
DraweeView用来显示顶层视图(getTopLevelDrawable())。DraweeController控制加载图片的配置、顶层显示哪个视图以及控制事件的分发。
【注】DraweeView目前版本时继承于ImageView,但这并不意味着我们可以随意的使用ImageView相关的方法(如:setScaleType等),官方并不建议我们使用,因为后期DraweeView将继承于View,所以最好只使用DraweeView控件内置的方法。


DraweeHierarchy

DraweeHierarchy除了描述了视图的信息和存储6种视图外,其中还对我们提供了一些额外的方法,比如:让图片渐渐显示Fade效果、设置默认状态下显示的图片、设置点击时显示的图片和加载失败时显示的图片等方法,这些方法可以在我们加载其它图片时保持一些良好的交互效果,值得注意的是,DraweeHierarchy是一个接口,只提供了一个默认方法:

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
   */
  Drawable getTopLevelDrawable();

这个方法从官方注释上来看,是得到当前视图中最顶层的那个Drawable,如:
下面这个视图层次最顶层的视图为FadeDrawable

   o FadeDrawable (top level drawable)
   |
   +--o ScaleTypeDrawable
   |  |
   |  +--o BitmapDrawable
   |
   +--o ScaleTypeDrawable
      |
      +--o BitmapDrawable

所以,在Fresco框架中,并不是一个DraweeView只能设置一个图片(Drawable),而是可以设置一个视图图层(类似android中的LayerDrawable可以设置视图叠加),然后通过DraweeHolder在不同状态下得到(getTopLevelDrawable())最顶层那个图片从而使得DraweeView显示不同的视图,比如下面这个按下时显示一个overlay(顶层)图片效果:

我想这也是Facebook不想把DraweeView这个组件单纯的定义为一个的ImageView的原因吧。

DraweeView

DraweeView是官方给我们提供显示图片的一个基类,我们在使用过程中大多时候并不需要用到它,而是用到一个官方已经简单封好的SimpleDraweeView类,DraweeView类中提供了与DraweeController和DraweeHierarchy交互的接口,而与它们之间的交互本质上是通过一个DraweeHolder类进行交互,这类DraweeHolder是协调DraweeView、DraweeHierarchy、DraweeController这三个类交互工作的核心类,像平时我们都会这样使用:

比如:配置一个DraweeHierarchy简便起见通常会使用SimpleDraweeView直接设置:

SimpleDraweeView simpleDraweeView = (SimpleDraweeView) findViewById(R.id.drawee_view);
        Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png");
        simpleDraweeView.setImageURI(uri);
        //创建一个DraweeHierarchy
        GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources()).setPlaceholderImage(getDrawable(R.drawable.holder)).build();
        //设置一个hierarchy
simpleDraweeView.setHierarchy(hierarchy);

其实上述方法最终都是通过mController.setHierarchy(hierarchy);来设置的,本质是调用DraweeHolder类封装好的setHierarchy()方法,可以看看其内部:
DraweeHolder::setHierarchy()

  public void setHierarchy(DH hierarchy) 
    //...
    if (mController != null) 
      mController.setHierarchy(hierarchy);
    
  

所以通过simpleDraweeView.setHierarchy(hierarchy);来设置等价于通过构建一个DraweeController来设置,如下:

        DraweeController controller = Fresco.newDraweeControllerBuilder().setUri(url).setXxx()...build();
        //通过DraweeController设置
        controller.setHierarchy();
        simpleDraweeView.setController(controller);

这个DraweeHolder类的出现,更准确的定位是降低耦合度、解耦的定位。

对于上面我们知道了通过simpleDraweeView.setHierarchy(hierarchy);来设置等价于通过DraweeController::setHierarchy()来设置,那么simpleDraweeView.setController(controller);有没有等价的呢?答案是没有,因为要加载图片那么就必须设置一个DraweeController来控制图片的加载,(当然我们如果设置了SimpleDraweeView的一些属性,那么默认也会创建一个DraweeHierarchy),而我们平时简便的写法:

SimpleDraweeView simpleDraweeView = (SimpleDraweeView) findViewById(R.id.drawee_view);
        Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png");
        simpleDraweeView.setImageURI(uri);

这是一段最基本的写法,我们都知道这样设置了Fresco内部就自动会给我们加载图片了,而网上也流传着另外一种加载图片的方法,为:

        DraweeController controller = Fresco.newDraweeControllerBuilder().setUri(url).build();
        simpleDraweeView.setController(controller);

其实这两种写法都是一种写法,Fresco真正加载图片仅仅只有这一种方法,就是通过simpleDraweeView.setController(controller);来设置,只不过我们可以对DraweeController和DraweeHierarchy做各种各样的配置来达到我们想要的效果,我们可以看看simpleDraweeView.setImageURI(uri);的源码,其实还是通过setController(controller);设置一个控制器来控制图片的加载,源码为:

  public void setImageURI(Uri uri, @Nullable Object callerContext) 
    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();
    setController(controller);
  

所以,使用Fresco通用的写法便是:

        SimpleDraweeView simpleDraweeView = (SimpleDraweeView) findViewById(R.id.drawee_view);
        Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png");

        GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources())
                .setFadeDuration(400)
                .setPlaceholderImage(getDrawable(R.drawable.holder))
                .setFailureImage(getDrawable(R.drawable.fail))
                .build();

        DraweeController controller = Fresco.newDraweeControllerBuilder().setUri(uri).setOldController(simpleDraweeView.getController()).build();
        controller.setHierarchy(hierarchy);

        simpleDraweeView.setController(controller);

或者直接使用ImageRequet:

    ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url)).build();
    DraweeController draweeController = Fresco.newDraweeControllerBuilder().setImageRequest(imageRequest).setOldController(simpleDraweeView.getController()).build();
    simpleDraweeView.setController(controller);

当然配置其它属性只要设置Controller和Hierarchy相应的方法即可。
】:上面中用到了一个这个方法simpleDraweeView.getController();当然也还有simpleDraweeView.getHierarchy();,这两个方法是返回当前DraweeView所设置的Control和Hierarchy,使用这两个方法的好处是复用以前创建的Control和Hierarchy对象,因为重新创建一个对象肯定不如复用好,而且创建相对耗时,所以官方也建议我们复用这两个对象。如果你用simpleDraweeView.getHierarchy()来加载图片,那么它将不可能为空,除非你什么不设置,而getController()则可能为空,所以在使用工厂方法

Fresco.newDraweeControllerBuilder().setUri(uri).setOldController(simpleDraweeView.getController()).build();

来创建一个DraweeController的时候给它配置一个setOldController(),如果这里面这个参数为null,那么就会重新创建一个DraweeController,如果不为空则复用当前传入的。


顺便说一句,直接使用SimpleDraweeView就足够了,毕竟配置功能都落在DraweeController和DraweeHierarchy身上,SimpleDraweeView仅仅起个显示最顶层视图的作用。

DraweeController

关于DraweeController,好像在上面已经讲的差不多了,它主要就是起个控制图片的加载和配置以及决定顶层显示哪个视图的作用,其它的它也可以设置设置个ControllerListener来监听图片加载的进度,也可以配置一个ImageRequest来设置渐进式JPEG图片的加载,具体使用可以看其官方文档。

认识Fresco中的视图层次

 *     o FadeDrawable (top level drawable)
 *     |
 *     +--o ScaleTypeDrawable
 *     |  |
 *     |  +--o Drawable (placeholder image)
 *     |
 *     +--o ScaleTypeDrawable
 *     |  |
 *     |  +--o SettableDrawable
 *     |     |
 *     |     +--o Drawable (actual image)
 *     |
 *     +--o ScaleTypeDrawable
 *     |  |
 *     |  +--o Drawable (retry image)
 *     |
 *     +--o ScaleTypeDrawable
 *        |
 *        +--o Drawable (failure image)

Fresco中对于DraweeHierarchy视图层次的描述中,视图层次的结构为6种不同的视图,依次为顶层Drawable(可用FadeDrawable配置显示效果)、默认显示的Drawable、实际的Drawable(其中有一层可设置的Drawable包裹它作为对我们实际加载到的Drawable进行配置)、重试的Drawable、加载失败的Drawable。

上面这种层次从GenericDraweeHierarchy源码中就有所体现,如下:

  private Drawable mEmptyPlaceholderDrawable;
  private final Drawable mEmptyActualImageDrawable = new ColorDrawable(Color.TRANSPARENT);
  private final Drawable mEmptyControllerOverlayDrawable = new ColorDrawable(Color.TRANSPARENT);
  private final RootDrawable mTopLevelDrawable;
  private final FadeDrawable mFadeDrawable;
  private final SettableDrawable mActualImageSettableDrawable;

  private final int mPlaceholderImageIndex;
  private final int mProgressBarImageIndex;
  private final int mActualImageIndex;
  private final int mRetryImageIndex;
  private final int mFailureImageIndex;
  private final int mControllerOverlayIndex;

上述定义了不同功能的Drawable,比如FadeDrawable和SettableDrawable都是可以对视图进行配置。而也定义了6个int下标,这6个int类型的下标主要就是用于存储视图层次中各个Drawable中的位置,都是用一个Drawable[] layers ;数组进行存储的:

    layers[mPlaceholderImageIndex] = placeholderImageBranch;
    layers[mActualImageIndex] = actualImageBranch;
    layers[mProgressBarImageIndex] = progressBarImageBranch;
    layers[mRetryImageIndex] = retryImageBranch;
    layers[mFailureImageIndex] = failureImageBranch;
    layers[mControllerOverlayIndex] = mEmptyControllerOverlayDrawable;

这些存储着视图层次资源的layers数组将会由ArrayDrawable类进行管理和绘制,最终将需要显示的视图回调给RootDrawable::mTopLevelDrawable。

ImagePipelineConfig

我们对Fresco进行初始化时,有两种方式:

        Fresco.initialize(this);
        //or
        ImagePipelineConfig pipelineConfig = ImagePipelineConfig.newBuilder(getApplicationContext()).build();
        Fresco.initialize(this,pipelineConfig);

对于不对ImagePipeline进行配置的话,Fresco将采用默认的配置,而默认的配置到底配置了哪些信息这个我们要清楚。
对于ImagePipelineConfig我们主要可以配置以下属性:

1、Bitmap.Config mBitmapConfig ——所加载图片的配置,默认为Bitmap.Config.ARGB_8888

2、Supplier<MemoryCacheParams> mBitmapMemoryCacheParamsSupplier——已解码图片的内存缓存,默认配置:缓存容量最大存储256个Bitmap元素,缓存大小是根据最大可用内存来动态改变的,如下:

  private int getMaxCacheSize() 
    final int maxMemory =
        Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE);
    if (maxMemory < 32 * ByteConstants.MB) 
      return 4 * ByteConstants.MB;
     else if (maxMemory < 64 * ByteConstants.MB) 
      return 6 * ByteConstants.MB;
     else 
      // We don't want to use more ashmem on Gingerbread for now, since it doesn't respond well to
      // native memory pressure (doesn't throw exceptions, crashes app, crashes phone)
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) 
        return 8 * ByteConstants.MB;
       else 
        return maxMemory / 4;
      
    
  

3、mDecodeMemoryFileEnabled——是否根据不同的平台来构建相应的解码器,默认为false。所以我们需要设置为true。

  public static PlatformDecoder buildPlatformDecoder(
      PoolFactory poolFactory,
      boolean decodeMemoryFileEnabled) 
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 
      return new ArtDecoder(
          poolFactory.getBitmapPool(),
          poolFactory.getFlexByteArrayPoolMaxNumThreads());
     else 
      if (decodeMemoryFileEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) 
        return new GingerbreadPurgeableDecoder();
       else 
        return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
      
    
  

4、mDownsampleEnabled——设置EncodeImage解码时是否解码图片样图,必须和ImageRequest的ResizeOptions一起使用,作用就是在图片解码时根据ResizeOptions所设的宽高的像素进行解码,这样解码出来可以得到一个更小的Bitmap。通过在Decode图片时,来改变采样率来实现得,使其采样ResizeOptions大小。
ResizeOptions和DownsampleEnabled参数都不影响原图片的大小,影响的是EncodeImage的大小,进而影响Decode出来的Bitmap的大小,ResizeOptions须和此参数结合使用是因为单独使用ResizeOptions的话只支持JPEG图,所以需支持png、jpg、webp需要先设置此参数。

      JobRunnable job = new JobRunnable() 
        @Override
        public void run(EncodedImage encodedImage, boolean isLast) 
          if (encodedImage != null) 
            if (mDownsampleEnabled) 
              ImageRequest request = producerContext.getImageRequest();
              if (mDownsampleEnabledForNetwork ||
                  !UriUtil.isNetworkUri(request.getSourceUri())) 
                encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(
                    request, encodedImage));
              
            
            doDecode(encodedImage, isLast);
          
        
      ;

5、mEncodedMemoryCacheParamsSupplier——编码图片的内存缓存。缓存大小默认是通过app运行时最大内存决定的,且最多可存储getMaxCacheSize()/8个缓存元素,如下代码:

  private int getMaxCacheSize() 
    final int maxMemory = (int) Math.min(Runtime.getRuntime().maxMemory(), Integer.MAX_VALUE);
    if (maxMemory < 16 * ByteConstants.MB) 
      return 1 * ByteConstants.MB;
     else if (maxMemory < 32 * ByteConstants.MB) 
      return 2 * ByteConstants.MB;
     else 
      return 4 * ByteConstants.MB;
    
  

想要自己改造那么就自己实现Supplier接口。
6、mImageCacheStatsTracker——缓存的统计数据追踪器。它是一个接口,提供了各个缓存中图片Hit与Miss的回调方法,通常可以使用它来统计缓存命中率,默认情况下Fresco提供了一个NoOp无操作的实现类,我们若是需要使用此功能,必须我们实现ImageCacheStatsTracker接口,在各个回调方法处理。各个方法回调的顺序如下:

12-25 15:36:35.170 25920-25920/com.sunzxy.myapplication E/zxy: zxy:registerBitmapMemoryCache
12-25 15:36:35.170 25920-25920/com.sunzxy.myapplication E/zxy: zxy:registerEncodedMemoryCache
12-25 15:36:35.330 25920-25920/com.sunzxy.myapplication E/zxy: zxy:onBitmapCacheMiss
12-25 15:36:35.330 25920-25958/com.sunzxy.myapplication E/zxy: zxy:onBitmapCacheMiss
12-25 15:36:35.340 25920-25958/com.sunzxy.myapplication E/zxy: zxy:onMemoryCacheMiss
12-25 15:36:35.340 25920-25960/com.sunzxy.myapplication E/zxy: zxy:onStagingAreaMiss
12-25 15:36:35.360 25920-25960/com.sunzxy.myapplication E/zxy: zxy:onDiskCacheHit
12-25 15:36:35.360 25920-25960/com.sunzxy.myapplication E/zxy: zxy:onMemoryCachePut
12-25 15:36:35.380 25920-25965/com.sunzxy.myapplication E/zxy: zxy:BitmapCachePut

每次请求图片时,都会走这个路径,当在各个缓存中hit时,将回调对应得方法,然后在hit时会将图片分别添加至Encode和Decode缓存。
7、mMainDiskCacheConfig——硬盘缓存的配置,默认缓存目录在app自身CacheDir的image_cache目录下,其中设置了三个必备的属性,分别为:最大缓存大小、在低内存设备下的缓存大小、在极低内存设备下的缓存大小。默认值为40M、10M、2M。
这是配置一个DiskCacheConfig的代码:

DiskCacheConfig.newBuilder()
        .setBaseDirectoryPathSupplier(
            new Supplier<File>() 
              @Override
              public File get() 
                return context.getApplicationContext().getCacheDir();
              
            )
        .setBaseDirectoryName("image_cache")
        .setMaxCacheSize(40 * ByteConstants.MB)
        .setMaxCacheSizeOnLowDiskSpace(10 * ByteConstants.MB)
        .setMaxCacheSizeOnVeryLowDiskSpace(2 * ByteConstants.MB)
        .build();

8、mMemoryTrimmableRegistry——注册一个内存调节器,它将根据不同的MemoryTrimType回收类型在需要降低内存使用时候进行回收一些内存缓存资源(Bitmap和Encode)。默认传入NoOp无操作的一个实现类。
我们自己实现需要实现MemoryTrimmableRegistry接口,然后在它的两个方法中根据自身需求进行MemoryTrimType的赋值来决定是采取哪个清除策略来回收内存缓存资源。MemoryTrimType是一个枚举,类型共有四个:

public enum MemoryTrimType 

  /** The application is approaching the device-specific Java heap limit. */
  OnCloseToDalvikHeapLimit(0.5),

  /** The system as a whole is running out of memory, and this application is in the foreground. */
  OnSystemLowMemoryWhileAppInForeground(0.5),

  /** The system as a whole is running out of memory, and this application is in the background. */
  OnSystemLowMemoryWhileAppInBackground(1),

  /** This app is moving into the background, usually because the user navigated to another app. */
  OnAppBackgrounded(1);

自己实现得内存调节器如下:

public class MyMemoryTrimmableRegistry implements MemoryTrimmableRegistry 
    @Override
    public void registerMemoryTrimmable(MemoryTrimmable trimmable) 

trimmable.trim(MemoryTrimType.OnSystemLowMemoryWhileAppInBackground);
    

    @Override
    public void unregisterMemoryTrimmable(MemoryTrimmable trimmable) 

其中Fresco并没有对unregister方法进行回调,其中MemoryTrimmable是一个接口,它只有一个方法trim(),就是回收内存缓存资源的,它的实现不需要我们自己写,而是在CountingMemoryCache类中帮我们实现好了,CountingMemoryCache是一个基于LRU策略来管理缓存中元素的一个类,它实现的trim()方法可以根据Type的不同来采取不同策略的回收为:

  @Override
  public void trim(MemoryTrimType trimType) 
    ArrayList<Entry<K, V>> oldEntries;
    final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);
    synchronized (this) 
      int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio));
      int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());
      oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize);
      makeOrphans(oldEntries);
    
    maybeClose(oldEntries);
    maybeNotifyExclusiveEntryRemoval(oldEntries);
    maybeUpdateCacheParams();
    maybeEvictEntries();
  

trim()方法中主要就是做了这么一件事:根据Type的不同来回收不同比例的内存缓存中最近未被使用的元素。体现在下面这行代码:

int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());

9、mNetworkFetcher——网络图片下载请求类,底层网络请求默认使用HttpUrlConnection,并且使用一个固定线程数为3的线程池来管理请求。也可以使用Volley和Okhttp进行扩展,官方默认已经实现好了,如需使用可以参考官方文档引入额外jar包。
10、mProgressiveJpegConfig——渐进式显示网络的JPEG图的配置,默认传入一个SimpleProgressiveJpegConfig,通常使用默认配置即可,图片加载时会从模糊慢慢的到清晰的一个显示过程,不过要使用渐进式显示图片,需要在ImageRequest中显示的设置是否支持渐进式显示:

ImageRequest request = ImageRequestBuilder
    .setProgressiveRenderingEnabled(true)
    .build();

说明一点,渐进式显示的效果和渐入图片显示效果是两码事,渐进式使用的策略是在通过设置统计扫描数,当扫描数大于某个阀值时,然后进行解码一次并显示图片,最后扫描数为峰值时,进行最后一次解码,这样就显示出清晰的图片了,也就是说它是采用断点解码的。而渐入显示效果则是通过属性动画来改变alpha属性来显示图片的。
11、mResizeAndRotateEnabledForNetwork——最终影响的是mDownsampleEnabledForNetwork参数。
这个参数的作用是在mDownsampleEnabled为true的情况下,设置是否当这次请求是从网络中加载图片时,来对EncodeImage重新改变大小。也就是说设置了这个为true,可以在从网络中加载图片时候根据Resizing参数Decode出更小的样图,具体是在Decode时通过采样Resizing的像素来实现的。

          if (encodedImage != null) 
            if (mDownsampleEnabled) 
              ImageRequest request = producerContext.getImageRequest();
              if (mDownsampleEnabledForNetwork ||
                  !UriUtil.isNetworkUri(request.getSourceUri())) 
                encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(
                    request, encodedImage));
              
            
            doDecode(encodedImage, isLast);
          

这段代码的意思为:mDownsampleEnabled为true的前提下,在第一次加载图片时候,是从网络中加载,这时

!UriUtil.isNetworkUri(request.getSourceUri())

肯定为false,所以默认不支持对网络中加载的图片的EncodeImage的改变。当以后加载该图片并解析时,由于应用重新启动或者在后台中启动,这时是从硬盘中加载的,这时Uri肯定为Local的

!UriUtil.isNetworkUri(request.getSourceUri())

肯定为true,所以可以从本地加载并解析出sampleBitmap所以设置mDownsampleEnabledForNetwork为ture,这样无论是从网络还是本地都可以改变EncodeImage的大小,在图片Decode的时候解析出更小的sampleBitmap

12、mSmallImageDiskCacheConfig——小图的硬盘缓存配置,默认传入mMainDiskCacheConfig,和主硬盘缓存目录是共用的。如果需要把小图和普通图片分开,则需重新配置。

13、mExecutorSupplier——执行各个任务的线程池配置,包括配置执行IO任务、后台任务、优先级低的后台任务、Decode任务的线程池的配置。这些线程池Fresco默认都配置为Fix固定线程数量的。

mDecodeExecutor——负责图片解码成Bitmap的线程池,最大并发数为CPU的数量。
mIoBoundExecutor——负责从硬盘缓存中读取缓存图片的IO线程池,最大并发数为2。
mBackgroundExecutor——负责后台的线程任务,一般是负责图片的resize和旋转、webp的转码、后处理器的执行,最大并发数为CPU数量。
mLightWeightBackgroundExecutor——低优先级的后台线程任务,最大并发数为1。

没有什么特别需求,这些后台任务的线程池配置一般用默认的即可,默认的已经差不多做到极致了。

ImagePipeline

Image pipeline是Fresco中负责图片加载的,它支持从本地和网络中加载,文件加载支持:File、content、asset、res目录下的文件,网络加载支持http和https,支持的图片格式有:PNG、GIF、WebP、JPEG。
获取ImagePipeline可以通过Fresco的静态工厂方法:

ImagePipeline ipl = Fresco.getImagePipeline();

或者通过ImagePipelineFactory的工厂方法:

ImagePipeline  ipl =ImagePipelineFactory.getInstance().getImagePipeline();

监听图片下载进度

ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() 
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) 
      if (imageInfo == null) 
        return;
      
      QualityInfo qualityInfo = imageInfo.getQualityInfo();
      FLog.d("Final image received! " + 
          "Size %d x %d",
          "Quality level %d, good enough: %s, full quality: %s",
          imageInfo.getWidth(),
          imageInfo.getHeight(),
          qualityInfo.getQuality(),
          qualityInfo.isOfGoodEnoughQuality(),
          qualityInfo.isOfFullQuality());
    

    @Override 
    public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) 
      FLog.d("Intermediate image received");
    

    @Override
    public void onFailure(String id, Throwable throwable) 
      FLog.e(getClass(), throwable, "Error loading %s", id)
    
;

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setControllerListener(controllerListener)
    .setUri(uri);
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

其中三个回调函数:

onFinalImageSet——在加载成功时回调
onFailure——在加载失败时回调
onIntermediateImageSet——在显示渐进式JPEG图片时,这个函数会在每个扫描被解码后回调

Fresco的多图请求

假如你需要加载一张大图,这通常会比较耗时,此时你可以先下载一张缩略图先显示,待大图下载完后则显示大图。这样用户体验会好很多。

Uri lowResUri, highResUri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setLowResImageRequest(ImageRequest.fromUri(lowResUri))
    .setImageRequest(ImageRequest.fromUri(highResUri))
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

图片复用

假如有一张图片,本地和服务端都存在。就比如用户上传头像,会在服务器存一张,而本地也本身就有,所以当加载用户头像时,可以设置两个Uri,本地和网络,当在本地找到时就不必去服务端加载了,达到复用的效果。

Uri uri1, uri2;
ImageRequest request = ImageRequest.fromUri(uri1);
ImageRequest request2 = ImageRequest.fromUri(uri2);
ImageRequest[] requests =  request1, request2 ;

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setFirstAvailableImageRequests(requests)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

扩展

图片的缩放和旋转

1、BitmapFactory.Options的inSampleSize

这种方式对图片进行解码压缩,一系列操作都是通过native层的c/c++代码进行的,所以进行压缩过后Bitmap所占用的空间大小会比原来的小很多。

应用场景:通常使用在图片的压缩上,先用inJustDecodeBounds获取图片的宽高大小,通过判断图片是否过大来进行压缩。

2、View的Scale

这种方式的缩放就是通过绘制(Canvas)来实现得,通常我们在动画方面用的较多,这种方式的特点只是单纯的对Bitmap进行放大或缩小的绘制,而实际上Bitmap所占用的内存空间在Bitmap放大、缩小和原始状态时的完全一样,并不能实际的改变内存的占用。

应用场景:如果图片本身不大的话,建议使用它,因为它的速度更快,而且输出的也是高质量的图。图片若是过大,则使用其它。

3、Fresco的Resizing

通过改变EncodeImage的大小来实现的,使用它重新resize的图片占用的内存通常是原始图片占用的1/8,目前只支持JPEG格式图片,所以目前都是与Downsampling结合使用来支持jpg、png、webp格式图片。

应用场景:ImageRequest中的参数,与ImagePipelineConfig中的Downsampling结合使用,Fresco内置方法。

4、Fresco的Downsampling

通过在Decode图片时,来改变采样率来实现得,使其采样EncodeImage的sampleSize的大小,这样Downsampling就变成了Decode过程中的一部分,只需改变Decode过程中对像素点的采样率就行,而不必新生成一份EncodeImage,而采样率则是建立在Resizing的宽高大小上,通过Resizing的宽高来决定采样的部分的像素点。比Resizing更快。

应用场景:和Resizing结合使用,不过在4.4版本中Decode出采样图将会占用更多的内存资源,这是个bug。

Fresco混淆配置

# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip

# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * 
    @com.facebook.common.internal.DoNotStrip *;


# Keep native methods
-keepclassmembers class * 
    native <methods>;


-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn com.android.volley.toolbox.**

Fresco官方文档

以上是关于Fresco图片框架内部实现原理探索的主要内容,如果未能解决你的问题,请参考以下文章

Android之图片加载框架Fresco基本使用

深入探索Glide图片加载框架:做了哪些优化?如何管理生命周期?怎么做大图加载?

android继续探索Fresco

Android之图片加载框架Fresco基本使用

Fresco源码分析之DraweeView

Fresco 原理浅析