Swift 2.3升级到Swift 3.0小记

简介: > 阿里云App从Swift 2.1开始使用Swift,随时不断的推进,现在所有的业务代码都用Swift编写。由于Swift 3.0语法上有诸多改变,所以从Swift 2.3升级到Swift 3.0是一件宜早不宜迟的事情。元旦期间抽了点时间做这个升级。 ### 外部依赖 * 目前开源社区对Swift 3.0支持是非常好的,我们依赖的开源组件最新版本都支持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的语法。组件化的好处是可以并行,先解决基础组件,然后大家并行解决高级组件。

screenshot.png

  • 最后升级主工程,能解决的解决掉,不能解决的加上//TODO: Swift 3.0 小明这样的注释。保证主工程能运行通过之后,大家并行解决这些TODO问题。
  • Xcode自带的convert还是很给力的,因为Xcode不会一次转到位,可以不断执行convert进行转换。不过convert有点费时,主工程有将近400个Swift文件,完整的convert一次需要两个小时左右,所以后面我都是自己根据已知的规则去做replace。Xcode目前只支持对target做convert,不支持对文件或者代码片段做convert,有点蛋疼。

screenshot.png

  • 总耗时。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也不能比较。
//Swift 3.0
let foo : Float? = 1.0
let bar : Float? = 2.0
if let _foo = foo,
    let _bar = bar,
    _foo > _bar {

}

为了避免编译失败,Xcode会自动给写了Optional比较的老代码的文件顶上加上下面这段代码。

// 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
  • 新增openfileprivate等关键字。需要被继承的类、需要被override的方法,都要用open修饰。extension不能用open修饰,但是它的方法只要加了open修饰,也可以在子类里面override。
  • 修改了Protocol Composition语法。
//swift 2.x
typealias CompositeType = protocol<Interface1, Interface2, Interface3>

//swift 3.0
typealias CompositeType = Interface1 & Interface2 & Interface3  
  • 去除了sizeof,统一使用MemoryLayout来计算内存大小。
  • 指针的使用更加严格。

    • swift 2.x可以使用UnsafePointer<Void>, UnsafeMutablePointer<Void>来表示void *。在swift 3.0中,UnsafePointer<T>, UnsafeMutablePointer<T>表示指针指向的内存必须是绑定数据类型的,新增了UnsafeRawPointer , UnsafeMutableRawPointer 来表示指针指向的内存并没有被绑定具体的数据类型。所以UnsafePointer<Void>, UnsafeMutablePointer<Void>都需要用UnsafeRawPointer , UnsafeMutableRawPointer 来替代。
    • 指针的值属性从memory改为了pointee。
    • 指针类型转换也发生了变化。强制UnsafePointer<T>UnsafePointer<U>的转换是不允许的,这是一种很危险的,不可预知的行为。不同类型指针的转换必须要用指定的函数:
//指针类型转换函数:
1、UnsafePointer.withMemoryRebound(to:capacity:_)
2、UnsafeRawPointer.assumingMemoryBound(to:)
3、UnsafeRawPointer.bindMemory(to:capacity:)

//简单示例:

//swift 2.x
let ptrT: UnsafeMutablePointer<Int> = UnsafeMutablePointer<Int>.alloc(1)
ptrT.memory = 3
let ptrU = UnsafePointer<UInt>(ptrT)

//上面的写法在swift 3.0会报错。
//error: 'init' is unavailable: use 'withMemoryRebound(to:capacity:_)' to temporarily view memory as another layout-compatible type.

//swift 3.0
let ptrT: UnsafeMutablePointer<Int> = UnsafeMutablePointer<Int>.allocate(capacity: 1)
ptrT.pointee = 3
var ptrU: UnsafeMutablePointer<UInt>?
ptrT.withMemoryRebound(to: UInt.self, capacity: 1) { ptr in
    ptrU = ptr
}

使用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);

screenshot.png

  • OC的构造函数如果返回id也会变成Any类型,用的时候需要强转一下,比较恶心。所以要使用更新的instanceType吧。
//会返回Any
+ (id) sharedInstantce;

//用的时候需要不断强转
(TBLoginCenter.sharedInstantce() as! TBLoginCenter).login()

//要用下面这种
+ (instancetype) sharedInstance;

  • 对于protocol中的optional接口,自动convert、手动处理可能会出错,搞错了不会有警告或者错误,但是代码逻辑会出错。

screenshot.png

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

screenshot.png

screenshot.png

客户端的问题很容易发现,传递给服务器端的参数如果也有同样的问题会很蛋疼,因此可以考虑在网络底层检查一下参数是否包含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;

Snip20170103_1.png

苹果官方bug系统上也有人提过这个问题,参考:https://bugs.swift.org/browse/SR-3272。目前的解决方案是希望ARUP SDK针对swift 3出个适配版,去掉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

screenshot.png

screenshot.png

我顿时感到我大好青春都浪费在编译上面了,所以我们赶快来看看这段代码写了什么东西。

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))

我们目前的做法是尽量不把这些复杂的操作写到一个表达式里面,先把变量存起来再放到表达式里计算,虽然是因为语言的问题不得不妥协但为了自己编译速度还是宁可多写几行。

参考资料

  1. Migrating to Swift 2.3 or Swift 3 from Swift 2.2

总结

因为Swift 3.0版本开始保证接口的稳定性,这次升级到3.0之后,使用Swift再无后顾之忧。希望苹果真的不要再干出Swift 1.0->2.0->3.0每次升级都要改大量代码的扯淡事。

相关文章
|
5月前
|
安全 编译器 Swift
苹果重磅发布Swift 6:在 Swift 6 中如何实现并发开发?相比Swift 5.5 有哪些重磅升级?
Swift 6 强化了并发编程,引入结构化并发、任务、执行器、隔离、同步原语、类型化错误处理和取消超时功能。对比Swift 5.5,它默认启用全面并发检查,改进错误处理,增加了隔离区域、类型化抛出、包迭代等新特性,优化了性能并更新了库。
108 2
|
Swift 开发者
Swift - swift3.0中代理方法的使用
Swift - swift3.0中代理方法的使用
98 0
|
移动开发 前端开发 weex
手淘 App 如何落地 Swift ?一边探索实践,一边“打怪升级”
自从 2014 年苹果发布 Swift 至今,历经多次迭代,Swift 终于迎来了 ABI 稳定,SwiftUI 的发布也引起无数 Apple 平台开发者的欢呼。多年来广受关注而又备受争议的 Swift,终于开始被很多大型 App 坚定地采用。在这其中,淘宝 App 就是一个典型:从 Swift 2.0 时代的浅尝辄止,到去年 3 月 ABI 稳定后充分调研并正式采用,这其间经历了什么样的考量过程?淘宝 App 落地 Swift 的最关键环节有哪些?如何解决落地 Swift 过程中的挑战?
1880 0
手淘 App 如何落地 Swift ?一边探索实践,一边“打怪升级”
|
7月前
|
安全 编译器 Swift
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
433 2
|
5月前
|
Unix 调度 Swift
苹果iOS新手开发之Swift 中获取时间戳有哪些方式?
在Swift中获取时间戳有四种常见方式:1) 使用`Date`对象获取秒级或毫秒级时间戳;2) 通过`CFAbsoluteTimeGetCurrent`获取Core Foundation的秒数,需转换为Unix时间戳;3) 使用`DispatchTime.now()`获取纳秒级精度的调度时间点;4) `ProcessInfo`提供设备启动后的秒数,不表示绝对时间。不同方法适用于不同的精度和场景需求。
182 3
|
1月前
|
安全 Swift iOS开发
Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法
本文深入探讨了 Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法。Swift 以其简洁、高效和类型安全的特点,结合 UIKit 丰富的组件和功能,为开发者提供了强大的工具。文章从 Swift 的语法优势、类型安全、编程模型以及与 UIKit 的集成,到 UIKit 的主要组件和功能,再到构建界面的实践技巧和实际案例分析,全面介绍了如何利用这些技术创建高质量的用户界面。
32 2
|
1月前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户点击按钮时,按钮将从圆形变为椭圆形,颜色从蓝色渐变到绿色;释放按钮时,动画以相反方式恢复。通过UIView的动画方法和弹簧动画效果,实现平滑自然的过渡。
59 1
|
2月前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
【10月更文挑战第18天】本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户按下按钮时,按钮将从圆形变为椭圆形并从蓝色渐变为绿色;释放按钮时,动画恢复原状。通过UIView的动画方法和弹簧动画效果,实现平滑自然的动画过渡。
62 5
|
4月前
|
存储 移动开发 Swift
使用Swift进行iOS应用开发:探索现代移动开发的魅力
【8月更文挑战第12天】使用Swift进行iOS应用开发,不仅能够享受到Swift语言带来的简洁、快速、安全的编程体验,还能够充分利用iOS平台提供的丰富资源和强大功能。然而,iOS应用开发并非易事,需要开发者具备扎实的编程基础、丰富的实践经验和不断学习的精神。希望本文能够为您的iOS应用开发之旅提供一些有益的参考和帮助。