FaceBook推出的Android图片载入库-Fresco
歡迎關(guān)注ndroid-tech-frontier開源項目,定期翻譯國外Android優(yōu)質(zhì)的技術(shù)、開源庫、軟件架構(gòu)設(shè)計、測試等文章
- 原文鏈接:Introducing Fresco: A new image library for Android
- 譯者 : ZhaoKaiQiang
- 校對者: Chaossss
- 校對者: bboyfeiyu
- 校對者: BillionWang
- 狀態(tài) : 完畢
在Android設(shè)備上面,高速高效的顯示圖片是極為重要的。
過去的幾年里,我們在怎樣高效的存儲圖像這方面遇到了很多問題。
圖片太大,可是手機的內(nèi)存卻很小。每個像素的R、G、B和alpha通道總共要占用4byte的空間。
假設(shè)手機的屏幕是480*800,那么一張屏幕大小的圖片就要占用1.5M的內(nèi)存。手機的內(nèi)存通常很小,特別是Android設(shè)備還要給各個應(yīng)用分配內(nèi)存。在某些設(shè)備上。分給Facebook App的內(nèi)存僅僅有16MB。
一張圖片就要占領(lǐng)其內(nèi)存的十分之中的一個。
當你的App內(nèi)存溢出會發(fā)生什么呢?它當然會崩潰!
我們開發(fā)了一個庫來解決問題,我們叫它Fresco。
它能夠管理使用到的圖片和內(nèi)存,從此App不再崩潰。
內(nèi)存區(qū)
為了理解Facebook究竟做了什么工作。在此之前我們須要了解在Android能夠使用的堆內(nèi)存之間的差別。Android中每個App的Java堆內(nèi)存大小都是被嚴格的限制的。
每個對象都是使用Java的new在堆內(nèi)存實例化,這是內(nèi)存中相對安全的一塊區(qū)域。
內(nèi)存有垃圾回收機制,所以當App不在使用內(nèi)存的時候。系統(tǒng)就會自己主動把這塊內(nèi)存回收。
不幸的是。內(nèi)存進行垃圾回收的過程正是問題所在。當內(nèi)存進行垃圾回收時,內(nèi)存不僅僅進行了垃圾回收。還把 Android 應(yīng)用全然終止了。這也是用戶在使用 App 時最常見的卡頓或短暫假死的原因之中的一個。
這會讓正在使用 App 的用戶很郁悶,然后他們可能會焦躁地滑動屏幕或者點擊button,但 App 唯一的響應(yīng)就是:在 App 恢復(fù)正常之前。請求用戶耐心等待
相比之下。Native堆是由C++程序的new進行分配的。
在Native堆里面有很多其它可用內(nèi)存,App僅僅被設(shè)備的物理可用內(nèi)存限制,而且沒有垃圾回收機制或其它東西拖后腿。
可是c++程序猿必須自己回收所分配的每一塊內(nèi)存,否則就會造成內(nèi)存泄露,終于導(dǎo)致程序崩潰。
Android有第二種內(nèi)存區(qū)域,叫做Ashmem。
它操作起來更像Native堆。可是也有額外的系統(tǒng)調(diào)用。Android 在操作 Ashmem 堆時,會把該堆中存有數(shù)據(jù)的內(nèi)存區(qū)域從 Ashmem 堆中抽取出來,而不是把它釋放掉,這是一種弱內(nèi)存釋放模式;被抽取出來的這部分內(nèi)存僅僅有當系統(tǒng)真正須要很多其它的內(nèi)存時(系統(tǒng)內(nèi)存不夠用)才會被釋放。當 Android 把被抽取出來的這部分內(nèi)存放回 Ashmem 堆,僅僅要被抽取的內(nèi)存空間沒有被釋放。之前的數(shù)據(jù)就會恢復(fù)到相應(yīng)的位置。
可消除的Bitmap
Ashmem不能被Java應(yīng)用直接處理。可是也有一些例外。圖片就是當中之中的一個。當你創(chuàng)建一張沒有經(jīng)過壓縮的Bitmap的時候。Android的API同意你指定是否是可清除的。
BitmapFactory.Options = new BitmapFactory.Options(); options.inPurgeable = true; Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);經(jīng)過上面的代碼處理后,可清除的Bitmap會駐留在 Ashmem 堆中。無論發(fā)生什么,垃圾回收器都不會自己主動回收這些 Bitmap。當 Android 繪制系統(tǒng)在渲染這些圖片,Android 的系統(tǒng)庫就會把這些 Bitmap 從 Ashmem 堆中抽取出來。而當渲染結(jié)束后,這些 Bitmap 又會被放回到原來的位置。
假設(shè)一個被抽取的圖片須要再繪制一次,系統(tǒng)僅僅須要把它再解碼一次,這個操作很迅速。
這聽起來像一個完美的解決方式。可是問題是Bitmap解碼的操作是運行在UI線程的。Bitmap解碼是很消耗CPU資源的,當消耗過大時會引起UI堵塞。由于這個原因,所以Google不推薦使用這個特性。如今它們推薦使用另外一個特性——inBitmap。可是這個特性直到Android3.0之后才被支持。即使是這樣,這個特性也不是很實用,除非 App 里的全部圖片大小都同樣。這對Fackbook來說顯然是不適用的。
一直到4.4版本號。這個限制才被移除了。但我們須要的是能夠運行在 Android 2.3 - 最新版本號中的通用解決方式。
自力更生
對于上面提到的“解碼操作致使 UI 假死”的問題,我們找到了一種同一時候使 UI 顯示和內(nèi)存管理都表現(xiàn)良好的解決方法。假設(shè)我們在 UI 線程進行渲染之前把被抽取的內(nèi)存區(qū)域放回到原來的位置,并確保它再也不會被抽取,那我們就能夠把這些圖片放在 Ashmem 里。同一時候不會出現(xiàn) UI 假死的問題。幸運的是,Android 的 NDK 中有一個函數(shù)能夠完美地實現(xiàn)這個需求。名字叫做 AndroidBitmap_lockPixels。這個函數(shù)最初的目的就是:在調(diào)用 unlockPixels 再次抽取內(nèi)存區(qū)域后被運行。
當我們意識到我們沒有必要這樣做的時候,我們?nèi)〉昧送黄啤<僭O(shè)我們僅僅調(diào)用lockPixels而不調(diào)用相應(yīng)的unlockPixels,那么我們就能夠在Java的堆內(nèi)存里面創(chuàng)建一個內(nèi)存安全的圖像。而且不會導(dǎo)致UI線程載入緩慢。僅僅須要幾行c++代碼,我們就完美的攻克了這個問題。
用C++的思想寫Java代碼
就像《蜘蛛俠》里面說的:“能力越強。責任越大。”可清除的 Bitmap 既不會被垃圾回收器回收,也不會被 Ashmem 內(nèi)置的清除機制處理,這使得使用它們可能會造成內(nèi)存泄露。
所以我們僅僅能靠自己啦。
在c++中,通常的解決方式是建立智能指針類,實現(xiàn)引用計數(shù)。這些須要利用到c++的語言特性——拷貝構(gòu)造函數(shù)、賦值操作符和確定的析構(gòu)函數(shù)。
這樣的語法在Java之中不存在。由于垃圾回收器能夠處理這一切。所以我們必須以某種方式在Java中實現(xiàn)C++的這些保證機制。
我們創(chuàng)建了兩個類去完畢這件事。當中一個叫做“SharedReference”,它有addReference和deleteReference兩個方法,調(diào)用者調(diào)用時必須採取基類對象或讓它在范圍之外。一旦引用計數(shù)器歸零,資源處理(Bitmap.recycle)就會發(fā)生。
然而。很顯然,讓Java開發(fā)人員去調(diào)用這些方法是很easy出錯的。Java語言就是為了避免做這樣的事情的!
所以SharedReference之上,我們構(gòu)建了CloseableReference類。它不僅實現(xiàn)了Java的Closeable接口,而且也實現(xiàn)了Cloneable接口。它的構(gòu)造器和clone()方法會調(diào)用addReference(),而close()方法會調(diào)用deleteReference()。所以Java開發(fā)人員須要遵守以下兩條簡單的的規(guī)則:
這些規(guī)則能夠有效地防止內(nèi)存泄漏,并讓我們在像Fackbook的Androidclient這樣的大型的Java程序中享受Native內(nèi)存管理和通信。
不僅僅是載入程序,它是一個管道
在移動設(shè)備上顯示圖片須要很多的步驟:
幾個優(yōu)秀的開源庫都是依照這個順序運行的,比方 Picasso,Universal Image Loader,Glide和 Volley等等。上面這些開源庫為Android的發(fā)展做出了很重要的貢獻。我們相信Fresco在幾個重要方面會表現(xiàn)的更好。
我們的不同之處在于把上面的這些步驟看作是管道,而不僅僅是載入器。每個步驟和其它方面應(yīng)該是盡可能獨立的,把數(shù)據(jù)和參數(shù)傳遞進去,然后產(chǎn)生一個輸出,就這么簡單。它應(yīng)該能夠做一些操作。無論是并行還是串行。
一些操作僅僅能在特性條件下才干運行。一些有特殊要求的在線程上運行。
除此之外,當我們考慮改進圖像的時候。全部的圖片就會變得很復(fù)雜。很多人在低網(wǎng)速情況下使用Facebook,我們想要這些人能夠盡快的看到圖片。甚至常常是在圖片沒有全然下載完之前。
不要煩惱,擁抱stream
在Java中。異步代碼歷來都是通過Future機制來運行的。在另外的線程里面代碼被提交運行,然后一個類似Future的對象能夠檢查運行的結(jié)果是不是已經(jīng)完畢了。可是。這僅僅在假設(shè)僅僅有一種結(jié)果的情況下行得通。在處理漸進的圖像的時候,我們希望能夠完整而且連續(xù)的顯示結(jié)果。
我們的解決方式是定義一個更廣義的Future版本號。叫做DataSource。它提供了一個訂閱方法。調(diào)用者必須傳入一個DataSubscriber和Executor。
DataSubscriber能夠從DataSource獲取到處理中和處理完畢的結(jié)果,而且提供了很簡單的方法來區(qū)分。
由于我們須要很頻繁的處理這些對象。所以必須有一個明白的close調(diào)用,幸運的是。DataSource本身就是Closeable。
在后臺。每個箱子上面都實現(xiàn)了一個叫做“生產(chǎn)者/消費者”的新框架。在這個問題是,我們是從ReactiveX獲取的靈感。
我們的系統(tǒng)擁有和RxJava類似的接口。可是更加適合移動設(shè)備,而且有內(nèi)置的對Closeables的支持。
保持簡單的接口。Producer僅僅有一個叫做produceResults的方法。這種方法須要一個Consumer對象。反過來,Consumer有一個onNewResult方法。
我們使用像這樣的系統(tǒng)把Producer聯(lián)系起來。
假設(shè)我們有一個producer的工作是把類型I轉(zhuǎn)化為類型O,那么它看起來應(yīng)該是這個樣子:
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); }} }這能夠使我們把很復(fù)雜的步驟串起來。同一時候也能夠保持他們邏輯的獨立性。
動畫全覆蓋
使用Facebook的人都很喜歡Stickers。由于它能夠以動畫形式存儲GIF和Web格式。假設(shè)支持這些格式,就須要面臨新的挑戰(zhàn)。由于每個動畫都是由不止一張圖片組成的,你須要解碼每一張圖片,存儲在內(nèi)存里,然后顯示出來。對于大一點的動畫。把每一幀圖片放在內(nèi)存是不可行的。
我們建立了AnimatedDrawable,一個強大的能夠呈現(xiàn)動畫的Drawable,同一時候支持GIF和WebP格式。AnimatedDrawable實現(xiàn)標準的Android Animatable接口,所以調(diào)用者能夠任意的啟動或者停止動畫。為了優(yōu)化內(nèi)存使用。假設(shè)圖片足夠小的時候。我們就在內(nèi)存里面緩存這些圖片,可是假設(shè)太大。我們能夠迅速的解碼這些圖片。這些行為調(diào)用者是全然可控的。
全部的后臺都用c++代碼實現(xiàn)。
我們保持一份解碼數(shù)據(jù)和元數(shù)據(jù)解析,如寬度和高度。我們引用技術(shù)數(shù)據(jù),它同意多個Java端的Drawables同一時候訪問一個WebP圖像。
怎樣去愛你?我來告訴你…
當一張圖片從網(wǎng)絡(luò)上下載下來之后,我們想顯示一張占位圖。假設(shè)下載失敗了,我們就會顯示一個錯誤標志。當圖片載入完之后。我們有一個漸變動畫。通過使用硬件加速,我們能夠按比例放縮,或者是矩陣變換成我們想要的大小然后渲染。
我們不總是依照圖片的中心進行放縮。那么我們能夠自定義放縮的聚焦點。有些時候,我們想顯示圓角甚至是圓形的圖片。全部的這些操作都應(yīng)該是迅速而平滑的。
我們之前的實現(xiàn)是使用Android的View對象——時機到了,能夠使用ImageView替換出占位的View。
這個操作是很慢的。
改變View會讓Android強制刷新整個布局,當用戶滑動的時候。這絕對不是你想看到的效果。
比較明智的做法是使用Android的Drawables,它能夠迅速的被替換。
所以我們創(chuàng)建了Drawee。這是一個像MVC架構(gòu)的圖片顯示框架。該模型被稱為DraweeHierarchy。它被實現(xiàn)為Drawables的一個層,對于底層的圖像而言,每個曾都有特定的功能——成像、層疊、漸變或者是放縮。
DraweeControllers通過管道的方式連接到圖像上——或者是其它的圖片載入庫——而且處理后臺的圖片操作。他們從管道接收事件并決定怎樣處理他們。他們控制DraweeHierarchy實際上的操作——無論是占位圖片,錯誤條件或是完畢的圖片。
DraweeViews 的功能不多,但都是至關(guān)重要的。
他們監(jiān)聽Android的View不再顯示在屏幕上的系統(tǒng)事件。
當圖片離開屏幕的時候,DraweeView能夠告訴DraweeController關(guān)閉使用的圖像資源。
這能夠避免內(nèi)存泄露。此外,假設(shè)它已經(jīng)不在屏幕范圍內(nèi)的話,控制器會告訴圖片管道取消網(wǎng)絡(luò)請求。因此,像Fackbook那樣滾動一長串的圖片的時候,不會頻繁的網(wǎng)絡(luò)請求。
通過這些努力,顯示圖片的辛苦操作一去不復(fù)返了。調(diào)用代碼僅僅須要實例化一個DraweeView,然后指定一個URI和其它可選的參數(shù)就能夠了。剩下的一切都會自己主動完畢。
開發(fā)人員不須要操心管理圖像內(nèi)存。或更新圖像流。Fresco為他們把一切都做了。
Fresco
完畢這個圖像顯示和操作復(fù)雜的工具庫之后,我們想要把它分享到Android開發(fā)人員社區(qū)。我們很高興的宣布,從今天起,這個項目已經(jīng)作為開源碼了!
壁畫是繪畫技術(shù),幾個世紀以來一直受到世界各地人們的歡迎。
我們很多偉大的藝術(shù)家使用這樣的名字,從意大利文藝復(fù)興時期的大師拉斐爾到壁畫藝術(shù)家斯里蘭卡。我們并非假裝達到這個偉大的水平,我們真的希望Android開發(fā)人員能像我們當初享受創(chuàng)建這個開源庫的過程一樣。很享受的使用它。
很多其它
Fresco中文文檔
轉(zhuǎn)載于:https://www.cnblogs.com/gccbuaa/p/7096318.html
總結(jié)
以上是生活随笔為你收集整理的FaceBook推出的Android图片载入库-Fresco的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 项目管理系列之质量管理
- 下一篇: Android数据存储五种方式总结