2019Android面試Fresco架構詳解
本文是Fresco原始碼分析系列的開篇,主要分析Fresco的整體架構、各個組成模組的功能以及圖片載入流程,希望通過本文可以對Fresco的整體框架設計有一個大概的瞭解,也為後續更為深入的分析打下基礎。
Fresco原始碼龐大,涉及的圖片載入情況眾多。本系列Fresco原始碼分析是沿著Fresco網路載入圖片這個點展開的。
Fresco的整體架構
Fresco的組成結構還是比較清晰的,大致如下圖所示:
下面結合程式碼分別解釋一下上面各模組的作用以及大概的工作原理。
UI層
DraweeView
它繼承自ImageView,是Fresco載入圖片各個階段過程中圖片顯示的載體,比如在載入圖片過程中它顯示的是佔位圖、在載入成功時切換為目標圖片。不過後續官方可能不再讓這個類繼承ImageView。目前DraweeView與ImageView唯一的交集是:它利用ImageView來顯示Drawable :
//DraweeView.setController() public void setController(@Nullable DraweeController draweeController) { mDraweeHolder.setController(draweeController); super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());//super 就是 ImageView } //DraweeHolder.getTopLevelDrawable() public @Nullable Drawable getTopLevelDrawable() { return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable(); // mHierarchy 是 DraweeHierachy }
複製程式碼
DraweeView.setController()會在Fresco載入圖片時會呼叫。其實在這裡可以看出Fresco的圖片顯示原理是 : 利用ImageView顯示DraweeHierachy的TopLevelDrawable。上面這段程式碼引出了UI層中另外兩個關鍵類:DraweeHolder和DraweeHierachy。
DraweeHierachy
可以說它是Fresco圖片顯示的實現者。它的輸出是Drawable,這個Drawable會被DraweeView拿來顯示(上面已經說了)。它內部有多個Drawable,當前顯示在DraweeView的Drawable叫做TopLevelDrawable。在不同的圖片載入階段,TopLevelDrawable是不同的(比如載入過程中是placeholder,載入完成是目標圖片)。具體的Drawable切換邏輯是由它來具體實現的。
它是由DraweeController直接持有的,因此對於不同圖片顯示的切換操作具體是由DraweeController來直接操作的。
DraweeHolder
它維護著DraweeView和DraweeController的attach關係(DraweeView只有attch了DraweeController才會具體載入網路圖片的能力)。可以把它理解為DraweeView、DraweeHierachy和DraweeController這3個類之間的粘合劑,具體引用關係如下圖:
DraweeController : 載入邏輯控制層
它的主要功能是: 接收DraweeView的圖片載入請求,控制ProducerSequence發起圖片載入和處理流程,監聽ProducerSequence載入過程中的事件(失敗、完成等),並更新最新的Drawable到DraweeHierachy。
DraweeController的構造邏輯
在Fresco中DraweeController是通過DraweeControllerBuilder來構造的。而DraweeControllerBuilder在Fresco中是以單例的形式存在的。Fresco在初始化時會呼叫下面的程式碼:
Fresco.java
private static void initializeDrawee(Context context, @Nullable DraweeConfig draweeConfig) { sDraweeControllerBuilderSupplier = new PipelineDraweeControllerBuilderSupplier(context, draweeConfig); SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier); }
所以所有的DraweeController都是通過同一個DraweeControllerBuilder來構造的。Fresco每次圖片載入都會對應到一個DraweeController,一個DraweeView的多次圖片載入可以複用同一個DraweeController:
SimpleDraweeView.java
public void setImageURI(Uri uri, @Nullable Object callerContext) { DraweeController controller = mControllerBuilder .setCallerContext(callerContext) .setUri(uri) //設定新的圖片載入路徑 .setOldController(getController())//複用 controller .build(); setController(controller); }
複製程式碼
所以一般情況下 : 一個DraweeView對應一個DraweeController。
通過DataSource發起圖片載入
在前面已經說了DraweeController是直接持有DraweeHierachy,所以它觀察到ProducerSequence的資料變化是可以很容易更新到DraweeHierachy(具體程式碼先不展示了)。那它是如何控制ProducerSequence來載入圖片的呢?其實DraweeController並不會直接和ProducerSequence發生關聯。對於圖片的載入,它直接接觸的是DataSource,由DataSource進而來控制ProducerSequence發起圖片載入和處理流程。下面就跟隨原始碼來看一下DraweeController是如果通過DataSource來控制ProducerSequence發起圖片載入和處理流程的。
DraweeController發起圖片載入請求的方法是(AbstractDraweeController.java):
protected void submitRequest() { mDataSource = getDataSource(); final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() { //可以簡單的把它理解為一個監聽者 @Override public void onNewResultImpl(DataSource<T> dataSource) { //圖片載入成功 ... } ... }; ... mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); //mUiThreadImmediateExecutor是指 dataSubscriber 回撥方法執行的執行緒,這裡是主執行緒 }
複製程式碼
那DataSource是什麼呢? getDataSource()最終會呼叫到:
ImagePipeline.java
public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage(ImageRequest imageRequest,...) {
//獲取載入圖片的ProducerSequence
Producer<CloseableReference<CloseableImage>> producerSequence = mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest);
return submitFetchRequest( producerSequence, imageRequest, lowestPermittedRequestLevelOnSubmit, callerContext, requestListener);
}
private <T> DataSource<CloseableReference<T>> submitFetchRequest(...) {
return CloseableProducerToDataSourceAdapter.create(roducerSequence, settableProducerContext, finalRequestListener);
}
所以DraweeController最終拿到的DataSource是CloseableProducerToDataSourceAdapter。這個類在構造的時候就會啟動圖片載入流程(它的構造方法會呼叫producer.produceResults(...),這個方法就是圖片載入的起點,我們後面再看)。 這裡我們總結一下Fresco中DataSource的概念以及作用:在Fresco中DraweeController每發起一次圖片載入就會建立一個DataSource,這個DataSource用來提供這次請求的資料(圖片)。DataSource只是一個介面,至於具體的載入流程Fresco是通過ProducerSequence來實現的。 Fresco圖片載入前的邏輯 瞭解了上面的知識後,我們過一遍圖片載入的原始碼(從UI到DraweeController),來理一下目前所瞭解的各個模組之間的聯絡。我們在使用Fresco載入圖片時一般是使用這個API:SimpleDraweeView.setImageURI(imageLink),這個方法最終會呼叫到: SimpleDraweeView.java
public void setImageURI(Uri uri, @Nullable Object callerContext) {
DraweeController controller = mControllerBuilder
.setCallerContext(callerContext)
.setUri(uri)
.setOldController(getController())
.build();//這裡會複用 controller
setController(controller);
}
public void setController(@Nullable DraweeController draweeController) { mDraweeHolder.setController(draweeController); super.setImageDrawable(mDraweeHolder.getTopLevelDrawable()); } 複製程式碼 即每次載入都會使用DraweeControllerBuilder來build一個DraweeController。其實這個DraweeController預設是複用的。然後會把DraweeController設定給DraweeHolder, 並在載入開始預設是從DraweeHolder獲取TopLevelDrawable並展示到DraweeView。繼續看一下DraweeHolder的邏輯: DraweeHolder.java public @Nullable Drawable getTopLevelDrawable() { return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable(); } public void setController(@Nullable DraweeController draweeController) { detachController(); mController = draweeController; ... mController.setHierarchy(mHierarchy); attachController(); } 複製程式碼 在DraweeHolder.setController()中把DraweeHierachy設定給DraweeController,並重新attachController(),attachController()主要呼叫了DraweeController.onAttach(): AbstractDraweeController.java public void onAttach() { ... mIsAttached = true; if (!mIsRequestSubmitted) { submitRequest(); } } protected void submitRequest() { mDataSource = getDataSource(); final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() { //可以簡單的把它理解為一個監聽者 @Override public void onNewResultImpl(DataSource<T> dataSource) { //圖片載入成功 ... } ... }; ... mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); //mUiThreadImmediateExecutor是指 dataSubscriber 回撥方法執行的執行緒,這裡是主執行緒 }
複製程式碼
即通過submitRequest()提交了一個請求,這個方法我們前面已經看過了,它所做的主要事情就是,構造了一個DataSource。這個DataSource我們經過追蹤,它的例項實際上是CloseableProducerToDataSourceAdapter。CloseableProducerToDataSourceAdapter在構造時就會呼叫producer.produceResults(...),進而發起整個圖片載入流程。
用下面這張圖總結從SimpleDraweeView->DraweeController的圖片載入邏輯:
到這裡我們梳理完了Fresco在真正發起圖片載入前所走的邏輯,那麼Fresco的圖片載入流程是如何控制的呢?到底經歷了哪些步驟呢?
圖片載入實現層
Fresco中有關圖片的記憶體快取、解碼、編碼、磁碟快取、網路請求都是在這一層實現的,而所有的實現的基本單元是Producer,所以我們先來理解一下Producer:
Producer
看一下它的定義:
/** * <p> Execution of image request consists of multiple different tasks such as network fetch, * disk caching, memory caching, decoding, applying transformations etc. Producer<T> represents * single task whose result is an instance of T. Breaking entire request into sequence of * Producers allows us to construct different requests while reusing the same blocks. */ public interface Producer<T> { /** * Start producing results for given context. Provided consumer is notified whenever progress is made (new value is ready or error occurs). */ void produceResults(Consumer<T> consumer, ProducerContext context); }
複製程式碼
結合註釋我們可以這樣定義Producer的作用:一個Producer用來處理整個Fresco圖片處理流程中的一步,比如從網路獲取圖片、記憶體獲取圖片、解碼圖片等等。而對於Consumer可以把它理解為監聽者,看一下它的定義:
public interface Consumer<T> {
...
void onNewResult(T newResult, @Status int status); //Producer處理成功
void onFailure(Throwable t); //Producer處理失敗 ...
}
複製程式碼
Producer的處理結果可以通過Consumer來告訴外界,比如是失敗還是成功。
Producer的組合
一個ProducerA可以接收另一個ProducerB作為引數,如果ProducerA處理完畢後可以呼叫ProducerB來繼續處理。並傳入Consumer來觀察ProducerB的處理結果。比如Fresco在載入圖片時會先去記憶體快取獲取,如果記憶體快取中沒有那麼就網路載入。這裡涉及到兩個Producer分別是BitmapMemoryCacheProducer和NetworkFetchProducer,假設BitmapMemoryCacheProducer為ProducerA,NetworkFetchProducer為ProducerB。我們用虛擬碼看一下他們的邏輯:
BitmapMemoryCacheProducer.java
public class BitmapMemoryCacheProducer implements Producer<CloseableReference<CloseableImage>> { private final Producer<CloseableReference<CloseableImage>> mInputProducer; // 我們假設 inputProducer在這裡為NetworkFetchProducer public BitmapMemoryCacheProducer(...,Producer<CloseableReference<CloseableImage>> inputProducer) { ... mInputProducer = inputProducer; } @Override public void produceResults(Consumer<CloseableReference<CloseableImage>> consumer,...) { CloseableReference<CloseableImage> cachedReference = mMemoryCache.get(cacheKey); if (cachedReference != null) { //從快取中獲取成功,直接通知外界 consumer.onNewResult(cachedReference, BaseConsumer.simpleStatusForIsLast(isFinal)); return; //結束處理流程 } Consumer<CloseableReference<CloseableImage>> wrappedConsumer = wrapConsumer(consumer..); //包了一層Consumer,即mInputProducer產生結果時,它自己可以觀察到 mInputProducer.produceResults(wrappedConsumer, producerContext); //網路載入 } }
複製程式碼
NetworkFetchProducer.java
public class NetworkFetchProducer implements Producer<EncodedImage> { 它並沒有 inputProducer, 對於Fresco的圖片載入來說如果網路都獲取失敗,那麼就是圖片載入失敗了 @Override public void produceResults(final Consumer<CloseableReference<CloseableImage>> consumer,..) { 網路獲取 ... if(獲取到網路圖片){ notifyConsumer(...); //把結果通知給consumer,即觀察者 } ... } } `` 複製程式碼 程式碼可能不是很好理解,可以結合下面這張圖來理解這個關係: Fresco可以通過組裝多個不同的Producer來靈活的定義不同的圖片處理流程的,多個Producer組裝在一塊稱為ProducerSequence(Fresco中並沒有這個類哦)。一個ProducerSequence一般定義一種圖片處理流程,比如網路載入圖片的ProducerSequence叫做NetworkFetchSequence,它包含多個不同型別的Producer。 網路圖片載入的處理流程 在Fresco中不同的圖片請求會有不同的ProducerSequence來處理,比如網路圖片請求: ProducerSequenceFactory.java
private Producer<CloseableReference<CloseableImage>> getBasicDecodedImageSequence(ImageRequest imageRequest) {
switch (imageRequest.getSourceUriType()) {
case SOURCE_TYPE_NETWORK: return getNetworkFetchSequence();
...
}
複製程式碼 所以對於網路圖片請求會呼叫getNetworkFetchSequence:
/**
- swallow result if prefetch -> bitmap cache get -> background thread hand-off -> multiplex ->
- bitmap cache -> decode -> multiplex -> encoded cache -> disk cache -> (webp transcode) ->
-
network fetch.
*/
private synchronized Producer<CloseableReference<CloseableImage>> getNetworkFetchSequence() {
...
mNetworkFetchSequence = new BitmapCacheGetToDecodeSequence(getCommonNetworkFetchToEncodedMemorySequence());
...
return mNetworkFetchSequence;
}
複製程式碼 getNetworkFetchSequence會經過重重呼叫來組合多個Producer。這裡我就不追程式碼邏輯了,直接用下面這張圖來描述Fresco網路載入圖片的處理流程: 可以看到Fresco的整個圖片載入過程還是十分複雜的。並且上圖我只是羅列一些關鍵的Producer,其實還有一些我沒有畫出來,有興趣可以去原始碼細細探討一下。 OK,到這裡本文算是結束了,希望你可以通過本文對Fresco的設計在整體上有一定的瞭解。後續文章會繼續討論Fresco的快取邏輯、圖片壓縮、DraweeHierachy的Drawable切換邏輯等。歡迎繼續關注。 想要Fresco的全套視訊可以加群:4112676群內有面試視訊