Fresco源码分析(3) - DraweeView显示图层树

Fresco的源码中,DraweeView的介绍简洁明了:我就是把DraweeHierarchy显示到屏幕上的家伙。那我们来分析一下相关代码,看看它的逻辑是什么样的。

1 时序图

首先可以用以下这个图初步理解SimpleDraweeView在调用了setUri(Uri uri)之后的流程:

DraweeView

可以将这张图描述为以下信息:

  • DraweeView直接显示DraweeHierarchy;
  • DraweeController根据Uri获取数据源DataSource,并绑定数据订阅者DataSubscriber
  • DataSource可以更新数据时通知DataSubscriber更新DraweeHierarchy。(在Fresco源码分析(4) - 异步加载数据中分析)

2 基类DraweeView

DraweeView<DH extends DraweeHierarchy>是Fresco视图中的基类,使用的泛型必须是DraweeHierarchy,它继承了ImageView

DraweeView内部的函数不多,它们全部都是通过DraweeHolder的相应函数实现的。主要有以下这几个:

  • void init(Context context) 初始化DraweeHolder
  • void setHierarchy(DH hierarchy) 设置图层树,会调用 super.setImageDrawable(mDraweeHolder.getTopLevelDrawable())将图层树显示出来;
  • void setController(@Nullable DraweeController draweeController) 设置DraweeController,像setHierarchy一样同样也会显示图层树;
  • Drawable getTopLevelDrawable() 获取通过包装好的图层树(见DraweeHierarchy构建图层);
  • View中的onAttachedToWindow()onDetachedFromWindow()onStartTemporaryDetach()onFinishTemporaryDetach() 四个回调函数,提供当视图被绑定/解绑到指定布局上时的回调函数,它们会触发DraweeHolderonDetach()onAttach()
  • void onTouchEvent(MotionEvent event) 提供触屏反应。

在使用它之前要注意官方的API注释中有这么一段话:

你只能在将DraweeView仅仅当做ImageView来使用时才调用setImageXXX函数。

要记住它:无论什么形式的DraweeView,目标图片的设置最终都是通过DraweeController。我们会在下一章中介绍DraweeController,它是连接DraweeHierarchyImage Pipeline的桥梁。虽然setHierarchysetController都会显示出图层树,但是实际上setHierarchy在显示图片的时候只是把DraweeView当做普通的ImageView使用,要使用Fresco的缓存、加载机制,必须使用DraweeController

不过所幸Fresco实现了SimpleDraweeVew帮我们来处理这些繁琐的过程,它封装了Controller的使用。

2.1 DraweeHolder

DraweeHolder充斥在DraweeView的各个位置,每个DraweeView的函数都是由它的对应函数执行的。它随着DraweeView的产生而初始化。在深入了解视图绘制之前,我们有必要了解它是做什么的。

DraweeHolder是用来维持DraweeHierarchyDraweeController之间的沟通的。通过create( DH hierarchy, Context context)来创建实例,DraweeHierarchy通过第一个参数赋值,其中第二个参数用于registerWithContext(Context)(该函数暂时没有完善好)。

它有几个主要使用的函数:

  • void setHierarchy(DH hierarchy)DraweeView.setHierarchy中被调用,将DraweeHierarchy传给持有的DraweeController;
  • void setController(DraweeController draweeController)DraweeeView.setController中被调用无条件解绑旧的Controller(如果存在的话),并将旧DraweeController的DraweeHierarchy设置为null,并调用DraweeController.onDetach()将它变为解除绑定状态;将持有的DraweeHierarchy(如果有的话)赋给新传入的DraweeController并调用DraweeController.onAttach()让它变为绑定状态。(更换DraweeController确是一个非常耗时的过程,应该尽量避免为视图指定新的DraweeController,参考官方文档
  • void attachController() 若所属的DraweeView未绑定DraweeControllerDraweeController成员变量不为空并且已经设置过图层树之后,调用该成员变量的onAttach方法;
  • void detachController() 若所属的DraweeView已绑定DraweeControllerDraweeController成员变量不为空,调用该成员变量的onDetach方法;
  • void attachOrDetachController() 当已绑定DraweeController并且所属的DraweeView可见时,调用attachController;否则调用detachController

特别地,DraweeHolder继承了VisibilityCallback,为DraweeHierarchy.RootDrawable提供回调:当图层的Visibility属性改变的时候对DraweeController调用attachOrDetachController操作,当图层不可见时释放资源。

2.2 GenericDraweeView

GenericDraweeView就是使用GenericDraweeHierarchy图层树的视图,它承担了所有的xml属性交互工作。

实际上在图层树上目前Fresco也只实现了一个GenericDraweeHierarchy,使用泛型是为了后续的开发便利。

它会在初始化的时候调用inflateHierarchy(Context context, AttributeSet attrs)函数,从xml的中获取如下几个属性(如果存在的话):

  • fadeDuation 渐隐/渐显动画时间;
  • aspectRatio 图片长宽比例,参考官方文档
  • `XXXImage/XXXImageScaleType 各图层要显示的Drawable(除了目标显示图层)及它们的ScaleType
  • RoundingParams中的参数

在它的Measure过程中,会依次判断是否高度、宽度属性中有wrap_content,会将先判断到的属性更正为实际长度。但是Fresco并不支持使用wrap_content。如果你非要使用,只能在width/height中使用一侧,然后搭配aspectRatio使用。

在GenericDraweeView获取完xml属性之后,它会通过GenericDraweeHierarchyBuilder.build创造一个与之对应的GenericDraweeHierarchy作为默认使用的图层树,并调用setHierarchy方法将其传递给DraweeHolder并显示出来。

2.3 SimpleDraweeView

在SimpleDraweeView中的函数就更少了,可以明确的说,它就只是将GenericDraweeHierarchy显示到UI界面上的空间而已。它比GenericDraweeView多出来的功能就是它内部提供了最简单的DraweeController实现。它有两个函数比较需要关注:

  • initialize函数,这个函数会在Fresco.initialize中调用,目的就是初始化DraweeControllerBuilder,用于构建DraweeController。

  • setImageURI 这个函数我们会经常调用,因此有必要看看它的实现和普通ImageView的setImageXXX方法有什么区别:

1
2
3
4
5
6
7
8
public void setImageURI(Uri uri, @Nullable Object callerContext) {
DraweeController controller = mSimpleDraweeControllerBuilder
.setCallerContext(callerContext)
.setUri(uri)
.setOldController(getController())
.build();
setController(controller);
}

setController会调用DraweeHolder.setController,将图层树的控制权交给DraweeController,并显示出图层树。

3 DraweeController

DraweeController是一个将Fresco中负责数据加载的组件组合起来并将信息反映到DraweeHierarchy的组件。它通过建造者模式初始化,基类AbstractDraweeControllerBuilder使用了四个泛型:(括号中为PipelineDraweeControllerBuilder所使用的类型)

  • BUILDER 建造者类型(AbstractDraweeControllerBuilder)
  • REQUEST 图像请求类型(ImageRequest)
  • IMAGE 图像类型(CloseableReference)
  • INFO 图像信息类型(ImageInfo)

.build()中会初始化DraweeController。下面我们会先介绍几个关键概念,然后介绍DraweeController的初始化过程。

3.1 ImageRequest

ImageRequest存储着Image Pipeline处理被请求图片所需要的有用信息(Uri、是否渐进式图片、是否返回缩略图、缩放、是否自动旋转等)。

ImagePipeline仅仅用来装信息,而且一经初始化后就只能获取内容,无法改变内容(即Immutable)。 初始化ImageRequest只能通过ImageRequest.fromUri(Uri uri)ImageRequestBuilder.build()来实现。

我们来看看它内部存储着一些什么信息:

  • ImageType:若为ImageType.SMALL,则所请求的图片会存储在专用小文件缓存中。具体参考Fresco缓存
  • SourceUri:图片源Uri;
  • SourceFile:图片文件地址(若是本地文件的话);
  • ProgressiveRenderingEnabled 若为true,则这个图片请求会返回质量递进的几次图片信息(渐进式图片);
  • LocalThumbnailPreviewsEnabled 若为true,则这个图片请求会在访问本地图片时先返回一个缩略图;
  • ResizeOptions 缩放尺寸,仅支持JPEG,而且不是每次都需要在ImageRequest中设置缩放尺寸的,具体使用请参照Fresco缩放和旋转图片
  • AutoRotateEnabled 是否允许图片旋转,具体参照Fresco缩放和旋转图片
  • RequestPriority 这个请求的优先级:
  • LowestPermittedRequestLevel 最低允许从哪层缓存中取数据,参考最低请求级别
  • IsDiskCacheEnabled 若为false,则此图片请求不会从文件缓存中获取数据;
  • PostProcessor 在图片请求成功之后对图片的处理操作,参考修改图片

DraweeController是使用ImageRequest来初始化数据订阅者的。SimpleDraweeView调用setUri(Uri)会产生一个默认的ImageRequest含有指定Uri信息,如果需要修改ImageRequest其他信息,必须手动创建ImageRequest,并在PipelineDraweeControllerBuilder调用.build()之前使用.setImageRequest设置它。

3.2 可关闭的引用

Facebook在Java中实现了具有引用计数功能的类:SharedReference<T>(注意不是Android里的SharedPreference)。它将类型为T的对象进行包装,为其实现增加引用数、减少引用数、删除引用的功能。

特别地,它会使用专门用于实现释放资源的接口:ResourceReleaser<T>,它内部只有一个函数:void release(T object)

当然,但是直接让程序员直接去操作它很可能会出现问题。所以CloseableReference出现了,它为任何实现了Closeable类的对象封装了引用计数、回收引用的功能。

CloseableReference中有几个主要函数:

  • CloseableReference<T> of(T object, ResourceReleaser<T> releaser) 用于初始化引用计数(而不是使用构造函数),该函数会新建一个SharedReference将对象包装起来,引用计数为1。若不传releaser,会使用默认的ResourceReleaser,调用Closeable.close()函数回收Closeable引用;
  • CloseableReference<T> clone() 用于添加对象引用,该函数会新建一个CloseableReference,同时持有的SharedReference引用+1, 不会创建SharedReference对象
  • T get() 返回引用对象;
  • void close() 减少一个引用计数,若引用计数减为0,则调用releaser的release(T object)将对象释放。一旦创建了一个CloseableReference,当持有者离开作用域时就必须调用这个函数!(finally函数块是释放资源工作最好的地方)

最好不要直接使用这个工具,如果非用不可的话,你需要谨慎地操作它。使用方法参考Fresco中文文档

Fresco中定义了CloseableImage,它会在finalize的时候调用close(),有这两个类继承了它:

  • CloseableStaticBitmap 它内部持有一个CloseableReference<Bitmap>包装目标Bitamp,以及关于图像质量、旋转角度的信息。在close()调用的时候会调用CloseableReferenceclose()函数释放资源,释放的原理是Bitmap.recycle()
  • CloseableAnimatedBitmap 它内部持有一个List<CloseableReference<Bitmap>>包装起每一帧的Bitmap,还存有每一帧的时长。在close()调用的时候会递归释放列表资源。

3.3 数据订阅

Fresco中使用DataSource与DataScriber进行异步数据请求。DataSubscriber具有以下几个函数:

  • onNewResult 在接收到新的数据;
  • onFailure 数据加载失败;
  • onCancellation 数据请求被取消;
  • onProgressUpdate 加载进度更新。

DataSource在接受到Image Pipeline提供的数据时调用notifyDataSubscribers使DataSubscriber做出反应。

更多对于数据订阅者的分析见Fresco源码分析(4) - 异步加载数据

3.4 初始化Draweetroller

AbstractDraweeControllerBuilderDraweeControllerBuilder的基类)的build()函数中会调用继承类实现的obtainController()函数,在默认使用的PipelineDraweeControllerBuilder中,它会做三件重要的事情:

  • obtainDataSourceSupplier() 根据Uri获取数据源DataSourceSupplier
  • generateUniqueControllerId() 获取独一无二的Controller标识(使用静态递增的AtomicLong变量记录唯一标识);
  • getCallerContext() 获取调用者的Context

之后将上面得到的三个变量赋值给Controller,若是第一次设置DraweeController,还会相应地初始化这几个组件(若存在的话):

  • RetryManager 管理失败重试的组件;
  • GestureDetector 传递触屏事件的组件;
  • DeferredReleaser 向主线程消息队列中添加释放资源任务的组件;
  • ControllerListener 设置回调函数,具体参考 3.5 使用ControllerListener
  • DraweeHierarchy 设置图层树。

setController调用后,如果传入的DraweeController发生了onAttach,它就会调用subtitRequest()提交数据请求(如果还没有提交的话),我们来看看这个函数是怎么实现的:

protected void submitRequest() {

    //一些初始化工作

    final String id = mId;
    final boolean wasImmediate = mDataSource.hasResult();
    final DataSubscriber<T> dataSubscriber =
        new BaseDataSubscriber<T>() {
          @Override
          public void onNewResultImpl(DataSource<T> dataSource) {
            boolean isFinished = dataSource.isFinished();
            float progress = dataSource.getProgress();
            T image = dataSource.getResult();
            if (image != null) {
              onNewResultInternal(id, dataSource, image, progress, isFinished, wasImmediate);
            } else if (isFinished) {
              onFailureInternal(id, dataSource, new NullPointerException(), /* isFinished */ true);
            }
          }
          @Override
          public void onFailureImpl(DataSource<T> dataSource) {
            onFailureInternal(id, dataSource, dataSource.getFailureCause(), /* isFinished */ true);
          }
          @Override
          public void onProgressUpdate(DataSource<T> dataSource) {
            boolean isFinished = dataSource.isFinished();
            float progress = dataSource.getProgress();
            onProgressUpdateInternal(id, dataSource, progress, isFinished);
          }
        };
    mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
}

从这段代码中我们就可以看出初步的逻辑了,它会创建一个DataScriber并将其绑定到DataSource上,随后只要DataSource处理好图片就会为绑定的DataScriber发布消息。通过在AbstractDraweeController中定义的onFailureInternalonNewResultInternalonProgressUpdateInternal来对DraweeHierarchy做相应的改动,从而控制显示层。

3.5 使用ControllerListener

ControllerListener 提供DraweeController主要事件的回调功能,主要有这几个事件:

  • onSubmit 在提交图片请求时调用;
  • onFinalImageSet 最终图片加载完成时调用;
  • onIntermediateImageSet 任何渐进式图片的过渡图片加载完成时调用;
  • onIntermediateImageFailed 过渡图片加载失败时调用;
  • onFailure 最终图片加载失败时调用;
  • onRelease 释放图片资源时调用。

你可以继承它并在DraweeControllerBuilder中设置,从而实现一些自定义的提醒事件。

4 类图

DraweeView Diagram