×

从Fresco源码中找到非侵入式的答案

96
拉丁吴
2018.03.08 20:56 字数 2045

项目地址 : 统一图片加载框架:一套API,两个加载库

前言

我发现,市面上最主流的加载框架大概只有这Fresco,Glide,Picasso,而Glide又脱胎于Picasso,他们的API结构是很类似的,只要能够兼容这Fresco和Glide这两个库,基本就可以形成一个统一的图片加载框架。

但是实际上,在构造统一的图片加载框架的时候,真正难题在于Fresco,因为它的侵入性太强了,它要求我们使用它定义的图片容器,因此,如何使Fresco非侵入的接入到我们的开发项目中,这成了一个比较困难的问题,之前发过一篇文章非侵入式的使用Fresco,提出了很多方案,但是总体而言没有一个特别完美的,但是后来研究源码以及Fresco自定义View发现了一种更简单更完美的非侵入式加载的方案。在这里,为了方便大家理解,我先从Fresco的结构说起。

从Fresco的结构说起

image

相信大家都看过这张图,这是Fresco的数据处理模块Image Pipline的结构图,如果省去其内部的处理逻辑,我们可以简单理解成下面这种结构图

image

一般我们并没有在项目中直接Image Pipline,而是使用DraweeView或者SimpleDraweeView来加载图片,那么,当我们使用DraweeView来加载图片的时候,整个加载过程是怎样的呢?

image

上图是我们经常提及Fresco的MVC的结构,也是Fresco中比较完整的图片加载的过程:DraweeView(V层)发送请求给DraweeController(C层),C层通过与Image pipeline交互获取到数据,然后把数据更新到M层,M层再更新数据展示再V层。这基本上就是Fresco内部基本的加载流程了。

聊聊神秘的DraweeHolder

我们去看V层的DraweeView的源码时,发现内部十分简单


public class DraweeView<DH extends DraweeHierarchy> extends ImageView {

  private final AspectRatioMeasure.Spec mMeasureSpec = new AspectRatioMeasure.Spec();
  private float mAspectRatio = 0;
  private DraweeHolder<DH> mDraweeHolder;
  private boolean mInitialised = false;

  ...
  ...

}

从它的内部属性,可以看到,除了控制图片宽高比例的属性外,只剩下一个mDraweeHolder,看起所有的图片加载请求都是要通过它的,那么这个DraweeHolder里面到底有什么呢?


public class DraweeHolder<DH extends DraweeHierarchy>
    implements VisibilityCallback {

  private boolean mIsControllerAttached = false;
  private boolean mIsHolderAttached = false;
  private boolean mIsVisible = true;
  
  // M层的控制类
  private DH mHierarchy;
  // C层的控制类,负责与数据处理模块做交互
  private DraweeController mController = null;

  private final DraweeEventTracker mEventTracker = DraweeEventTracker.newInstance();
  
 ...
 ...
 ...
  
}

从源码中可以发现,其实DraweeHolder内部就是持有了DraweeHierarchy和DraweeController这两个类的引用,相当于包裹了M层和C层。

image

可以说,DraweeHolder负责了Fresco所有的核心操作的调度。而DraweeView只是作为图片容器,只是承担一些生命周期之类的信号传递,真正的图片加载的工作都是由它内部的这个DraweeHolder来实现的。

把ImageView“变成”DraweeView

根据上面的一系列的观察,我们可以思考这样一个问题:DraweeView继承自ImageView,Fresco内部主要的复杂的工作都是由DraweeHolder负责,那么我们是不是可以这么理解:ImageView和DraweeView之间,只差了一个DraweeHolder

于是,我们就会考虑尝试在外部构造这个DraweeHolder,然后在合适的时机,把数据给ImageView来展示?这样不就相当于把ImageView“变成”DraweeView了么?这样就能相对完美的实现一个非侵入式的图片加载方案。

根据fresco的自定义View(中文文档)的内容,我们发现,构造DraweeHolder大致上有如下要求:

  • 处理触摸事件 (兼容点击重试功能)
  • 设置Drawable.Callback (刷新)
  • 重写 verifyDrawable:
  • 处理 attach/detach 事件 (处理内存的问题)

也就是说,只要有能力实现上面几种要求,我们就可以在自己构造一个DraweeHolder,实现Fresco的内部调度。

上面提到的一些条件我们也并不是全部都要实现,具体那些是重要的,我们可以做一些分析。

  • 处理触摸事件 需要处理么?
    • 这个特性是为了兼容Fresco点击重试的功能,这并不是核心的功能
  • 设置Drawable.Callback 需要处理么?
    • 我观察设置这个setImageDrawable()接口之后,就会把Drawable设置给ImageView中的mDrawable,接口内部会设置Callback.
  • 重写 verifyDrawable需要处理么?
    • 设置这个setImageDrawable()接口之后, ImageView内部的verifyDrawable()方法就够用了。
  • attach/detach 事件能监听么?可能存在监听不到的情况么?
    • View.OnAttachStateChangeListener就能监听View的状态,但是也可能存在监听不全的情况,比如,imageview已经attach to window了,然后再去加载图片,那么View.OnAttachStateChangeListener就监听不到onViewAttachedToWindow()事件了(但是我观察即使发生这种情况在Fresco中也没有发生明显的错误),我的解决方案是在加载之前在判断一下imageview是否已经attach to window了。

基本上,我们在构建DraweeHolder的几条要求中,基本上只有最后一条是需要需要我们重视的,就是处理 attach/detach 事件。(当然,如果我说错了,欢迎指出我的问题)

具体实现

上面的一整套的分析下来,真正的编码实现反而很简单了。我们只需要模仿DraweeView的加载流程,去构造我们ImageView的加载流程,

关键代码如下:

    {
        ...
        ...
        
        DraweeController controller;

        if (draweeHolder == null) {
            draweeHolder=DraweeHolder.create(hierarchy,options.getViewContainer().getContext());
            controller=controllerBuilder.build();

        }else {
            controller= controllerBuilder.setOldController(draweeHolder.getController()).build();

        }

        // 请求
        draweeHolder.setController(controller);

        ViewStatesListener mStatesListener=new ViewStatesListener(draweeHolder);
        // 外部传入的需要加载图片的ImageView
        imageView.addOnAttachStateChangeListener(mStatesListener);

        // 判断是否ImageView已经 attachToWindow
        if (ViewCompat.isAttachedToWindow(imageView)) {
            draweeHolder.onAttach();
        }

        // 保证每一个ImageView中只存在一个draweeHolder
        imageView.setTag(R.id.fresco_drawee,draweeHolder);
        // 设置好Drawable,准备拿到图片数据
        imageView.setImageDrawable(draweeHolder.getTopLevelDrawable());
    }
        
    public class ViewStatesListener implements View.OnAttachStateChangeListener{
        private DraweeHolder holder;
        public ViewStatesListener(DraweeHolder holder){
            this.holder=holder;
        }

        @Override
        public void onViewAttachedToWindow(View v) {
            this.holder.onAttach();
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            this.holder.onDetach();
        }
    }

还存在的瑕疵:观察DraweeView的源码,我发现它还在onStartTemporaryDetach,onFinishTemporaryDetach这两个方法,我查阅了一下,是ListView中的View在滑动过程中,被缓存的时候调用的生命周期方法。我找了很久,好像也没有可以监听这个的方法,不过因为我们现在使用的更多的应该是RecycleView而不是ListView,而且内存没有被主动释放,但是到了阈值,内存还是会被释放的。这不构成大问题。

统一图片加载框架

根据这个方案,我重新整理了统一图片加载框架。

项目地址 : 统一图片加载框架:一套API,两个加载库

我很早就写了这个统一的图片加载框架,目前涵盖了Glide和Fresco,只需要引入相关的依赖,就可以依托上层的加载框架来调用Glide或者Fresco这种底层库,这种方式除了高度集中的使用了图片库之外,可以实现两种图片库之间几乎无代价的切换。

一套API,两种加载库。

后记

这套非侵入式的方案其实在去年年末的时候做内部技术分享的时候就分享了,不过后来一直比较忙,然后我又很能拖延,也就一直没有整理成文章,导致现在已经过去了好几个月了,感觉再不写以后就烂在草稿箱里了,最后就一鼓作气把它整理出来了。

统一图片加载框架的想法起源于去年我们的项目因为一些需求必须引入Fresco图片库,但是我们的项目一直使用的是Glide,因此,在一段时间内,我们的项目同时引用了两个图片加载库(捂脸)

因此我考虑慢慢把整个项目从Glide无缝过渡到Fresco中,这样就可以撤掉一个图片库了,因此做了一个统一图片加载框架,磨平两个库的差异性,保证一套API在两个底层库中能有同样的效果。

整个过程针对Fresco的非侵入式的方案想了很多种,目前这是最简单,而且相对完美的方案了。

随笔
Web note ad 1