從Fresco源碼中找到非侵入式的答案

項目地址 : 統一圖片加載框架:一套API,兩個加載庫html

前言

我發現,市面上最主流的加載框架大概只有這Fresco,Glide,Picasso,而Glide又脫胎於Picasso,他們的API結構是很相似的,只要可以兼容這Fresco和Glide這兩個庫,基本就能夠造成一個統一的圖片加載框架。git

可是實際上,在構造統一的圖片加載框架的時候,真正難題在於Fresco,由於它的侵入性太強了,它要求咱們使用它定義的圖片容器,所以,如何使Fresco非侵入的接入到咱們的開發項目中,這成了一個比較困難的問題,以前發過一篇文章非侵入式的使用Fresco,提出了不少方案,可是整體而言沒有一個特別完美的,可是後來研究源碼以及Fresco自定義View發現了一種更簡單更完美的非侵入式加載的方案。在這裏,爲了方便你們理解,我先從Fresco的結構提及。github

從Fresco的結構提及

相信你們都看過這張圖,這是Fresco的數據處理模塊Image Pipline的結構圖,若是省去其內部的處理邏輯,咱們能夠簡單理解成下面這種結構圖緩存

通常咱們並無在項目中直接Image Pipline,而是使用DraweeView或者SimpleDraweeView來加載圖片,那麼,當咱們使用DraweeView來加載圖片的時候,整個加載過程是怎樣的呢?bash

上圖是咱們常常說起Fresco的MVC的結構,也是Fresco中比較完整的圖片加載的過程:DraweeView(V層)發送請求給DraweeController(C層),C層經過與Image pipeline交互獲取到數據,而後把數據更新到M層,M層再更新數據展現再V層。這基本上就是Fresco內部基本的加載流程了。框架

聊聊神祕的DraweeHolder

咱們去看V層的DraweeView的源碼時,發現內部十分簡單ide

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裏面到底有什麼呢?ui

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層。this

能夠說,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的非侵入式的方案想了不少種,目前這是最簡單,並且相對完美的方案了。

相關文章
相關標籤/搜索