swift作爲蘋果的親兒子,從2014年開始,在6年的發展過程中,終於在2019年3月份迎來了ABI(Application Binary Interface)的穩定。
背景
ABI的穩定意味着Binary接口穩定,也就是運行的時候只要是通過swift 5或者以上的編譯器編譯出來的binary,就可以跑在任意的swift 5.0及以上的runtime上。這樣,我們就不需要像以前那樣在app裏面放一個swift runtime,Apple會把相應的ABI整合到iOS或者macOS中。同時App的尺寸會變小,在iOS12.2以上系統,會預裝swift5 runtime,所以不在需要swift的庫,它們會從app bundle中被刪除,對於iOS12.2以下的系統,還是照舊需要引入。同時因爲系統集成了swift,app啓動的時候也就不需要額外加載swift,在新系統中會更加的節省內存空間。
對於好大夫的醫患兩端,在這時接入swift是一個不可多得的好機會。那麼,從好大夫的iOS客戶端團隊角度出發,接入swift需要有哪些準備工作?基礎框架如何設計?如何解決在swift開發中出現的問題?這些都會在下文中一一解答。
接入swift的準備工作
好大夫的iOS客戶端代碼年份非常久遠,整體使用Objective-C編寫,最早的代碼可以追溯到2011年8月份,對於接入swift來說,首先需要進行技術調研,什麼版本的swift適合好大夫客戶端,而不是一味的追求最新的swift版本。其次,對於swift三方框架的調研,國內的swift社區活躍度並不是特別高,相反國外的swift社區反而更加的活躍,從github上的Alamofire網絡庫到Uber早已使用swift重構客戶端,可見swift在過國外早已盛行已久。
通過調研,swift在開發效率和性能上明顯優於Objective-C,蘋果開發者官網上各種Programming Guide也已經以swift作爲首要顯示語言,從蘋果對swift的推廣來看,如果我們一直堅守Objective-C陣營,在未來蘋果強制推進swift,對我們來說衝擊還是比較大的。WWDC2019中,蘋果同時推出了swiftUI,正式統一了Apple全平臺UI開發解決方案,未來屬於swift,可見好大夫iOS客戶端接入swift刻不容緩。
基礎框架設計
iOS客戶端整體採用組件化的架構,使用cocoapods導入各個組件,業務之間通過中間件進行通信,基礎組件包括網路庫、圖片庫、支付庫等。在接入swift之後,雖然基礎私有庫可以通過修改podspec文件進行swift私有庫調用,但是爲了統一和壯大iOS客戶端的swift生態系統,對於部分私有庫進行了swift化重構,生成一套專供swift調用的基礎庫,當然不是所有的私有庫都進行了重構,而是根據具體的業務需求以及公司未來發展綜合考慮,對一些工程中經常用到的宏定義,遍歷構造函數以及視圖約束框架等進行了swift化。
對於UI常用控件,網絡庫,圖片庫,支付庫等,如果進行重構,則會出現影響範圍較大,測試耗時很長,影響項目迭代,拿UI控件庫來說只是進行了podspec文件修改,使其支持了swift私有庫的調用,同時如果有新增的基礎控件,首選swift語言來編寫,減少Objective-C代碼的使用。
經過一年時間的沉澱,iOS客戶端目前全量swift業務有直播業務、留言板業務,其他的業務的主流業務也已經使用了swift和OC混編的方式進行開發。
推進工程swift化遇到的問題
傳統Objective-C項目接入swift一些問題始終是繞不過去的,如:
- 主工程或者模塊化中swift和Objective-C的混編怎麼實現
- Module系統
- swift代碼的規範性怎麼解決
- …
一、swift和OC混編
針對上面的第一個問題,我們分爲兩種情況,第一種情況爲App Target內部swift和OC混編,對於好大夫的醫患兩端來說之前一直使用Objective-C編寫,所以在創建第一個swift文件時,會提示生成一個bridging header文件,點擊create即可。
在swift文件中如果需要引入某個OC類,將需要使用的OC類在bridging文件進行#import即可。在OC類中如果需要引入swift文件,只需要在OC類中引入import “ProductModuleName-Swift.h”,其中ProductModuleName表示當前Target的名稱。ProductModuleName-Swift.h文件爲編譯產物,可以看到該文件將swift文件中的代碼生成爲對應OC中的interface和implementation。
第二種情況爲模塊內部進行swift和OC混編,或者OC私有庫需要提供給外部的swift文件或者swift私有庫使用,針對這種情況我們可以統一配置,好大夫客戶端採用的cocoapods方式引入組件,所以通過對組件podspec進行配置,添加 s.pod_target_xcconfig = {‘DEFINES_MODULE’ => ‘YES’} 將組件模塊進行module化,可以使用cocoapods自動生成的umbrella.h文件或者自己新建一個 umbrella header 將需要暴露給swift調用的ObjC頭文件在這個 umbrella header 中導入,在需要調用的swift文件中直接 import Module 即可。
二、Module系統
2.1 LLVM Module系統
既然說到了OC和swift混編,那麼不得不提蘋果在2012年11月提出LLVM的Module系統,簡單講就是用樹形的結構化描述來取代以往#include,例如傳統的#include <stdio.h>變成了 import std.io,逼格更高,這樣做的好處主要有:
- 語義上完整描述了一個框架的作用;
- 提高編譯時的可擴展性,同一模塊只需要編譯或導入一次,避免了頭文件的多次引用、解析;
- 減少碎片化,每個模塊只處理一次,環境的變化不會導致不一致;
- 對工具友好,工具(語言編譯器)可以獲取更多關於 module 的信息,比如鏈接庫,比如語言是 C++ 還是 C;
- …
2.2 modulemap文件
modulemap文件就是對一個框架,一個庫所有文件的結構化描述,默認文件名是module.modulemap。如下圖,可以看到使用 umbrellar header 關鍵字導入 HDFIMComponent-umbrella.h 中所有的.h文件。
umbrellar header關鍵字的意思爲 Master Header File,可以使用
#import <UIKit/UIKit.h>
替代
#import <UIKit/UIViewController.h>
#import <UIKit/UILabel.h>
#import <UIKit/UIButton.h>
#import <UIKit/UIDatePicker.h>
2.3 Swift Module
項目中的每一個Target(無論是framework還是app),就叫做一個Swift Module。這是Swift分發代碼的方式,我們可以通過import命令,來使用定義在其他Module中的代碼,而每個Module意味着:
- 一個和Module同名的命名空間
- 一個獨立的訪問控制範圍
對第一條來說,這也就是爲什麼在一個項目中,定義在不同Swift文件中的類可以在不同的文件中直接使用而不需要include的原因,因爲它們本身就在同一個namespace裏。
對於第二條來說,當我們通過import在項目中引入一個module時,就相當於打開了這個module對應的命名空間,就可以使用這個module中所有標記爲public或者open的代碼。
三、規範swift代碼
不同的人有不同的編碼習慣,導致了工程中代碼風格的多樣性,爲了提供swift代碼的可讀性以及統一性,iOS客戶端引入了swiftlint,swiftlint是Realm開源的一個加強檢查swift語言規範的庫,可以滿足強迫症的所有幻想,通過對swift代碼進行掃描,統一編碼風格,從每一個文件到每一個函數,甚至每一個變量都進行規範,將swiftlint掃描後的結果接入到SonarQube,使用網頁進行展示,方便分類修改,將具體的代碼分配給對應的負責人,嚴格按照要求進行修改,保證客戶端swift代碼壞味道爲0。
未來規劃
swift是一門優秀的編程語言,就目前蘋果主推swift語言以及編程語言排行榜上swift已經從18位上升到12位,可見未來國內swift語言也將會盛行。接入swift簡單,但是如果想要完美的兼容當前工程並非一件容易的事,只有選擇正確的方式,合適的解決方案才能將swift與原工程成功融合在一起。
好大夫iOS客戶端目前已經全量使用swift進行開發,計劃每年做新系統適配時會升級swift版本,緊跟時代的發展。後續接入swift package manager替代cocoapods、聲明式UI替代現有命令式UI。
作者介紹:
陶慶瑋:好大夫iOS開發工程師,主要負責組件化工程、基礎庫開發和客戶端穩定性相關工作,以及Swift語言落地。