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

项目地址 : 统一图片加载框架:一套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的非侵入式的方案想了很多种,目前这是最简单,而且相对完美的方案了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容