仓前iOS研究组Swift开发

阿里云iOS客户端适配Swift 3.0小记

2017-01-03  本文已影响967人  阿呆少爷

阿里云App从Swift 2.1开始使用Swift,随时不断的推进,现在所有的业务代码都用Swift编写。由于Swift 3.0语法上有诸多改变,所以从Swift 2.3升级到Swift 3.0是一件宜早不宜迟的事情。元旦期间抽了点时间做这个升级。

外部依赖

升级过程

screenshot.png

细节

Swift 3.0改变最大的地方如下所示。

//Swift 2.3
label.textAlignment = .Right

//Swift 3.0
label.textAlignment = .right
//Swift 2.3
label.textColor = UIColor.redColor()

//Swift 3.0
label.textColor = UIColor.red
//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 {

}
//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
  }
}
_ = self.navigationController?.popViewController(animated: true)

自己定义的接口可以使用@discardableResult消除警告,对于链式构造函数来说非常有用。

@discardableResult
open class func routeURL(_ url: URL?) -> Bool {
    return JLRoutes.routeURL(url)
}
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()
//    }
}
//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()
})
//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

使用OC代码

//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.
}
typedef void (^LOGIN_COMPLETION_HANDLER) (BOOL isSuccessful, NSDictionary* loginResult);
screenshot.png
//会返回Any
+ (id) sharedInstantce;

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

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

screenshot.png screenshot.png screenshot.png

客户端的问题很容易发现,传递给服务器端的参数如果也有同样的问题会很蛋疼,因此可以考虑在网络底层检查一下参数是否包含Optional。如果发现有,那么直接abort掉。

override open func webView(_ webView: UIView!, didFailLoadWithError error: Swift.Error!) {
    super.webView(webView, didFailLoadWithError: error)
}
- (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。去掉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
  2. Swift3.0变化

总结

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

上一篇 下一篇

猜你喜欢

热点阅读