- 原文鏈接:Introducing Fresco: A new image library for Android
- 譯者 : ZhaoKaiQiang
- 校訂者: Chaossss
- 校訂者: bboyfeiyu
- 校訂者: BillionWang
- 狀態(tài) : 完成
在Android裝備上面,快速高效的顯示圖片是極其重要的。過去的幾年里,我們在如何高效的存儲圖象這方面遇到了很多問題。圖片太大,但是手機的內(nèi)存卻很小。每個像素的R、G、B和alpha通道總共要占用4byte的空間。如果手機的屏幕是480*800,那末1張屏幕大小的圖片就要占用1.5M的內(nèi)存。手機的內(nèi)存通常很小,特別是Android裝備還要給各個利用分配內(nèi)存。在某些裝備上,分給Facebook App的內(nèi)存僅唯一16MB。1張圖片就要占據(jù)其內(nèi)存的10分之1。
當你的App內(nèi)存溢出會產(chǎn)生甚么呢?它固然會崩潰!我們開發(fā)了1個庫來解決這個問題,我們叫它Fresco。它可以管理使用到的圖片和內(nèi)存,從此App不再崩潰。
為了理解Facebook到底做了甚么工作,在此之前我們需要了解在Android可使用的堆內(nèi)存之間的區(qū)分。Android中每一個App的Java堆內(nèi)存大小都是被嚴格的限制的。每一個對象都是使用Java的new在堆內(nèi)存實例化,這是內(nèi)存中相對安全的1塊區(qū)域。內(nèi)存有垃圾回收機制,所以當App不在使用內(nèi)存的時候,系統(tǒng)就會自動把這塊內(nèi)存回收。
不幸的是,內(nèi)存進行垃圾回收的進程正是問題所在。當內(nèi)存進行垃圾回收時,內(nèi)存不單單進行了垃圾回收,還把 Android 利用完全終止了。這也是用戶在使用 App 時最多見的卡頓或短暫假死的緣由之1。這會讓正在使用 App 的用戶非常愁悶,然后他們可能會煩躁地滑動屏幕或點擊按鈕,但 App 唯1的響應就是:在 App 恢復正常之前,要求用戶耐心等待
相比之下,Native堆是由C++程序的new進行分配的。在Native堆里面有更多可用內(nèi)存,App只被裝備的物理可用內(nèi)存限制,而且沒有垃圾回收機制或其他東西拖后腿。但是c++程序員必須自己回收所分配的每塊內(nèi)存,否則就會造成內(nèi)存泄漏,終究致使程序崩潰。
Android有另外1種內(nèi)存區(qū)域,叫做Ashmem。它操作起來更像Native堆,但是也有額外的系統(tǒng)調(diào)用。Android 在操作 Ashmem 堆時,會把該堆中存有數(shù)據(jù)的內(nèi)存區(qū)域從 Ashmem 堆中抽取出來,而不是把它釋放掉,這是1種弱內(nèi)存釋放模式;被抽取出來的這部份內(nèi)存只有當系統(tǒng)真正需要更多的內(nèi)存時(系統(tǒng)內(nèi)存不夠用)才會被釋放。當 Android 把被抽取出來的這部份內(nèi)寄存回 Ashmem 堆,只要被抽取的內(nèi)存空間沒有被釋放,之前的數(shù)據(jù)就會恢復到相應的位置。
Ashmem不能被Java利用直接處理,但是也有1些例外,圖片就是其中之1。當你創(chuàng)建1張沒有經(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 堆中。不管產(chǎn)生甚么,垃圾回收器都不會自動回收這些 Bitmap。當 Android 繪制系統(tǒng)在渲染這些圖片,Android 的系統(tǒng)庫就會把這些 Bitmap 從 Ashmem 堆中抽取出來,而當渲染結(jié)束后,這些 Bitmap 又會被放回到原來的位置。如果1個被抽取的圖片需要再繪制1次,系統(tǒng)僅僅需要把它再解碼1次,這個操作非常迅速。
這聽起來像1個完善的解決方案,但是問題是Bitmap解碼的操作是運行在UI線程的。Bitmap解碼是非常消耗CPU資源的,當消耗過大時會引發(fā)UI阻塞。由于這個緣由,所以Google不推薦使用這個特性。現(xiàn)在它們推薦使用另外1個特性――inBitmap。但是這個特性直到Android3.0以后才被支持。即便是這樣,這個特性也不是非常有用,除非 App 里的所有圖片大小都相同,這對Fackbook來講明顯是不適用的。1直到4.4版本,這個限制才被移除。但我們需要的是能夠運行在 Android 2.3 - 最新版本中的通用解決方案。
對上面提到的“解碼操作導致 UI 假死”的問題,我們找到了1種同時使 UI 顯示和內(nèi)存管理都表現(xiàn)良好的解決方法。如果我們在 UI 線程進行渲染之前把被抽取的內(nèi)存區(qū)域放回到原來的位置,并確保它不再會被抽取,那我們就能夠把這些圖片放在 Ashmem 里,同時不會出現(xiàn) UI 假死的問題。榮幸的是,Android 的 NDK 中有1個函數(shù)可以完善地實現(xiàn)這個需求,名字叫做 AndroidBitmap_lockPixels。這個函數(shù)最初的目的就是:在調(diào)用 unlockPixels 再次抽取內(nèi)存區(qū)域后被履行。
當我們意想到我們沒有必要這樣做的時候,我們獲得了突破。如果我們只調(diào)用lockPixels而不調(diào)用對應的unlockPixels,那末我們就能夠在Java的堆內(nèi)存里面創(chuàng)建1個內(nèi)存安全的圖象,并且不會致使UI線程加載緩慢。只需要幾行c++代碼,我們就完善的解決了這個問題。
就像《蜘蛛俠》里面說的:“能力越強,責任越大。”可清除的 Bitmap 既不會被垃圾回收器回收,也不會被 Ashmem 內(nèi)置的清除機制處理,這使得使用它們可能會造成內(nèi)存泄漏。所以我們只能靠自己啦。
在c++中,通常的解決方案是建立智能指針類,實現(xiàn)援用計數(shù)。這些需要利用到c++的語言特性――拷貝構(gòu)造函數(shù)、賦值操作符和肯定的析構(gòu)函數(shù)。這類語法在Java當中不存在,由于垃圾回收器能夠處理這1切。所以我們必須以某種方式在Java中實現(xiàn)C++的這些保證機制。
我們創(chuàng)建了兩個類去完成這件事。其中1個叫做“SharedReference”,它有addReference和deleteReference兩個方法,調(diào)用者調(diào)用時必須采取基類對象或讓它在范圍以外。1旦援用計數(shù)器歸零,資源處理(Bitmap.recycle)就會產(chǎn)生。
但是,很明顯,讓Java開發(fā)者去調(diào)用這些方法是很容易出錯的。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的Android客戶端這類大型的Java程序中享受Native內(nèi)存管理和通訊。
在移動裝備上顯示圖片需要很多的步驟:
幾個優(yōu)秀的開源庫都是依照這個順序履行的,比如 Picasso,Universal Image Loader,Glide和 Volley等等。上面這些開源庫為Android的發(fā)展做出了非常重要的貢獻。我們相信Fresco在幾個重要方面會表現(xiàn)的更好。
我們的不同的地方在于把上面的這些步驟看做是管道,而不單單是加載器。每個步驟和其他方面應當是盡量獨立的,把數(shù)據(jù)和參數(shù)傳遞進去,然后產(chǎn)生1個輸出,就這么簡單。它應當可以做1些操作,不論是并行還是串行。1些操作只能在特性條件下才能履行。1些有特殊要求的在線程上履行。除此以外,當我們斟酌改進圖象的時候,所有的圖片就會變得非常復雜。很多人在低網(wǎng)速情況下使用Facebook,我們想要這些人能夠盡快的看到圖片,乃至常常是在圖片沒有完全下載完之前。
在Java中,異步代碼歷來都是通過Future機制來履行的。在另外的線程里面代碼被提交履行,然后1個類似Future的對象可以檢查履行的結(jié)果是否是已完成了。但是,這只在假定只有1種結(jié)果的情況下行得通。在處理漸進的圖象的時候,我們希望可以完全而且連續(xù)的顯示結(jié)果。
我們的解決方式是定義1個更廣義的Future版本,叫做DataSource。它提供了1個定閱方法,調(diào)用者必須傳入1個DataSubscriber和Executor。DataSubscriber可以從DataSource獲得到處理中和處理終了的結(jié)果,并且提供了很簡單的方法來辨別。由于我們需要非常頻繁的處理這些對象,所以必須有1個明確的close調(diào)用,榮幸的是,DataSource本身就是Closeable。
在后臺,每個箱子上面都實現(xiàn)了1個叫做“生產(chǎn)者/消費者”的新框架。在這個問題是,我們是從ReactiveX獲得的靈感。我們的系統(tǒng)具有和RxJava類似的接口,但是更加合適移動裝備,并且有內(nèi)置的對Closeables的支持。
保持簡單的接口。Producer只有1個叫做produceResults的方法,這個方法需要1個Consumer對象。反過來,Consumer有1個onNewResult方法。
我們使用像這樣的系統(tǒng)把Producer聯(lián)系起來。假定我們有1個producer的工作是把類型I轉(zhuǎn)化為類型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格式。如果支持這些格式,就需要面臨新的挑戰(zhàn)。由于每個動畫都是由不止1張圖片組成的,你需要解碼每張圖片,存儲在內(nèi)存里,然后顯示出來。對大1點的動畫,把每幀圖片放在內(nèi)存是不可行的。
我們建立了AnimatedDrawable,1個強大的可以顯現(xiàn)動畫的Drawable,同時支持GIF和WebP格式。AnimatedDrawable實現(xiàn)標準的Android Animatable接口,所以調(diào)用者可以隨便的啟動或停止動畫。為了優(yōu)化內(nèi)存使用,如果圖片足夠小的時候,我們就在內(nèi)存里面緩存這些圖片,但是如果太大,我們可以迅速的解碼這些圖片。這些行動調(diào)用者是完全可控的。
所有的后臺都用c++代碼實現(xiàn)。我們保持1份解碼數(shù)據(jù)和元數(shù)據(jù)解析,如寬度和高度。我們援用技術(shù)數(shù)據(jù),它允許多個Java真?zhèn)€Drawables同時訪問1個WebP圖象。
當1張圖片從網(wǎng)絡上下載下來以后,我們想顯示1張占位圖。如果下載失敗了,我們就會顯示1個毛病標志。當圖片加載完以后,我們有1個漸變動畫。通過使用硬件加速,我們可以按比例放縮,或是矩陣變換成我們想要的大小然后渲染。我們不總是依照圖片的中心進行放縮,那末我們可以自己定義放縮的聚焦點。有些時候,我們想顯示圓角乃至是圓形的圖片。所有的這些操作都應當是迅速而平滑的。
我們之前的實現(xiàn)是使用Android的View對象――時機到了,可使用ImageView替換出占位的View。這個操作是非常慢的。改變View會讓Android強迫刷新全部布局,當用戶滑動的時候,這絕對不是你想看到的效果。比較明智的做法是使用Android的Drawables,它可以迅速的被替換。
所以我們創(chuàng)建了Drawee。這是1個像MVC架構(gòu)的圖片顯示框架。該模型被稱為DraweeHierarchy。它被實現(xiàn)為Drawables的1個層,對底層的圖象而言,每個曾都有特定的功能――成像、層疊、漸變或是放縮。
DraweeControllers通過管道的方式連接到圖象上――或是其他的圖片加載庫――并且處理后臺的圖片操作。他們從管道接收事件并決定如何處理他們。他們控制DraweeHierarchy實際上的操作――不管是占位圖片,毛病條件或是完成的圖片。
DraweeViews 的功能不多,但都是相當重要的。他們監(jiān)聽Android的View不再顯示在屏幕上的系統(tǒng)事件。當圖片離開屏幕的時候,DraweeView可以告知DraweeController關(guān)閉使用的圖象資源。這可以免內(nèi)存泄漏。另外,如果它已不在屏幕范圍內(nèi)的話,控制器會告知圖片管道取消網(wǎng)絡要求。因此,像Fackbook那樣轉(zhuǎn)動1長串的圖片的時候,不會頻繁的網(wǎng)絡要求。
通過這些努力,顯示圖片的辛苦操作1去不復返了。調(diào)用代碼只需要實例化1個DraweeView,然后指定1個URI和其他可選的參數(shù)就能夠了。剩下的1切都會自動完成。開發(fā)人員不需要擔心管理圖象內(nèi)存,或更新圖象流。Fresco為他們把1切都做了。
完成這個圖象顯示和操作復雜的工具庫以后,我們想要把它分享到Android開發(fā)者社區(qū)。我們很高興的宣布,從今天起,這個項目已作為開源代碼了!
壁畫是繪畫技術(shù),幾個世紀以來1直遭到世界各地人們的歡迎。我們許多偉大的藝術(shù)家使用這類名字,從意大利文藝復興時期的大師拉斐爾到壁畫藝術(shù)家斯里蘭卡。我們其實不是偽裝到達這個偉大的水平,我們真的希望Android開發(fā)者能像我們當初享受創(chuàng)建這個開源庫的進程1樣,非常享受的使用它。
Fresco中文文檔