首頁 > 軟體

Airbnb遷移到Swift 3的實踐

2020-06-16 17:20:43

Airbnb公司自Swift語言誕生起就一直堅持加以使用。在這一過程中,我們通過親身經歷體會到這款現代化、安全且由社群驅動的新興語言帶來的各類助益。

直到最近,我們的程式碼庫中有很大一部分由Swift 2編寫而成。我們剛剛完成了面向Swift 3的遷移工作,剛好在新版本的Xcode放棄支援Swift 2之時。

我們希望與技術社群共用我們在遷移過程中積累的經驗與心得、Swift 3為我們應用帶來的提升以及期間值得一談的技術性結論。

“無中斷式開發”方法

我們有數十套模組及部分第三方庫是由Swift編寫而成,其中包括數以千計的檔案與成千上萬程式碼行。如果你認為如此規模的Swift程式碼庫仍不足以構成挑戰,那麼這裡要給大家提個醒——Swift 2與Swift 3的模組之間無法相互匯入,這進一步提高了遷移過程的複雜性。即使是正確的Swift 3程式碼匯入Swift 2庫後亦無法編譯通過。這種不相容性導致我們很難以並行方式實現程式碼轉換。

為了確保對程式碼進行逐步轉換與驗證,我們開始建立一套依賴性圖表,其中以拓撲方式對我們的36套Swift模組進行了排序。我們的具體升級規劃如下所示:

  1. 將CocoaPods升級至1.1.0(以支援必要的pod升級);
  2. 第三方pods升級至Swift 3版本;
  3. 按照拓撲順序對我們的自有模組進行轉換。

通過與其它已經完成此類遷移工作的企業進行溝通,我們意識到大多數專案需要在遷移時暫時凍結開發任務。我們希望盡一切可能避免對程式碼庫進行凍結,即使這意味著會給遷移工作增添種種複雜性因素。由於轉換工作本身很難以並行方式進行,因此所有已有的解決方案都存在效率低下的問題。另外,因為很難估計整個轉換所需要的具體時間週期,所以我們希望能確保在遷移過程中繼續發布App新版本。

整項遷移工作由三名工作人員負責。其中兩位專注於程式碼轉移,第三位則專注於協調工作內容、與團隊溝通以及進行基準審查。

包括準備工作在內,我們設定的專案時間表如下所示:

  • 1週:調查並籌備(1人負責);
  • 2.5週:轉換(2人負責),與主團隊進行轉換影響通報及交流(1人負責);
  • 2週:QA與bug修復(QA團隊與對應的iOS功能負責人);

Swift 3的影響

儘管我們對於Swift 3帶來的諸多全新語言特性相當興奮,但我們亦希望確切了解此次更新會給我們的終端使用者以及整體開發者體驗帶來怎樣的影響。我們密切關注Swift 3對發布IPA大小及偵錯build時間的影響,因為這一切是我們在使用Swift過程中的兩大痛點。遺憾的是,在通過多種不同的優化設定實驗之後,我們發現Swift在這兩方面的表現仍然差強人意。

發布IPA的大小

在遷移至Swift 3後,我們發現所發布的IPA出現了2.2 MB體積增量。通過初步發掘,我們發現這幾乎完全是由於Swift庫自身大小的增加而導致(我們自己的二進位制檔案大小幾乎沒有變化)。以下為幾項未壓縮二進位制檔案未經壓縮的體積增加範例:

  • libswiftFoundation.dylib: 增長233.40% (3.8 MB)
  • libswiftCore.dylib: 增長11.76% (1.5 MB)
  • libswiftDispatch.dylib: 增長344.61% (0.8 MB)

考慮到Foundation等Swift 3庫得到的顯著增強,這種變化也完全能夠理解。另外,當穩定版Swift ABI發布時,相信這些應用程式將不再需要因上述強化而遭遇體積增長的問題。

測試版Build時間

在遷移完成之後,我們的測試版build時間延長了4.6%,即在原本6分鐘的基礎上增加了16秒。

我們嘗試比較了Swift 2與Swift 3之間的各函數編譯時長,但卻無法得出具體結論——因為二者間的格式存在很大差異。不過我們確實找到了一條函數,其編譯時長在遷移之後激增至12秒。幸運的是,我們通過調整將其編譯時間還原到了正常水平,但這也證明了檢查轉換程式碼的重要意義。Build Time Analyzer for Xcode等工具能夠在這方面幫上大忙,當然大家也可以設定合適的Swift編譯器標記並解析build結果紀錄檔以達到類似的效果。

執行時問題

遺憾的是,雖然程式碼已經在Swift 3中全部編譯完成,但遷移工作仍未徹底結束。Xcode程式碼轉換工具並不能保證實現完全相同的執行時行為。另外,正如我們在後面所提到,程式碼轉換仍然需要人工介入並且裡面有一些坑。這意味著我們恐怕還需要通過回歸測試將其解決。由於我們的單元測試覆蓋率不足以提供充足的信心,因此我們不得不花費額外的QA週期以審查新近遷移完成的應用。

經過第一輪QA測試,新近遷移完成的應用暴露出數十項顯著問題。其中大部分問題在三人遷移團隊的處理下很快得到了解決(數小時之內),具體涉及的品質應用將在後文中進行具體討論。在最初的調整之後,剩下的是一些重要的回歸測試工作,iOS團隊最多留下15項潛在的問題——其中3項會引發崩潰,意味著我們需要在應用下個版本發布前進行調查。

程式碼轉換流程

我們首先在master建立了一個新的swift-3分支。如前文所述,我們對各個模組中的程式碼進行逐一轉換,首先是主幹模組、而後逐步推進至依賴性樹結構。只要有可能,我們就會嘗試以並行方式進行不同模組的轉換。如果不行,我們會一同討論該如何處理以儘可能避免衝突狀況。

對於各個模組,其轉換流程基本如下:

  1. 在swift-3分支下建立一個新的分支。
  2. 在該模組上執行Xcode程式碼轉換工具。
  3. 提交並推播變更。
  4. Build。
  5. 手動修復一部分Build錯誤。
  6. 提交並推播變更。
  7. Rebuild。
  8. 重複前三個步驟直到完成。

在手動進行程式碼更新時,我們一直秉持著“進行最直觀的程式碼轉換”這一理念。這意味著我們並不需要在轉換過程中改進程式碼安全性。之所以選擇這種思路,主要出於兩個理由。其一,由於該團隊以往一直利用Swift 2進行開發,因此這個過程實際上是在與時間賽跑,意味著並沒有多餘的精力進行品質調整。其二,我們希望盡可能減少新增的回歸測試。

幸運的是,我們的專案在推進一段時間後即遇到了法定假期,這意味著我們能夠騰出幾天時間在master上對swift-3進行基礎重建而不會導致進度延後。在進行基礎重建時,我們利用git rebase -Xours master以保證盡可能不影響swift-3,同時解決master中的各類衝突。

當swift-3與master進度對接後,我們意識到需要大約一天時間整理現有問題,而後才能放心地對二者加以合併。不過考慮到iOS團隊的龐大規模,master實際上一直在不斷變化。因此為了完整Swift 3遷移,我們強烈建議整個iOS團隊(除去參與遷移工作的成員)安心享受週末,而不要再對程式碼進行任何改動。

需要注意的問題

Objective-C中的Block引數

作為一大常見且無法在Xcode內得到自動修復的問題,我們發現Objective-C與Swift無法實現對block引數的順利橋接。我們首先來看以下Objective-C標題頭內的這條方法宣告:

+ (void)fetchReviewWithID:(NSString *)reviewId
            completion:(void (^)(AIRReview *review))completionBlock

而在Swift 2.3中,生成的介面如下所示:

public class func fetchReviewWithID(
  reviewId: String!,
  completion completionBlock: ((AIRReview!) -> Void)!)

在Swift 3中,生成的介面則為:

open class func fetch(
  withID reviewId: String!,
  completion completionBlock: ((AIRReview?) -> Swift.Void)!)

很多內容都出現了變化,不過其中最重要的是completionBlock中的引數由隱式解析可選項變成了可選項。這可能破壞其在各blocks中的使用方式。

我們決定以最為直觀的方式將其翻譯為Swift 3(而不觸及Objective-C程式碼),即在該block的開頭宣告一條變數,其擁有與該引數相同的名稱但為隱式解析狀態:

fetch(
  withID: reviewId,
  completion: { (review) in
    let review: AIRReview! = review
    // ...
  }
)

如此一來,相較於在使用時對該引數進行實際解析,我們現在至少能夠確保其不會破壞block內其它位置的語意。在以上範例中,if let someReview = review { /* … */ }review ?? anotherReview等後續宣告將繼續按預期方式工作。

隱式解析Optional分配中的型別推斷

另一大常見問題在於,我們需要設定Swift 3將變數型別推斷為已分配的隱式解析Optional:

func doSomething() -> Int! {
  return 5
}
var result = doSomething()

在Swift 2.3中,result會被推斷為型別Int!。在Swift 3中,其則會被推斷為型別Int?

出於block引數概述的原因,最簡單的解決辦法就是將變數宣告為隱式解析Optional型別:

var result: Int! = doSomething()

這一特定問題的出現頻率要比預期更高,因為橋接後的Objective-C初始化工具會返回隱式解析Optional型別。

個別函數的編譯時間激增

在我們的程式碼轉換工作當中,編譯器有時候會卡住幾分鐘。

我們的專案中存在一些 需要複雜型別推測的函數。在正常情況下,其編譯時耗不會太長。但一旦其中存在編譯錯誤,則可能令編譯過程陷入混亂。

當我們的進度因為這類問題而受阻時,我們採用Build Time Analyzer for Xcode協助發現瓶頸所在。在此之後,我們開始專注於那些會給程式碼轉換週期造成阻塞的函數,加以調整、進行rebuild再轉換更多程式碼。

可選協定方法實現險些出現問題

在Swift 3轉換過程中,可選協定方法往往很容易被大家所忽略。

下面來看UICollectionViewDataSource上的此方法:

func collectionView(
  _ collectionView: UICollectionView, 
  viewForSupplementaryElementOfKind kind: String, 
  at indexPath: IndexPath) -> UICollectionReusableView

假設您的類實現UICollectionViewDataSource並宣告以下方法:

func collectionView(
  collectionView: UICollectionView, 
  viewForSupplementaryElementOfKind kind: String, 
  atIndexPath indexPath: IndexPath) -> UICollectionReusableView

您能發現其中的差異嗎?這並不輕鬆,但差異卻的確存在。 這時您的類將只進行編譯而不會對定義的簽名進行更新,因為其屬於一項可選協定方法。

幸運的是,編譯器會給出警告資訊以幫助大家發現此類問題——但並非全部問題。因此我們必須認真審查一切包含可選方法的型別實現協定——例如大部分UIKist委託以及資料來源協定——並驗證其正確性。搜尋“func collectionView(collectionView:”等文字(請注意第一項引數標籤,其明顯屬於Swift 2產物)能幫助大家在自己的程式碼庫中發現此類問題。

涉及預設方法實現的協定

某些協定可能會通過協定擴充套件採用預設方法實現。如果某項協定的方法簽名在Swift 2到Swift 3的遷移過程中發生了變更,則必須確保這一變更體現在全部相關位置。編譯器本身不會理會協定擴充套件是否實現或者您的型別實現是否正確,但若二者出現問題則編譯過程顯然無法成功。

包含字串原始值的列舉型別

在Swift 3中,列舉型別由lowerCamelCase的命名慣例進行指定。Xcode程式碼轉換工具會自動對現有列舉進行適當變更。然而如果其中的原始值型別為String,則其會自動跳過對應列舉。這樣的處理方式可以理解,因為開發者有可能會利用某個與列舉型別名稱相匹配的String對其中一條列舉進行初始化。如果大家變更該列舉型別名稱,則可能破壞其餘位置的初始化過程。您可能認為能夠通過手動小寫某些列舉型別的方式“完成任務”,但這種作法只適用於不會破壞其它基於客串的初始化過程的前提之下。

第三方庫API變更

與大多數應用類似,我們的應用也存在一定的第三方庫依賴性。在遷移當中,我們需要更新這些利用Swift編寫的庫。這項工作看似簡單,但卻仍然需要高度關注:包括仔細閱讀發行說明,特別是您所依賴的庫已經經歷了大版本升級(特別是其對應的語言進行了大版本升級)。通過這種方式,我們得以發現了不少易被編譯器所忽略的API變更。

下一步工作

現在我們的master分支已經徹底轉換為Swift 3,而且不再有任何利用Swift 2進行的開發工作。那麼,這是否意味著遷移工作已經全面結束?

還不一定。正如之前所提到,在程式碼轉換過程中,我們僅僅對Swift 2程式碼進行了直觀的Swift 3轉換,這意味著我們還未能充分發揮Swift 3在安全性及其它便利層面的增強優勢。

作為一項需要持續推進的工作,我們將始終關注各類潛在的改進空間。

更高水平的細粒度控制

在預設情況下,Xcode程式碼轉換工具會將存取控制修飾符private轉換為fileprivate,而public則被轉換為open。這代表著程式碼進行實現“字面”形式轉換,保證程式碼的工作效果與原本相統一。

然而,這同時意味著開發者亦錯過了考量新的private與public是否真的最適合用於實現預期效果的機會。下一步,我們將審查存取控制機制的字面轉換結果,並思考如何利用Swift 3更為強大的表達能力提供更高水平的細粒度控制方案。

Swift 3方法命名

在對程式碼進行手動轉換時(即當Xcode轉換工具無法實現或者進行基礎重建時),我們通常採取“欄位”方式變更方法名稱,確保呼叫機制能夠繼續正常起效。以下列Swift 2.3方法簽名為例:

func incrementCounter(counter: Counter, atIndex index: Int)

為了更快更簡便地完成面向Swift 3的程式碼編譯轉換,我們將其變更為:

func incrementCounter(_ counter: Counter, atIndex index: Int)

不過更具Swift 3風格的調整方式顯然應該是:

func increment(_ counter: Counter, at index: Int)

接下來的工作是找到那些進行快速命名的範例,有針對性地更新方法簽名以遵循Swift 3的命名約定。

更安全地運用隱式解析可選項

如前文所述,我們處理新型可選項(Swift 3中)Objective-C block引數的方式是直接為其分配隱式解析可選項變數,這意味著我們不需要在block之內進行大量程式碼更新。然而,更理想的處理方法應該是考慮該引數為nil的可能性。

解決warning

為了快速完成轉換轉換流程,我們直接忽略了大量編譯器提出的警告資訊——只要其內容不是非常關鍵。著眼於未來,我們必須回頭審查這些問題以將警告數量控制在正常範圍內。

總結

考慮到Airbnb公司很早就開始採用Swift語言,我們積累了大量遺留Swift程式碼。將其遷移至Swift 3的頭號問題在於,我們很難弄清這項工作該如何進行或者會對應用程式產生怎樣的影響。如果大家還沒有著手進行這項面向Swift 3的遷移工作,希望我們通過實踐積累到的上述經驗能夠幫助大家對未來的挑戰擁有更為明確的認知。

原文: https://medium.com/airbnb-engineering/getting-to-swift-3-at-airbnb-79a257d2b656

本文永久更新連結地址http://www.linuxidc.com/Linux/2017-03/141195.htm


IT145.com E-mail:sddin#qq.com