Fresco原始碼分析之DraweeView

怪談時間發表於2019-03-04

Android中圖片載入的框架很多,例如:FrescoPicassoGlideImageloader。它們都有各自的優點,但總的來說,使用起來方便簡單、可配置性高與提供良好的快取機制。由於平常主要用的還是Fresco,所以這裡有必要對Fresco的原理進行深入研究。這樣對於以後的使用與理解將會得到巨大的幫助。

Fresco是專注於對圖片載入而設計的框架,所以對於以圖片為主的App強烈推薦使用。Fresco對於圖片的展示支援多種情況:backgroud image(背景圖)、placeholder image(佔點陣圖)、actual image(載入的圖片)、progress bar image(進度條)、retry image(重新載入的圖片)、failure image(失敗圖片)與overlay image(疊加圖)。Fresco既然支援這麼多圖片展示情況,那麼它對這次圖層的管理模式又是怎麼樣的呢?Fresco對於這些圖層的管理都交給了Hierarchy,而這些圖層的資料都通過Controller來設定。先不分析這些,這些後續文章會詳細分析,今天先從Fresco的基本元件開始。

SimpleDraweeView

首先這是對Fresco的原始碼分析,所以在看這篇文章之前你應該要有使用Fresco的基礎,如果沒有的強烈推薦看下Fresco官方文件

我們使用Fresco進行圖片載入,使用最多的還是已經封裝好的SimpleDraweeView,而在SimpleDraweeView的構造方法中會呼叫init()方法,它的原始碼如下:

private void init(Context context, @Nullable AttributeSet attrs) {
    if (isInEditMode()) {
      return;
    }
    Preconditions.checkNotNull(
        sDraweeControllerBuilderSupplier,
        "SimpleDraweeView was not initialized!");
    mSimpleDraweeControllerBuilder = sDraweeControllerBuilderSupplier.get();
 
    if (attrs != null) {
      TypedArray gdhAttrs = context.obtainStyledAttributes(
          attrs,
          R.styleable.SimpleDraweeView);
      try {
        if (gdhAttrs.hasValue(R.styleable.SimpleDraweeView_actualImageUri)) {
          setImageURI(
              Uri.parse(gdhAttrs.getString(R.styleable.SimpleDraweeView_actualImageUri)),
              null);
        } else if (gdhAttrs.hasValue((R.styleable.SimpleDraweeView_actualImageResource))) {
          int resId = gdhAttrs.getResourceId(
              R.styleable.SimpleDraweeView_actualImageResource,
              NO_ID);
          if (resId != NO_ID) {
            setActualImageResource(resId);
          }
        }
      } finally {
        gdhAttrs.recycle();
      }
    }
  }
複製程式碼

這個方法做的事情很簡單,但我們要注意的是它會對sDraweeControllerBuilderSupplier進行null判斷,如果為null將會丟擲異常。sDraweeControllerBuilderSupplier是供應類,通過它的get方法來獲取DraweeControllerBuilder,這個是controller構造器,這個以後的章節會詳細說明。空判斷的目的就是在使用SimpleDraweeView之前必須初始化sDraweeControllerBuilderSupplier。在SimpleDraweeView中我們能找到它的初始化方法

public static void initialize(
      Supplier<? extends SimpleDraweeControllerBuilder> draweeControllerBuilderSupplier) {
    sDraweeControllerBuilderSupplier = draweeControllerBuilderSupplier;
  }
複製程式碼

它是一個static方法,在SimpleDraweeView初始化之前載入一次即可。這是SimpleDraweeView的關鍵。它還有一個關鍵方法

public void setImageURI(Uri uri, @Nullable Object callerContext) {
    //通過controller 來儲存Uri 等相關資訊
    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();
    setController(controller);
  }
複製程式碼

使用mSimpleDraweeControllerBuilder來構建一個Controller,而Controller是由build模式所建立,這裡我們能看到uri也交由Controller管理。其實最終uri會封裝成一個ImageRequestController真正持有的是uri的封裝體ImageRequest。在SimpleDraweeView中它會重寫setImageURI方法,最終也就是將ImageView中的原生方法給覆蓋掉。還有其它的類似的setImageResourcesetImageBitmap等,Fresco都在這些方法上加了@Deprecated,意思就是說不推薦使用,如果使用的話就跟直接使用ImageView沒什麼區別,這就無法體驗到Fresco的強大的特性。在Fresco中統一由setController來替代。

關於Controller後續文章會詳細分析。

Fresco

如果我們根據Fresco官方文件的正常步驟來使用的話就無需擔心這一步,因為在我們在使用Fresco之前都要先呼叫Fresco.initialize(context)

public static void initialize(
      Context context,
      @Nullable ImagePipelineConfig imagePipelineConfig,
      @Nullable DraweeConfig draweeConfig) {
    if (sIsInitialized) {
      FLog.w(
          TAG,
          "Fresco has already been initialized! `Fresco.initialize(...)` should only be called " +
            "1 single time to avoid memory leaks!");
    } else {
      sIsInitialized = true;
    }
    // we should always use the application context to avoid memory leaks
    context = context.getApplicationContext();
    if (imagePipelineConfig == null) {
      //初始化ImagePipeline工廠,包含ImagePipelineConfig 相關初始化配置資訊
      // (三級快取、圖片解碼/編碼、轉化、漸變、bitmap配置、四種executor 分別為 io、decode、background、lightweight background)等
      ImagePipelineFactory.initialize(context);
    } else {
       ImagePipelineFactory.initialize(imagePipelineConfig);
    }
    //初始化Drawee相關配置資訊
    initializeDrawee(context, draweeConfig);
  }
複製程式碼

除了初始化ImagePipeline之外,最後還會呼叫initializeDrawee (context, draweeConfig),我們來看下initializeDrawee做了什麼

private static void initializeDrawee(
      Context context,
      @Nullable DraweeConfig draweeConfig) {
    //構建PipelineDraweeControllerBuilderSupplier,
    //其中ImagePipeline、PipelineDraweeControllerFactory、ControllerListener set集合
    sDraweeControllerBuilderSupplier =
        new PipelineDraweeControllerBuilderSupplier(context, draweeConfig);
    //初始化SimpleDrawee
    //初始化時通過呼叫PipelineDraweeControllerBuilderSupplier實現的Supplier的get()方法
    // 返回配置資訊的封裝體PipelineDraweeControllerBuilder implements SimpleDraweeControllerBuilder
     SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier);
  }
複製程式碼

在這裡我們會看到之前所提到的sDraweeControllerBuilderSupplierSimpleDraweeView中必須優先呼叫的initialize方法。相信現在應該明白的為什麼在使用Fresco之前必須呼叫它的initialize方法了。因為它必須要初始化一些必要的配置資訊,其中就包括使用的控制元件SimpleDraweeView的配置資訊。

GenericDraweeView

上面所說的SimpleDraweeView的父類是GenericDraweeView,它做的事情很簡單,處理xml相關的屬性。它會通過inflateHierarchy方法進行初始化。

protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
    GenericDraweeHierarchyBuilder builder =
        GenericDraweeHierarchyInflater.inflateBuilder(context, attrs);
    setAspectRatio(builder.getDesiredAspectRatio());
    setHierarchy(builder.build());
  }
複製程式碼

它是由GenericDraweeHierarchyBuilder來統一封裝這些屬性。最終通過build方法來構建GenericDraweeHierarchy,這就是Fresco的圖層。然後通過setHierarchy將圖層傳遞給DraweeHolderDraweeHolder是用來管理HierarchyController的。而DraweeHolder是在最底層的DraweeView中,這也是GenericDraweeView的父類。下面我們進入DraweeView,來看看它到底做了什麼。

DraweeView

DraweeViewFresco最底層的控制元件,也是我們使用它展示圖片的基礎,它繼承於ImageView,所以它能做的事也無非於在原生ImageView上做擴充套件或者方法重寫,從而來實現自己的一套邏輯。先看下它的構造方法

public DraweeView(Context context) {
    super(context);
    init(context);
  }
複製程式碼

沒什麼特別的邏輯,就一個init方法,那麼就進入init看看

/** This method is idempotent so it only has effect the first time it`s called */
  private void init(Context context) {
    if (mInitialised) {
      return;
    }
    mInitialised = true;
    mDraweeHolder = DraweeHolder.create(null, context);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      ColorStateList imageTintList = getImageTintList();
      if (imageTintList == null) {
        return;
      }
      setColorFilter(imageTintList.getDefaultColor());
    }
    // In Android N and above, visibility handling for Drawables has been changed, which breaks
    // activity transitions with DraweeViews.
    mLegacyVisibilityHandlingEnabled = sGlobalLegacyVisibilityHandlingEnabled &&
        context.getApplicationInfo().targetSdkVersion >= 24; //Build.VERSION_CODES.N
  }
複製程式碼

我們可以看到它會通過mInitialised來判斷是否需要初始化,原始碼註釋也說明的該方法只會呼叫一次。這是因為建立Hierarchy的代價太大,所以只會建立一次,以後都會使用同一個mDraweeHolder中的Hierarchy,所以會看到這裡就必須初始化一個mDraweeHolder。在DraweeView中還有以下幾個主要方法:

  • void setHierarchy(DH hierarchy)設定Hierarchy,同時會將Hierarchy交由mDraweeHolder管理,最後還會呼叫super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());來將Hierarchy中的圖層樹顯示出來。
  • Drawable getTopLevelDrawable()會通過mDraweeHolder中的Hierarchy來獲取圖層樹。
  • void setController(@Nullable DraweeController draweeController)設定Controller,同時也會將Hierarchy中的圖層樹顯示出來。
  • void onAttachedToWindow()void onDetachedFromWindow()void onStartTemporaryDetach()void onFinishTemporaryDetach()是來控制圖層的顯示與隱藏、繫結與解綁的回撥函式,它們都分別會呼叫mDraweeHolderonAttach()onDetach()。其實最終呼叫的還是Controller中的onAttach()onDetach()
  • boolean onTouchEvent(MotionEvent event)控制控制元件的觸控,如果Controller有效的話會呼叫Controller中的onTouchEvent
  • void setAspectRatio(float aspectRatio)用來設定DraweeView顯示的寬高比例。
  • setImageDrawablesetImageBitmapsetImageResourcesetImageURI這些方法都是原生ImageView的方法,但在DraweeView中這些方法都被加上了@Deprecated標記。標明不推薦使用,如果一定使用的話,那麼DraweeView將會退化成一個普通的ImageView。因為在DraweeView中都是通過Controller來體現它的快取、載入機制等特性。

上面這些就是DraweeView的主要涉及到的方法與特性,不過在DraweeView中基本上每一個方法都涉及到了DraweeHolder,那它到底是幹什麼的呢?別急下面就輪到它了。

DraweeHolder

A holder class for Drawee controller and hierarchy.
複製程式碼

上面的是官方註釋,說明DraweeHolder是用來管理HierarchyController的,同時也是它們之間的聯絡的橋樑。DraweeView以及它的子類都是通過它來間接操作ControllerHierarchy

public static <DH extends DraweeHierarchy> DraweeHolder<DH> create(
      @Nullable DH hierarchy,
      Context context) {
    DraweeHolder<DH> holder = new DraweeHolder<DH>(hierarchy);
    holder.registerWithContext(context);
    return holder;
  }
複製程式碼

它是通過公有的靜態方法來建立自身例項的。在上面的DraweeViewinit方法中會呼叫。在其內部的DraweeEventTracker,是用來記錄事件的傳遞,方便dubug的除錯。如果不需要的話,可以在Fresco.initialize()之前呼叫DraweeEventTracker.disable()。那麼剩下的方法其實基本上在DraweeView中都說過。

  • onAttach()onDetach(),都會呼叫attachOrDetachController(),根據情況分別呼叫attachController()detachController(),最終呼叫的就是ControlleronAttach()onDetach()
  • Drawable getTopLevelDrawable()呼叫mHierarchy.getTopLevelDrawable()獲取圖層樹。
  • void setController(@Nullable DraweeController draweeController)設定Controller,在設定之前會先判斷是否已經wasAttached,如果是的話就先呼叫detachController(),然後清除老的Controller,再將Hierarchy設定到新的Controller中。最後再attachController()進行繫結顯示圖層。
  • void setHierarchy(DH hierarchy)設定Hierarchy,如果Controller有效的話就與Hierarchy建立連結,將Hierarchy設定到Controller中。

以上就是DraweeHolder的主要方法,都跟ControllerHierarchy相關。而DraweeHolder又與DraweeView相連,所以最終還是要回到ControllerHierarchy中。

End

這次主要是分析了Fresco中的基本元件DraweeView與它的子類。如果你還想進一步瞭解HierarchyController的原理,下篇文章將會詳細分析相關的原理,敬請期待!

Fresco原始碼分析系列Github地址

Recommend

Android共享動畫相容實現

Kotlin最佳實踐

RecyclerView下拉重新整理與上拉更多

Android高仿微信之mvp實現(四)

tensorflow-梯度下降,有這一篇就足夠了

部落格

相關文章