1. 程式人生 > >Uber使用Swift重寫APP的踩坑經歷及解決方案(轉載)

Uber使用Swift重寫APP的踩坑經歷及解決方案(轉載)

result 框架 退出 帶來 hole 懶漢 將在 例子 穩定

本文出自Uber移動架構和框架組負責人托馬斯·阿特曼於2016年在灣區Swift峰會上的演講,分享了使用Swfit重寫Uber的好與壞。以下為譯文:

我是托馬斯·阿特曼,目前是Uber移動架構和框架組負責人。Uber現在的用戶量已經達到數百萬,這麽大的用戶量,Uber是如何用框架實現的呢?

Swift與百位工程師的故事 — 原因、架構、經驗

今天我想談談一百多名Uber工程師是如何使用Swift編程語言的,在上周三新發布的Rider App主應用程序全部都是用Swift語言重構的。接下來我的分享主要包括三個部分:選擇Swift的原因、Uber新架構;重構經驗。

優步的開端——重構的原因

這是整個移動團隊四年前的樣子(指向屏幕顯示有三名工程師的照片),就是從那時開始,他們著手搭建了我們現在這套老應用的基礎。老的應用程序已經穩定使用了四年,但由於移動開發團隊的指數級的增長,這套架構的缺點也逐漸顯示出來,基於這套老架構想做功能開發也變得越來越困難。由於跟不同團隊之間共用了很多ViewController,所以每次也需要對其它的代碼進行測試。老架構真正讓我們感到崩潰的主要原因是它是由兩位工程師寫出來的,但是目前團隊已經發展到了100多人。與此同時,那套產品本身的用戶量也不大。我們已經在多個城市開始運行,產品滑塊底部密集的問題也顯示出來了,原因就是因為所有的團隊都希望在他們所在的城市能夠推出新的產品。我們也想對Rider App做一套全新的用戶體驗界面。基於上述的這些問題,其實歸納起來也就是目前那套應用的架構問題和用戶體驗界面的全新設計問題。未來不再是研究老架構然後去解決問題這種形式了,而是一切都從頭開始。

2015年做了很多糾正錯誤工作,試圖去完善老的結構,但對Uber的全新設計,將會從根本上解決問題,到時會處於一個更安全的階段,從頭去重新設計也是最理想的一種解決問題的方式。

重構架構的目標——穩定可靠並且支持未來發展



基於這兩個重構原因開始了新架構的研發。最基本的需求就是滿足上述兩個要求,保證四條核心流程的穩定,這基本上就意味著崩潰率處於最低級別。 如果您的應用程序沒有崩潰,但用戶仍然停留在某些屏幕上,顯然這問題很重大,這會讓用戶覺得不可靠。

我們當然也希望新開發的架構能夠支持Uber接下來數年的發展,就像當時設計這套老架構的時候是為了滿足過去這四年發展的想法是一樣的。

Swift成為了我們的選擇

為了實現上述的兩個目標,我們選擇了Swift。當時我們認為Swift是更加安全的,至少在設想裏是的,然而實際生活中並沒有人去驗證這一點。

我們認為編譯器中的類型安全性會讓問題更早的暴露出來,而不是等到產品上線以後再出現問題。

而我們知道,從現在開始的這四年,Swift將會進入到一段黃金發展期,它將會成為蘋果公司未來唯一一門大力推廣的語言。

時間線


從今年年初開始啟動的,在二月份的時候,我們當時還希望我們所做的事情是正確的,因為有一些工程師在以前的公司就花費了大量的時間去做重構的事,但最終都以失敗告終。為了保證重構能成功,挑選出了幾位核心工程師,讓他們花了5個月的時間去研究老的架構,在這5個月的時間內,我們就只幹這一件事:架構,框架,完成一些基礎的工作,最終搭建了一套很完美的基礎框架,所有人都是以這套基礎框架為原型進行開發。

6月,架構搭建好,開始讓核心流團隊開始使用。核心流打算采用一種新的uberX騎行或者是uberPOOL騎行,因此我們增加了20位工程師,花了兩個月的時間去審查新的架構,確保我們提出來的東西與之前構建一款新產品的要求是吻合的。事實證明,與最開始的產品要求相比,的確遺漏了一些東西,比如在視圖層,一旦工程師開始進行轉換或者做一些復雜的視圖操作,那麽我們必須調整架構以滿足他們的需求。但是過了兩個月,我們取得了新的進展,我們不再需要對代碼庫進行大量遷移,並且把平臺開放給了每一個人,如果他們需要的話,也可以移交他們的功能了。

新架構


新架構叫”Riblets”,它是由Router、Interaction、Builder、Presenter、View這幾個核心組件構成的,這也是VIPER框架的一種思想。我們研究了VIPER、MVVM和MVC,最終提出的方案是在VIPER基礎之上增加一些我們自己創新的元素在裏面。最終目標就是將每個功能模塊化,並且每一個模塊可以獨立的進行測試。Riblet框架裏的每一個核心組件都有一個協議接口,所以開發者可以把每一個單元單獨拿出來,對它進行充分的測試。Riblets框架裏的每一個模塊都會在樹裏面進行管理,因此沒有狀態機,取而代之的是一個狀態樹。狀態樹裏面的每一個節點就是一個Riblet,新架構中的核心部分是基於業務邏輯的,而不是視圖邏輯,並且所有的業務邏輯都是由本地決定的。


以這張樹形圖裏的“註冊”模塊為例,並不知道它的父節點是誰,但是它所需要的都已經註入進來了,是它的父節點註入了它所需要依賴的東西,可能還會有一個監聽器正在監聽註冊流,但是監聽器是不知道註冊模塊位於樹的哪裏。所以說,這些模塊是完全獨立的,每一個單獨的模塊都會做本地決策。再比如,從“App”模塊開始,它僅僅只負責一個業務模塊:“目前系統是否有session令牌”,這就是它監聽的唯一一件事,如果App模塊發現在流裏面沒有session,它就會把路徑指向到“Welcome”處;如果它發現了有session,那麽它就會跳過“Welcome”模塊,直接進入到“Bootstrap”模塊。

之後,樹形圖裏面右邊的每一個組件都知道系統目前是處於“已登錄”狀態,它們都會有一個令牌,它們都可以從獨立註入中取到session令牌,它們也沒必要去關心用戶是否已經退出了。如果在下面的某一個節點處突然進來了一個網絡電話,並且最終導致了session無效,那麽App組件就會監聽到,它就會通過流被調用,然後知道系統目前是沒有session的狀態,緊接著App組件就會中斷Bootstrap樹,並且最終將流指向Welcome組件。

這就可以讓不同團隊之間只關註自己負責的那部分業務,而沒有必要說每做一步都需要去跟其他團隊進行溝通交流。每個團隊都可以做出自己的本地決策,並且依賴關系始終得到滿足。

多個文件裏面的多行代碼

開發過程中會產生很多代碼,每一個模塊之間我們都定義了協議。有些組件會關聯一個Riblit,同時又關聯5個不同的文件,因此在代碼庫裏面會有五千多個文件,同時還有五十多萬行代碼。此外還有一些核心組件是用的Objective-C,這也是完全沒問題的。

學習過程中的經驗

在學習Swift的過程中,我們也得到了一些經驗。

  • 好的一面

很顯然Swift是一門更好的語言,也正因為這一點,我們才有了一個很好的開端,我們幾乎用到了Swift提供的所有的功能。

1. 可靠性

Swift的可靠性是它帶給我們的第一件驚喜,好像是在框架研發的四個月內,我突然發現在整個研發的過程中,我的集成開發工具還有我的應用都沒出現崩潰現象,即使是在調試的模式下。我問了團隊裏面的其它成員,他們的回答都是沒有出現崩潰。而在整個開發過程中,第一次出現崩潰是我們嘗試著用了一臺32位的機器,最終導致在解析JSON時出現了整數溢出。那是整個開發周期中出現的第一次崩潰現象。


Swift的可靠性讓我們感到非常的振奮,最終的數據顯示絕對無故障率是99.99%,這已經很接近100%了。一個應用的第一次運行就幾乎是沒有出現崩潰,這種情況我還從來沒遇到過。

必須考慮的一件事是不能允許其他人無條件對新應用進行解壓,正因這樣,也就不會有99.99%的絕對無故障率了,所以我們放了一個linting在裏面,從而確保沒有人可以在任何條件下進行解壓。

你必須考慮到所有的臨界情況,就好比你寫了很多if,但是沒有對應的else,那應用程就有可能出現異常,因此在調試階段必須使用聲明,最終上線時需要去掉,這樣應用程序就很少會出現崩潰。

  • 糟糕的一面

現在我們需要說一些糟糕的事情了,但如果你能從失敗中和逆境中得到成長,這也是非常有意義的。

1. 艱難的測試

首先,如何進行測試就是一件很困難的事。Swift是一門靜態語言,因此就沒有辦法像在Objective-C開發中那樣去依靠mock測試框架進行測試。由於都是基於協議的形式進行開發,並且協議還是以我們這邊為主,因此我們必須找出針對這些協議的測試方案。舉個例子,這個協議是用來為實現類創建的一個接口,這個接口允許你根據一個key進行數據保存,也允許你根據這個key進行檢索數據,如果你有了交互器,想對一些業務邏輯進行測試,比如當它得到某些輸入值的時候,是否能夠將這些值保存到硬盤當中,那你就必須得有一個實現者,也必須得有一個模擬這種存儲場景的頁面,有了這些東西,你才能測試哪一個方法被調用了。我們開始手動創建這些模擬,開始編寫代碼,最終得到不可擴展這個結論。 我們不能為多個工程師都提供支持。

我們所做的就是生成了一個小腳本,這個小腳本就是負責把大腳本給轉換成小腳本。雖然它自身也有一些問題,但最終我們都解決掉了,無論你在哪個環節想生成測試內容,只需引入script/generate-mocks。它將會通過你的源碼,[email protected],希望Swift在某種程度上給我們提供屬性,並為你創建mocks。所以當你通過代碼庫運行時,這份協議最終會變成一個StoringMock,它實現了存儲。它所做的就是實現協議裏面所有共有的方法。如果你想知道這份協議被調用了多少次,它還提供了計數功能。它將會為你實現所有的實際的方法,無論何時它都有可能返回一個默認的類型。例如在dataForKey中,你有一個可選的NSData,而mock只返回nil,因為這是完美的。它符合接口,如果要排序測試您的輸入,您也可以隨時調用dataForKeyHandlers,將其設置為關閉,並且可以在測試中測試您從測試中得到正確的輸入。

同樣的原理,storageDataForKey返回一個StorageResult,它是枚舉類型的,默認情況下會返回枚舉中的第一個成員。測試工作的問題就解決了,並且還可以生成所有的mock,我大概算了一下,生成的mock大概有100,000行,這100,000行完全是自動生成的,是不需要我們再去手工敲代碼的。

2. 工具問題

另一件糟糕的事情就是開發工具的問題了。我們稱之為“無限索引”。我不知道為什麽會出現這種情況,也許你已經遇到過,就是索引器一直在進行索引。不知道為什麽,它就是無法完成索引工作。與此同時它帶來的負面影響就是CPU使用率高達328%,這樣筆記本就會變熱,在不插入電源的情況下,筆記本大概只可以使用一個半小時。這真是一件奇怪的事,由於代碼每天都在增長,這個問題也變得越來越嚴重。之前我們並沒有遇到過這些問題,但是我們一旦超過了了200000或者300000行代碼,這個問題就將會變得更加的嚴重。

此外,IDE開始這樣做:(屏幕顯示Xcode的視頻,慢慢鍵入的字符串)。 這不是我打字慢,而是我已經輸入了整個字符串,但是IDE是用SourceKit對每一個關鍵筆劃進行檢查,它可不管我寫的代碼是不是正確的,而且此時你也根本沒辦法打字。

引用

解決方案:

如果你碰到這個問題,不妨做這樣(屏幕顯示刪除Xcode的視頻)。 您可以換成其他應用,比如AppCode,團隊裏有些人就是使用的AppCode。 也有人是這麽做的,先在AppCode中編寫代碼,然後復制粘貼到Xcode進行編譯,這樣也不會出現問題,真是太奇怪了。當然你也可以改善Nuclide,Nuclide是Facebook的IDE,目前還不支持Swift,但需要完善才能支持。

我們的解決方案是增加更多的框架。 將整套應用程序分解成多個框架,每個框架只包含很少的文件,他這樣做帶來的好處就是所有的一切都變得更快了。 因為根據我們解決的經驗來看,如果框架裏面的東西越多,工具出現問題的概率也就越大。

最開始的時候,定義了70還是80個框架,如果想定義更多也不是什麽難事。 當然了,如果你只想編寫代碼,不需要進行編譯,那麽也可以關閉索引功能,也有一部分人是這麽做的。


3. 二進制文件的大小

再來說說二進制文件的大小問題。 任何一款App應用,它的大小必須控制在100M以內,如果超出了,那麽就必須通過WIFI進行下載,這樣就會遇到一些問題。如果你的APP應用中存在結構體,應用就會變大,如果列表中存在結構體,它們會在堆棧中被創建出來,導致應用變大。最開始的時候,我們將模型都設置成了結構體,最終編譯出的二進制文件好像是80M,這不是我們所希望的。

可選的功能也會增加文件的體積,表面上這些功能你可以選擇性使用,但是其實你並不知道,編譯器已經在後臺默默地做了很多事,編譯器必須去檢查這部分代碼,還得去解壓等等,實際上編譯出來了很多東西。

泛型特化是我們遇到的另一個問題。 只要你使用了泛型,如果你希望這些泛型變快,編譯器將會對它們進行特化,最終編譯後的二進制文件也會變大。

Swift運行時所依賴的那些庫文件也會包含到應用程序中,我們對這些庫文件進行了壓縮,最終實際大小只有4.5MB。

引用

解決方案:

你可以通過優化設置解決這個問題。打開O-whole-module-optimization優化等級,有時可能會將編譯文件變小,有時也會導致變大,這就需要你知道哪裏的編譯比較消耗時間,因此我們也做了一個工具,它會將一個單獨的符號映射到一個文件,最終結合這些文件,你就可以直觀的看見應用程序的文件夾結構以及每一個Swift文件的大小。


4. 啟動速度

啟動速度是我們開發過程中遇到的另一件棘手事情。如果你看過了蘋果全球開發者大會的演講,那麽你就會得出這樣一個結論 - Swift可以實現更快的啟動速度,現在卻出現了完全相反的情況。通常情況下二進制文件中的動態庫的數量將會直接影響在pre-main中啟動時間,可pre-main和post-main就是由這兩個決定的。Pre-main發生在主方法調用之前,如果動態庫的數量太大,花費的時間也就會更多。

比如,在一臺iPhone 6s手機上面,Swift運行時的庫需要花費250毫秒才能完成他們的動作,這也就意味著在這250毫秒期間,你使用Swift也沒辦法返回,這是一種懶漢現象。

我們發現我們所遇到的工具問題是由於創建了更多的框架引起的,你框架裏的東西越多,那麽你的啟動速度就越慢。

引用

解決方案

可以將所有內容重新鏈接到二進制文件中,這就是我們采用的方案。 我們構建了這些框架,並做了後期處理,將所有的符號從這些框架中取出,將它們鏈接到靜態二進制文件中,這就是我們解決啟動速度慢的方案。

在企業證書方面你也有可能會遇到問題。如果你的設備具有企業證書,那麽APP時可能需要花費十秒鐘的時間去進行初始化加載,具體得依賴於證書數量。

你可以通過重鏈接降低時間,當然你也可以通過做其它的一些調整來增加post-main 時間。

目前我們正在嘗試使用DTrace來探測啟動序列中的訪問符號。由於做了重鏈接,所以保證它們是按照正確順序進行,這樣就防止在一些老設備當中,不需要將加載大量的頁面到內存中,但是啟動過程中你可以按照需求將某些頁面給讀到內存中。


令我們感到羞愧的一面

如果你參加了昨天或者一年前的Swift峰會的話,你就能感受到了,在演示的過程中我們遇到了一件真正的麻煩事,那就是編譯速度非常的慢,我們的基礎應用需要花費15到20分鐘才能完成clean工作。

對於這件事情,我們都很擔心,因此我們去咨詢了團隊中的每個人:“這個問題到底有多大”,當時我們是這麽問的:”根據以往編程過程中遇到的問題,整體思考一下,在優步未來發展的過程中,哪一門語言你覺得會更適合於iOS的開發?”

這是根據根據結果做的統計圖,幾乎是一半一半:

技術分享

結果顯示即使Swift有出錯率、無限索引、編譯速度等各種問題,但是他們還是堅持會使用Swift,另一半人則選擇換回Objective-C。

因此我們又增加了另外一個問題:

“如果實現了下面哪一件事或者哪兩件事,那麽你就會選擇Swift,甚至也改變了你對Swift的認知”

如果僅僅是由於編譯速度的問題,實際上我們是可以解決的。

編譯速度優化

弄清楚原因以後,我們做的第一件事就是切換回Swift。盡量不要在代碼中使用類型判斷,我們研究出了一個使用SourceKit開發的腳本,這個腳本可以在後期構建所有類型,只需更改代碼,使其具有所有類型信息就可以了。

最後,我們開始組合文件,我們發現將我們所有的200個模型組合成一個文件以後,可以將編譯時間從1分35秒減少到只有17秒。 所以我們覺得“如果繼續將其它的一切都結合成一個文件,那速度豈不是可以更快,這真是太有趣了”。 這樣做的原因是因為編譯器會對每個獨立的文件進行類型檢查,所以如果您生成了Swift編譯器的200個進程,則需要檢查所有其他文件的200x,因此將所有內容組合成一個文件可以使其編譯的更快。

全模塊的優化正是我們想要做的。 它將所有的文件都編譯成了一個文件。 全模塊優化問題就是優化,所以它相當慢。 但是如果添加用戶定義的自定義標誌SWIFT_WHOLE_MODULE_OPTIMIZATION,將其設置為yes,並將優化級別設置為none,那麽,它將完成全模塊的優化,而不進行優化,它會超級快。

目前我們最大的框架是基於Core Flow做的,它有900個文件,以前需要四分鐘才能編譯完,現在只需要23秒就可以了。只需要花23秒的時間就能夠將最大的庫給編譯完,所以即使再也不能進行增量編譯了,我也覺得無所謂了。大多數其他目標的文件少得多,速度也會更快。

Uber正在為Facebook的“Buck”做出貢獻,並加入了Swift的支持

如果整個模塊的CPU使用率已經優化到30%以下,那麽你就可以考慮做一些其他事情了。如果使用Objective-C語言進行開發,那我們就必須使用Buck。Buck提供了更好的依賴管理,可靠的增量編譯以及遠程編譯緩存。它是由Facebook創建,如果在編譯期間出現問題你可能就會關註它了。我們之前曾分別在objective-c和Android編譯過,最終我們的清理編譯速度提高了4倍,我們的增量編譯快了20倍,因為它使用了遠程編譯緩存,所以如果你正在編譯多個目標,而另一些人已經在其他機器上編譯了該代碼,那麽它將在遠程編譯緩存中可用,並且只會使用該文件,它不會重新編譯任何東西。 在Android上,它的速度更快,像6倍快的清理編譯時間,而增量版本只是快速的。

這不是Swift,但我們正在努力,所以我們一直在為Facebook能夠支持Swift而努力,我們現在開始嘗試在生成Xcode項目文件的時候能夠添加Swift支持,我認為現在這個目標已經幾乎或者接近實現了,我們已經在內部開始這麽做了,現在已經可以根據文件夾結構是用Buck來生成工程文件了。

接下來,我們正在為實現Buck編譯添加Swift支持而努力,這麽做的目的就是以後可以使用Buck編譯我們的應用程序。 最終我們還想要去研究如何將已經添加到Buck的Swift支持整合到Xcode中,如果研究成功的話,那麽當你打cmd + B,它不會使用Xcode編譯,而是會使用Buck進行編譯。

如果使用Buck的話,現在6分鐘的編譯時間,以後可能會減少到的2分鐘甚至更短。 這將從本質上解決Swift編譯時間的問題。 這一切都可以按照Buck repo進行,您最終會看到Swift支持也會加進來的。

Uber使用Swift重寫APP的踩坑經歷及解決方案(轉載)