擁抱Swift!優酷Mac遷移Swift實踐

一、背景

隨着 Swift 5.0 的發佈,Swift 的 ABI 終於穩定下來了。如果是很早就擁抱 Swift 的開發者,一定經歷過各 Swift 大版本發佈時的痛苦。回想在前一家公司將 Swift 2.2 升級到 Swift 3.0,基本上是換了個語言,兩個版本之間的差異非常之大,升級起來簡直是苦不堪言。

另外 ABI 的穩定也讓 Swift 運行時環境可以隨着蘋果系統(iOS, Mac OS, Watch OS, TV OS)一起發佈,不用再將 Swift 加入應用包,減小了包的體積。

所以,如果以前不使用 Swift 的原因之一,是 Swift 不穩定造成的開發成本過大,那隨着 Swift 5.0 的發佈,終於可以拋開這個顧慮了。

二、爲什麼遷移 Swift

當然,僅僅就 ABI 穩定這一個原因,肯定不足以說服我們將 Objective-C(以下簡稱 OC)遷移到 Swift,我們還得看看使用 Swift 能夠給我們帶來什麼東西是 OC 沒有的。

  1. 安全編程

Swift 是一門靜態語言,雖然沒有 OC 動態特性的靈活,但是這也讓 Swift 更加安全。當然,靜態語言非常多,爲何 Swift 會特意強調“安全”這一特性?因爲 Swift 在語言層面做了很多工作。

1)可選值。可選值的引入明確了這樣一個問題:這個值是否存在。這使得我們在處理某個值的時候,能夠很清楚的知道是否應該去判斷這個值的有無,避免了不必要的 crash 問題;

2)值類型。Swift 中的 Struct 是一個值類型,它和引用類型的最大區別就是,將一個值類型賦給另外一個變量時,是通過值拷貝完成的(當然 Swift 用了 Copy-on-Write 的技術保證性能),我們就不用擔心拷貝之後使用的安全問題,不用擔心新變量的值修改之後會影響到原來的值;

3)更多安全的關鍵字。guard讓我們在執行接下來的代碼前保證某一個條件的成立,並且使程序可讀性更高;defer避免我們忘記在代碼塊執行完畢後所需要執行的清理工作。

  1. 編程範式的豐富

1)支持函數式編程。函數作爲 Swift 中的一等公民,Swift 可以支持函數式編程。我們可以使用函數式編程的無狀態性,不可變性,無副作用這些特性寫出更健壯的代碼;

2)面向協議編程。Swift 中也有協議 protocol,不過 Swift 中的 protocol 比起 OC 中的 protocol 強大太多。我們可以擴展協議給協議中的方法給一個默認實現。這樣我們就可以在不改動已有類或 Struct 的前提下添加能力,非常的方便;

3)強大泛型。泛型的引入可以讓我們編寫一些更加通用的代碼,使代碼更加靈活,可用性更高。

  1. 其它

如沒有頭文件減輕了複雜性,讓代碼量更少;利用元組(tuple)支持多返回值減少了一些不必要的模型等等。這些都使代碼更簡潔,更清晰。

三、遷移實戰

  1. 從哪裏開始遷移?

Swift 和 OC 可以相互調用,但是由於 Swift 新增了一些新的數據結構,如 Enum、 Struct 等,因此 OC 調用 Swift 時有一定的侷限性,需要做的一些額外的工作。反過來,當 Swift 調用 OC 時則容易得多。

一般來說,大多數 iOS 工程都有如下結構:

從上圖可以看出,UIViewController 和 UIView 都是在最上層,很少有其它模塊依賴它們。即使有,也是其它模塊的 UIViewController 或 UIView 對其有依賴。因此,我們在遷移的時候可以從 UIViewController 和 UIView 相關類進行遷移。這樣的話,將這些類遷移爲 Swift 後,可以順利的調用 OC 相關的類和 API。

另外,從穩定性的角度來看,從上到下的遷移也更安全。如果我們從下層的一些中間件或基礎庫開始遷移的話,由於上層的大多數業務模塊都對中間件或者基礎庫有依賴,我們對下層模塊的修改就會影響多個業務模塊,而且通常不知道這些下層模塊到底被上層的哪些模塊所調用。如果修改出現問題就會影響到非常多的模塊,更糟的是,如果是一些使用頻率比較少的業務場景對這些下層模塊有依賴,那麼可能在開發過程中很難發現問題,不知不覺的就帶到線上,造成比較大的影響。

因此,如需要將 OC 遷移到 Swift,建議按照“從上到下”的原則進行遷移。這樣既保證了工作量不會太大(不用去寫一些 OC 調用 Swift 的適配代碼)也保證了遷移後的穩定性,測試

只需迴歸一下遷移過的業務模塊即可,還可以快速定位問題。

  1. 如何使用 Swift 的值類型

我們都知道 Swift 引入了兩個重要的數據結構 Struct 和 Enum,這兩個都是值類型。值類型我們都知道是非常安全的,例如下面這段代碼:

var a = 1
var b = a
b = 2

我們可以隨意更改 b 的值而不用擔心 a 的值會受任何影響,因爲值類型的賦值都是通過拷貝來進行的(並使用 Copy-on-Write 的技術來保證性能)。另外,值類型相較於引用類型來說,減少了堆上的內存分配和回收次數。理論上來說,如果能用 Struct 類型就儘量使用 Struct 類型。

@interface Video: NSObject

@property (nonatomic, copy) NSString *videoId;
@property (nonatomic, copy) NSString *videoTitle;
@property (nonatomic, copy) NSString *videoSubtitle;

@end

例如我們有一個 Model 叫 Video,然後我們用 Struct 可以寫成這樣:

struct Video {
   let videoId: String
   let videoTitle: String
   let videoSubtitle: String
}

這樣改寫不僅將我們的 Model 改成更加安全的值類型 Struct,還利用了 let 關鍵字將裏面的屬性改爲不可變的,使代碼更加安全。

哪些又需要改成 Enum 類型呢?Enum 類型特別適合那種有明顯種類區別的場景。例如以下代碼:

typedef NS_ENUM(NSUInteger, TradeType) {
   TradeTypeVip = 0,
   TradeTypeSingleVideo
};


@interface TradeManager: NSObject

- (void)buyWithType:(TradeType)type;

@end

比如我們的支付場景分爲購買 VIP 會員,購買單片。在 OC 中我們會定義一個 enum 來區分不同的購買類型,然後通過TradeManager相關 API 來進行購買。

[[TradeManager sharedManager] buyWithType:TradeTypeVip]

而在 Swift 中我們可以把它改成這樣

enum Trade {
   case VIP(userId: String)
   case singleVideo(videoID: String)

   func buy() {
      switch self {
      case .VIP(let userId):
         // buy vip with user Id

      case .singleVideo(let videoId):
         // buy single video with video id
      }
   }
}

我們把購買的邏輯全部放到 enum 中,利用 enum 的關聯值來進行相關參數的傳遞,而且 Swift 中的 enum 類型可以添加方法,所以我們可以把購買的業務邏輯都放到一起,通過 switch 來進行判斷。

然後我們就可以這樣使用:

Trade.VIP(userId: "349951").buy()

非常的簡潔明瞭。

  1. 混編問題

雖然我們可以按照“從上到下”的原則開始遷移,但是即使是這樣也免不了需要 OC 去調用 Swift 的代碼,有一些地方還是得處理一下。

我們都知道,OC 是一門動態語言,所有對象都是基於運行時的。而 Swift 則是一門靜態語言,除了某一些特性可能需要在運行時完成(如反射),絕大部分的工作都是在編譯時就確定了的(例如 Swift 類型的成員變量或方法)。我們來看一段代碼:

// method in OC file
- (void)someMethod {
   A *a = [A new];
   [a doWork];
}

// class in A.swift
class A: NSObject {
   func doWork() {
      // do something
   }
}

上面的代碼中,能編譯通過嗎?

當然不能,因爲 Swift 的類型缺少一些運行時所需要信息,會導致失敗,編譯器會報出No visible @interface for 'CMSFilterViewController' declares the selector ‘reloadWith:channelName:'的錯誤。解決方法也很簡單,在所需要使用到的前添加@objc即可。

需要注意的是,Swift 的 class 類型必須繼承 NSObject 才能被 OC 所調用。

// class in A.swift
class A: NSObject {
   @objc func doWork() {
      // do something
   }
}

這樣,OC 就能夠找到 Swift 類型中相應的方法(屬性同理)。

必須說明的一點是,標記爲@objc 並不意味着這個方法就是動態派發的,它依然是靜態調用。如果想要運行時相關的特性,必須使用 dynamic 關鍵字,這裏不再贅述。

  1. 在 Swift 那些消失的東西

在遷移到 Swift 的過程中,我們會發現某些代碼並不能在 Swift 中找到對應類或者方法來處理,下面是一些典型的例子。

1)@synchronized

在性能要求不是太高的情況下,我們通常會使用@synchronized來爲一個對象加上鎖,而 Swift 已經沒有相關的關鍵字了,所以我們需要做一些額外的工作。

@synchronized 本質上來講是一個互斥鎖,背後其實是調用了 objc_sync_enterobjc_sync_exit 方法來實現的,所以,我們可以自己寫一個類似的方法:

func synchronized(_ lock: AnyObject, block: () -> Void) {
    objc_sync_enter(lock)
    block()
    objc_sync_exit(lock)
}

在使用時我們利用 Swift 的 Trailing Closure 可以寫出類似 OC 的代碼,非常的優美:

func addObject(obj: AnyObject) {
    synchronized(self) {
        // do something
    }
}

2)單例

在 OC 中,我們的單例基本都是這樣寫的:

+ (instancetype)sharedInstance {
   static id sharedInstance = nil;
   static dispatch_once_t onceToken = 0;
   dispatch_once(&onceToken, ^{
      sharedInstance = [[self alloc] init];
   });
   return sharedInstance;
}

而在 Swift 中,我們直接定義一個靜態常量就可以定義一個單例:

static let shared = YourObject()

不僅代碼量更少,意義也更加明確。

3)dispatch_once

就像上面 OC 代碼那樣,一般是使用 dispatch_once 來實現一個單例。但是 Swift 中已經沒有 dispatch_once 這個方法了,那如果非要要使用的話應該怎麼辦呢?我們可以這樣定義:

public extension DispatchQueue {

   private static var _onceTokens = String

   public class func once(token: String, block:()->Void) {
      objc_sync_enter(self)
      defer { objc_sync_exit(self) }

      if _onceTokens.contains(token) {
         return
      }

      _onceTokens.append(token)
      block()
   }
}

我們利用 Swift 的extension給 DispatchQueue 添加一個類方法,然後可以這樣使用:

DispatchQueue.once(token: "oncetoken") {
    // do something
}

當然,OC 和 Swift 區別遠遠不止於此,包括 Swift 對 C 的調用,日誌的打印等等都有很

多可以深究的點,限於篇幅原因就不再贅述。

四、計劃和展望

目前優酷 Mac 端還在繼續遷移中,一方面需要進行正常的業務迭代,並不能投入太多的人力專門進行遷移,目前的做法是新的需求使用 Swift 進行開發,如果有依賴到原來 OC 的相關模塊,根據工作量來進行一部分的遷移;另一方面就是考慮到項目的穩定性,也不會直接把所有 OC 代碼遷移到 Swift 上,逐步遷移也方便測試人員進行針對性的迴歸。

Swift 所帶來的肯定不只是語言層面的這些優點。 WWDC 2019 年發佈的 Swift UI 不僅可以使用更加簡潔的語法來進行 UI 開發,最重要的是可以使用同一個 UI 組件庫來開發 Mac OS 和 iOS 上的界面,讓一套代碼在 Mac 和 iOS 設備上運行提供了可能性。

另外,Swift 作爲一個跨平臺語言不只是在蘋果相關的平臺上運行,目前 Swift 還支持 Linux 系統,我們可以在 Linux 系統上將 Swift 作爲開發語言進行開發。目前已經有跨 Android 和 iOS 的 UI 庫 SCADE,可以讓我們同一套代碼來開發 Android 和 iOS 的界面。

可能也有人會問,跨平臺現在有了 flutter,我們還學習 Swift 幹嘛呢?確實 flutter 作爲一個非常優秀的跨平臺方案,它有着優秀的渲染性能,並且支持非常多的平臺(iOS,Android, Mac OS 甚至是 Windows)。但是我們也知道,flutter 是一個 UI 組件庫,它可以幫助我們解決一部分 UI 問題,但是再往下呢?還是得使用 OC 或者 Swift。有人也會說直接用 OC 不就好了。可是我們可以看到一個現象,現在蘋果的官方文檔上面,基本上都是使用 Swift 來編寫相關的代碼示例,蘋果也是在慢慢的“拋棄”OC 這一門語言。

不管從“明裏”還是“暗裏”來看,蘋果都是在大力推薦使用 Swift 這一門語言。作爲蘋果的“親兒子”,相信 Swift 語言將會是開發 MacOS 和 iOS 的第一選擇。

所以,如果有人問我什麼時候可以開始學習 Swift,那我的答案是:現在。

作者 | 阿里文娛高級無線開發工程師 大斗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章