FaceBook推出的Android圖片載入庫-Fresco

歡迎關注ndroid-tech-frontier開源項目,按期翻譯國外Android優質的技術、開源庫、軟件架構設計、測試等文章html


在Android設備上面,高速高效的顯示圖片是極爲重要的。java

過去的幾年裏,咱們在怎樣高效的存儲圖像這方面遇到了不少問題。react

圖片太大,但是手機的內存卻很小。每一個像素的R、G、B和alpha通道總共要佔用4byte的空間。android

假設手機的屏幕是480*800,那麼一張屏幕大小的圖片就要佔用1.5M的內存。手機的內存一般很小,特別是Android設備還要給各個應用分配內存。在某些設備上。分給Facebook App的內存僅僅有16MB。c++

一張圖片就要佔領其內存的十分之中的一個。git

當你的App內存溢出會發生什麼呢?它固然會崩潰!github

咱們開發了一個庫來解決問題,咱們叫它Fresco。緩存

它可以管理使用到的圖片和內存,今後App再也不崩潰。安全

內存區

爲了理解Facebook究竟作了什麼工做。在此以前咱們需要了解在Android可以使用的堆內存之間的差異。Android中每一個App的Java堆內存大小都是被嚴格的限制的。markdown

每一個對象都是使用Java的new在堆內存實例化,這是內存中相對安全的一塊區域。

內存有垃圾回收機制,因此當App不在使用內存的時候。系統就會本身主動把這塊內存回收。

不幸的是。內存進行垃圾回收的過程正是問題所在。當內存進行垃圾回收時,內存不只僅進行了垃圾回收。還把 Android 應用全然終止了。這也是用戶在使用 App 時最多見的卡頓或短暫假死的緣由之中的一個。

這會讓正在使用 App 的用戶很鬱悶,而後他們可能會焦躁地滑動屏幕或者點擊button,但 App 惟一的響應就是:在 App 恢復正常以前。請求用戶耐心等待

相比之下。Native堆是由C++程序的new進行分配的。

在Native堆裏面有不少其它可用內存,App僅僅被設備的物理可用內存限制,並且沒有垃圾回收機制或其它東西拖後腿。

但是c++程序猿必須本身回收所分配的每一塊內存,不然就會形成內存泄露,終於致使程序崩潰。

Android有第二種內存區域,叫作Ashmem。

它操做起來更像Native堆。但是也有額外的系統調用。Android 在操做 Ashmem 堆時,會把該堆中存有數據的內存區域從 Ashmem 堆中抽取出來,而不是把它釋放掉,這是一種弱內存釋放模式;被抽取出來的這部份內存僅僅有當系統真正需要不少其它的內存時(系統內存不夠用)纔會被釋放。當 Android 把被抽取出來的這部份內存放回 Ashmem 堆,僅僅要被抽取的內存空間沒有被釋放。以前的數據就會恢復到相應的位置。

可消除的Bitmap

Ashmem不能被Java應用直接處理。但是也有一些例外。圖片就是當中之中的一個。當你建立一張沒有通過壓縮的Bitmap的時候。Android的API贊成你指定是不是可清除的。

BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

通過上面的代碼處理後,可清除的Bitmap會駐留在 Ashmem 堆中。不管發生什麼,垃圾回收器都不會本身主動回收這些 Bitmap。當 Android 繪製系統在渲染這些圖片,Android 的系統庫就會把這些 Bitmap 從 Ashmem 堆中抽取出來。而當渲染結束後,這些 Bitmap 又會被放回到原來的位置。

假設一個被抽取的圖片需要再繪製一次,系統僅僅需要把它再解碼一次,這個操做很迅速。

這聽起來像一個完美的解決方式。但是問題是Bitmap解碼的操做是運行在UI線程的。Bitmap解碼是很消耗CPU資源的,當消耗過大時會引發UI堵塞。因爲這個緣由,因此Google不推薦使用這個特性。現在它們推薦使用另一個特性——inBitmap。但是這個特性直到Android3.0以後才被支持。即便是這樣,這個特性也不是很實用,除非 App 裏的所有圖片大小都一樣。這對Fackbook來講顯然是不適用的。

一直到4.4版本號。這個限制才被移除了。但咱們需要的是可以運行在 Android 2.3 - 最新版本號中的通用解決方式。

自力更生

對於上面提到的「解碼操做導致 UI 假死」的問題,咱們找到了一種同一時候使 UI 顯示和內存管理都表現良好的解決方法。假設咱們在 UI 線程進行渲染以前把被抽取的內存區域放回到原來的位置,並確保它不再會被抽取,那咱們就可以把這些圖片放在 Ashmem 裏。同一時候不會出現 UI 假死的問題。幸運的是,Android 的 NDK 中有一個函數可以完美地實現這個需求。名字叫作 AndroidBitmap_lockPixels。這個函數最初的目的就是:在調用 unlockPixels 再次抽取內存區域後被運行。

當咱們意識到咱們沒有必要這樣作的時候,咱們取得了突破。假設咱們僅僅調用lockPixels而不調用相應的unlockPixels,那麼咱們就可以在Java的堆內存裏面建立一個內存安全的圖像。並且不會致使UI線程載入緩慢。僅僅需要幾行c++代碼,咱們就完美的攻克了這個問題。

用C++的思想寫Java代碼

就像《蜘蛛俠》裏面說的:「能力越強。責任越大。」可清除的 Bitmap 既不會被垃圾回收器回收,也不會被 Ashmem 內置的清除機制處理,這使得使用它們可能會形成內存泄露。

因此咱們僅僅能靠本身啦。

在c++中,一般的解決方式是創建智能指針類,實現引用計數。這些需要利用到c++的語言特性——拷貝構造函數、賦值操做符和肯定的析構函數。

這樣的語法在Java之中不存在。因爲垃圾回收器可以處理這一切。因此咱們必須以某種方式在Java中實現C++的這些保證機制。

咱們建立了兩個類去完畢這件事。當中一個叫作「SharedReference」,它有addReference和deleteReference兩個方法,調用者調用時必須採取基類對象或讓它在範圍以外。一旦引用計數器歸零,資源處理(Bitmap.recycle)就會發生。

然而。很顯然,讓Java開發人員去調用這些方法是很easy出錯的。Java語言就是爲了不作這樣的事情的!

因此SharedReference之上,咱們構建了CloseableReference類。它不只實現了Java的Closeable接口,並且也實現了Cloneable接口。它的構造器和clone()方法會調用addReference(),而close()方法會調用deleteReference()。因此Java開發人員需要遵照如下兩條簡單的的規則:

  1. 在分配CloseableReference新對象的時候,調用.clone()。
  2. 在超出做用域範圍的時候,調用.close(),這通常是在finally代碼塊中。

這些規則可以有效地防止內存泄漏,並讓咱們在像Fackbook的Androidclient這樣的大型的Java程序中享受Native內存管理和通訊。

不只僅是載入程序,它是一個管道

在移動設備上顯示圖片需要不少的步驟:

幾個優秀的開源庫都是依照這個順序運行的,比方 Picasso,Universal Image Loader,Glide和 Volley等等。上面這些開源庫爲Android的發展作出了很重要的貢獻。咱們相信Fresco在幾個重要方面會表現的更好。

咱們的不一樣之處在於把上面的這些步驟看做是管道,而不只僅是載入器。每一個步驟和其它方面應該是儘量獨立的,把數據和參數傳遞進去,而後產生一個輸出,就這麼簡單。它應該可以作一些操做。不管是並行仍是串行。

一些操做僅僅能在特性條件下才幹運行。一些有特殊要求的在線程上運行。

除此以外,當咱們考慮改進圖像的時候。所有的圖片就會變得很複雜。不少人在低網速狀況下使用Facebook,咱們想要這些人可以儘快的看到圖片。甚至經常是在圖片沒有全然下載完以前。

不要煩惱,擁抱stream

在Java中。異步代碼從來都是經過Future機制來運行的。在另外的線程裏面代碼被提交運行,而後一個相似Future的對象可以檢查運行的結果是否是已經完畢了。但是。這僅僅在假設僅僅有一種結果的狀況下行得通。在處理漸進的圖像的時候,咱們但願可以完整並且連續的顯示結果。

咱們的解決方式是定義一個更廣義的Future版本號。叫作DataSource。它提供了一個訂閱方法。調用者必須傳入一個DataSubscriber和Executor。

DataSubscriber可以從DataSource獲取處處理中和處理完畢的結果,並且提供了很簡單的方法來區分。

因爲咱們需要很頻繁的處理這些對象。因此必須有一個明白的close調用,幸運的是。DataSource自己就是Closeable。

在後臺。每一個箱子上面都實現了一個叫作「生產者/消費者」的新框架。在這個問題是,咱們是從ReactiveX獲取的靈感。

咱們的系統擁有和RxJava相似的接口。但是更加適合移動設備,並且有內置的對Closeables的支持。

保持簡單的接口。Producer僅僅有一個叫作produceResults的方法。這種方法需要一個Consumer對象。反過來,Consumer有一個onNewResult方法。

咱們使用像這樣的系統把Producer聯繫起來。

假設咱們有一個producer的工做是把類型I轉化爲類型O,那麼它看起來應該是這個樣子:

public class OutputProducer<I, O> implements Producer<O> {

  private final Producer<I> mInputProducer;

  public OutputProducer(Producer<I> inputProducer) {
    this.mInputProducer = inputProducer;
  }

  public void produceResults(Consumer<O> outputConsumer, ProducerContext context) {
    Consumer<I> inputConsumer = new InputConsumer(outputConsumer);
    mInputProducer.produceResults(inputConsumer, context);
  }

  private static class InputConsumer implements Consumer<I> {
    private final Consumer<O> mOutputConsumer;

    public InputConsumer(Consumer<O> outputConsumer) {
      mOutputConsumer = outputConsumer;
    }

    public void onNewResult(I newResult, boolean isLast) {
      O output = doActualWork(newResult);
      mOutputConsumer.onNewResult(output, isLast);      
    }
  }
}

這可以使咱們把很複雜的步驟串起來。同一時候也可以保持他們邏輯的獨立性。

動畫全覆蓋

使用Facebook的人都很喜歡Stickers。因爲它可以以動畫形式存儲GIF和Web格式。假設支持這些格式,就需要面臨新的挑戰。因爲每一個動畫都是由不止一張圖片組成的,你需要解碼每一張圖片,存儲在內存裏,而後顯示出來。對於大一點的動畫。把每一幀圖片放在內存是不可行的。

咱們創建了AnimatedDrawable,一個強大的可以呈現動畫的Drawable,同一時候支持GIF和WebP格式。AnimatedDrawable實現標準的Android Animatable接口,因此調用者可以任意的啓動或者中止動畫。爲了優化內存使用。假設圖片足夠小的時候。咱們就在內存裏面緩存這些圖片,但是假設太大。咱們可以迅速的解碼這些圖片。這些行爲調用者是全然可控的。

所有的後臺都用c++代碼實現。

咱們保持一份解碼數據和元數據解析,如寬度和高度。咱們引用技術數據,它贊成多個Java端的Drawables同一時候訪問一個WebP圖像。

怎樣去愛你?我來告訴你…

當一張圖片從網絡上下載下來以後,咱們想顯示一張佔位圖。假設下載失敗了,咱們就會顯示一個錯誤標誌。當圖片載入完以後。咱們有一個漸變更畫。經過使用硬件加速,咱們可以按比例放縮,或者是矩陣變換成咱們想要的大小而後渲染。

咱們不老是依照圖片的中心進行放縮。那麼咱們可以自定義放縮的聚焦點。有些時候,咱們想顯示圓角甚至是圓形的圖片。所有的這些操做都應該是迅速而平滑的。

咱們以前的實現是使用Android的View對象——時機到了,可以使用ImageView替換出佔位的View。

這個操做是很慢的。

改變View會讓Android強制刷新整個佈局,當用戶滑動的時候。這絕對不是你想看到的效果。

比較明智的作法是使用Android的Drawables,它可以迅速的被替換。

因此咱們建立了Drawee。這是一個像MVC架構的圖片顯示框架。該模型被稱爲DraweeHierarchy。它被實現爲Drawables的一個層,對於底層的圖像而言,每一個曾都有特定的功能——成像、層疊、漸變或者是放縮。

DraweeControllers經過管道的方式鏈接到圖像上——或者是其它的圖片載入庫——並且處理後臺的圖片操做。他們從管道接收事件並決定怎樣處理他們。他們控制DraweeHierarchy實際上的操做——不管是佔位圖片,錯誤條件或是完畢的圖片。

DraweeViews 的功能很少,但都是相當重要的。

他們監聽Android的View再也不顯示在屏幕上的系統事件。

當圖片離開屏幕的時候,DraweeView可以告訴DraweeController關閉使用的圖像資源。

這可以避免內存泄露。此外,假設它已經不在屏幕範圍內的話,控制器會告訴圖片管道取消網絡請求。所以,像Fackbook那樣滾動一長串的圖片的時候,不會頻繁的網絡請求。

經過這些努力,顯示圖片的辛苦操做一去不復返了。調用代碼僅僅需要實例化一個DraweeView,而後指定一個URI和其它可選的參數就可以了。剩下的一切都會本身主動完畢。

開發人員不需要操心管理圖像內存。或更新圖像流。Fresco爲他們把一切都作了。

Fresco

完畢這個圖像顯示和操做複雜的工具庫以後,咱們想要把它分享到Android開發人員社區。咱們很高興的宣佈,從今天起,這個項目已經做爲開源碼了!

壁畫是繪畫技術,幾個世紀以來一直受到世界各地人們的歡迎。

咱們不少偉大的藝術家使用這樣的名字,從意大利文藝復興時期的大師拉斐爾到壁畫藝術家斯里蘭卡。咱們並不是僞裝達到這個偉大的水平,咱們真的但願Android開發人員能像咱們當初享受建立這個開源庫的過程同樣。很享受的使用它。

不少其它

Fresco中文文檔

相關文章
相關標籤/搜索