?作者 | Dustin Shahidehpour
策劃 | 言征
iOS版Facebook(FBiOS)可以說(shuō)是Meta最古老的移動(dòng)代碼庫(kù)了。自2012年該應(yīng)用程序被重寫以來(lái),數(shù)千名工程師對(duì)其進(jìn)行了研究,并將其交付給數(shù)十億用戶,它可以支持?jǐn)?shù)百名工程師一次對(duì)其進(jìn)行迭代。
FBiOS架構(gòu)演變到今天,并不是有意為之的。它反映了10年以來(lái)的發(fā)展,這是由越來(lái)越多的工程師開(kāi)發(fā)該App所需的技術(shù)決策、穩(wěn)定性以及最重要的用戶體驗(yàn)所推動(dòng)的。
補(bǔ)充知識(shí):
截止到2022年,該代碼庫(kù)已經(jīng)走過(guò)了十周年,筆者將對(duì)這一演變背后的技術(shù)決策以及它們的歷史背景進(jìn)行一些說(shuō)明。
經(jīng)過(guò)多年的迭代,F(xiàn)acebook代碼庫(kù)與典型的iOS代碼庫(kù)不同:
(1)它包含了C++、Objective-C(++)和Swift。
(2)它有幾十個(gè)動(dòng)態(tài)加載的庫(kù)(dylib),以及太多的類,無(wú)法一次將它們加載到Xcode中。
(3)蘋果SDK的原始使用幾乎為零——一切都被內(nèi)部抽象所包裝或替換。
(4)該應(yīng)用程序大量使用代碼生成,這是由我們的自定義構(gòu)建系統(tǒng) Buck 推動(dòng)的。
(5)如果我們的構(gòu)建系統(tǒng)沒(méi)有大量緩存,工程師將不得不花一整天時(shí)間等待應(yīng)用程序的構(gòu)建。
一、2014:建立我們自己的移動(dòng)框架
2014年,對(duì)Facebook應(yīng)用程序進(jìn)行本地重寫已經(jīng)過(guò)去兩年,這時(shí),News Feed的代碼庫(kù)開(kāi)始出現(xiàn)可靠性問(wèn)題。當(dāng)時(shí),News Feed的數(shù)據(jù)模型得到了蘋果管理數(shù)據(jù)模型的默認(rèn)框架:核心數(shù)據(jù)的支持。核心數(shù)據(jù)中的對(duì)象是可變的,這并不適合新聞提要的多線程架構(gòu)。更糟糕的是,News Feed利用了雙向數(shù)據(jù)流,這源于它對(duì)Cocoa應(yīng)用程序使用了蘋果事實(shí)上的設(shè)計(jì)模式:模型視圖控制器。
最終,這種設(shè)計(jì)加劇了不確定性代碼的產(chǎn)生,這些代碼很難調(diào)試或再現(xiàn)錯(cuò)誤。很明顯,這種架構(gòu)是不可持續(xù)的,是時(shí)候重新思考了。
在考慮新的設(shè)計(jì)時(shí),一位工程師研究了React,F(xiàn)acebook的(開(kāi)源)UI框架,該框架在Javascript社區(qū)中非常流行。React的聲明性設(shè)計(jì)抽象了導(dǎo)致Feed(在web上)出現(xiàn)問(wèn)題的棘手命令式代碼,并利用了單向數(shù)據(jù)流,這使得代碼更易于推理。這些特征似乎很適合News Feed面臨的問(wèn)題:蘋果的SDK中沒(méi)有聲明性UI。
Swift將在幾個(gè)月內(nèi)發(fā)布,SwiftUI(蘋果的聲明性UI框架)將在2019年之前發(fā)布。如果NewsFeed想要有一個(gè)聲明性UI,那么團(tuán)隊(duì)必須構(gòu)建一個(gè)新的UI框架。
最終,這就是他們所做的。
在花了幾個(gè)月時(shí)間構(gòu)建和遷移新聞提要以在新的聲明性UI和新的數(shù)據(jù)模型上運(yùn)行后,F(xiàn)BiOS的性能提高了50%。
幾個(gè)月后,他們開(kāi)源了基于React的移動(dòng)UI框架ComponentKit。時(shí)至今日,ComponentKit仍然是在Facebook中構(gòu)建本機(jī)UI的事實(shí)上的選擇。它通過(guò)視圖重用池、視圖展平和背景布局計(jì)算為應(yīng)用程序提供了無(wú)數(shù)性能改進(jìn)。它也啟發(fā)了其Android對(duì)手Litho和SwiftUI。
最終,選擇用自定義infra替換UI和數(shù)據(jù)層是一種權(quán)衡。為了獲得可以可靠維護(hù)的令人愉快的用戶體驗(yàn),新員工必須擱置他們對(duì)Apple API的行業(yè)知識(shí),學(xué)習(xí)定制的內(nèi)部基礎(chǔ)設(shè)施。
這將不是FBiOS最后一次做出平衡最終用戶體驗(yàn)與開(kāi)發(fā)者體驗(yàn)和速度的決定。進(jìn)入2015年,該應(yīng)用的成功將引發(fā)我們所稱的功能爆炸。這也帶來(lái)了一系列獨(dú)特的挑戰(zhàn)。
二、2015:架構(gòu)拐點(diǎn)
到2015年,Meta在其“移動(dòng)第一”的口號(hào)上翻了一番,F(xiàn)BiOS代碼庫(kù)的每日貢獻(xiàn)者數(shù)量急劇增加。隨著越來(lái)越多的產(chǎn)品被集成到應(yīng)用程序中,其發(fā)布時(shí)間開(kāi)始縮短,人們開(kāi)始注意到。到2015年底,啟動(dòng)性能非常緩慢(接近30秒?。灾劣谟锌赡鼙皇謾C(jī)的操作系統(tǒng)殺死。
經(jīng)過(guò)調(diào)查,很明顯有許多因素導(dǎo)致啟動(dòng)性能下降。為了簡(jiǎn)潔起見(jiàn),我們將只關(guān)注那些對(duì)應(yīng)用程序架構(gòu)有長(zhǎng)期影響的方面:
(1)隨著應(yīng)用程序的規(guī)模隨著每種產(chǎn)品的增長(zhǎng)而增長(zhǎng),該應(yīng)用程序的“前置”時(shí)間正在以無(wú)限的速度增長(zhǎng)。
(2)該應(yīng)用程序的“模塊”系統(tǒng)為每個(gè)產(chǎn)品提供了對(duì)該應(yīng)用程序所有資源的無(wú)管制訪問(wèn)。這導(dǎo)致了一個(gè)公共問(wèn)題的悲劇,因?yàn)槊總€(gè)產(chǎn)品都利用它的“鉤子”來(lái)啟動(dòng),以執(zhí)行計(jì)算上昂貴的操作,從而快速導(dǎo)航到該產(chǎn)品。
緩解和改善啟動(dòng)所需的更改將從根本上改變產(chǎn)品工程師為FBiOS編寫代碼的方式。
三、2016年:Dylibs和模塊化
根據(jù)蘋果關(guān)于改進(jìn)發(fā)布時(shí)間的維基,在調(diào)用應(yīng)用程序的“主”功能之前,必須執(zhí)行許多操作。通常,一個(gè)應(yīng)用程序的代碼越多,所需時(shí)間就越長(zhǎng)。
雖然“pre-main”在發(fā)布過(guò)程中僅貢獻(xiàn)了30秒的一小部分時(shí)間,但這是一個(gè)特別令人擔(dān)憂的問(wèn)題,因?yàn)殡S著FBiOS不斷積累新功能,它將繼續(xù)以無(wú)限的速度增長(zhǎng)。
為了幫助緩解應(yīng)用程序發(fā)布時(shí)間的無(wú)限增長(zhǎng),我們的工程師開(kāi)始將大量產(chǎn)品代碼移入一個(gè)稱為動(dòng)態(tài)庫(kù)(dylib)的延遲加載容器中。當(dāng)代碼移動(dòng)到動(dòng)態(tài)加載的庫(kù)中時(shí),不需要在應(yīng)用程序的main()函數(shù)之前加載。
最初,F(xiàn)BiOS dylib結(jié)構(gòu)如下:
創(chuàng)建了兩個(gè)產(chǎn)品dylib(FBCamera和NotOnStartup),第三個(gè)dylib(FBShared)用于在不同的dylib和主應(yīng)用程序的二進(jìn)制文件之間共享代碼。
dylib溶液效果很好。FBiOS能夠抑制應(yīng)用程序啟動(dòng)時(shí)間的無(wú)限增長(zhǎng)。隨著時(shí)間的推移,大多數(shù)代碼都會(huì)以dylib結(jié)尾,這樣啟動(dòng)時(shí)的性能就會(huì)保持快速,并且不會(huì)受到應(yīng)用程序中添加或刪除產(chǎn)品的持續(xù)波動(dòng)的影響。
dylibs的加入引發(fā)了Meta產(chǎn)品工程師編寫代碼方式的思想轉(zhuǎn)變。隨著dylib的添加,像NSClassFromString()這樣的運(yùn)行時(shí)API冒著運(yùn)行時(shí)失敗的風(fēng)險(xiǎn),因?yàn)樗璧念惔嬖谟谛遁d的dylib中。由于FBiOS的許多核心抽象都是在遍歷內(nèi)存中的所有類的基礎(chǔ)上構(gòu)建的,因此FBiOS必須重新思考其核心系統(tǒng)的工作情況。
除了運(yùn)行時(shí)失敗之外,dylibs還引入了一類新的鏈接器錯(cuò)誤。如果Facebook(啟動(dòng)集)中的代碼引用了dylib中的代碼,工程師將看到如下鏈接器錯(cuò)誤:
為了解決這個(gè)問(wèn)題,工程師們需要用一個(gè)特殊的函數(shù)來(lái)包裝他們的代碼,如果需要的話,可以加載dylib,比如:
該解決方案有效,但有很多奇怪的地方:
(1)應(yīng)用程序特定的dylib枚舉被硬編碼到各種調(diào)用站點(diǎn)中。Meta的所有應(yīng)用程序都必須共享一個(gè)dylib枚舉,讀者有責(zé)任確定代碼運(yùn)行的應(yīng)用程序是否使用了該dylib。
(2)如果使用了錯(cuò)誤的dylib枚舉,代碼將失敗,但僅在運(yùn)行時(shí)失敗??紤]到應(yīng)用程序中大量的代碼和功能,這個(gè)延遲的信號(hào)導(dǎo)致了開(kāi)發(fā)過(guò)程中的許多挫折。
最重要的是,我們唯一能防止在啟動(dòng)過(guò)程中引入這些調(diào)用的系統(tǒng)是基于運(yùn)行時(shí)的,在應(yīng)用程序引入最后一分鐘的回歸時(shí),許多發(fā)布都被延遲。
最終,dylib優(yōu)化抑制了應(yīng)用程序發(fā)布時(shí)間的無(wú)限增長(zhǎng),但這意味著應(yīng)用程序架構(gòu)的巨大轉(zhuǎn)折點(diǎn)。FBiOS工程師將在接下來(lái)的幾年里重新設(shè)計(jì)應(yīng)用程序,以消除dylib帶來(lái)的一些粗糙邊緣,我們(最終)推出了一個(gè)比以往任何時(shí)候都更強(qiáng)大的應(yīng)用程序架構(gòu)。
四、2017:重新思考FBiOS架構(gòu)
隨著dylibs的引入,F(xiàn)BiOS的幾個(gè)關(guān)鍵組件需要重新思考:
(1)“模塊注冊(cè)系統(tǒng)”不能再基于運(yùn)行時(shí)。
(2)工程師們需要一種方法來(lái)了解啟動(dòng)期間的任何代碼路徑是否會(huì)觸發(fā)dylib加載。
(3)為了解決這些問(wèn)題,F(xiàn)BiOS轉(zhuǎn)向Meta的開(kāi)源構(gòu)建系統(tǒng)Buck。
在Buck中,每個(gè)“目標(biāo)”(app、dylib、library等)都用一些配置聲明,如下所示:
每個(gè)“目標(biāo)”都列出了構(gòu)建它所需的所有信息(依賴項(xiàng)、編譯器標(biāo)志、源等),當(dāng)調(diào)用“buck build”時(shí),它會(huì)將所有這些信息構(gòu)建成一個(gè)可以查詢的圖形。
使用這個(gè)核心概念(以及一些特殊的醬汁),F(xiàn)BiOS開(kāi)始生成一些buck查詢,這些查詢可以在構(gòu)建過(guò)程中生成應(yīng)用程序中的類和函數(shù)的整體視圖。這些信息將成為該應(yīng)用程序下一代架構(gòu)的基石。
五、2018:生成代碼的激增
既然FBiOS能夠利用Buck查詢依賴關(guān)系中的代碼信息,那么它就可以創(chuàng)建一個(gè)“function/classes->dylibs”的映射,可以在運(yùn)行中生成。
使用該映射作為輸入,F(xiàn)BiOS使用它生成從調(diào)用站點(diǎn)抽象出dylib枚舉的代碼:
左右滑動(dòng)查看完整代碼
使用代碼生成之所以吸引人,有幾個(gè)原因:
(1)因?yàn)榇a是基于本地輸入重新生成的,所以沒(méi)有什么可簽入的,也沒(méi)有更多的合并沖突!考慮到FBiOS的工程規(guī)模每年都會(huì)翻倍,這是一個(gè)巨大的開(kāi)發(fā)效率勝利。
(2)不再需要應(yīng)用程序特定的dylib(因此可以重命名為“FBCallFunction”)。相反,調(diào)用將從構(gòu)建期間為每個(gè)應(yīng)用程序生成的靜態(tài)映射中讀取。
事實(shí)證明,將Buck查詢與代碼生成相結(jié)合是如此成功,以至于FBiOS將其作為新插件系統(tǒng)的基礎(chǔ),最終取代了基于運(yùn)行時(shí)的應(yīng)用程序模塊系統(tǒng)。
1.左移信號(hào)
使用Buck支持的插件系統(tǒng)。FBiOS能夠通過(guò)將infra遷移到基于插件的架構(gòu)中,以構(gòu)建時(shí)警告取代大多數(shù)運(yùn)行時(shí)失敗。
構(gòu)建FBiOS時(shí),Buck可以生成一個(gè)圖表,顯示應(yīng)用程序中所有插件的位置,如下所示:
從這個(gè)角度來(lái)看,插件系統(tǒng)可以顯示構(gòu)建時(shí)間錯(cuò)誤,以便工程師發(fā)出警告:
(1)“插件D、E可能會(huì)觸發(fā)dylib加載。這是不允許的,因?yàn)檫@些插件的調(diào)用方位于應(yīng)用程序的啟動(dòng)路徑中?!?/p>
(2)“應(yīng)用程序中沒(méi)有用于呈現(xiàn)配置文件的插件……這意味著導(dǎo)航到該屏幕將無(wú)法工作?!?/p>
(3)“有兩個(gè)插件用于呈現(xiàn)組(插件A、插件B)。其中一個(gè)應(yīng)該刪除?!?/p>
對(duì)于舊的應(yīng)用程序模塊系統(tǒng),這些錯(cuò)誤將是“懶惰”的運(yùn)行時(shí)斷言?,F(xiàn)在,工程師們相信,當(dāng)FBiOS成功構(gòu)建時(shí),它不會(huì)因?yàn)楣δ苋笔?、?yīng)用程序啟動(dòng)期間的dylib加載或模塊運(yùn)行時(shí)系統(tǒng)中的不變量而失敗。
2.代碼生成的代價(jià)
雖然將FBiOS遷移到插件系統(tǒng)提高了應(yīng)用程序的可靠性,為工程師提供了更快的信號(hào),并使應(yīng)用程序可以與其他移動(dòng)應(yīng)用程序輕松共享代碼,但這是有代價(jià)的:
(1)插件錯(cuò)誤在Stack Overflow上很難找到答案,調(diào)試時(shí)會(huì)感到有些吃力。
(2)基于代碼生成和Buck的插件系統(tǒng)與傳統(tǒng)的iOS開(kāi)發(fā)有著天壤之別。
(3)插件為代碼庫(kù)引入了一層中間層。大多數(shù)應(yīng)用程序都會(huì)有一個(gè)包含所有功能的注冊(cè)表文件,這些都是在FBiOS中生成的,很難找到。
毫無(wú)疑問(wèn),插件使FBiOS遠(yuǎn)離了慣用的iOS開(kāi)發(fā),但這種權(quán)衡似乎是值得的。我們的工程師可以更改Meta的許多應(yīng)用程序中使用的代碼,并確保如果插件系統(tǒng)運(yùn)行良好,任何應(yīng)用程序都不會(huì)因缺少很少測(cè)試的代碼路徑中的功能而崩潰。像News Feed和Groups這樣的團(tuán)隊(duì)可以為插件構(gòu)建一個(gè)擴(kuò)展點(diǎn),并確保產(chǎn)品團(tuán)隊(duì)可以在不觸及核心代碼的情況下集成到其表面。
六、2020:Swift與語(yǔ)言架構(gòu)
應(yīng)用程序規(guī)模問(wèn)題導(dǎo)致的架構(gòu)變化上,但蘋果SDK的變化也迫使FBiOS重新考慮其一些架構(gòu)決策。
2020年,F(xiàn)BiOS開(kāi)始看到來(lái)自蘋果的Swift專用API的數(shù)量增加,并且越來(lái)越多的人希望在代碼庫(kù)中使用更多的Swift。終于是時(shí)候接受這樣一個(gè)事實(shí)了:Swift是FB應(yīng)用程序中不可避免的租戶。
歷史上,F(xiàn)BiOS曾使用C++作為構(gòu)建抽象的杠桿,因?yàn)镃++的“零開(kāi)銷”原則,這節(jié)省了代碼大小。但C++尚未與Swift互操作。對(duì)于大多數(shù)FBiOS API(如ComponentKit),必須創(chuàng)建某種墊片以在Swift中使用,從而導(dǎo)致代碼膨脹。
下面是一個(gè)圖表,概述了代碼庫(kù)中的問(wèn)題:
考慮到這一點(diǎn),我們開(kāi)始形成一種關(guān)于何時(shí)何地使用各種代碼的語(yǔ)言策略:
最終,F(xiàn)BiOS團(tuán)隊(duì)開(kāi)始建議:面向產(chǎn)品的API/代碼不應(yīng)包含C++,這樣我們就可以自由使用蘋果公司的Swift和未來(lái)的Swift API。使用插件,F(xiàn)BiOS可以抽象出C++實(shí)現(xiàn),這樣它們?nèi)匀粸閼?yīng)用提供動(dòng)力,但對(duì)大多數(shù)工程師來(lái)說(shuō)是隱藏的。
這種類型的工作流意味著FBiOS工程師構(gòu)建抽象的方式發(fā)生了一些變化。自2014年以來(lái),影響框架構(gòu)建中的最大因素是對(duì)應(yīng)用程序大小和表現(xiàn)力的貢獻(xiàn)度(這就是為什么ComponentKit選擇Objective-C++而不是Objective-C的原因)。
Swift的引入導(dǎo)致開(kāi)發(fā)人員的效率降低,不急,未來(lái)還能看到更多。
七、2022年:旅程已完成1%
自2014年以來(lái),F(xiàn)BiOS架構(gòu)發(fā)生了很大變化:
(1)它引入了大量?jī)?nèi)部抽象,如ComponentKit和GraphQL。
(2)它使用dylibs將“pre-main”時(shí)間保持在最小,并有助于快速啟動(dòng)應(yīng)用程序。
(3)它引入了一個(gè)插件系統(tǒng)(由Buck提供支持),這樣就可以從工程師那里抽象出dylib,因此代碼很容易在應(yīng)用程序之間共享。
(4)它引入了關(guān)于何時(shí)何地使用各種語(yǔ)言的語(yǔ)言指南,并開(kāi)始改變代碼庫(kù)以反映這些語(yǔ)言指南。
與此同時(shí),蘋果對(duì)其手機(jī)、操作系統(tǒng)和SDK進(jìn)行了令人興奮的改進(jìn):
(1) 他們的新手機(jī)速度很快。裝載成本比以前小得多。
(2) dyld3和鏈修復(fù)等操作系統(tǒng)改進(jìn)提供了軟件,使代碼加載更快。
(3) 他們引入了SwiftUI,這是一個(gè)用于UI的聲明性API,它與ComponentKit共享了很多概念。
(4) 他們提供了改進(jìn)的SDK,以及我們可以為其構(gòu)建自定義框架的API(如iOS8中的可中斷動(dòng)畫(huà))。
隨著Facebook、Messenger、Instagram和WhatsApp分享了更多的體驗(yàn),F(xiàn)BiOS正在重新審視所有這些優(yōu)化,以了解在哪些方面可以更接近平臺(tái)正統(tǒng)。最終,我們發(fā)現(xiàn),共享代碼的最簡(jiǎn)單方法是使用應(yīng)用程序免費(fèi)提供的東西,或者構(gòu)建一個(gè)幾乎無(wú)依賴性且可以在所有應(yīng)用程序之間集成的東西。
我們將于2032年在這里與您見(jiàn)面,回顧代碼庫(kù)的20周年紀(jì)念!?