Fresco圖片框架內部實現原理探索
流行的網路框架
目前流行的網路圖片框架:
Picasso、Universal Image Loader、Volley的(ImageLoader、NetworkImageView)、Glide和Fresco
簡明的介紹下(具體細節和功能可看原始碼和wiki):
其中Picasso和Universal Image Loader相比其它的算是最輕量級的圖片框架了,它們擁有較少的方法數,Universal Image Loader是這五個框架中定製性最強的,它內部實現還是按網路框架的套路走:HttpUrlConnection+執行緒池+Handler,支援漸顯效果。
而Picasso只有一些圖片載入框架應有的基本功能,所以因此它是最輕量的,在需求只要基本的圖片載入與雙快取功能下,可以選Picasso作為專案的基礎庫,Picasso它內部預設是使用OkHttpClient作為載入網路圖片的下載器,畢竟不用自家用誰的,在OkHttpClient沒有的情況下則使用HttpUrlConnection,同上面一樣,下載器+執行緒池+Handler,不過它內部的執行緒池比較有意思,執行緒池的執行緒數量是根據當前的網路環境來動態改變的,wifi網路下為4,4G為3,3G為2,2G為1,其它情況下預設為3,支援漸顯效果。
Volley的沒什麼可說的,基本功能都有,網路框架的附贈功能。
Glide的話,Google官方推薦,支援Gif、圖片縮圖、本地視訊解碼、請求和動畫生命週期的自動管理、漸顯動畫、支援OkHttp和Volley等等,預設是使用HttpUrlConnection載入圖片的,原始碼灰常多,200多個類,不想看
Fresco我認為是這幾個框架中效能最佳的一個框架,著重介紹,它內部用了大量的建造者模式、單例模式、靜態工廠模式、生產/消費者模式。內部實現比較複雜,就拿圖片載入來說,是通過在非同步執行緒中回撥圖片的輸入流,然後通過一系列讀取、寫入、轉化成EncodedImage,然後再Decode成Bitmap,通過Handler轉給UI執行緒顯示,通過IO操作儲存在硬碟快取目錄下。
Fresco效能上的優點
優一:
1、支援webp格式的圖片,是Google官方推行的,它的大小比其它格式圖片的大小要小一半左右,目前各個大公司都漸入的使用這種圖片格式了,比如:Youtube、Gmail、淘寶、QQ空間等都已嚐鮮,使用該格式最大的優點就是輕量、省流量、圖片載入迅速。而Fresco是通過jni來實現支援WebP格式圖片。
優二:
2、5.0以下系統:使用”ashmem”(匿名共享記憶體)區域儲存Bitmap快取,這樣Bitmap物件的建立、釋放將永遠不會觸發GC,關於”ashmem”儲存區域,它是一個不在Java堆區的一片儲存記憶體空間,它的管理由
關於”ashmem”的儲存區域,我們的應用程式並不能像訪問堆記憶體一樣直接訪問這塊記憶體塊,但是也有一些例外,對於Bitmap而言,有一種為”Purgeable Bitmap”可擦除的Bitmap點陣圖是儲存在這塊記憶體區域中的,BitmapFactory.Options中有這麼一個屬性inPurgeable
:
- 1
- 2
- 3
- 1
- 2
- 3
所以通過配置inPurgeable = true這個屬性,這樣解碼出來的Bitmap點陣圖就儲存在”ashmem”區域中,之後用到”ashmem”中得圖片時,則把這個圖片從這個區域中取出來,渲染完畢後則放回這個位置。
既然Fresco中Bitmap快取在5.0以前是放在”ashmem”中,GC並不會回收它們,且也不會被”ashmeme”內建的清除機制回收它們,所以這樣雖然使得在堆中不會造成記憶體洩露,而在這塊區域可能造成記憶體洩露,Fresco中採取的辦法則是使用引用計數的方式,其中有一個SharedReference這個類,這個類中有這麼兩個方法:addReference()和deleteReference(),通過這兩個基本方法來對引用進行計數,一旦計數為零時,則對應的資源將會清除(如:Bitmap.recycle()等),而Fresco為了考慮更容易被我們使用,又提供了一個CloseableReference類,該類可以說是SharedReference類上功能的封裝,CloseableReference同時也實現了Cloneable、Closeable介面,它在呼叫.clone()方法時同時會呼叫addReference()來增加一個引用計數,在呼叫.close()方法時同時會呼叫deleteReference()來刪除一個引用計數,所以在使用Fresco的使用,我們都是與CloseableReference類打交道,使用CloseableReference必須遵循以下兩條規則:
1、在賦值CloseableReference給新物件的時候,呼叫.clone()進行賦值
2、在超出作用域範圍的時候,必須呼叫.close(),通常會在finally程式碼塊中呼叫
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
遵循這些規則可以有效地防止記憶體洩漏。
優三:
3、使用了三級快取:Bitmap快取+未解碼圖片快取+硬碟快取。
其中前兩個就是記憶體快取,Bitmap快取根據系統版本不同放在了不同記憶體區域中,而未解碼圖片的快取只在堆記憶體中,Fresco分了兩步做記憶體快取,這樣做有什麼好處呢?第一個好處就如上的第二條,第二個好處是加快圖片的載入速度,Fresco的載入圖片的流程為:查詢Bitmap快取中是否存在,存在則直接返回Bitmap直接使用,不存在則查詢未解碼圖片的快取,如果存在則進行Decode成Bitmap然後直接使用並加入Bitmap快取中,如果未解碼圖片快取中查詢不到,則進行硬碟快取的檢查,如有,則進行IO、轉化、解碼等一系列操作,最後成Bitmap供我們直接使用,並把未解碼(Encode)的圖片加入未解碼圖片快取,把Bitmap加入Bitmap快取中,如硬碟快取中沒有,則進行Network操作下載圖片,然後加入到各個快取中。
既然Fresco使用了三級快取,而有兩級是記憶體快取,所以當我們的App在後臺時或者在記憶體低的情況下在onLowMemory()方法中,我們應該手動清除應用的記憶體快取,我們可以使用下面的方式:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
優四:
4、Fresco框架的ImagePipeline設計圖
從設計圖中可以看出,UIThread只做圖片的顯示和從記憶體快取中載入圖片這兩件事,而其它事情如:圖片的Decode、記憶體快取的寫、硬碟快取的IO操作、網路操作等都用非UIThread來處理了,這使得UIThread專注介面的顯示,而其它工作由其它執行緒完成,使UI更加流暢。
Fresco中的MVC模式
Fresco框架整體是一個MVC模式
DraweeView——View
DraweeController——Control
DraweeHierarchy——Model
它們之間的關係大致如下:
DraweeHierarchy意為檢視的層次結構,用來儲存和描述圖片的資訊,同時也封裝了一些圖片的顯示和檢視層級的方法。
DraweeView用來顯示頂層檢視(getTopLevelDrawable())。DraweeController控制載入圖片的配置、頂層顯示哪個檢視以及控制事件的分發。
【注】DraweeView目前版本時繼承於ImageView,但這並不意味著我們可以隨意的使用ImageView相關的方法(如:setScaleType等),官方並不建議我們使用,因為後期DraweeView將繼承於View,所以最好只使用DraweeView控制元件內建的方法。
DraweeHierarchy
DraweeHierarchy除了描述了檢視的資訊和儲存6種檢視外,其中還對我們提供了一些額外的方法,比如:讓圖片漸漸顯示Fade效果、設定預設狀態下顯示的圖片、設定點選時顯示的圖片和載入失敗時顯示的圖片等方法,這些方法可以在我們載入其它圖片時保持一些良好的互動效果,值得注意的是,DraweeHierarchy是一個介面,只提供了一個預設方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
這個方法從官方註釋上來看,是得到當前檢視中最頂層的那個Drawable,如:
下面這個檢視層次最頂層的檢視為FadeDrawable
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
所以,在Fresco框架中,並不是一個DraweeView只能設定一個圖片(Drawable),而是可以設定一個檢視圖層(類似Android中的LayerDrawable可以設定檢視疊加),然後通過DraweeHolder在不同狀態下得到(getTopLevelDrawable())最頂層那個圖片從而使得DraweeView顯示不同的檢視,比如下面這個按下時顯示一個overlay(頂層)圖片效果:
我想這也是Facebook不想把DraweeView這個元件單純的定義為一個的ImageView的原因吧。
DraweeView
DraweeView是官方給我們提供顯示圖片的一個基類,我們在使用過程中大多時候並不需要用到它,而是用到一個官方已經簡單封好的SimpleDraweeView類,DraweeView類中提供了與DraweeController和DraweeHierarchy互動的介面,而與它們之間的互動本質上是通過一個DraweeHolder類進行互動,這類DraweeHolder是協調DraweeView、DraweeHierarchy、DraweeController這三個類互動工作的核心類,像平時我們都會這樣使用:
比如:配置一個DraweeHierarchy簡便起見通常會使用SimpleDraweeView直接設定:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
其實上述方法最終都是通過mController.setHierarchy(hierarchy);來設定的,本質是呼叫DraweeHolder類封裝好的setHierarchy()方法,可以看看其內部:
DraweeHolder::setHierarchy()
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
所以通過simpleDraweeView.setHierarchy(hierarchy);來設定等價於通過構建一個DraweeController來設定,如下:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
這個DraweeHolder類的出現,更準確的定位是降低耦合度、解耦的定位。
對於上面我們知道了通過simpleDraweeView.setHierarchy(hierarchy);來設定等價於通過DraweeController::setHierarchy()來設定,那麼simpleDraweeView.setController(controller);有沒有等價的呢?答案是沒有,因為要載入圖片那麼就必須設定一個DraweeController來控制圖片的載入,(當然我們如果設定了SimpleDraweeView的一些屬性,那麼預設也會建立一個DraweeHierarchy),而我們平時簡便的寫法:
- 1
- 2
- 3
- 1
- 2
- 3
這是一段最基本的寫法,我們都知道這樣設定了Fresco內部就自動會給我們載入圖片了,而網上也流傳著另外一種載入圖片的方法,為:
- 1
- 2
- 1
- 2
其實這兩種寫法都是一種寫法,Fresco真正載入圖片僅僅只有這一種方法,就是通過simpleDraweeView.setController(controller);來設定,只不過我們可以對DraweeController和DraweeHierarchy做各種各樣的配置來達到我們想要的效果,我們可以看看simpleDraweeView.setImageURI(uri);的原始碼,其實還是通過setController(controller);設定一個控制器來控制圖片的載入,原始碼為:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
所以,使用Fresco通用的寫法便是:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
或者直接使用ImageRequet:
- 1
- 2
- 3
- 1
- 2
- 3
當然配置其它屬性只要設定Controller和Hierarchy相應的方法即可。
【注】:上面中用到了一個這個方法simpleDraweeView.getController();當然也還有simpleDraweeView.getHierarchy();,這兩個方法是返回當前DraweeView所設定的Control和Hierarchy,使用這兩個方法的好處是複用以前建立的Control和Hierarchy物件,因為重新建立一個物件肯定不如複用好,而且建立相對耗時,所以官方也建議我們複用這兩個物件。如果你用simpleDraweeView.getHierarchy()來載入圖片,那麼它將不可能為空,除非你什麼不設定,而getController()則可能為空,所以在使用工廠方法
- 1
- 1
來建立一個DraweeController的時候給它配置一個setOldController(),如果這裡面這個引數為null,那麼就會重新建立一個DraweeController,如果不為空則複用當前傳入的。
順便說一句,直接使用SimpleDraweeView就足夠了,畢竟配置功能都落在DraweeController和DraweeHierarchy身上,SimpleDraweeView僅僅起個顯示最頂層檢視的作用。
DraweeController
關於DraweeController,好像在上面已經講的差不多了,它主要就是起個控制圖片的載入和配置以及決定頂層顯示哪個檢視的作用,其它的它也可以設定設定個ControllerListener來監聽圖片載入的進度,也可以配置一個ImageRequest來設定漸進式JPEG圖片的載入,具體使用可以看其官方文件。