用FlatBuffers提升Android平台上Facebook的性能
在Facebook上,人們可以通過(guò)閱讀狀態(tài)更新和查看照片同他們的家人和朋友來(lái)往。在我們的后端,我們保存了組成這些連接的社交圖譜的所有數(shù)據(jù)。在我們的移動(dòng)客戶端,我們不能下載完整的圖譜,而是以一個(gè)本地的樹(shù)結(jié)構(gòu)的形式下載一個(gè)節(jié)點(diǎn)及它的一些連接。
下面的圖片描述了在一個(gè)含有照片附件的story中這是如何工作的。在這個(gè)例子中,John創(chuàng)建了一個(gè)story,他的朋友們很喜歡它并加了評(píng)論。圖片的左邊是社交圖譜,用來(lái)描述Facebook后端的人際關(guān)系。當(dāng)Android app查詢story時(shí),我們獲得一個(gè)以story開(kāi)始的樹(shù)結(jié)構(gòu),包含了參與者的信息,反饋,和附件(如圖片的右邊所展示的那樣)。
我們要處理的一個(gè)重要問(wèn)題是如何在app中表示并存儲(chǔ)數(shù)據(jù)。規(guī)格化所有這些數(shù)據(jù)為不同的表并放進(jìn)SQLite數(shù)據(jù)庫(kù)不太現(xiàn)實(shí),因?yàn)槲覀儾樵児?jié)點(diǎn)并關(guān)聯(lián)來(lái)自于后端的樹(shù)結(jié)構(gòu)的方式非常非常多,因此我們直接存儲(chǔ)樹(shù)結(jié)構(gòu)。一個(gè)解決方案是將樹(shù)結(jié)構(gòu)存儲(chǔ)為JSON,但那將需要我們?cè)谒軌虮挥糜赨I展示之前先解析JSON,并轉(zhuǎn)換為一個(gè)Java對(duì)象。而且,JSON解析耗費(fèi)時(shí)間。我們過(guò)去常常在Android平臺(tái)上使用Jackson JSON解析器,但我們發(fā)現(xiàn)了它的一些問(wèn)題:
- 解析速度。它要耗費(fèi)35 ms來(lái)解析一個(gè)20 KB(Facebook中一個(gè)典型的響應(yīng)的大小)的JSON流,這超出了UI幀刷新的16.6 ms的間隔了。使用這種方法,我們無(wú)法在滾動(dòng)時(shí),按照需要及時(shí)地從磁盤緩存中加載stories而不出現(xiàn)丟幀(視覺(jué)上的抖動(dòng))。
- 解析初始化。一個(gè)JSON解析器在它可以開(kāi)始解析之前,需要構(gòu)建一個(gè)字段映射,這可能要耗費(fèi)100 ms到200 ms,這大幅地降低了應(yīng)用的啟動(dòng)時(shí)間。
- 垃圾回收。在JSON解析的過(guò)程中要?jiǎng)?chuàng)建大量的小對(duì)象,在我們的探索研究中發(fā)現(xiàn),解析一個(gè)20 KB的JSON 流時(shí),大概要分配 100 KB的瞬時(shí)內(nèi)存,這將給Java的垃圾回收器帶來(lái)巨大的壓力。
我們想要找到一個(gè)更好的存儲(chǔ)格式來(lái)提升我們的Android app的性能。
FlatBuffers
在我們對(duì)可選的格式的探索中,我們找到了FlatBuffers,來(lái)自于Google的一個(gè)開(kāi)源項(xiàng)目。FlatBuffers是protocol buffers(protobuf)的一個(gè)進(jìn)化版,后者已經(jīng)包含了對(duì)象元數(shù)據(jù),允許直接地訪問(wèn)數(shù)據(jù)的獨(dú)立的子部件,而不必須先反序列化整個(gè)對(duì)象(在這個(gè)例子中,是一棵樹(shù))。
想象一下,我們有一個(gè)簡(jiǎn)單的person類對(duì)象,它有四個(gè)字段: name,friendship status,spouse,及friends列表。spouse和friends字段還包含了person對(duì)象,這些形成了一個(gè)樹(shù)結(jié)構(gòu)。這是一個(gè)說(shuō)明了這樣一個(gè)對(duì)象,一個(gè)person,John和他的妻子,Mary,在FlatBuffer中是如何布局的簡(jiǎn)化的圖解。
class Person {String name;int friendshipStatus;Person spouse;List<Person>friends; }在上面的布局中,你需要注意:
- 每個(gè)對(duì)象都被分為兩個(gè)部分:元數(shù)據(jù)的部分(或vtable)在軸心點(diǎn)的左邊,真實(shí)的數(shù)據(jù)部分在右邊。
-
每個(gè)字段對(duì)應(yīng)于vtable中的一個(gè)槽,其中存儲(chǔ)了那個(gè)字段的真實(shí)數(shù)據(jù)的偏移量。比如,John的table的第一個(gè)槽的值為1,表明了John的名字被存放在了距離Jonh的軸心點(diǎn)向右偏移一個(gè)字節(jié)的地方。
-
對(duì)于對(duì)象字段,vtable中的偏移量指向子對(duì)象的軸心點(diǎn)。比如,John的vtable中的第三個(gè)槽指向了Mary的軸心點(diǎn)。
-
要表示字段沒(méi)有值,我們可以在一個(gè)vtable槽中使用一個(gè)0偏移量。
下面的代碼片段展示了我們可以如何在上面展示的結(jié)構(gòu)中找到Jonh的妻子的名字。
// Root object position is normally stored at beginning of flatbuffer. int johnPosition = FlatBufferHelper.getRootObjectPosition(flatBuffer); int maryPosition = FlatBufferHelper.getChildObjectPosition(flatBuffer,johnPosition, // parent object position2 /* field number for spouse field */);String maryName = FlatBufferHelper.getString(flatBuffer,maryPosition, // parent object position0 /* field number for name field */);注意,不需要?jiǎng)?chuàng)建中間對(duì)象,這節(jié)省了瞬時(shí)內(nèi)存的分配。我們甚至可以通過(guò)直接地把FlatBuffer數(shù)據(jù)存儲(chǔ)在文件中,然后把它mmap進(jìn)內(nèi)存中來(lái)做進(jìn)一步的優(yōu)化。這意味著我只需要加載文件中我們需要讀取的部分,這將進(jìn)一步降低總的內(nèi)存消耗。
而且,在讀取字段之前,無(wú)需反序列化對(duì)象樹(shù)。這減少了存儲(chǔ)層和UI之間的延遲,它將提升總的性能。
FlatBuffers上的變化
有時(shí)我們需要修改FlatBuffers里的值。由于FlatBuffers有意地設(shè)計(jì)為不可變的,而沒(méi)有一個(gè)直接的方法來(lái)做到這一點(diǎn)。我們想到的一個(gè)方案是與最初的FlatBuffer一并追蹤變化。
FlatBuffer中的每個(gè)數(shù)據(jù)片段可以通過(guò)它在FlatBuffer中的絕對(duì)位置來(lái)唯一的標(biāo)識(shí)。我們支持變化,因而我們不一定要為很小的更新重新下載整個(gè)story——比如friendship status的改變。由于只是例子,這是一個(gè)概念的可視化,關(guān)于如何使用它來(lái)追蹤兩個(gè)變化:
-
Jonh的friendship status由FlatBuffer的絕對(duì)索引2處的vtable槽指向。要改變John的status,我們只需要記錄絕對(duì)索引2所對(duì)應(yīng)的那個(gè)數(shù)據(jù)現(xiàn)在為1(意味著一個(gè)friend)而不是2(意味著不是一個(gè)friend,但需要發(fā)送friendship請(qǐng)求)。
-
Mary的名字(“Mary”)由位于絕對(duì)索引13處的一個(gè)vtable槽指向。類似地,要修改Mary的名字,我們只需要記錄對(duì)應(yīng)于絕對(duì)索引13的新的String值。
最后,我們可以把所有的變化打包進(jìn)變化緩沖區(qū)。變化緩沖區(qū)由兩部分組成:變化索引和變化數(shù)據(jù)。變化索引記錄了從base buffer中絕對(duì)索引到新數(shù)據(jù)的位置的映射。變化數(shù)據(jù)以FlatBuffer的格式存儲(chǔ)了新的數(shù)據(jù)。
當(dāng)查詢FlatBuffers中的一個(gè)數(shù)據(jù)片段時(shí),我們可以計(jì)算出數(shù)據(jù)的絕對(duì)位置,然后查詢變化緩沖區(qū)來(lái)查看是否發(fā)生了變化并返回它,否則返回base buffer中的數(shù)據(jù)。
平坦模型
FlatBuffers不僅可以被用于存儲(chǔ),也可以被用于網(wǎng)絡(luò),以app中的in-memory的格式。這消除了服務(wù)器響應(yīng)的數(shù)據(jù)到UI顯示之間的轉(zhuǎn)換。這已經(jīng)允許我們走向一個(gè)更干凈平坦模型架構(gòu),這將消除UI和存儲(chǔ)層之間額外的復(fù)雜性。
當(dāng)使用JSON作為存儲(chǔ)格式時(shí),我們需要添加一個(gè)內(nèi)存緩存來(lái)迂回地處理反序列化的性能問(wèn)題。我們最終也在UI和存儲(chǔ)層之間添加應(yīng)用和網(wǎng)絡(luò)邏輯。
盡管這個(gè)三層架構(gòu)在iOS和桌面上已經(jīng)相當(dāng)?shù)牧餍辛?#xff0c;它在Android上依然有一些重要問(wèn)題:
-
一個(gè)內(nèi)存緩存通常意味著,相對(duì)于UI顯示的需要,我們將在內(nèi)存中放多得多的東西。市場(chǎng)上的許多Android設(shè)備依然有著每個(gè)app 48 MB或更少內(nèi)存的限制。當(dāng)你加入了Java的垃圾收集器的開(kāi)銷,這可能對(duì)性能有影響。
-
應(yīng)用邏輯需要處理內(nèi)存緩存,UI和存儲(chǔ),但是典型地與UI和存儲(chǔ)相關(guān)的代碼是發(fā)生在不同的線程中的。但在一個(gè)巨大的應(yīng)用程序中保持線程模型簡(jiǎn)單可能是很困難的。
-
UI典型地從多個(gè)源接收數(shù)據(jù),比如存儲(chǔ)中緩存的數(shù)據(jù),來(lái)自于網(wǎng)絡(luò)的新數(shù)據(jù),來(lái)自于應(yīng)用邏輯的本地?cái)?shù)據(jù)變化,還有更多其它的。這需要UI不得不處理不同類型的數(shù)據(jù)改變場(chǎng)景,而可能導(dǎo)致UI透支。
通過(guò) 平坦模型 方法,UI和存儲(chǔ)層可以被更簡(jiǎn)單地集成,如下面的圖片所展示的那樣。
- 使用標(biāo)準(zhǔn)的Android cursors直接在存儲(chǔ)之上構(gòu)建UI,而且由于storage-to-UI是大多數(shù)Android apps中最熱的執(zhí)行路徑,這可以幫助保持UI響應(yīng)性。
- 應(yīng)用邏輯和網(wǎng)絡(luò)組件已經(jīng)被移到了存儲(chǔ)層的下方,允許那里的所有邏輯發(fā)生在一個(gè)后臺(tái)線程中,并確保結(jié)果首先反映在存儲(chǔ)中。然后,通過(guò)使用標(biāo)準(zhǔn)Android content provider通知,UI可以被通知去重繪。
- 這個(gè)架構(gòu)可以將UI和應(yīng)用邏輯進(jìn)行干凈的分離——我們可以簡(jiǎn)化每個(gè)的邏輯。UI組件只需要反映存儲(chǔ)的狀態(tài) ,而應(yīng)用邏輯只需要向存儲(chǔ)層寫(xiě)入最終的(正確的)信息。UI和應(yīng)用邏輯層運(yùn)行于不同的線程,它們從不需要彼此直接的通信。
結(jié)論
FlatBuffers是一個(gè)數(shù)據(jù)格式,它使得存儲(chǔ)和UI之間的數(shù)據(jù)轉(zhuǎn)換變得不必要了。采用這種技術(shù),我們也已經(jīng)驅(qū)動(dòng)了我們的app的額外的架構(gòu)提升,如平坦模型。我們?cè)贔latBuffers之上構(gòu)建的mutation擴(kuò)展使我們可以在一個(gè)單獨(dú)的結(jié)構(gòu)中,追蹤服務(wù)器數(shù)據(jù),變化,和本地狀態(tài),這可以簡(jiǎn)化我們的數(shù)據(jù)模型,并暴露一個(gè)一致的API給我們的UI組件。
在過(guò)去的六個(gè)月,我們已經(jīng)將Android平臺(tái)上的Facebook的大部分過(guò)度到使用FlatBuffers作為存儲(chǔ)格式了。一些性能提升數(shù)字包括:
- 從磁盤緩存中加載每個(gè)Story的時(shí)間從35 ms減小到了4 ms。
- 瞬時(shí)內(nèi)存分配減少了75%。
- 冷啟動(dòng)時(shí)間提升了10-15個(gè)百分點(diǎn)。
- 我們已經(jīng)減少了15%的存儲(chǔ)大小。
看到數(shù)據(jù)格式的選擇使得人們可以只花費(fèi)一點(diǎn)點(diǎn)的時(shí)間就讀到他們的朋友的更新及查看他們的家人的照片令人感到興奮。感謝你,FlatBuffers!
原文鏈接
總結(jié)
以上是生活随笔為你收集整理的用FlatBuffers提升Android平台上Facebook的性能的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在Android中使用FlatBuffe
- 下一篇: 在Android中使用Protocol