Swift2.3升級到Swift3.0小記

阿呆少爺發表於2018-07-10

阿里雲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每次升級都要改大量程式碼的扯淡事。


相關文章