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

拉丁吳發表於2019-03-04

專案地址 : 統一圖片載入框架:一套API,兩個載入庫

前言

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

但是實際上,在構造統一的圖片載入框架的時候,真正難題在於Fresco,因為它的侵入性太強了,它要求我們使用它定義的圖片容器,因此,如何使Fresco非侵入的接入到我們的開發專案中,這成了一個比較困難的問題,之前發過一篇文章非侵入式的使用Fresco,提出了很多方案,但是總體而言沒有一個特別完美的,但是後來研究原始碼以及Fresco自定義View發現了一種更簡單更完美的非侵入式載入的方案。在這裡,為了方便大家理解,我先從Fresco的結構說起。

從Fresco的結構說起

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

相信大家都看過這張圖,這是Fresco的資料處理模組Image Pipline的結構圖,如果省去其內部的處理邏輯,我們可以簡單理解成下面這種結構圖

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

一般我們並沒有在專案中直接Image Pipline,而是使用DraweeView或者SimpleDraweeView來載入圖片,那麼,當我們使用DraweeView來載入圖片的時候,整個載入過程是怎樣的呢?

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

上圖是我們經常提及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層。

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

可以說,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的非侵入式的方案想了很多種,目前這是最簡單,而且相對完美的方案了。

相關文章