阿里云App從Swift 2.1開(kāi)始使用Swift穗椅,隨時(shí)不斷的推進(jìn),現(xiàn)在所有的業(yè)務(wù)代碼都用Swift編寫(xiě)。由于Swift 3.0語(yǔ)法上有諸多改變册舞,所以從Swift 2.3升級(jí)到Swift 3.0是一件宜早不宜遲的事情。元旦期間抽了點(diǎn)時(shí)間做這個(gè)升級(jí)障般。
外部依賴
- 目前開(kāi)源社區(qū)對(duì)Swift 3.0支持是非常好的调鲸,我們依賴的開(kāi)源組件最新版本都支持Swift 3.0了,所以并沒(méi)有什么不能解決的依賴挽荡。
- 因?yàn)楹芏嘟M件依賴CocoaPods 1.x才能編譯藐石,所以前期我們花了一些時(shí)間支持CocoaPods 1.1.1。
- 因?yàn)槟壳癝wift相關(guān)的動(dòng)態(tài)庫(kù)仍然會(huì)打進(jìn)App里面定拟,所以升級(jí)到Swift 3.0于微,并不影響對(duì)iOS8、iOS9的支持青自。
升級(jí)過(guò)程
- 先將所有我們自己的組件一個(gè)一個(gè)升級(jí)到Swift 3.0的語(yǔ)法株依。組件化的好處是可以并行,先解決基礎(chǔ)組件延窜,然后大家并行解決高級(jí)組件恋腕。
- 最后升級(jí)主工程,能解決的解決掉需曾,不能解決的加上
//TODO: Swift 3.0 小明
這樣的注釋吗坚。保證主工程能運(yùn)行通過(guò)之后,大家并行解決這些TODO問(wèn)題呆万。 - Xcode自帶的convert還是很給力的商源,因?yàn)閄code不會(huì)一次轉(zhuǎn)到位,可以不斷執(zhí)行convert進(jìn)行轉(zhuǎn)換谋减。不過(guò)convert有點(diǎn)費(fèi)時(shí)牡彻,主工程有將近400個(gè)Swift文件,完整的convert一次需要兩個(gè)小時(shí)左右,所以后面我都是自己根據(jù)已知的規(guī)則去做replace庄吼。Xcode目前只支持對(duì)target做convert缎除,不支持對(duì)文件或者代碼片段做convert带欢,有點(diǎn)蛋疼亩进。
- 總耗時(shí)。10萬(wàn)行代碼抵窒,6位同學(xué)元旦期間利用業(yè)余時(shí)間完成渐行。
細(xì)節(jié)
Swift 3.0改變最大的地方如下所示轰坊。
- 所有枚舉類型首字母改成小寫(xiě)
//Swift 2.3
label.textAlignment = .Right
//Swift 3.0
label.textAlignment = .right
- 所有顏色去掉了后面的Color()
//Swift 2.3
label.textColor = UIColor.redColor()
//Swift 3.0
label.textColor = UIColor.red
- 所有的方法第一個(gè)參數(shù)默認(rèn)增加argument label。但是函數(shù)類型不能使用argument label祟印。這導(dǎo)致了很詭異的不一致肴沫,并且參數(shù)多的時(shí)候,體驗(yàn)也不好蕴忆,是一個(gè)奇怪的改變颤芬。詳細(xì)情況請(qǐng)參看:Remove type system significance of function argument labels。
//argument label構(gòu)成重載
func foo(x: Int) {}
func foo(foo x: Int) {}
func foo(bar x: Int) {}
foo(x:)(5)
foo(foo:)(5)
foo(bar:)(5)
//可以隨意賦值給參數(shù)類型相同的函數(shù)
var fn1 : (Int) -> Void
fn1 = foo(x:)
fn1 = foo(foo:)
fn1 = foo(bar:)
var Fooblock : ((Int, Int) -> Void) = { (a, b) in
print("\(a) \(b)")
}
//這樣寫(xiě)也OK
var Fooblock : ((_ a: Int, _ b : Int) -> Void) = { (a, b) in
print("\(a) \(b)")
}
//用的時(shí)候沒(méi)有任何arguement label
fooBlock(3, 4)
- 大量方法改名
//Swift 2.3
label.font = UIFont.systemFontOfSize(17)
//Swift 3.0
label.font = UIFont.systemFont(ofSize: 17)
有些OC方法被改得連爹媽都不認(rèn)識(shí)了套鹅,比如OC ALYGetURLParams->Swift alyGetParams
站蝠。
- 少量方法變成了屬性
//Swift 2.3
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
override public func intrinsicContentSize() -> CGSize {
}
//Swift 3.0
override var preferredStatusBarStyle : UIStatusBarStyle {
return .lightContent
}
override var intrinsicContentSize: CGSize {
}
if/guard let,每個(gè)條件都要寫(xiě)自己的let卓鹿,where要換成
,
沉衣。Optional更加嚴(yán)謹(jǐn)了,Optional和非Optional不能比較减牺,Optional和Optional也不能比較。為了支持原有的比較代碼存谎,Converter會(huì)自動(dòng)插入運(yùn)算符重載的代碼拔疚,比如下面這一大段FIXME代碼。
//Swift 3.0
let foo : Float? = 1.0
let bar : Float? = 2.0
if let _foo = foo,
let _bar = bar,
_foo > _bar {
}
// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.
// Consider refactoring the code to use the non-optional operators.
fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l < r
case (nil, _?):
return true
default:
return false
}
}
// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.
// Consider refactoring the code to use the non-optional operators.
fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l > r
default:
return rhs < lhs
}
}
實(shí)現(xiàn)大量的非NS對(duì)象既荚,比如Data稚失、Date、IndexPath恰聘、URL句各、Error等等,這些類型和NS類型是可以相互轉(zhuǎn)型的晴叨,所以改起來(lái)還是很快的凿宾。
大量的
unused
警告,需要_
接一下兼蕊。
_ = self.navigationController?.popViewController(animated: true)
自己定義的接口可以使用@discardableResult
消除警告初厚,對(duì)于鏈?zhǔn)綐?gòu)造函數(shù)來(lái)說(shuō)非常有用。
@discardableResult
open class func routeURL(_ url: URL?) -> Bool {
return JLRoutes.routeURL(url)
}
- 逃逸的block都要加上
@escaping
修飾符孙技。@escaping
不構(gòu)成重載产禾,但是成為判斷是否實(shí)現(xiàn)協(xié)議接口的依據(jù)
排作。
public typealias FooBlock = () -> Void
func FooFunc(_ block: FooBlock) {
}
//@escaping 并不會(huì)構(gòu)成重載,聲明下面兩個(gè)函數(shù)會(huì)報(bào)redeclaration錯(cuò)誤亚情。
//func FooFunc(_ block: @escaping FooBlock) {
//}
protocol FooProtocol {
func doBlock(block: @escaping FooBlock)
}
//但是@escaping會(huì)影響實(shí)現(xiàn)協(xié)議接口
class FooClass : FooProtocol {
//OK
func doBlock(block: @escaping () -> Void) {
block()
}
//會(huì)提示沒(méi)有實(shí)現(xiàn)FooProtocol
// func doBlock(block: () -> Void) {
// block()
// }
}
- dispatch相關(guān)的方法都改寫(xiě)了妄痪,變得更加簡(jiǎn)潔,更加面向?qū)ο罅恕?/li>
//Swift 2.3
let delayTime = dispatch_time(DISPATCH_TIME_NOW, 0.5)
dispatch_after(delayTime, queue, {
block()
})
//Swift 3.0
DispatchQueue.main.asyncAfter(deadline: 0.5, execute: {
block()
})
- CGPoint楞件、CGRect相關(guān)函數(shù)構(gòu)造都需要加上對(duì)應(yīng)的argument label衫生。
//Swift 3.0
CGPoint(x: 0, y: 0)
CGSize(width: 500, height: 500)
CGRect(x: 0, y: 0, width: 500, height: 500)
CGPoint.zero
CGSize.zero
CGRect.zero
- 新增
open
、fileprivate
等關(guān)鍵字履因。需要被繼承的類障簿、需要被override的方法,都要用open修飾栅迄。extension不能用open修飾站故,但是它的方法只要加了open修飾,也可以在子類里面override毅舆。
使用OC代碼
- OC的
NS_Options
類型會(huì)轉(zhuǎn)換成OptionSet
西篓,而等于0的那一項(xiàng)作為默認(rèn)值是看不到,非常詭異憋活。默認(rèn)值可以通過(guò)AspectOptions(rawValue: 0)
和[]
得到岂津。
//OC
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// Called after the original implementation (default)
AspectPositionInstead = 1, /// Will replace the original implementation.
AspectPositionBefore = 2, /// Called before the original implementation.
AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};
//Swift
public struct AspectOptions : OptionSet {
public init(rawValue: UInt)
/// Called after the original implementation (default)
public static var positionInstead: AspectOptions { get } /// Will replace the original implementation.
/// Will replace the original implementation.
public static var positionBefore: AspectOptions { get } /// Called before the original implementation.
/// Called before the original implementation.
public static var optionAutomaticRemoval: AspectOptions { get } /// Will remove the hook after the first execution.
}
之前Swift類型放到OC容器里面,會(huì)自動(dòng)轉(zhuǎn)型為AnyObject≡眉矗現(xiàn)在需要自己用
as
轉(zhuǎn)型吮成。Any和AnyObject的區(qū)別可以看到這篇文章:ANY 和 ANYOBJECT。使用OC代碼的時(shí)候辜梳,NSDictionary會(huì)變成
[AnyHashable: Any]
粱甫,很多時(shí)候還得轉(zhuǎn)回Dictionary/NSDictionary繼續(xù)使用,好在as
轉(zhuǎn)型也是OK的作瞄。
typedef void (^LOGIN_COMPLETION_HANDLER) (BOOL isSuccessful, NSDictionary* loginResult);
- OC的構(gòu)造函數(shù)如果返回
id
也會(huì)變成Any
類型茶宵,用的時(shí)候需要強(qiáng)轉(zhuǎn)一下,比較惡心宗挥。所以要使用更新的instanceType
吧乌庶。
//會(huì)返回Any
+ (id) sharedInstantce;
//用的時(shí)候需要不斷強(qiáng)轉(zhuǎn)
(TBLoginCenter.sharedInstantce() as! TBLoginCenter).login()
//要用下面這種
+ (instancetype) sharedInstance;
坑
- 對(duì)于protocol中的optional接口,自動(dòng)convert契耿、手動(dòng)處理可能會(huì)出錯(cuò)瞒大,搞錯(cuò)了不會(huì)有警告或者錯(cuò)誤,但是代碼邏輯會(huì)出錯(cuò)宵喂。
-
ImplicitlyUnwrappedOptional
語(yǔ)義變了糠赦,不會(huì)自動(dòng)解包了。以前如果用到IUO的地方需要注意,要不然可能會(huì)出現(xiàn)下面這種情況拙泽。
客戶端的問(wèn)題很容易發(fā)現(xiàn)淌山,傳遞給服務(wù)器端的參數(shù)如果也有同樣的問(wèn)題會(huì)很蛋疼,因此可以考慮在網(wǎng)絡(luò)底層檢查一下參數(shù)是否包含Optional
顾瞻。如果發(fā)現(xiàn)有泼疑,那么直接abort掉。
- 類型沖突荷荤。比如自己也實(shí)現(xiàn)了一個(gè)
Error
對(duì)象退渗,那么會(huì)跟Swift Error沖突,可以用Swift.Error
這種方式解決蕴纳。
override open func webView(_ webView: UIView!, didFailLoadWithError error: Swift.Error!) {
super.webView(webView, didFailLoadWithError: error)
}
- 有些代碼會(huì)導(dǎo)致Swift編譯器進(jìn)程崩潰会油。比如一個(gè)OC庫(kù)里面有個(gè)這樣的接口,最后的error參數(shù)用Nonnull修飾會(huì)導(dǎo)致Swift編譯器編譯過(guò)程中崩潰古毛。
- (id _Nonnull)initWithFileInfo:(ARUPFileInfo * _Nonnull)fileInfo
bizeType:(NSString *_Nonnull)bizType
propress:(ProgressBlock _Nullable )progress
success:(SuccessBlock _Nullable )success
faile:(FailureBlock _Nullable )faile
networkSwitch:(NetworkSwitchBlock _Nullable)networkSwitch
error:(NSError *_Nonnull*_Nonnull)error;
蘋(píng)果官方bug系統(tǒng)上也有人提過(guò)這個(gè)問(wèn)題翻翩,參考:https://bugs.swift.org/browse/SR-3272。去掉Nonnull修飾即可通過(guò)編譯稻薇。
編譯顯著變慢
升級(jí)Swift 3.0之后嫂冻,感覺(jué)編譯賊慢。根據(jù):Profiling your Swift compilation times這篇文章的方法加上-Xfrontend -debug-time-function-bodies
之后塞椎,發(fā)現(xiàn)排名前五的方法都是50s左右桨仿。總的編譯時(shí)間比2.3要慢一倍案狠。2.3和3.0版本編譯都很慢服傍,但是3.0要更慢。
Swift 2.3: 342,403ms
Swift 3.0: 579,519ms
我頓時(shí)感到我大好青春都浪費(fèi)在編譯上面了骂铁,所以我們趕快來(lái)看看這段代碼寫(xiě)了什么東西伴嗡。
override func fetchDataForSinglePageTableView(_ actionType: ALYLoadDataActionType, successCallback: @escaping GetPageDataSuccessCallback, failedCallback: @escaping GetPageDataFailedCallback) {
//blablabla
successCallback(UInt((self?.statusInfo.count ?? 0) + (self?.regularInfo.count ?? 0) + (self?.nameServerInfo.count ?? 0)))
}) { [weak self] (exception) -> Void in
failedCallback(exception)
self?.refreshButton.isHidden = false
self?.showFailureToast(exception.reason)
}
}
這么大一段函數(shù),初看沒(méi)有明確的目標(biāo)从铲,于是我查找了資料,看看是否有前人的經(jīng)驗(yàn)可以借鑒澄暮,結(jié)果果然有很多人遇到相同的問(wèn)題名段,現(xiàn)有的總結(jié)已經(jīng)很詳細(xì)我不再贅述,這里主要參考了:Swift 工程速度編譯慢泣懊。對(duì)比這篇總結(jié)伸辟,我猜測(cè)應(yīng)該是下面這行將幾個(gè)連續(xù)的??
相加導(dǎo)致的。
//老代碼
successCallback(UInt((self?.statusInfo.count ?? 0) + (self?.regularInfo.count ?? 0) + (self?.nameServerInfo.count ?? 0)))
//新代碼
let statusCnt = self?.statusInfo.count ?? 0
let regularCnt = self?.regularInfo.count ?? 0
let nameServerCnt = self?.nameServerInfo.count ?? 0
successCallback(UInt(statusCnt + regularCnt + nameServerCnt))
再跑一下測(cè)試命令馍刮,編譯時(shí)間馬上變成78ms信夫,差了將近1000倍!
78.3ms /Users/chantu/test3/cloudconsole-iOS/CloudConsoleApp/Domain/Domain+Register/ALYDomainRegisterMsgViewController.swift:102:19 @objc override func fetchDataForSinglePa geTableView(_ actionType: ALYLoadDataActionType, successCallback: @escaping GetPageDataSuccessCallback, failedCallback: @escaping GetPageDataFailedCallback)
基于這個(gè)思路,我主要修改了以下兩種情況的代碼静稻。
- 幾個(gè)
??
同時(shí)出現(xiàn)在一個(gè)表達(dá)式里面的警没,如上述代碼 -
??
出現(xiàn)在字典里面的,如下面這種振湾。
var param:[String: String] = [
"securityGroupId": self.belongGroupId ?? "",
"regionId": self.regionId ?? "",
"ipProtocol": self.protocolType ?? "",
"portRange": self.portRange ?? "",
"policy": self.policy ?? "",
"priority": self.priority ?? "",
"nicType": self.nicType ?? ""
]
為了保持寫(xiě)代碼的流暢性杀迹,不因?yàn)榫幾g問(wèn)題影響大家編碼的體驗(yàn),因此我只對(duì)幾個(gè)特別耗時(shí)的地方做了修改押搪,但是可以從測(cè)試結(jié)果看到树酪,編譯速度有了明顯的提升,下面是測(cè)試后跑出來(lái)的時(shí)間大州⌒铮可以看到最慢的也只有1s多了,比之前的47s好太多了厦画。
1289.2ms /Users/chantu/test3/cloudconsole-iOS/CloudConsoleApp/Domain/DomainRealNameVerify/ALYDomainRealNameVerifyUploadInfoDataService.swift:117:10 func uploadInfo(_ templateId: String, credentialsNo: String, credentialsType: ALYDomainRealNameVerifyCredentialsType, credentialsImageData: Data, completionBlock: @escaping ((_ isSuccess: Bool, _ errorMsg: String) -> Void))
1084.8ms /Users/chantu/test3/cloudconsole-iOS/CloudConsoleApp/Instance/ECS/Disk/ALYECSDiskDetailViewController.swift:242:10 func setcellContentsWithModel(_ model: ALYECSDiskDetailModel)
1038.6ms /Users/chantu/test3/cloudconsole-iOS/CloudConsoleApp/Instance/ECS/Disk/ALYECSDiskDetailViewController.swift:242:10 func setcellContentsWithModel(_ model: ALYECSDiskDetailModel)
1027.7ms /Users/chantu/test3/cloudconsole-iOS/CloudConsoleApp/Instance/Data/ALYCloudMetricDataService.swift:15:10 func getInstanceMetric(withPluginId pluginId: String, dimensions: String, metric: String, startTime: Double, endTime: Double, successCallback: @escaping ALYServiceSuccessCallback, failureCallback: @escaping ALYServiceFailureCallback)
999.3ms /Users/chantu/test3/cloudconsole-iOS/CloudConsoleApp/Domain/DomainRealNameVerify/ALYDomainRealNameVerifyUploadInfoDataService.swift:117:10 func uploadInfo(_ templateId: String, credentialsNo: String, credentialsType: ALYDomainRealNameVerifyCredentialsType, credentialsImageData: Data, completionBlock: @escaping ((_ isSuccess: Bool, _ errorMsg: String) -> Void))
我們目前的做法是盡量不把這些復(fù)雜的操作寫(xiě)到一個(gè)表達(dá)式里面疮茄,先把變量存起來(lái)再放到表達(dá)式里計(jì)算,雖然是因?yàn)檎Z(yǔ)言的問(wèn)題不得不妥協(xié)但為了自己編譯速度還是寧可多寫(xiě)幾行苛白。
參考資料
總結(jié)
因?yàn)镾wift 3.0版本開(kāi)始保證接口的穩(wěn)定性娃豹,這次升級(jí)到3.0之后,使用Swift再無(wú)后顧之憂购裙。希望蘋(píng)果真的不要再干出Swift 1.0->2.0->3.0每次升級(jí)都要改大量代碼的扯淡事懂版。