阿里云iOS客户端适配Swift 3.0小记
阿里云App从Swift 2.1开始使用Swift,随时不断的推进,现在所有的业务代码都用Swift编写。由于Swift 3.0语法上有诸多改变,所以从Swift 2.3升级到Swift 3.0是一件宜早不宜迟的事情。元旦期间抽了点时间做这个升级。
外部依赖
- 目前开源社区对Swift 3.0支持是非常好的,我们依赖的开源组件最新版本都支持Swift 3.0了,所以并没有什么不能解决的依赖。
- 因为很多组件依赖CocoaPods 1.x才能编译,所以前期我们花了一些时间支持CocoaPods 1.1.1。
- 因为目前Swift相关的动态库仍然会打进App里面,所以升级到Swift 3.0,并不影响对iOS8、iOS9的支持。
升级过程
- 先将所有我们自己的组件一个一个升级到Swift 3.0的语法。组件化的好处是可以并行,先解决基础组件,然后大家并行解决高级组件。
- 最后升级主工程,能解决的解决掉,不能解决的加上
//TODO: Swift 3.0 小明
这样的注释。保证主工程能运行通过之后,大家并行解决这些TODO问题。 - Xcode自带的convert还是很给力的,因为Xcode不会一次转到位,可以不断执行convert进行转换。不过convert有点费时,主工程有将近400个Swift文件,完整的convert一次需要两个小时左右,所以后面我都是自己根据已知的规则去做replace。Xcode目前只支持对target做convert,不支持对文件或者代码片段做convert,有点蛋疼。

- 总耗时。10万行代码,6位同学元旦期间利用业余时间完成。
细节
Swift 3.0改变最大的地方如下所示。
- 所有枚举类型首字母改成小写
//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
- 所有的方法第一个参数默认增加argument label。但是函数类型不能使用argument label。这导致了很诡异的不一致,并且参数多的时候,体验也不好,是一个奇怪的改变。详细情况请参看:Remove type system significance of function argument labels。
//argument label构成重载
func foo(x: Int) {}
func foo(foo x: Int) {}
func foo(bar x: Int) {}
foo(x:)(5)
foo(foo:)(5)
foo(bar:)(5)
//可以随意赋值给参数类型相同的函数
var fn1 : (Int) -> Void
fn1 = foo(x:)
fn1 = foo(foo:)
fn1 = foo(bar:)
var Fooblock : ((Int, Int) -> Void) = { (a, b) in
print("\(a) \(b)")
}
//这样写也OK
var Fooblock : ((_ a: Int, _ b : Int) -> Void) = { (a, b) in
print("\(a) \(b)")
}
//用的时候没有任何arguement label
fooBlock(3, 4)
- 大量方法改名
//Swift 2.3
label.font = UIFont.systemFontOfSize(17)
//Swift 3.0
label.font = UIFont.systemFont(ofSize: 17)
有些OC方法被改得连爹妈都不认识了,比如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,每个条件都要写自己的let,where要换成
,
。 -
Optional更加严谨了,Optional和非Optional不能比较,Optional和Optional也不能比较。为了支持原有的比较代码,Converter会自动插入运算符重载的代码,比如下面这一大段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
}
}
-
实现大量的非NS对象,比如Data、Date、IndexPath、URL、Error等等,这些类型和NS类型是可以相互转型的,所以改起来还是很快的。
-
大量的
unused
警告,需要_
接一下。
_ = self.navigationController?.popViewController(animated: true)
自己定义的接口可以使用@discardableResult
消除警告,对于链式构造函数来说非常有用。
@discardableResult
open class func routeURL(_ url: URL?) -> Bool {
return JLRoutes.routeURL(url)
}
- 逃逸的block都要加上
@escaping
修饰符。@escaping
不构成重载,但是成为判断是否实现协议接口的依据
。
public typealias FooBlock = () -> Void
func FooFunc(_ block: FooBlock) {
}
//@escaping 并不会构成重载,声明下面两个函数会报redeclaration错误。
//func FooFunc(_ block: @escaping FooBlock) {
//}
protocol FooProtocol {
func doBlock(block: @escaping FooBlock)
}
//但是@escaping会影响实现协议接口
class FooClass : FooProtocol {
//OK
func doBlock(block: @escaping () -> Void) {
block()
}
//会提示没有实现FooProtocol
// func doBlock(block: () -> Void) {
// block()
// }
}
- dispatch相关的方法都改写了,变得更加简洁,更加面向对象了。
//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相关函数构造都需要加上对应的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
等关键字。需要被继承的类、需要被override的方法,都要用open修饰。extension不能用open修饰,但是它的方法只要加了open修饰,也可以在子类里面override。
使用OC代码
- OC的
NS_Options
类型会转换成OptionSet
,而等于0的那一项作为默认值是看不到,非常诡异。默认值可以通过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容器里面,会自动转型为AnyObject。现在需要自己用
as
转型。Any和AnyObject的区别可以看到这篇文章:ANY 和 ANYOBJECT。 -
使用OC代码的时候,NSDictionary会变成
[AnyHashable: Any]
,很多时候还得转回Dictionary/NSDictionary继续使用,好在as
转型也是OK的。
typedef void (^LOGIN_COMPLETION_HANDLER) (BOOL isSuccessful, NSDictionary* loginResult);

- OC的构造函数如果返回
id
也会变成Any
类型,用的时候需要强转一下,比较恶心。所以要使用更新的instanceType
吧。
//会返回Any
+ (id) sharedInstantce;
//用的时候需要不断强转
(TBLoginCenter.sharedInstantce() as! TBLoginCenter).login()
//要用下面这种
+ (instancetype) sharedInstance;
坑
- 对于protocol中的optional接口,自动convert、手动处理可能会出错,搞错了不会有警告或者错误,但是代码逻辑会出错。

-
ImplicitlyUnwrappedOptional
语义变了,不会自动解包了。以前如果用到IUO的地方需要注意,要不然可能会出现下面这种情况。


客户端的问题很容易发现,传递给服务器端的参数如果也有同样的问题会很蛋疼,因此可以考虑在网络底层检查一下参数是否包含Optional
。如果发现有,那么直接abort掉。
- 类型冲突。比如自己也实现了一个
Error
对象,那么会跟Swift Error冲突,可以用Swift.Error
这种方式解决。
override open func webView(_ webView: UIView!, didFailLoadWithError error: Swift.Error!) {
super.webView(webView, didFailLoadWithError: error)
}
- 有些代码会导致Swift编译器进程崩溃。比如一个OC库里面有个这样的接口,最后的error参数用Nonnull修饰会导致Swift编译器编译过程中崩溃。
- (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;

苹果官方bug系统上也有人提过这个问题,参考:https://bugs.swift.org/browse/SR-3272。去掉Nonnull修饰即可通过编译。
编译显著变慢
升级Swift 3.0之后,感觉编译贼慢。根据:Profiling your Swift compilation times这篇文章的方法加上-Xfrontend -debug-time-function-bodies
之后,发现排名前五的方法都是50s左右。总的编译时间比2.3要慢一倍。2.3和3.0版本编译都很慢,但是3.0要更慢。
Swift 2.3: 342,403ms
Swift 3.0: 579,519ms


我顿时感到我大好青春都浪费在编译上面了,所以我们赶快来看看这段代码写了什么东西。
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)
}
}
这么大一段函数,初看没有明确的目标,于是我查找了资料,看看是否有前人的经验可以借鉴,结果果然有很多人遇到相同的问题,现有的总结已经很详细我不再赘述,这里主要参考了:Swift 工程速度编译慢。对比这篇总结,我猜测应该是下面这行将几个连续的??
相加导致的。
//老代码
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))
再跑一下测试命令,编译时间马上变成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)
基于这个思路,我主要修改了以下两种情况的代码。
- 几个
??
同时出现在一个表达式里面的,如上述代码 -
??
出现在字典里面的,如下面这种。
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 ?? ""
]
为了保持写代码的流畅性,不因为编译问题影响大家编码的体验,因此我只对几个特别耗时的地方做了修改,但是可以从测试结果看到,编译速度有了明显的提升,下面是测试后跑出来的时间。可以看到最慢的也只有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))
我们目前的做法是尽量不把这些复杂的操作写到一个表达式里面,先把变量存起来再放到表达式里计算,虽然是因为语言的问题不得不妥协但为了自己编译速度还是宁可多写几行。
参考资料
总结
因为Swift 3.0版本开始保证接口的稳定性,这次升级到3.0之后,使用Swift再无后顾之忧。希望苹果真的不要再干出Swift 1.0->2.0->3.0每次升级都要改大量代码的扯淡事。