Android開源框架原始碼鑑賞:Fresco

蘇策發表於2018-02-03

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章目錄

  • 一 圖片載入流程
    • 1.1 初始化Fresco
    • 1.2 獲取DataSource
    • 1.3 繫結DraweeController與DraweeHierarchy
    • 1.4 從記憶體快取/磁碟快取/網路獲取圖片,並設定到對應的Drawable層
  • 二 DraweeController與DraweeHierarchy
    • 2.1 圖層的層級構造
    • 2.2 圖層的構建流程
  • 三 Producer與Consumer
  • 四 快取機制
    • 3.1 記憶體快取
    • 3.2 磁碟快取

更多Android開源框架原始碼分析文章請參見Android open framework analysis

這個系列的文章原來叫做《Android開源框架原始碼分析》,後來這些優秀開源庫的程式碼看的多了,感覺大佬們程式碼寫的真真美如畫?,所以就更名為《Android開源框架原始碼鑑賞》了。閒話 不多說,我們進入正題,今天分析的開源庫是Fresco。

Fresco是一個功能完善的圖片載入框架,在Android開發中有著廣泛的應用,那麼它作為一個圖片載入框架,有哪些特色讓它備受推崇呢?

  • 完善的記憶體管理功能,減少圖片對記憶體的佔用,即便在低端機器上也有著不錯的表現。
  • 自定義圖片載入的過程,可以先顯示低清晰度圖片或者縮圖,載入完成後再顯示高清圖,可以在載入的時候縮放和旋轉圖片。
  • 自定義圖片繪製的過程,可以自定義谷中焦點、圓角圖、佔點陣圖、overlay、進圖條。
  • 漸進式顯示圖片。
  • 支援Gif。
  • 支援Webp。

好,又吹了一波Fresco(人家好像也不給廣告費T_T),但是光知道人家好並沒有用,我們還需要為什麼這麼好,怎麼實現的,日後在做我們的框架的時候偷師一手,豈不美哉。 Fresco的原始碼還是比較多的,看起來會比較費勁,但是不怕,Android的系統原始碼都被我們啃下來了,還怕一個小小的Fresco嗎?。要更好的去理解Fresco的實現,還是要從 整體入手,瞭解它的模組和層次劃分,層層推進,逐個理解,才能達到融會貫通的效果。

由於Fresco比較大,我們先來看一下它的整體結構,有個整體的把握,Fresco的整體架構如下圖所示:

? 點選圖片檢視大圖

Android開源框架原始碼鑑賞:Fresco
  • DraweeView:繼承於ImageView,只是簡單的讀取xml檔案的一些屬性值和做一些初始化的工作,圖層管理交由Hierarchy負責,圖層資料獲取交由負責。
  • DraweeHierarchy:由多層Drawable組成,每層Drawable提供某種功能(例如:縮放、圓角)。
  • DraweeController:控制資料的獲取與圖片載入,向pipeline發出請求,並接收相應事件,並根據不同事件控制Hierarchy,從DraweeView接收使用者的事件,然後執行取消網路請求、回收資源等操作。
  • DraweeHolder:統籌管理Hierarchy與DraweeHolder。
  • ImagePipeline:Fresco的核心模組,用來以各種方式(記憶體、磁碟、網路等)獲取影象。
  • Producer/Consumer:Producer也有很多種,它用來完成網路資料獲取,快取資料獲取、圖片解碼等多種工作,它產生的結果由Consumer進行消費。
  • IO/Data:這一層便是資料層了,負責實現記憶體快取、磁碟快取、網路快取和其他IO相關的功能。

縱觀整個Fresco的架構,DraweeView是門面,和使用者進行互動,DraweeHierarchy是檢視層級,管理圖層,DraweeController是控制器,管理資料。它們構成了整個Fresco框架的三駕馬車。當然還有我們 幕後英雄Producer,所有的髒活累活都是它乾的,最佳勞模?

理解了Fresco整體的架構,我們還有了解在這套礦建裡發揮重要作用的幾個關鍵角色,如下所示:

  • Supplier:提供一種特定型別的物件,Fresco裡有很多以Supplier結尾的類都實現了這個介面。
  • SimpleDraweeView:這個我們就很熟悉了,它接收一個URL,然後呼叫Controller去載入圖片。該類繼承於GenericDraweeView,GenericDraweeView又繼承於DraweeView,DraweeView是Fresco的頂層View類。
  • PipelineDraweeController:負責圖片資料的獲取與載入,它繼承於AbstractDraweeController,由PipelineDraweeControllerBuilder構建而來。AbstractDraweeController實現了DraweeController介面,DraweeController 是Fresco的資料大管家,所以的圖片資料的處理都是由它來完成的。
  • GenericDraweeHierarchy:負責SimpleDraweeView上的圖層管理,由多層Drawable組成,每層Drawable提供某種功能(例如:縮放、圓角),該類由GenericDraweeHierarchyBuilder進行構建,該構建器 將placeholderImage、retryImage、failureImage、progressBarImage、background、overlays與pressedStateOverlay等 xml檔案或者Java程式碼裡設定的屬性資訊都傳入GenericDraweeHierarchy中,由GenericDraweeHierarchy進行處理。
  • DraweeHolder:該類是一個Holder類,和SimpleDraweeView關聯在一起,DraweeView是通過DraweeHolder來統一管理的。而DraweeHolder又是用來統一管理相關的Hierarchy與Controller
  • DataSource:類似於Java裡的Futures,代表資料的來源,和Futures不同,它可以有多個result。
  • DataSubscriber:接收DataSource返回的結果。
  • ImagePipeline:用來調取獲取圖片的介面。
  • Producer:載入與處理圖片,它有多種實現,例如:NetworkFetcherProducer,LocalAssetFetcherProducer,LocalFileFetchProducer。從這些類的名字我們就可以知道它們是幹什麼的。 Producer由ProducerFactory這個工廠類構建的,而且所有的Producer都是像Java的IO流那樣,可以一層巢狀一層,最終只得到一個結果,這是一個很精巧的設計?
  • Consumer:用來接收Producer產生的結果,它與Producer組成了生產者與消費者模式。

注:Fresco原始碼裡的類的名字都比較長,但是都是按照一定的命令規律來的,例如:以Supplier結尾的類都實現了Supplier介面,它可以提供某一個型別的物件(factory, generator, builder, closure等)。 以Builder結尾的當然就是以構造者模式建立物件的類。

通過上面的描述,想必大家都Fresco有了一個整體的認識,那面對這樣龐大的一個庫,我們在去分析它的時候需要重點關注哪些點呢??

  1. 圖片載入流程
  2. DraweeController與DraweeHierarchy
  3. Producer與Consumer
  4. 快取機制

? 注:Fresco裡還大量運用各種設計模式,例如:Builder、Factory、Wrapper、Producer/Consumer、Adapter等,在閱讀原始碼的時候,大家也要留心這些設計模式的應用與實踐。

接下來我們就帶著這4個問題去原始碼中一探究竟。

一 圖片載入流程

至於分析的手段,還是老套路,先從一個簡單的例子入手,展示Fresco是如何載入圖片的,然後去分析它的圖片載入流程,讓大家有個整體的理解,然後再逐個去分析Fresco每個 子模組的功能實現。

好,我們先來寫一個小例子。

? 舉例

初始化

Fresco.initialize(this);
複製程式碼

載入圖片

String url = "https://github.com/guoxiaoxing/android-open-framwork-analysis/raw/master/art/fresco/scenery.jpg";
SimpleDraweeView simpleDraweeView = findViewById(R.id.drawee_view);
simpleDraweeView.setImageURI(Uri.parse(url));
複製程式碼

我們來看一下它的呼叫流程,序列圖如下所示:

? 點選圖片檢視大圖

Android開源框架原始碼鑑賞:Fresco

嗯,圖看起來有點大,但是不要緊,我們按照顏色將整個流程分為了四大步:

  1. 初始化Fresco。
  2. 獲取DataSource。
  3. 繫結Controller與Hierarchy。
  4. 從記憶體快取/磁碟快取/網路獲取圖片,並設定到對應的Drawable層。

? 注:Fresco裡的類雖多,類名雖長,但都是基於介面和Abstract類的設計,每個模組自成一套繼承體系,所以只要掌握了它們的繼承關係以及不同模組之間的聯絡,整個 流程還是比較簡單的。

由於序列圖設計具體細節,為了輔助理解,我們再提供一張總結新的流程圖,如下所示:

? 點選圖片檢視大圖

Android開源框架原始碼鑑賞:Fresco

接下來,我們就針對這兩張圖結合具體細節來一一分析。

1.1 初始化Fresco

? 序列圖 1.1 -> 1.11

public class Fresco {
    public static void initialize(
        Context context,
        @Nullable ImagePipelineConfig imagePipelineConfig,
        @Nullable DraweeConfig draweeConfig) {
      //... 重複初始化檢驗
      try {
        //1. 載入so庫,這個主要是一些第三方的native庫,例如:giflib,libjpeg,libpng,
        //主要用來做圖片解碼。
        SoLoader.init(context, 0);
      } catch (IOException e) {
        throw new RuntimeException("Could not initialize SoLoader", e);
      }
      //2. 設定傳入的配置引數magePipelineConfig。
      context = context.getApplicationContext();
      if (imagePipelineConfig == null) {
        ImagePipelineFactory.initialize(context);
      } else {
        ImagePipelineFactory.initialize(imagePipelineConfig);
      }
      //3. 初始化SimpleDraweeView。
      initializeDrawee(context, draweeConfig);
    }
  
    private static void initializeDrawee(
        Context context,
        @Nullable DraweeConfig draweeConfig) {
      //構建PipelineDraweeControllerBuilderSupplier物件,並傳給SimpleDraweeView。
      sDraweeControllerBuilderSupplier =
          new PipelineDraweeControllerBuilderSupplier(context, draweeConfig);
      SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier);
    }  
}
複製程式碼

可以發現,Fresco在初始化的過程中,主要做了三件事情:

  1. 載入so庫,這個主要是一些第三方的native庫,例如:giflib,libjpeg,libpng,主要用來做圖片解碼。
  2. 設定傳入的配置引數magePipelineConfig。
  3. 初始化SimpleDraweeView。

這裡面我們需要重點關注三個物件:

  • ImagePipelineConfig:ImagePipeline引數配置。
  • DraweeControllerBuilderSupplier:提供DraweeControllerBuilder用來構建DraweeController。

我們先來看ImagePipelineConfig,ImagePipelineConfig通過建造者模式來構建傳遞給ImagePipeline的引數,如下所示:

  • Bitmap.Config mBitmapConfig; 圖片質量。
  • Supplier mBitmapMemoryCacheParamsSupplier; 記憶體快取的配置引數提供者。
  • CountingMemoryCache.CacheTrimStrategy mBitmapMemoryCacheTrimStrategy; 記憶體快取的削減策略。
  • CacheKeyFactory mCacheKeyFactory; CacheKey的建立工廠。
  • Context mContext; 上下文環境。
  • boolean mDownsampleEnabled; 是否開啟圖片向下取樣。
  • FileCacheFactory mFileCacheFactory; 磁碟快取建立工廠。
  • Supplier mEncodedMemoryCacheParamsSupplier; 未解碼圖片快取配置引數提供者。
  • ExecutorSupplier mExecutorSupplier; 執行緒池提供者。
  • ImageCacheStatsTracker mImageCacheStatsTracker; 圖片快取狀態追蹤器。
  • ImageDecoder mImageDecoder; 圖片解碼器。
  • Supplier mIsPrefetchEnabledSupplier; 是否開啟預載入。
  • DiskCacheConfig mMainDiskCacheConfig; 磁碟快取配置。
  • MemoryTrimmableRegistry mMemoryTrimmableRegistry; 記憶體變化監聽登錄檔,那些需要監聽系統記憶體變化的物件需要新增到這個表中類。
  • NetworkFetcher mNetworkFetcher; 下載網路圖片,預設使用內建的HttpUrlConnectionNetworkFetcher,也可以自定義。
  • PlatformBitmapFactory mPlatformBitmapFactory; 根據不同的Android版本生成不同的Bitmap的工廠,主要的區別在Bitmap在記憶體中的位置,Android 5.0以下儲存在Ashmem中,Android 5.0以上存在Java Heap中。
  • PoolFactory mPoolFactory; Bitmap池等各種池的構建工廠。
  • ProgressiveJpegConfig mProgressiveJpegConfig; 漸進式JPEG配置。
  • Set mRequestListeners; 請求監聽器集合,監聽請求過程中的各種事件。
  • boolean mResizeAndRotateEnabledForNetwork; 是否開啟網路圖片的壓縮和旋轉。
  • DiskCacheConfig mSmallImageDiskCacheConfig; 磁碟快取配置
  • ImageDecoderConfig mImageDecoderConfig; 圖片解碼配置
  • ImagePipelineExperiments mImagePipelineExperiments; Fresco提供的關於Image Pipe的實驗性功能。

上述引數基本不需要我們手動配置,除非專案上有定製性的需求。

我們可以發現,在初始化方法的最後呼叫initializeDrawee()給SimpleDraweeView傳入了一個PipelineDraweeControllerBuilderSupplier,這是一個很重要的物件,我們 來看看它都初始化了哪些東西。

public class PipelineDraweeControllerBuilderSupplier implements
    Supplier<PipelineDraweeControllerBuilder> {
    
      public PipelineDraweeControllerBuilderSupplier(
          Context context,
          ImagePipelineFactory imagePipelineFactory,
          Set<ControllerListener> boundControllerListeners,
          @Nullable DraweeConfig draweeConfig) {
        mContext = context;
        //1. 獲取ImagePipeline
        mImagePipeline = imagePipelineFactory.getImagePipeline();
    
        if (draweeConfig != null && draweeConfig.getPipelineDraweeControllerFactory() != null) {
          mPipelineDraweeControllerFactory = draweeConfig.getPipelineDraweeControllerFactory();
        } else {
          mPipelineDraweeControllerFactory = new PipelineDraweeControllerFactory();
        }
        //2. 獲取PipelineDraweeControllerFactory,並初始化。
        mPipelineDraweeControllerFactory.init(
            context.getResources(),
            DeferredReleaser.getInstance(),
            imagePipelineFactory.getAnimatedDrawableFactory(context),
            UiThreadImmediateExecutorService.getInstance(),
            mImagePipeline.getBitmapMemoryCache(),
            draweeConfig != null
                ? draweeConfig.getCustomDrawableFactories()
                : null,
            draweeConfig != null
                ? draweeConfig.getDebugOverlayEnabledSupplier()
                : null);
        mBoundControllerListeners = boundControllerListeners;
      }
}
複製程式碼

可以發現在這個方法裡初始化了兩個重要的物件:

  1. 獲取ImagePipeline。
  2. 獲取PipelineDraweeControllerFactory,並初始化。

這個PipelineDraweeControllerFactory就是用來構建PipelineDraweeController,我們前面說過PipelineDraweeController繼承於AbstractDraweeController,用來控制圖片 資料的獲取和載入,這個PipelineDraweeControllerFactory()的init()方法也是將引數裡的遍歷傳入PipelineDraweeControllerFactory中,用來準備構建PipelineDraweeController。 我們來看一下它都傳入哪些東西進去。

  • context.getResources():Android的Resources物件。
  • DeferredReleaser.getInstance():延遲釋放資源,等主執行緒處理完訊息後再進行回收。
  • mImagePipeline.getBitmapMemoryCache():已解碼的圖片快取。

? 注:所謂拔出蘿蔔帶出泥,在分析圖片載入流程的時候難免會帶進來各種各樣的類,如果一時理不清它們的關係也沒關係,第一步只是要掌握整體的載入流程即可,後面 我們會對這些類逐一分析。

該方法執行完成後呼叫SimpleDraweeView的initizlize()方法將PipelineDraweeControllerBuilderSupplier物件設定進SimpleDraweeView的靜態物件sDraweeControllerBuilderSupplier中 整個初始化流程便完成了。

1.2 獲取DataSource

? 序列圖 2.1 -> 2.12

在分析如何生成DataSource之前,我們得先了解什麼DataSource。

DataSource是一個介面其實現類是AbstractDataSource,它可以提交資料請求,並能獲取progress、fail result與success result等資訊,類似於Java裡的Future。

DataSource介面如下所示:

public interface DataSource<T> {
  //資料來源是否關閉
  boolean isClosed();
  //非同步請求的結果
  @Nullable T getResult();
  //是否有結果返回
  boolean hasResult();
  //請求是否結束
  boolean isFinished();
  //請求是否發生錯誤
  boolean hasFailed();
  //發生錯誤的原因
  @Nullable Throwable getFailureCause();
  //請求的進度[0, 1]
  float getProgress();
  //結束請求,釋放資源。 
  boolean close();
  //傳送並訂閱請求,等待請求結果。
  void subscribe(DataSubscriber<T> dataSubscriber, Executor executor);
}
複製程式碼

AbstractDataSource實現了DataSource介面,它是一個基礎類,其他DataSource類都擴充套件自該類。AbstractDataSource實現了上述介面裡的方法,維護這DataSource的success、progress和fail的狀態。 除此之外還有以下DataSource類:

  • AbstractProducerToDataSourceAdapter:繼承自AbstractDataSource,包裝了Producer取資料的過程,也就是建立了一個Consumer,詳細的過程我們後面還會說。
  • CloseableProducerToDataSourceAdapter:繼承自AbstractProducerToDataSourceAdapter,實現了closeResult()方法,繪製自己銷燬時同時銷燬Result,這個是最主要使用的DataSource。
  • ProducerToDataSourceAdapter:沒有實現額外的方法,僅僅用於預載入圖片。
  • IncreasingQualityDataSource:內部維護一個CloseableProducerToDataSourceAdapter列表,按資料的清晰度從後往前遞增,它為列表裡的每個DataSour測繫結一個DataSubscriber,該類負責保證 每次獲取清晰度更高的資料,獲取資料的同時銷燬清晰度更低的資料。
  • FirstAvailableDataSource:內部維護一個CloseableProducerToDataSourceAdapter列表,它會返回列表裡最先獲取資料的DataSource,它為列表裡的每個DataSour測繫結一個DataSubscriber,如果 資料載入成功,則將當前成功的DataSource指定為目標DataSource,否則跳轉到下一個DataSource繼續嘗試。
  • SettableDataSource:繼承自AbstractDataSource,並將重寫settResult()、setFailure()、setProgress()在內部呼叫父類的相應函式,但是修飾符變成了public(原來是protected)。即使 用SettableDataSource時可以在外部呼叫這三個函式設定DataSource狀態。一般用於在獲取DataSource失敗時直接產生一個設定為Failure的DataSource。

瞭解了DataSource,我們再來看看它是如何生成的。

我們知道,在使用Fresco展示圖片的時候,只需要呼叫setImageURI()設定圖片URL即可,我們就以這個方法為入口開始分析,如下所示:

public class SimpleDraweeView extends GenericDraweeView {
    
      public void setImageURI(Uri uri, @Nullable Object callerContext) {
        DraweeController controller = mSimpleDraweeControllerBuilder
            .setCallerContext(callerContext)
            .setUri(uri)
            .setOldController(getController())
            .build();
        setController(controller);
      }
}
複製程式碼

可以發現,SimpleDraweeView將外面傳遞的URL資料封裝進了DraweeController,並呼叫mSimpleDraweeControllerBuilder構造了一個DraweeController物件,這個 DraweeController物件實際上就是PipelineDraweeController。

我們來看看它是如何構建的,mSimpleDraweeControllerBuilder由sDraweeControllerBuilderSupplier呼叫get()方法獲得,我們前面已經說過sDraweeControllerBuilderSupplier是在 SimpleDraweeView的initialize()被傳遞進來的,我們接著來看PipelineDraweeController的構建過程。

SimpleDraweeControllerBuilder是呼叫器父類AbstractDraweeControllerBuilder的build()方法來進行構建,而該build()方法又反過來呼叫其子類SimpleDraweeControllerBuilder 的obtainController()方法來完成具體子類SimpleDraweeControllerBuilder的構建,我們來看看它的實現。

? 注:Fresco的設計很好的體現了面向介面程式設計這一點,大部分功能都基於介面設計,然後設計出抽象類AbstractXXX,用來封裝通用的功能,個別具體的功能交由其子類實現。

public class PipelineDraweeControllerBuilder extends AbstractDraweeControllerBuilder<
    PipelineDraweeControllerBuilder,
    ImageRequest,
    CloseableReference<CloseableImage>,
    ImageInfo> {
    
      @Override
      protected PipelineDraweeController obtainController() {
        DraweeController oldController = getOldController();
        PipelineDraweeController controller;
        //如果已經有PipelineDraweeController,則進行復用,否則構建新的PipelineDraweeController。
        if (oldController instanceof PipelineDraweeController) {
          controller = (PipelineDraweeController) oldController;
          controller.initialize(
              obtainDataSourceSupplier(),
              generateUniqueControllerId(),
              getCacheKey(),
              getCallerContext(),
              mCustomDrawableFactories);
        } else {
          controller = mPipelineDraweeControllerFactory.newController(
              obtainDataSourceSupplier(),
              generateUniqueControllerId(),
              getCacheKey(),
              getCallerContext(),
              mCustomDrawableFactories);
        }
        return controller;
      }
}
複製程式碼

可以發現上述函式的邏輯也很簡單,如果已經有PipelineDraweeController,則進行復用,否則呼叫PipelineDraweeControllerFactory.newController()方法構建 新的PipelineDraweeController。PipelineDraweeControllerFactory.newController()方法最終呼叫PipelineDraweeController的構造方法完成PipelineDraweeController 物件的構建,後續的流程很簡單,我們重點關注在構建的過程中傳入了哪些物件,這些物件是如何生成的。

  • obtainDataSourceSupplier():獲取資料來源。
  • generateUniqueControllerId():生成唯一的Controller ID。
  • getCacheKey():獲取快取key。
  • getCallerContext():獲取呼叫者的上下為環境。
  • ImmutableList列表,用來生成各種圖片效果的Drawable。

其他的實現都比較簡單,我們重點關注obtainDataSourceSupplier()的實現,如下所示:

public class PipelineDraweeControllerBuilder extends AbstractDraweeControllerBuilder<
    PipelineDraweeControllerBuilder,
    ImageRequest,
    CloseableReference<CloseableImage>,
    ImageInfo> {
    
      protected Supplier<DataSource<IMAGE>> obtainDataSourceSupplier() {
        if (mDataSourceSupplier != null) {
          return mDataSourceSupplier;
        }
    
        Supplier<DataSource<IMAGE>> supplier = null;
    
        //1. 生成最終的image supplier。
        if (mImageRequest != null) {
          supplier = getDataSourceSupplierForRequest(mImageRequest);
        } else if (mMultiImageRequests != null) {
          supplier = getFirstAvailableDataSourceSupplier(mMultiImageRequests, mTryCacheOnlyFirst);
        }
    
        //2. 生成一個ncreasing-quality supplier,這裡會有兩級的清晰度,高清晰度的supplier優先。
        if (supplier != null && mLowResImageRequest != null) {
          List<Supplier<DataSource<IMAGE>>> suppliers = new ArrayList<>(2);
          suppliers.add(supplier);
          suppliers.add(getDataSourceSupplierForRequest(mLowResImageRequest));
          supplier = IncreasingQualityDataSourceSupplier.create(suppliers);
        }
    
        //如果沒有圖片請求,則提供一個空的supplier。
        if (supplier == null) {
          supplier = DataSources.getFailedDataSourceSupplier(NO_REQUEST_EXCEPTION);
        }
    
        return supplier;
      }
}
複製程式碼

getDataSourceSupplierForRequest()方法最終呼叫(具體呼叫鏈可以參照序列圖,這裡就不再贅述)的是PipelineDraweeControllerBuilder的getDataSourceForRequest()

public class PipelineDraweeControllerBuilder extends AbstractDraweeControllerBuilder<
    PipelineDraweeControllerBuilder,
    ImageRequest,
    CloseableReference<CloseableImage>,
    ImageInfo> {
    
      @Override
      protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(
          ImageRequest imageRequest,
          Object callerContext,
          AbstractDraweeControllerBuilder.CacheLevel cacheLevel) {
        
        //呼叫ImagePipeline的fetchDecodedImage()方法獲取DataSource
        return mImagePipeline.fetchDecodedImage(
            imageRequest,
            callerContext,
            convertCacheLevelToRequestLevel(cacheLevel));
      }
}
複製程式碼

ImagePipeline是Fresco Image Pipeline的入口類,前面也說過ImagePipeline是Fresco的核心模組,用來以各種方式(記憶體、磁碟、網路等)獲取影象。

這個mImagePipeline就是在PipelineDraweeControllerBuilderSupplier中呼叫ImagePipelineFactory的getImagePipeline()方法建立的。 我們接著來看ImagePipeline的fetchDecodedImage()方法,如下所示:

public class ImagePipeline {
    
    public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage(
        ImageRequest imageRequest,
        Object callerContext,
        ImageRequest.RequestLevel lowestPermittedRequestLevelOnSubmit) {
      try {
        //1. 獲取Producer序列,為DataSource提供不同的資料輸入管道。
        Producer<CloseableReference<CloseableImage>> producerSequence =
            mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest);
        //2. 呼叫submitFetchRequest()方法生成DataSource。
        return submitFetchRequest(
            producerSequence,
            imageRequest,
            lowestPermittedRequestLevelOnSubmit,
            callerContext);
      } catch (Exception exception) {
        return DataSources.immediateFailedDataSource(exception);
      }
    }  
}
複製程式碼

關於什麼是Producer,我們前面也已經說過。

Producer用來載入與處理圖片,它有多種實現,例如:NetworkFetcherProducer,LocalAssetFetcherProducer,LocalFileFetchProducer。從這些類的名字我們就可以知道它們是幹什麼的。 Producer由ProducerFactory這個工廠類構建的,而且所有的Producer都是像Java的IO流那樣,可以一層巢狀一層,最終只得到一個結果,

關於Producer的更多內容,我們後面會專門講,這個方法主要做了兩件事情:

  1. 獲取Producer序列,為DataSource提供不同的資料輸入管道,Producer是由很多種的,代表從不同途徑獲取圖片資料,我們下面會詳細講。
  2. 呼叫submitFetchRequest()方法生成DataSource。

可以發現該方法最終呼叫submitFetchRequest()方法生成了DataSource,如下所示:

public class ImagePipeline {
    
    private <T> DataSource<CloseableReference<T>> submitFetchRequest(
          Producer<CloseableReference<T>> producerSequence,
          ImageRequest imageRequest,
          ImageRequest.RequestLevel lowestPermittedRequestLevelOnSubmit,
          Object callerContext) {
        final RequestListener requestListener = getRequestListenerForRequest(imageRequest);
    
        try {
          //1. 獲取快取級別,RequestLevel將快取分為四級 FULL_FETCH(1) 從網路或者本地儲存獲取,DISK_CACHE(2) 從磁碟快取獲取,ENCODED_MEMORY_CACHE(3)從
          //未接嗎的記憶體快取獲取,BITMAP_MEMORY_CACHE(4)已解碼的記憶體快取獲取。
          ImageRequest.RequestLevel lowestPermittedRequestLevel =
              ImageRequest.RequestLevel.getMax(
                  imageRequest.getLowestPermittedRequestLevel(),
                  lowestPermittedRequestLevelOnSubmit);
          //2. 將ImageRequest、RequestListener等資訊封裝進SettableProducerContext,ProducerContext是Producer
          //的上下文環境,利用ProducerContext可以改變Producer內部的狀態。
          SettableProducerContext settableProducerContext = new SettableProducerContext(
              imageRequest,
              generateUniqueFutureId(),
              requestListener,
              callerContext,
              lowestPermittedRequestLevel,
            /* isPrefetch */ false,
              imageRequest.getProgressiveRenderingEnabled() ||
                  imageRequest.getMediaVariations() != null ||
                  !UriUtil.isNetworkUri(imageRequest.getSourceUri()),
              imageRequest.getPriority());
          //3. 建立CloseableProducerToDataSourceAdapter,CloseableProducerToDataSourceAdapter是DataSource的一種。
          return CloseableProducerToDataSourceAdapter.create(
              producerSequence,
              settableProducerContext,
              requestListener);
        } catch (Exception exception) {
          return DataSources.immediateFailedDataSource(exception);
        }
      }
}
複製程式碼

該方法主要做了三件事情:

  1. 獲取快取級別,RequestLevel將快取分為四級 FULL_FETCH(1) 從網路或者本地儲存獲取,DISK_CACHE(2) 從磁碟快取獲取,ENCODED_MEMORY_CACHE(3)從 未接嗎的記憶體快取獲取,BITMAP_MEMORY_CACHE(4)已解碼的記憶體快取獲取。
  2. 將ImageRequest、RequestListener等資訊封裝進SettableProducerContext,ProducerContext是Producer 的上下文環境,利用ProducerContext可以改變Producer內部的狀態。
  3. 建立CloseableProducerToDataSourceAdapter,CloseableProducerToDataSourceAdapter是DataSource的一種。

接著CloseableProducerToDataSourceAdapter呼叫了自己create()方法構建一個CloseableProducerToDataSourceAdapter物件。至此DataSource已經完成完成了,然後把它設定到 PipelineDraweeController裡。

我們接著來看繫結Controller與Hierarchy的流程。?

1.3 繫結DraweeController與DraweeHierarchy

? 序列圖 3.1 -> 3.7

前面提到在SimpleDraweeView的setImageURI()方法裡會為SimpleDraweeView設定前面構建好的PipelineDraweeController,如下所示:

  public void setImageURI(Uri uri, @Nullable Object callerContext) {
    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();
    setController(controller);
  }
複製程式碼

從上面的序列圖得知,setController()方法經過層層呼叫,最終呼叫的是DraweeHolder的setController()方法,DraweeHolder用來統籌管理Controller與Hierarchy,它是DraweeView的一個 成員變數,在DraweeHolder物件初始化的時候被構建,我們來看看它的setController()方法,如下所示:

public class DraweeHolder<DH extends DraweeHierarchy>
    implements VisibilityCallback {
      public void setController(@Nullable DraweeController draweeController) {
        boolean wasAttached = mIsControllerAttached;
        //1. 如果已經和Controller建立聯絡,則先detach。
        if (wasAttached) {
          detachController();
        }
    
        //2. 清楚舊的Controller。
        if (isControllerValid()) {
          mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER);
          mController.setHierarchy(null);
        }
        
        //3. 為Controller重新設定Hierarchy)建立新的Controller。
        mController = draweeController;
        if (mController != null) {
          mEventTracker.recordEvent(Event.ON_SET_CONTROLLER);
          mController.setHierarchy(mHierarchy);
        } else {
          mEventTracker.recordEvent(Event.ON_CLEAR_CONTROLLER);
        }
    
        //4. 對DraweeHolder和Controller進行attach操作。
        if (wasAttached) {
          attachController();
        }
      }
 }
複製程式碼

上述方法的流程也十分簡單,如下所示:

  1. 如果已經和Controller建立聯絡,則先detach。
  2. 清楚舊的Controller。
  3. 為Controller重新設定Hierarchy)建立新的Controller。
  4. 對DraweeHolder和Controller進行attach操作。

上述流程裡有兩個關鍵的地方:設定Hierarchy和attch操作,我們分別來看看,

從上面的序列圖可以看出,這個mHierarchy是在GenricDraweeView的構造方法裡呼叫inflateHierarchy()方法建立的,它實際上是一個GenericDraweeHierarchy物件,而setHierarchy()方法 最終呼叫的是AbstractDraweeController的setHierarchy()方法,如下所示:

public abstract class AbstractDraweeController<T, INFO> implements
    DraweeController,
    DeferredReleaser.Releasable,
    GestureDetector.ClickListener {
    
      public void setHierarchy(@Nullable DraweeHierarchy hierarchy) {
        //... log
        mEventTracker.recordEvent(
            (hierarchy != null) ? Event.ON_SET_HIERARCHY : Event.ON_CLEAR_HIERARCHY);
        //1. 釋放掉當前正在進行的請求。
        if (mIsRequestSubmitted) {
          mDeferredReleaser.cancelDeferredRelease(this);
          release();
        }
        //2. 清除已經存在的Hierarchy。
        if (mSettableDraweeHierarchy != null) {
          mSettableDraweeHierarchy.setControllerOverlay(null);
          mSettableDraweeHierarchy = null;
        }
        //3. 設定新的Hierarchy。
        if (hierarchy != null) {
          Preconditions.checkArgument(hierarchy instanceof SettableDraweeHierarchy);
          mSettableDraweeHierarchy = (SettableDraweeHierarchy) hierarchy;
          mSettableDraweeHierarchy.setControllerOverlay(mControllerOverlay);
        }
      }
    }
複製程式碼

這個mSettableDraweeHierarchy實際的實現類是GenericDraweeHierarchy,

走到這裡,DraweeController與DraweeHierarchy的繫結流程就完成了。

1.4 從記憶體快取/磁碟快取/網路獲取圖片,並設定到對應的Drawable層

? 序列圖 4.1 -> 4.14

這一塊的內容主要執行上面建立的各種Producer,從從記憶體快取/磁碟快取/網路獲取圖片,並呼叫對應的Consumer消費結果,最終 不同的Drawable設定到對應的圖層中去,關於DraweeHierarchy與Producer我們下面都會詳細的講,我們先來看看上面層層請求到 圖片最終是如何設定到SimpleDraweeView中去的,如下所示:

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {
    @Override
    public void setImage(Drawable drawable, float progress, boolean immediate) {
      drawable = WrappingUtils.maybeApplyLeafRounding(drawable, mRoundingParams, mResources);
      drawable.mutate();
      //mActualImageWrapper就是實際載入圖片的那個圖層,此處要設定的SimpleDraweeView最終要顯示的圖片。
      mActualImageWrapper.setDrawable(drawable);
      mFadeDrawable.beginBatchMode();
      fadeOutBranches();
      fadeInLayer(ACTUAL_IMAGE_INDEX);
      setProgress(progress);
      if (immediate) {
        mFadeDrawable.finishTransitionImmediately();
      }
      mFadeDrawable.endBatchMode();
    }  
}
複製程式碼

mActualImageWrapper就是實際載入圖片的那個圖層,此處要設定的SimpleDraweeView最終要顯示的圖片。

如此,一個SimpleDraweeView的圖片載入流程就完成了,面對如此長的流程,讀者不免疑惑,我們只要掌握了整體流程,就可以 分而治之,逐個擊破。

二 DraweeHierarchy

Fresco的圖片效果是依賴於Drawee實現的,也就是Drawable層級。

DraweeHierarchy是Fresco裡的Drawable層級,它是一層一層疊加在DraweeView上的來實現各種效果,例如:佔點陣圖、失敗圖、載入進度圖等,DraweeHierarchy是一個介面,它還有個 子介面SettableDraweeHierarchy,它們的實現類是GenericDraweeHierarchy。

DraweeHierarchy介面與SettableDraweeHierarchy介面如下所示:

public interface DraweeHierarchy {
  //獲取頂層的Drawable,也就是其父節點的圖層
  Drawable getTopLevelDrawable();
}

public interface SettableDraweeHierarchy extends DraweeHierarchy {
  //由DraweeController呼叫,重置DraweeHierarchy狀態
  void reset();
   //由DraweeController呼叫,設定圖片資料,progress在漸進式JPEG裡使用,immediate表示是否立即顯示這張圖片
  void setImage(Drawable drawable, float progress, boolean immediate);
   //由DraweeController呼叫,更新圖片載入進度【0, 1】,progress為1或者immediate為true時的時候會隱藏進度條。
  void setProgress(float progress, boolean immediate);
   //由DraweeController呼叫,設定失敗原因,DraweeHierarchy可以根據不同的原因展示不同的失敗圖片。
  void setFailure(Throwable throwable);
   //由DraweeController呼叫,設定重試原因,DraweeHierarchy可以根據不同的原因展示不同的重試圖片。
  void setRetry(Throwable throwable);
   //由DraweeController呼叫,設定其他的Controller覆蓋層
  void setControllerOverlay(Drawable drawable);
}
複製程式碼

理解了DraweeHierarchy的大致介面,我們繼續從以下幾個角度來解析DraweeHierarchy:

  • 圖層的層級構造
  • 圖層的建立流程

2.1 圖層的層級構造

Fresco裡定義了許多Drawable,它們都直接或者間接的繼承了Drawable,來實現不同的功能。它們的圖層層級如下所示:

o RootDrawable (top level drawable)
|
+--o FadeDrawable
  |
  +--o ScaleTypeDrawable (placeholder branch, optional)
  |  |
  |  +--o Drawable (placeholder image)
  |
  +--o ScaleTypeDrawable (actual image branch)
  |  |
  |  +--o ForwardingDrawable (actual image wrapper)
  |     |
  |     +--o Drawable (actual image)
  |
  +--o null (progress bar branch, optional)
  |
  +--o Drawable (retry image branch, optional)
  |
  +--o ScaleTypeDrawable (failure image branch, optional)
     |
     +--o Drawable (failure image)
複製程式碼

Fresco裡的Drawable子類有很多,按照功能劃分可以分為三大類:

容器類Drawable

  • ArrayDrawable:內部儲存著一個Drawable陣列,與Android裡的LayerDrawable類似,將陣列裡的Drawable當作圖層,按照陣列的順序繪製Drawable,陣列最後 的成員會在最上方,不過它和LayerDrawable也有不同的地方:① 繪製順序是陣列的順序,但是ArrayDrawable會跳過暫時不需要繪製的圖層。② 不支援動態新增圖層。
  • FadeDrawable:繼承與ArrayDrawable,除了具有ArrayDrawable的功能外,它還可以隱藏和顯示圖層。

容器類Drawable

  • ForwardingDrawable:內部為患者一個Drawable成員變數,將Drawable的一些基本操作和回撥傳遞給目標Drawable,它是所以容器類Drawable的基類。
  • ScaleTypeDrawable:繼承於ForwardingDrawable,封裝了對代理圖片的縮放處理。
  • SettableDrawable:繼承於ForwardingDrawable,可以多次設定內容Drawable的容器,多用在目標圖片的圖層中。
  • AutoRotateDrawable:繼承於ForwardingDrawable,提供內容動態旋轉的容器。
  • OrientedDrawable:繼承於ForwardingDrawable,可以將內容Drawable以一個特定的角度繪製的容器。
  • MatrixDrawable:繼承於ForwardingDrawable,可以為內容應用變形矩陣的容器,它只能賦予給顯示目標圖片的那個圖層。不能在一個圖層上同時使用MatrixDrawable與ScaleTypeDrawable!
  • RoundedCornersDrawable:繼承於ForwardingDrawable,可以將內容的邊界修剪成圓角矩形(目前版本暫不支援)或用實心的圓角矩形覆蓋內容的容器。
  • GenericDraweeHierarchy.RootDrawable:繼承於ForwardingDrawable,專門用於頂層圖層的容器。

檢視類Drawable

  • ProgressBarDrawable:負責繪製進度條。
  • RoundedBitmapDrawable:將自身內容修剪成圓角矩形繪製出來,可以使用Bitmap作為物件,返回一個BitmapDrawable。

除了這些Drawable類以為,還有一個Drawable介面,凡是做matrix變換和圓角處理的Drawable都實現了這個介面,這是為了子Drawable可以父Drawable的 變換矩陣和圓角節點,以便可以正確的繪製自己。

如下所示:

public interface TransformAwareDrawable {
  //設定TransformCallback回撥
  void setTransformCallback(TransformCallback transformCallback);
}

public interface TransformCallback {
  //獲取應用在Drawable上的所有matrices矩陣,儲存在transform中
  void getTransform(Matrix transform);
  //獲取Drawable的根節點邊界,儲存在bounds中。
  void getRootBounds(RectF bounds);
}
複製程式碼

從使用者的角度,SimpleDraweeView上的圖層主要被分成了以下幾層:

  • 背景圖(backgroundImage)
  • 佔點陣圖(placeholderImage=)
  • 載入的圖片(actualImage)
  • 進度條(progressBarImage)
  • 重試載入的圖片(retryImage)
  • 失敗圖片(failureImage)
  • 疊加圖(overlayImage)

理解了圖層的層級構造,我們接著來看看圖層的建立流程。?

2.2 圖層的建立流程

我們前面說過在GenericDraweeView的構造方法裡,呼叫了它的inflateHierarchy()方法構建了一個GenericDraweeHierarchy物件,GenericDraweeHierarchy的實際 是由GenericDraweeHierarchyBuild呼叫build()方法來完成的。

GenericDraweeHierarchy是負責載入每個圖層資訊的載體,我們來看看它的構造方法的實現,如下所示:

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {
    
      //就跟我們上面說的一樣,7個圖層。
      
      //背景圖層
      private static final int BACKGROUND_IMAGE_INDEX = 0;
      //佔點陣圖層
      private static final int PLACEHOLDER_IMAGE_INDEX = 1;
      //載入的圖片圖層
      private static final int ACTUAL_IMAGE_INDEX = 2;
      //進度條
      private static final int PROGRESS_BAR_IMAGE_INDEX = 3;
      //重試載入的圖片
      private static final int RETRY_IMAGE_INDEX = 4;
      //失敗圖片
      private static final int FAILURE_IMAGE_INDEX = 5;
      //疊加圖
      private static final int OVERLAY_IMAGES_INDEX = 6;
    
      GenericDraweeHierarchy(GenericDraweeHierarchyBuilder builder) {
        mResources = builder.getResources();
        mRoundingParams = builder.getRoundingParams();
    
        //實際載入圖片的Drawable
        mActualImageWrapper = new ForwardingDrawable(mEmptyActualImageDrawable);
    
        int numOverlays = (builder.getOverlays() != null) ? builder.getOverlays().size() : 1;
        numOverlays += (builder.getPressedStateOverlay() != null) ? 1 : 0;
    
        //圖層數量
        int numLayers = OVERLAY_IMAGES_INDEX + numOverlays;
    
        // array of layers
        Drawable[] layers = new Drawable[numLayers];
        //1. 構建背景圖層Drawable。
        layers[BACKGROUND_IMAGE_INDEX] = buildBranch(builder.getBackground(), null);
        //2. 構建佔點陣圖層Drawable。
        layers[PLACEHOLDER_IMAGE_INDEX] = buildBranch(
            builder.getPlaceholderImage(),
            builder.getPlaceholderImageScaleType());
        //3. 構建載入的圖片圖層Drawable。
        layers[ACTUAL_IMAGE_INDEX] = buildActualImageBranch(
            mActualImageWrapper,
            builder.getActualImageScaleType(),
            builder.getActualImageFocusPoint(),
            builder.getActualImageColorFilter());
        //4. 構建進度條圖層Drawable。
        layers[PROGRESS_BAR_IMAGE_INDEX] = buildBranch(
            builder.getProgressBarImage(),
            builder.getProgressBarImageScaleType());
        //5. 構建重新載入的圖片圖層Drawable。
        layers[RETRY_IMAGE_INDEX] = buildBranch(
            builder.getRetryImage(),
            builder.getRetryImageScaleType());
        //6. 構建失敗圖片圖層Drawable。
        layers[FAILURE_IMAGE_INDEX] = buildBranch(
            builder.getFailureImage(),
            builder.getFailureImageScaleType());
        if (numOverlays > 0) {
          int index = 0;
          if (builder.getOverlays() != null) {
            for (Drawable overlay : builder.getOverlays()) {
              //7. 構建疊加圖圖層Drawable。
              layers[OVERLAY_IMAGES_INDEX + index++] = buildBranch(overlay, null);
            }
          } else {
            index = 1; // reserve space for one overlay
          }
          if (builder.getPressedStateOverlay() != null) {
            layers[OVERLAY_IMAGES_INDEX + index] = buildBranch(builder.getPressedStateOverlay(), null);
          }
        }
    
        // fade drawable composed of layers
        mFadeDrawable = new FadeDrawable(layers);
        mFadeDrawable.setTransitionDuration(builder.getFadeDuration());
    
        // rounded corners drawable (optional)
        Drawable maybeRoundedDrawable =
            WrappingUtils.maybeWrapWithRoundedOverlayColor(mFadeDrawable, mRoundingParams);
    
        // top-level drawable
        mTopLevelDrawable = new RootDrawable(maybeRoundedDrawable);
        mTopLevelDrawable.mutate();
    
        resetFade();
      }
}
複製程式碼

這個方法主要是構建各個圖層的Drawable物件,如下所示:

  1. 構建背景圖層Drawable。
  2. 構建佔點陣圖層Drawable。
  3. 構建載入的圖片圖層Drawable。
  4. 構建進度條圖層Drawable。
  5. 構建重新載入的圖片圖層Drawable。
  6. 構建失敗圖片圖層Drawable。
  7. 構建疊加圖圖層Drawable。

而構建的方法設計到兩個方法

public class GenericDraweeHierarchy implements SettableDraweeHierarchy {
     @Nullable
     private Drawable buildActualImageBranch(
         Drawable drawable,
         @Nullable ScaleType scaleType,
         @Nullable PointF focusPoint,
         @Nullable ColorFilter colorFilter) {
       drawable.setColorFilter(colorFilter);
       drawable = WrappingUtils.maybeWrapWithScaleType(drawable, scaleType, focusPoint);
       return drawable;
     }
   
     /** Applies scale type and rounding (both if specified). */
     @Nullable
     private Drawable buildBranch(@Nullable Drawable drawable, @Nullable ScaleType scaleType) {
       //如果需要為Drawable設定Round,RoundedBitmapDrawable或者RoundedColorDrawable。
       drawable = WrappingUtils.maybeApplyLeafRounding(drawable, mRoundingParams, mResources);
       //如果需要為Drawable設定ScaleType,則將它包裝成一個ScaleTypeDrawable。
       drawable = WrappingUtils.maybeWrapWithScaleType(drawable, scaleType);
       return drawable;
     } 
}
複製程式碼

構建Drawable的過程中都要應用相應的縮放型別和圓角角度,如下所示:

  • 如果需要為Drawable設定Round,RoundedBitmapDrawable或者RoundedColorDrawable。
  • 如果需要為Drawable設定ScaleType,則將它包裝成一個ScaleTypeDrawable。

這樣一個圖層的載體GenericDraweeHierarchy就構建完成了,後續GenericDraweeHierarchy裡的各種操作都是呼叫器內部的各種Drawable的方法來完成的。

三 Producer與Consumer

我們前面說過Producer是Fresco的最佳勞模,所有的髒話累活都是它乾的,我們來看看它的實現。

public interface Producer<T> {
  //開始處理任務,執行的結果右Consumer進行消費。
  void produceResults(Consumer<T> consumer, ProducerContext context);
}
複製程式碼

Fresco裡實現了多個Producer,按照功能劃分可以分為以下幾類:

本地資料獲取P類roducer,這類Producer負責從本地獲取資料。

  • LocalFetchProducer:實現了Producer介面,所有本地資料Producer獲取的基類。

  • LocalAssetFetchProducer 繼承於LocalFetchProducer,通過AssetManager獲取ImageRequest物件的輸入流及物件位元組碼長度,將它轉換為EncodedImage;

  • LocalContentUriFetchProducer 繼承於LocalFetchProducer,若Uri指向聯絡人,則獲取聯絡人頭像;若指向相簿圖片,則會根據是否傳入ResizeOption進行一定縮放(這裡不是完全按ResizeOption縮放);若止這兩個條件都不滿足,則直接呼叫ContentResolver的函式openInputStream(Uri uri)獲取輸入流並轉化為EncodedImage;

  • LocalFileFetchProducer 繼承於LocalFetchProducer,直接通過指定檔案獲取輸入流,從而轉化為EncodedImage;

  • LocalResourceFetchProducer 繼承於LocalFetchProducer,通過Resources的函式openRawResources獲取輸入流,從而轉化為EncodedImage。

  • LocalExifThumbnailProducer 沒有繼承於LocalFetchProducer,可以獲取Exif影象的Producer;

  • LocalVideoThumbnailProducer 沒有繼承於LocalFetchProducer,可以獲取視訊縮圖的Producer。

網路資料獲取類Producer,這類Producer負責從網路獲取資料。

  • NetworkFetchProducer:實現了Producer介面,從網路上獲取圖片資料。

快取資料獲取類Producer,這類Producer負責從快取中獲取資料。

  • BitmapMemoryCacheGetProducer 它是一個Immutable的Producer,僅用於包裝後續Producer;
  • BitmapMemoryCacheProducer 在已解碼的記憶體快取中獲取資料;若未找到,則在nextProducer中獲取資料,並在獲取到資料的同時將其快取;
  • BitmapMemoryCacheKeyMultiplexProducer 是MultiplexProducer的子類,nextProducer為BitmapMemoryCacheProducer,將多個擁有相同已解碼記憶體快取鍵的ImageRequest進行“合併”,若快取命中,它們都會獲取到該資料;
  • PostprocessedBitmapMemoryCacheProducer 在已解碼的記憶體快取中尋找PostProcessor處理過的圖片。它的nextProducer都是PostProcessorProducer,因為如果沒有獲取到被PostProcess的快取,就需要對獲取的圖片進行PostProcess。;若未找到,則在nextProducer中獲取資料;
  • EncodedMemoryCacheProducer 在未解碼的記憶體快取中尋找資料,如果找到則返回,使用結束後釋放資源;若未找到,則在nextProducer中獲取資料,並在獲取到資料的同時將其快取;
  • EncodedCacheKeyMultiplexProducer 是MultiplexProducer的子類,nextProducer為EncodedMemoryCacheProducer,將多個擁有相同未解碼記憶體快取鍵的ImageRequest進行“合併”,若快取命中,它們都會獲取到該資料;
  • DiskCacheProducer 在檔案記憶體快取中獲取資料;若未找到,則在nextProducer中獲取資料,並在獲取到資料的同時將其快取

功能類Producer,這類Producer在初始化的時候會傳入一個nextProducer,它們會對nextProducer產生的結果進行處理。

  • MultiplexProducer 將多個擁有相同CacheKey的ImageRequest進行“合併”,讓他們從都從nextProducer中獲取資料;
  • ThreadHandoffProducer 將nextProducer的produceResult方法放在後臺執行緒中執行(執行緒池容量為1);
  • SwallowResultProducer 將nextProducer的獲取的資料“吞”掉,回在Consumer的onNewResult中傳入null值;
  • ResizeAndRotateProducer 將nextProducer產生的EncodedImage根據EXIF的旋轉、縮放屬性進行變換(如果物件不是JPEG格式影象,則不會發生變換);
  • PostProcessorProducer 將nextProducer產生的EncodedImage根據PostProcessor進行修改,關於PostProcessor詳見修改圖片;
  • DecodeProducer 將nextProducer產生的EncodedImage解碼。解碼在後臺執行緒中執行,可以在ImagePipelineConfig中通過setExecutorSupplier來設定執行緒池數量,預設為最大可用的處理器數;
  • WebpTranscodeProducer 若nextProducer產生的EncodedImage為WebP格式,則將其解碼成DecodeProducer能夠處理的EncodedImage。解碼在後代程式中進行。

那麼這些Producer是在哪裡構建的呢??

我們前面說過,在構建DataSource的時候,會呼叫ProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest);方法為指定的ImageRequest構建 想要的Producer序列,事實上,ProducerSequenceFactory裡除了getDecodedImageProducerSequence()方法以為,還有幾個針對其他情況獲取序列的方法,這裡我們 列一下從網路獲取圖片的時候Producer序列是什麼樣的。

如下所示:

  1. PostprocessedBitmapMemoryCacheProducer,非必須 ,在Bitmap快取中查詢被PostProcess過的資料。
  2. PostprocessorProducer,非必須,對下層Producer傳上來的資料進行PostProcess。
  3. BitmapMemoryCacheGetProducer,必須,使Producer序列只讀。
  4. ThreadHandoffProducer,必須,使下層Producer工作在後臺程式中執行。
  5. BitmapMemoryCacheKeyMultiplexProducer,必須,使多個相同已解碼記憶體快取鍵的ImageRequest都從相同Producer中獲取資料。
  6. BitmapMemoryCacheProducer,必須,從已解碼的記憶體快取中獲取資料。
  7. DecodeProducer,必須,將下層Producer產生的資料解碼。
  8. ResizeAndRotateProducer,非必須,將下層Producer產生的資料變換。
  9. EncodedCacheKeyMultiplexProducer,必須,使多個相同未解碼記憶體快取鍵的ImageRequest都從相同Producer中獲取資料。
  10. EncodedMemoryCacheProducer,必須,從未解碼的記憶體快取中獲取資料。
  11. DiskCacheProducer,必須,從檔案快取中獲取資料。
  12. WebpTranscodeProducer,非必須,將下層Producer產生的Webp(如果是的話)進行解碼。
  13. NetworkFetchProducer,必須,從網路上獲取資料。

我們上面說道Producer產生的結果由Consumer來消費,那它又是如何建立的呢??

Producer在處理資料時是向下傳遞的,而Consumer來接收結果時則是向上傳遞的,基本上Producer在接收上層傳遞的Consumer進行包裝,我們舉個小例子。

在上面的流程分析中,我們說過最終建立的DataSource是CloseableProducerToDataSourceAdapter,CloseableProducerToDataSourceAdapter的父類是AbstractProducerToDataSourceAdapter,在它的 構造方法中會呼叫createConsumer()來建立第一層Consumer,如下所示:

public abstract class AbstractProducerToDataSourceAdapter<T> extends AbstractDataSource<T> {
    
     private Consumer<T> createConsumer() {
       return new BaseConsumer<T>() {
         @Override
         protected void onNewResultImpl(@Nullable T newResult, @Status int status) {
           AbstractProducerToDataSourceAdapter.this.onNewResultImpl(newResult, status);
         }
   
         @Override
         protected void onFailureImpl(Throwable throwable) {
           AbstractProducerToDataSourceAdapter.this.onFailureImpl(throwable);
         }
   
         @Override
         protected void onCancellationImpl() {
           AbstractProducerToDataSourceAdapter.this.onCancellationImpl();
         }
   
         @Override
         protected void onProgressUpdateImpl(float progress) {
           AbstractProducerToDataSourceAdapter.this.setProgress(progress);
         }
       };
     } 
}
複製程式碼

從上面列出的Producer序列可以看出,第一層Producer就是PostprocessedBitmapMemoryCacheProducer,在它的produceResults()方法中,會對上面傳遞下來的Consumer進行包裝,如下所示:

public class PostprocessedBitmapMemoryCacheProducer
    implements Producer<CloseableReference<CloseableImage>> {
    
     @Override
     public void produceResults(
         final Consumer<CloseableReference<CloseableImage>> consumer,
         final ProducerContext producerContext) {
         //...
         final boolean isRepeatedProcessor = postprocessor instanceof RepeatedPostprocessor;
         Consumer<CloseableReference<CloseableImage>> cachedConsumer = new CachedPostprocessorConsumer(
             consumer,
             cacheKey,
             isRepeatedProcessor,
             mMemoryCache);
         listener.onProducerFinishWithSuccess(
             requestId,
             getProducerName(),
             listener.requiresExtraMap(requestId) ? ImmutableMap.of(VALUE_FOUND, "false") : null);
         mInputProducer.produceResults(cachedConsumer, producerContext);
         //...
     }
}
複製程式碼

當PostprocessedBitmapMemoryCacheProducer呼叫自己的produceResults()處理自己的任務時,會繼續呼叫下一層的Producer,當所有的Producer都完成自己的工作 以後,結果就由下至上層層返回到最上層的Consumer回撥中,最終將結果返回給呼叫者。

Fresco裡的Producer是按照一定的順序進行排列,一個執行完了,繼續執行下一個。

以上便是Fresco裡整個Producer/Consumer結構。

三 快取機制

Fresco裡有三級快取,兩級記憶體快取,一級磁碟快取,如下圖所示:

? 點選圖片檢視大圖

Android開源框架原始碼鑑賞:Fresco
  • 未編碼圖片記憶體快取
  • 已編碼圖片記憶體快取
  • 磁碟快取

磁碟快取因為涉及到檔案讀寫要比記憶體快取複雜一些,從下至上可以將磁碟快取分為三層:

  • 緩衝快取層:由BufferedDiskCache實現,提供緩衝功能。
  • 檔案快取層:由DiskStroageCache實現,提供實際的快取功能。
  • 檔案儲存層:由DefaultDiskStorage實現,提供磁碟檔案讀寫的功能。

我們先來看看Fresco的快取鍵值的設計,Fresco為快取鍵設計了一個介面,如下所示:

public interface CacheKey {
  String toString();
  boolean equals(Object o);
  int hashCode();
  //是否是由Uri構建而來的
  boolean containsUri(Uri uri);
  //獲取url string
  String getUriString();
}
複製程式碼

CacheKey有兩個實現類:

  • BitmapMemoryCacheKey 用於已解碼的記憶體快取鍵,會對Uri字串、縮放尺寸、解碼引數、PostProcessor等關鍵引數進行hashCode作為唯一標識;
  • SimpleCacheKey 普通的快取鍵實現,使用傳入字串的hashCode作為唯一標識,所以需要保證相同鍵傳入字串相同。

好,我們繼續來分析記憶體快取和磁碟快取的實現。

3.1 記憶體快取

我們前面說到,記憶體快取分為兩級:

  • 未解碼圖片記憶體快取:由EncodedImage描述真正的快取物件。
  • 已解碼圖片記憶體快取:由BitmapMemoryCache描述真正的快取物件。

它們的區別在於快取的資料格式不同,未編碼圖片記憶體快取使用的是CloseableReference,已編碼圖片記憶體快取使用的是CloseableReference,它們的區別在於資源 的測量和釋放方式是不同,它們使用VauleDescriptor來描述不同資源的資料大小,使用不同的ResourceReleaser來釋放資源。

內部的資料結構使用的是CountingLruMap,我們之前在文章07Android開源框架原始碼賞析:LruCache與DiskLruCache中 提到,LruCache與DiskLruCache都使用的是LinkedHashMap,Fresco沒有直接使用LinkedHashMap,而是對它做了一層封裝,這個就是CountingLruMap,它內部有一個雙向連結串列,在查詢的時候,可以從最 早插入的單位開始查詢,這樣就可以快速刪除掉最早插入的資料,提高效率。

我們接著來看記憶體快取是如何實現的,記憶體快取的實現源於一個共同的介面,如下所示:

public interface MemoryCache<K, V> {
  //快取指定的key-value對,該方法會返回一份新的快取拷貝用來程式碼原有的引用,但不需要的時候需要關閉這個快取引用
  CloseableReference<V> cache(K key, CloseableReference<V> value);
  //獲取快取
  CloseableReference<V> get(K key);
  //根據指定的key移除快取
  public int removeAll(Predicate<K> predicate);
  //查詢是否包含該key對應的快取
  public boolean contains(Predicate<K> predicate);
}
複製程式碼

和記憶體快取相關的還有一個介面MemoryTrimmable,實現該介面,並將自己註冊的MemoryTrimmableRegistry中,當記憶體變化時,可以 通知到自己,如下所示:

public interface MemoryTrimmable {
  //記憶體發生變化
  void trim(MemoryTrimType trimType);
}

複製程式碼

我們來看看有哪些類直接或者間接實現了該快取介面。

  • CountingMemoryCache。它實現了MemoryCache與MemoryTrimmable介面,內部維護這一個Entry用來封裝快取物件,Entry物件除了記錄快取鍵、快取值之外,還記錄著 該物件的引用數量(clientCount),以及是否被快取追蹤(isOrphan)。
  • InstrumentedMemoryCache:也實現了MemoryCache介面,但它沒有直接實現相應的功能,它相當於是個Wrapper類,對CountingMemoryCache進行了包裝。增加了MemoryCacheTracker ,在快取未命中時提供回撥函式,供呼叫者實現自定義功能。

在CountingMemoryCache內部使用Entry物件來描述快取對,它包含以下資訊:

  static class Entry<K, V> {
    //快取key
    public final K key;
    //快取物件
    public final CloseableReference<V> valueRef;
    // The number of clients that reference the value.
    //快取的引用計數
    public int clientCount;
    //該Entry物件是否被其所描述的快取所追蹤
    public boolean isOrphan;
    //快取狀態監聽器
    @Nullable public final EntryStateObserver<K> observer;
}
複製程式碼

? 注:只有引用數量(clientCount)為0,且沒有被快取追蹤(isOrphan = true)時快取物件才可以被釋放。

我們接著開看看CountingMemoryCache是如何插入、獲取和刪除快取的。

插入快取

首先我們要了解快取的操作涉及到兩個集合:

  //待移除快取集合,這裡面的快取沒有被外面使用
  @VisibleForTesting
  final CountingLruMap<K, Entry<K, V>> mExclusiveEntries;

  //所有快取的集合,包括待移除的快取
  @GuardedBy("this")
  @VisibleForTesting
  final CountingLruMap<K, Entry<K, V>> mCachedEntries;
複製程式碼

我們接著來看插入快取的實現。

public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable {
    
      public CloseableReference<V> cache(
          final K key,
          final CloseableReference<V> valueRef,
          final EntryStateObserver<K> observer) {
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(valueRef);
    
        //1. 檢查是否需要更新快取引數。
        maybeUpdateCacheParams();
    
        Entry<K, V> oldExclusive;
        CloseableReference<V> oldRefToClose = null;
        CloseableReference<V> clientRef = null;
        synchronized (this) {
          //2. 在快取中查詢要插入的物件,若存在則將其從待移除快取集合移除,並呼叫它的close()方法
          //當該快取物件的引用數目為0的時候會釋放掉該物件。
          oldExclusive = mExclusiveEntries.remove(key);
          Entry<K, V> oldEntry = mCachedEntries.remove(key);
          if (oldEntry != null) {
            makeOrphan(oldEntry);
            oldRefToClose = referenceToClose(oldEntry);
          }
          //3. 檢查是否快取物件達到最大顯示或者快取池已滿,如果都為否,則插入新快取物件。
          if (canCacheNewValue(valueRef.get())) {
            Entry<K, V> newEntry = Entry.of(key, valueRef, observer);
            mCachedEntries.put(key, newEntry);
            //4. 將插入的物件包裝成一個CloseableReference,重新包裝物件主要是為了重設
            //一下ResourceReleaser,它會在釋放資源的時候減少Entry的clientCount,並將該快取物件
            // 加入到mExclusiveEntries中,mExclusiveEntries裡存放的是已經被使用過的快取(等待被釋放),
            // 如果快取物件可以釋放,則直接釋放快取物件。
            clientRef = newClientReference(newEntry);
          }
        }
        CloseableReference.closeSafely(oldRefToClose);
        maybeNotifyExclusiveEntryRemoval(oldExclusive);
    
        //5. 判斷是否需要釋放資源,當超過了EvictEntries最大容量或者快取池已滿,則移除EvictEntries最早插入的物件。
        maybeEvictEntries();
        return clientRef;
      }
}
複製程式碼

插入快取主要做了以下幾件事情:

  1. 檢查是否需要更新快取引數。
  2. 在快取中查詢要插入的物件,若存在則將其從待移除快取集合移除,並呼叫它的close()方法當該快取物件的引用數目為0的時候會釋放掉該物件。
  3. 檢查是否快取物件達到最大顯示或者快取池已滿,如果都為否,則插入新快取物件。
  4. 將插入的物件包裝成一個CloseableReference,重新包裝物件主要是為了重設一下ResourceRelr,它會在釋放資源的時候減少Entry的clientCount,並將該快取物件 加入到mExclusiveEntries中,mExclusiveEntries裡存放的是已經被使用過的快取(等待被釋放),如果快取物件可以釋放,則直接釋放快取物件。
  5. 判斷是否需要釋放資源,當超過了EvictEntries最大容量或者快取池已滿,則移除EvictEntries最早插入的物件。

獲取快取

public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable {
    
      @Nullable
      public CloseableReference<V> get(final K key) {
        Preconditions.checkNotNull(key);
        Entry<K, V> oldExclusive;
        CloseableReference<V> clientRef = null;
        synchronized (this) {
          //1. 查詢該快取,說明該快取可能要被使用,則嘗試將其從待移除快取集合移除。
          oldExclusive = mExclusiveEntries.remove(key);
          //2. 從快取集合中查詢該快取。
          Entry<K, V> entry = mCachedEntries.get(key);
          if (entry != null) {
            //3. 如果查詢到該快取,將該快取物件包裝成一個CloseableReference,重新包裝物件主要是為了重設
           //一下ResourceReleaser,它會在釋放資源的時候減少Entry的clientCount,並將該快取物件
            // 加入到mExclusiveEntries中,mExclusiveEntries裡存放的是已經被使用過的快取(等待被釋放),
            // 如果快取物件可以釋放,則直接釋放快取物件。
            clientRef = newClientReference(entry);
          }
        }
       //4. 判斷是否需要通知待刪除集合裡的元素被移除了。
        maybeNotifyExclusiveEntryRemoval(oldExclusive);
        //5. 判斷是否需要更新快取引數。
        maybeUpdateCacheParams();
        //6. 判斷是否需要釋放資源,當超過了EvictEntries最大容量或者快取池已滿,則移除EvictEntries最早插入的物件。
        maybeEvictEntries();
        return clientRef;
      }

}
複製程式碼

獲取快取主要執行了以下操作:

  1. 查詢該快取,說明該快取可能要被使用,則嘗試將其從待移除快取集合移除。
  2. 從快取集合中查詢該快取。
  3. 如果查詢到該快取,將該快取物件包裝成一個CloseableReference,重新包裝物件主要是為了重設一下ResourceReleaser,它會在釋放資源的時候減少Entry的clientCount,並將該快取物件 加入到mExclusiveEntries中,mExclusiveEntries裡存放的是已經被使用過的快取(等待被釋放),如果快取物件可以釋放,則直接釋放快取物件。 . 判斷是否需要通知待刪除集合裡的元素被移除了。
  4. 判斷是否需要更新快取引數。
  5. 判斷是否需要釋放資源,當超過了EvictEntries最大容量或者快取池已滿,則移除EvictEntries最早插入的物件。

移除快取

移除快取就是呼叫集合的removeAll()方法移除所有的元素,如下所示:

public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable {
    
      public int removeAll(Predicate<K> predicate) {
        ArrayList<Entry<K, V>> oldExclusives;
        ArrayList<Entry<K, V>> oldEntries;
        synchronized (this) {
          oldExclusives = mExclusiveEntries.removeAll(predicate);
          oldEntries = mCachedEntries.removeAll(predicate);
          makeOrphans(oldEntries);
        }
        maybeClose(oldEntries);
        maybeNotifyExclusiveEntryRemoval(oldExclusives);
        maybeUpdateCacheParams();
        maybeEvictEntries();
        return oldEntries.size();
      }
}
複製程式碼

這個方法比較簡單,我們重點關注的是一個多次出現的方法:maybeEvictEntries(),它是用來調節總快取的大小的,保證快取不超過最大快取個數和最大容量,如下所示:

public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable {
    
      private void maybeEvictEntries() {
        ArrayList<Entry<K, V>> oldEntries;
        synchronized (this) {
          int maxCount = Math.min(
              //待移除集合最大持有的快取個數
              mMemoryCacheParams.maxEvictionQueueEntries,
              //快取集合最大持有的快取個數 - 當前正在使用的快取個數
              mMemoryCacheParams.maxCacheEntries - getInUseCount());
          int maxSize = Math.min(
              //待移除集合最大持有的快取容量
              mMemoryCacheParams.maxEvictionQueueSize,
              //快取集合最大持有的快取容量 - 當前正在使用的快取容量
              mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes());
          //1. 根據maxCount和maxSize,不斷的從mExclusiveEntries移除隊頭的元素,知道滿足快取限制規則。
          oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize);
          //2. 將快取Entry的isOrphan置為true,表示該Entry物件不再被追蹤,等待被刪除。
          makeOrphans(oldEntries);
        }
        //3. 關閉快取。
        maybeClose(oldEntries);
        //4. 通知快取被關閉。
        maybeNotifyExclusiveEntryRemoval(oldEntries);
      }

}
複製程式碼

整個調整容量的流程就是根據當前快取的個數和容量進行調整直到滿足最大快取個數和最大快取容量的限制,如下所示:

  1. 根據maxCount和maxSize,不斷的從mExclusiveEntries移除隊頭的元素,知道滿足快取限制規則。
  2. 將快取Entry的isOrphan置為true,表示該Entry物件不再被追蹤,等待被刪除。
  3. 關閉快取。
  4. 通知快取被關閉。

以上就是記憶體快取的全部內容,我們接著來看磁碟快取的實現。?

3.2 磁碟快取

我們前面已經說過,磁碟快取也分為三層,我們再來回顧一下,如下圖所示:

? 點選圖片檢視大圖

Android開源框架原始碼鑑賞:Fresco

磁碟快取因為涉及到檔案讀寫要比記憶體快取複雜一些,從下至上可以將磁碟快取分為三層:

  • 緩衝快取層:由BufferedDiskCache實現,提供緩衝功能。
  • 檔案快取層:由DiskStroageCache實現,提供實際的快取功能。
  • 檔案儲存層:由DefaultDiskStorage實現,提供磁碟檔案讀寫的功能。

我們來看看相關的介面。

磁碟快取的介面是FileCache,如下所示:

public interface FileCache extends DiskTrimmable {
  //是否可以進行磁碟快取,主要是本地儲存是否存在以及是否可以讀寫。
  boolean isEnabled();
  //返回快取的二進位制資源
  BinaryResource getResource(CacheKey key);
  //是否包含該快取key,非同步呼叫。
  boolean hasKeySync(CacheKey key);
  //是否包含該快取key,同步呼叫。
  boolean hasKey(CacheKey key);
  boolean probe(CacheKey key);
  //插入快取
  BinaryResource insert(CacheKey key, WriterCallback writer) throws IOException;
  //移除快取
  void remove(CacheKey key);
  //獲取快取總大小
  long getSize();
   //獲取快取個數
  long getCount();
  //清除過期的快取
  long clearOldEntries(long cacheExpirationMs);
  //清除所有快取
  void clearAll();
  //獲取磁碟dump資訊
  DiskStorage.DiskDumpInfo getDumpInfo() throws IOException;
}
複製程式碼

可以發現FileCahce介面繼承於DisTrimmable,它是一個用來監聽磁碟容量變化的介面,如下所示:

public interface DiskTrimmable {
  //當磁碟只有很少的空間可以使用的時候回撥。
  void trimToMinimum();
  //當磁碟沒有空間可以使用的時候回撥
  void trimToNothing();
}

複製程式碼

除了快取介面DiskStorageCache,Fresco還定義了DiskStorage介面來封裝檔案IO的讀寫邏輯,如下所示:

public interface DiskStorage {

  class DiskDumpInfoEntry {
    public final String path;
    public final String type;
    public final float size;
    public final String firstBits;
    protected DiskDumpInfoEntry(String path, String type, float size, String firstBits) {
      this.path = path;
      this.type = type;
      this.size = size;
      this.firstBits = firstBits;
    }
  }

  class DiskDumpInfo {
    public List<DiskDumpInfoEntry> entries;
    public Map<String, Integer> typeCounts;
    public DiskDumpInfo() {
      entries = new ArrayList<>();
      typeCounts = new HashMap<>();
    }
  }

  //檔案儲存是否可用
  boolean isEnabled();
  //是否包含外部儲存
  boolean isExternal();
  //獲取檔案描述符指向的檔案
  BinaryResource getResource(String resourceId, Object debugInfo) throws IOException;
  //檢查是否包含檔案描述符所指的檔案
  boolean contains(String resourceId, Object debugInfo) throws IOException;
  //檢查resourceId對應的檔案是否存在,如果存在則更新上次讀取的時間戳。
  boolean touch(String resourceId, Object debugInfo) throws IOException;
  void purgeUnexpectedResources();
  //插入
  Inserter insert(String resourceId, Object debugInfo) throws IOException;
  //獲取磁碟快取裡所有的Entry。
  Collection<Entry> getEntries() throws IOException;
  //移除指定的快取Entry。
  long remove(Entry entry) throws IOException;
  //根據resourceId移除對應的磁碟快取檔案
  long remove(String resourceId) throws IOException;
  //清除所有快取檔案
  void clearAll() throws IOException;

  DiskDumpInfo getDumpInfo() throws IOException;

  //獲取儲存名
  String getStorageName();

  interface Entry {
    //ID
    String getId();
    //時間戳
    long getTimestamp();
    //大小
    long getSize();
    //Fresco使用BinaryResource物件來描述磁碟快取物件,通過該物件可以獲取檔案的輸入流、位元組碼等資訊。
    BinaryResource getResource();
  }

  interface Inserter {
    //寫入資料
    void writeData(WriterCallback callback, Object debugInfo) throws IOException;
    //提交寫入的資料
    BinaryResource commit(Object debugInfo) throws IOException;
    //取消此次插入操作
    boolean cleanUp();
  }
}
複製程式碼

理解了主要介面的功能我們就看看看主要的實現類:

  • DiskStroageCache:實現了FileCache介面與DiskTrimmable介面是快取的主要實現類。
  • DefaultDiskStorage:實現了DiskStorage介面,封裝了磁碟IO的讀寫邏輯。
  • BufferedDiskCache:在DiskStroageCache的基礎上提供了Buffer功能。

BufferedDiskCache主要提供了三個方面的功能:

  • 提供寫入緩衝StagingArea,所有要寫入的資料在發出寫入命令到最終寫入之前會儲存在這裡,在查詢快取的時候會首先在這塊區域內查詢,若命中則直接返回;
  • 提供了寫入資料的辦法,在writeToDiskCache中可以看出它提供的WriterCallback將要寫入的EncodedImage轉碼成輸入流;
  • 將get、put兩個方法放在後臺執行緒中執行(get時在緩衝區域查詢時除外),分別都是容量為2的執行緒池。

我們來看看它們的實現細節。

上面DiskStorage裡定義了個介面Entry來描述磁碟快取物件的資訊,真正持有快取物件的是BinaryResource介面,它的實現類是FileBinaryResource,該類主要定義了 File的一些操作,可以通過它獲取檔案的輸入流和位元組碼等。

此外,Fresco定義了每個檔案的唯一描述符,此描述符由CacheKey的toString()方法匯出字串的SHA-1雜湊碼,然後該雜湊碼再經過Base64加密得出。

我們來看看磁碟快取的插入、查詢和刪除的實現。

插入快取

public class DiskStorageCache implements FileCache, DiskTrimmable {
    
   @Override
     public BinaryResource insert(CacheKey key, WriterCallback callback) throws IOException {
       //1. 先將磁碟快取寫入到快取檔案,這可以提供寫快取的併發速度。
       SettableCacheEvent cacheEvent = SettableCacheEvent.obtain()
           .setCacheKey(key);
       mCacheEventListener.onWriteAttempt(cacheEvent);
       String resourceId;
       synchronized (mLock) {
         //2. 獲取快取的resoucesId。
         resourceId = CacheKeyUtil.getFirstResourceId(key);
       }
       cacheEvent.setResourceId(resourceId);
       try {
         //3. 建立要插入的檔案(同步操作),這裡構建了Inserter物件,該物件封裝了具體的寫入流程。
         DiskStorage.Inserter inserter = startInsert(resourceId, key);
         try {
           inserter.writeData(callback, key);
           //4. 提交新建立的快取檔案到快取中。
           BinaryResource resource = endInsert(inserter, key, resourceId);
           cacheEvent.setItemSize(resource.size())
               .setCacheSize(mCacheStats.getSize());
           mCacheEventListener.onWriteSuccess(cacheEvent);
           return resource;
         } finally {
           if (!inserter.cleanUp()) {
             FLog.e(TAG, "Failed to delete temp file");
           }
         }
       } catch (IOException ioe) {
         //... 異常處理
       } finally {
         cacheEvent.recycle();
       }
     } 
}
複製程式碼

整個插入快取的流程如下所示:

  1. 先將磁碟快取寫入到快取檔案,這可以提供寫快取的併發速度。
  2. 獲取快取的resoucesId。
  3. 建立要插入的檔案(同步操作),這裡構建了Inserter物件,該物件封裝了具體的寫入流程。
  4. 提交新建立的快取檔案到快取中。

我們重點來看看這兩個方法startInsert()與endInsert()。

public class DiskStorageCache implements FileCache, DiskTrimmable {
    
      //建立一個臨時檔案,字尾為.tmp
      private DiskStorage.Inserter startInsert(
          final String resourceId,
          final CacheKey key)
          throws IOException {
        maybeEvictFilesInCacheDir();
        //呼叫DefaultDiskStorage的insert()方法建立一個臨時檔案
        return mStorage.insert(resourceId, key);
      }

      //將快取檔案提交到快取中,如何快取檔案已經存在則嘗試刪除原來的檔案
      private BinaryResource endInsert(
          final DiskStorage.Inserter inserter,
          final CacheKey key,
          String resourceId) throws IOException {
        synchronized (mLock) {
          BinaryResource resource = inserter.commit(key);
          //將resourceId新增點resourceId集合中,DiskStorageCache裡只維護了這一個集合
          //來記錄快取
          mResourceIndex.add(resourceId);
          mCacheStats.increment(resource.size(), 1);
          return resource;
        }
      }
}
複製程式碼

DiskStorageCache裡只維護了這一個集合Set mResourceIndex來記錄快取的Resource ID,而DefaultDiskStorage負責對磁碟上 的快取就行管理,體為DiskStorageCache提供索引功能。

我們接著來看看查詢快取的實現。

查詢快取

根據CacheKey查詢快取BinaryResource,如果快取以及存在,則更新它的LRU訪問時間戳,如果快取不存在,則返回空。

public class DiskStorageCache implements FileCache, DiskTrimmable {
    
     @Override
     public BinaryResource getResource(final CacheKey key) {
       String resourceId = null;
       SettableCacheEvent cacheEvent = SettableCacheEvent.obtain()
           .setCacheKey(key);
       try {
         synchronized (mLock) {
           BinaryResource resource = null;
           //1. 獲取快取的ResourceId,這裡是一個列表,因為可能存在MultiCacheKey,它wrap多個CacheKey。
           List<String> resourceIds = CacheKeyUtil.getResourceIds(key);
           for (int i = 0; i < resourceIds.size(); i++) {
             resourceId = resourceIds.get(i);
             cacheEvent.setResourceId(resourceId);
             //2. 獲取ResourceId對應的BinaryResource。
             resource = mStorage.getResource(resourceId, key);
             if (resource != null) {
               break;
             }
           }
           if (resource == null) {
             //3. 快取沒有命中,則執行onMiss()回撥,並將resourceId從mResourceIndex移除。
             mCacheEventListener.onMiss(cacheEvent);
             mResourceIndex.remove(resourceId);
           } else {
             //4. 快取命中,則執行onHit()回撥,並將resourceId新增到mResourceIndex。
             mCacheEventListener.onHit(cacheEvent);
             mResourceIndex.add(resourceId);
           }
           return resource;
         }
       } catch (IOException ioe) {
         //... 異常處理
         return null;
       } finally {
         cacheEvent.recycle();
       }
     } 
}
複製程式碼

整個查詢的流程如下所示:

  1. 獲取快取的ResourceId,這裡是一個列表,因為可能存在MultiCacheKey,它wrap多個CacheKey。
  2. 獲取ResourceId對應的BinaryResource。
  3. 快取沒有命中,則執行onMiss()回撥,並將resourceId從mResourceIndex移除。
  4. 快取命中,則執行onHit()回撥,並將resourceId新增到mResourceIndex。mCacheEventListener.onHit(cacheEvent);

這裡會呼叫DefaultDiskStorage的getReSource()方法去查詢快取檔案的路徑並構建一個BinaryResource物件。

Fresco在本地儲存快取檔案的路徑如下所示:

parentPath + File.separator + resourceId + type;
複製程式碼

parentPath是根目錄,type分為兩種:

  • private static final String CONTENT_FILE_EXTENSION = ".cnt";
  • private static final String TEMP_FILE_EXTENSION = ".tmp";

以上就是查詢快取的邏輯,我們接著來看看刪除快取的邏輯。

刪除快取

public class DiskStorageCache implements FileCache, DiskTrimmable {
    
      @Override
      public void remove(CacheKey key) {
        synchronized (mLock) {
          try {
            String resourceId = null;
            //獲取Resoucesid,根據resouceId移除快取,並將自己從mResourceIndex移除。
            List<String> resourceIds = CacheKeyUtil.getResourceIds(key);
            for (int i = 0; i < resourceIds.size(); i++) {
              resourceId = resourceIds.get(i);
              mStorage.remove(resourceId);
              mResourceIndex.remove(resourceId);
            }
          } catch (IOException e) {
             //...移除處理
          }
        }
      }
}
複製程式碼

刪除快取的邏輯也很簡單,獲取Resoucesid,根據resouceId移除快取,並將自己從mResourceIndex移除。

磁碟快取也會自己調節自己的快取大小來滿足快取最大容量限制條件,我們也來簡單看一看。

Fresco裡的磁碟快取過載時,會以不超過快取容量的90%為目標進行清理,具體清理流程如下所示:

public class DiskStorageCache implements FileCache, DiskTrimmable {
    
      @GuardedBy("mLock")
      private void evictAboveSize(
          long desiredSize,
          CacheEventListener.EvictionReason reason) throws IOException {
        Collection<DiskStorage.Entry> entries;
        try {
          //1. 獲取快取目錄下所有檔案的Entry的集合,以最近被訪問的時間為序,最近被訪問的Entry放在後面。
          entries = getSortedEntries(mStorage.getEntries());
        } catch (IOException ioe) {
          //... 捕獲異常
        }
    
        //要刪除的資料量
        long cacheSizeBeforeClearance = mCacheStats.getSize();
        long deleteSize = cacheSizeBeforeClearance - desiredSize;
        //記錄刪除資料數量
        int itemCount = 0;
        //記錄刪除資料大小
        long sumItemSizes = 0L;
        //2. 迴圈遍歷,從頭部開始刪除元素,直到剩餘容量達到desiredSize位置。
        for (DiskStorage.Entry entry: entries) {
          if (sumItemSizes > (deleteSize)) {
            break;
          }
          long deletedSize = mStorage.remove(entry);
          mResourceIndex.remove(entry.getId());
          if (deletedSize > 0) {
            itemCount++;
            sumItemSizes += deletedSize;
            SettableCacheEvent cacheEvent = SettableCacheEvent.obtain()
                .setResourceId(entry.getId())
                .setEvictionReason(reason)
                .setItemSize(deletedSize)
                .setCacheSize(cacheSizeBeforeClearance - sumItemSizes)
                .setCacheLimit(desiredSize);
            mCacheEventListener.onEviction(cacheEvent);
            cacheEvent.recycle();
          }
        }
        //3. 更新容量,刪除不需要的臨時檔案。
        mCacheStats.increment(-sumItemSizes, -itemCount);
        mStorage.purgeUnexpectedResources();
      }
}
複製程式碼

整個清理流程可以分為以下幾步:

  1. 獲取快取目錄下所有檔案的Entry的集合,以最近被訪問的時間為序,最近被訪問的Entry放在後面。
  2. 迴圈遍歷,從頭部開始刪除元素,直到剩餘容量達到desiredSize位置。
  3. 更新容量,刪除不需要的臨時檔案。

關於Fresco的原始碼分析就到這裡了,本來還想再講一講Fresco記憶體管理方面的知識,但是這牽扯到Java Heap以及Android匿名共享記憶體方面的知識,相對比較深入,所以 等著後續分析《Android記憶體管理框架》的時候結合著一塊講。

相關文章