標籤:類型 枚舉類 視圖 學習 包括 父節點 枚舉 溝通 結構
本文出自Uber移動架構和架構組負責人托馬斯·阿特曼於2016年在灣區Swift峰會上的演講,分享了使用Swfit重寫Uber的好與壞。以下為譯文:
我是托馬斯·阿特曼,目前是Uber移動架構和架構組負責人。Uber現在的使用者量已經達到數百萬,這麼大的使用者量,Uber是如何用架構實現的呢?
Swift與百位工程師的故事 — 原因、架構、經驗
今天我想談談一百多名Uber工程師是如何使用Swift程式設計語言的,在上周三新發布的Rider App主應用程式全部都是用Swift語言重構的。接下來我的分享主要包括三個部分:選擇Swift的原因、Uber新架構;重構經驗。
優步的開端——重構的原因
這是整個移動團隊四年前的樣子(指向螢幕顯示有三名工程師的照片),就是從那時開始,他們著手搭建了我們現在這套老應用的基礎。老的應用程式已經穩定使用了四年,但由於移動Team Dev的指數級的增長,這套架構的缺點也逐漸顯示出來,基於這套老架構想做功能開發也變得越來越困難。由於跟不同團隊之間共用了很多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。它將會通過你的源碼,去協議裡面尋找帶有@CreateMock的那些聲明,希望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的踩坑經曆及解決方案(轉載)