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

來源:互聯網
上載者:User

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

轉載

 

  • 原文連結:Introducing Fresco: A new image library for Android
  • 作者 : tyrone Nicholas
  • 譯者 : ZhaoKaiQiang
  • 校對者: Chaossss
  • 校對者: bboyfeiyu
  • 校對者: BillionWang
  • 狀態 : 完成

 

在Android裝置上面,快速高效的顯示圖片是極為重要的。過去的幾年裡,我們在如何高效的儲存映像這方面遇到了很多問題。圖片太大,但是手機的記憶體卻很小。每一個像素的R、G、B和alpha通道總共要佔用4byte的空間。如果手機的螢幕是480*800,那麼一張螢幕大小的圖片就要佔用1.5M的記憶體。手機的記憶體通常很小,特別是Android裝置還要給各個應用程式指派記憶體。在某些裝置上,分給Facebook App的記憶體僅僅有16MB。一張圖片就要佔據其記憶體的十分之一。

當你的App記憶體溢出會發生什麼呢?它當然會崩潰!我們開發了一個庫來解決這個問題,我們叫它Fresco。它可以管理使用到的圖片和記憶體,從此App不再崩潰。

記憶體區

為了理解Facebook到底做了什麼工作,在此之前我們需要瞭解在Android可以使用的堆記憶體之間的區別。Android中每個App的Java堆記憶體大小都是被嚴格的限制的。每個對象都是使用Java的new在堆記憶體執行個體化,這是記憶體中相對安全的一塊地區。記憶體有記憶體回收機制,所以當App不在使用記憶體的時候,系統就會自動把這塊記憶體回收。

不幸的是,記憶體進行記憶體回收的過程正是問題所在。當記憶體進行記憶體回收時,記憶體不僅僅進行了記憶體回收,還把 Android 應用完全終止了。這也是使用者在使用 App 時最常見的卡頓或短暫假死的原因之一。這會讓正在使用 App 的使用者非常鬱悶,然後他們可能會焦躁地滑動螢幕或者點擊按鈕,但 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開發人員去調用這些方法是很容易出錯的。Java語言就是為了避免做這樣的事情的!所以SharedReference之上,我們構建了CloseableReference類。它不僅實現了Java的Closeable介面,而且也實現了Cloneable介面。它的構造器和clone()方法會調用addReference(),而close()方法會調用deleteReference()。所以Java開發人員需要遵守下面兩條簡單的的規則:

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

    這些規則可以有效地防止記憶體流失,並讓我們在像Fackbook的Android用戶端這種大型的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 implements Producer {  private final Producer mInputProducer; public OutputProducer(Producer inputProducer) { this.mInputProducer = inputProducer; } public void produceResults(Consumer outputConsumer, ProducerContext context) { Consumer inputConsumer = new InputConsumer(outputConsumer); mInputProducer.produceResults(inputConsumer, context); } private static class InputConsumer implements Consumer { private final Consumer mOutputConsumer; public InputConsumer(Consumer 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中文文檔

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.