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