Swift Tips:二、從Objective-C到Swift

Selector

selector是Objective-C runtime的概念,在調用一個selector前,要求selector方法加上@objc修飾。

實例方法的動態(tài)調用

我們可以做到在運行時才決定對哪個實例調用哪個方法。

class MyClass {
    func method(number: Int) -> Int {
        return number + 1
    }
}

let object = MyClass()
let f = MyClass.method
let objectMethod = f(object)
let result = objectMethod(1)

條件編譯

Swift依然可以使用條件編譯。
Swift內建了幾種平臺和架構的組合,來幫助我們?yōu)椴煌钠脚_編譯不同的代碼:

方法 可選參數(shù)
ox OSX, iOS
arch() x86_64, arm, arm64, i386
#if os(OSX)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif

也可以對自定義的符號進行條件編譯,比如定義一個免費版本標記FREE_VERSION:

#if ok
    print("免費版本")
#else
    print("收費版本")
#endif

為了使之有效,還需要在項目的編譯選項中進行設置,在項目的Build Settings中,找到Swift Compiler - Custom Flags,并在其中的 Other Swift Flags 加上 -D FREE_VERSION 就可以了。

編譯標記

Xcode將在導航欄顯示出編譯標記

// MARK: 你的標記
// MARK: -
// TODO:
// FIXME:

@objc和dynamic

添加@objc修飾符并不意味著這個方法或者屬性會變成動態(tài)派發(fā),Swift依然可能會將其優(yōu)化為靜態(tài)調用。如果你確實需要動態(tài)調用的特性,就加上dynamic

weak 和 unowned

如果您是一直寫 Objective-C 過來的,那么從表面的行為上來說 unowned 更像以前的 unsafe_unretained,而 weak 就是以前的 weak。用通俗的話說,就是 unowned 設置以后即使它原來引用的內容已經被釋放了,它仍然會保持對被已經釋放了的對象的一個 "無效的" 引用,它不能是 Optional 值,也不會被指向 nil。如果你嘗試調用這個引用的方法或者訪問成員屬性的話,程序就會崩潰。而 weak 則友好一些,在引用的內容被釋放后,標記為 weak 的成員將會自動地變成 nil (因此被標記為 weak 的變量一定需要是 Optional 值)。
關于兩者使用的選擇,Apple 給我們的建議是如果能夠確定在訪問時不會已被釋放的話,盡量使用unowned,如果存在被釋放的可能,那就選擇用 weak。

值類型和引用類型

Swift 的值類型,特別是數(shù)組和字典這樣的容器,在內存管理上經過了精心的設計。值類型的一個特點是在傳遞和賦值時進行復制,每次復制肯定會產生額外開銷,但是在 Swift 中這個消耗被控制在了最小范圍內,在沒有必要復制的時候,值類型的復制都是不會發(fā)生的。也就是說,簡單的賦值,參數(shù)的傳遞等等普通操作,雖然我們可能用不同的名字來回設置和傳遞值類型,但是在內存上它們都是同一塊內容。
值類型被復制的時機是值類型的內容發(fā)生改變時。
如果確實需要引用類型的容器,可以使用Cocoa的NSMutableArrayNSMutableDictionary

UnsafePointer

為了與龐大的 C 系帝國進行合作,Swift 定義了一套對 C 語言指針的訪問和轉換方法,那就是 UnsafePointer 和它的一系列變體。
對于使用 C API 時如果遇到接受內存地址作為參數(shù),或者返回是內存地址的情況,在 Swift 里會將它們轉為 UnsafePointer<Type> 的類型,比如說如果某個 API 在 C 中是這樣的話:

void method(const int *num) {
    printf("%d",*num);
}

其對應的 Swift 方法應該是:

func method(num: UnsafePointer<CInt>) {
    print(num.memory)
}

對于其他的 C 中基礎類型,在 Swift 中對應的類型都遵循統(tǒng)一的命名規(guī)則:在前面加上一個字母 C 并將原來的第一個字母大寫:比如 int,bool 和 char 的對應類型分別是 CInt,CBool 和 CChar。在上面的 C 方法中,我們接受一個 int 的指針,轉換到 Swift 里所對應的就是一個 CInt 的 UnsafePointer 類型。

獲取對象類型

使用type(of:)

let str = "hello"
let t = type(of: str)
debugPrint(t)
// Swift.String

自省

向一個對象發(fā)出詢問,以確定它是不是屬于某個類,這種操作就稱為自省。
在以前的Objective-C項目中,我們用

[obj1 isKindOfClass: [ClassA class]];
[obj2 isMemberOfClass: [ClassB class]];

-isKindOfClass:判斷obj1是否是ClassA或者其子類的實例對象;

-isMemberOfClass:判斷obj2是否就是ClassB的實例。

在Swift中,用關鍵字is就可以起到isKindOfClass的作用,并且可以用在structenum類型上。

class A {
    
}

class A1: A {
    
}

class B: NSObject {
    
}

class B1: B {
    
}

enum E1 {
    case ok
}

let aaa = A1()
print(aaa is A)

let bbb = B1()
print(bbb is B)

let eee = E1.ok
print(eee is E1)

KVO

在Swift中我們也是可以使用KVO的,但是僅限于在NSObject的子類中。這是可以理解的,因為KVO是基于KVC(Key-Value Coding)以及動態(tài)派發(fā)技術實現(xiàn)的,而這些東西都是Objective-C運行時的概念。

由于Swift為了效率,默認禁用了動態(tài)派發(fā),我們想要讓KVO正常工作,需要在被觀測屬性前加dynamic,在 Swift 4 后,需要同時加@objc dynamic

舉個栗子:

class MyClass: NSObject {
    @objc dynamic var date = Date()
}

private var myContext = 0

class ViewController: UIViewController {
    
    var myObject: MyClass!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        myObject = MyClass()
        print("當前日期:\(myObject.date)")
        
        myObject.addObserver(self, forKeyPath: "date", options: .new, context: &myContext)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            print("3秒后")
            self.myObject.date = Date()
        }
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let change = change, context == &myContext {
            if let date = change[.newKey] {
                print("日期發(fā)生變化:\(date)")
            }
        }
    }
}

打印結果:

當前日期:2017-12-04 06:18:52 +0000
3秒后
日期發(fā)生變化:2017-12-04 06:18:55 +0000

在Swift中使用KVO有兩個問題:

  1. 屬性必須要有dynamic才能被觀察,而有的類我們可能無法修改其源碼。這種情況下,一個可能可行的方案是繼承這個類,并將需要觀察的屬性使用dynamic進行重寫。
class MyClass: NSObject {
    var date = Date()
}

class MyChildClass: MyClass {
    @objc dynamic override var date: Date {
        get {
            return super.date
        }
        set {
            super.date = newValue
        }
    }
}
  1. 對于非NSObject的類型,Swift暫時還沒有類似KVO的觀察機制。我們可能只能通過屬性觀察來實現(xiàn)一套自己的類似替代了。

局部scope

在Objective-C中,我們有時會在方法內使用一對大括號來創(chuàng)創(chuàng)建臨時的作用域,以此分隔不相關聯(lián)的代碼,這在手寫視圖布局時特別有用。
但是在Swift中,直接寫大括號與閉包的定義沖突,這時可以定義一個接受() -> ()作為參數(shù)的全局方法,然后執(zhí)行它:

func local(closure: () -> ()) {
    closure()
}

class ViewController: UIViewController {
    override func loadView() {
        local {
            // ...
        }
        
        local {
            // ...
        }
    }
}

在Swift 2.0 中,為了處理異常,Apple加入了do這個關鍵字來作為捕獲異常的作用域。這一功能恰好為我們提供了一個完美的局部作用域,現(xiàn)在我們可以簡單地使用do來分隔代碼了:

class ViewController: UIViewController {
    override func loadView() {
        do {
            // ...
        }
        
        do {
            // ...
        }
    }
}

判等

對字符串的內容判等,我們可以簡單地使用==操作符來進行。
Equatable里聲明了這個操作符的接口方法:

public protocol Equatable {
    public static func ==(lhs: Self, rhs: Self) -> Bool
}

實現(xiàn)了Equatable的類型就可以使用==以及!=來進行相等判定了。!=由標準庫自動取反實現(xiàn)。

Swift的基本類型都重載了自己對應版本的==,而對于NSObject的子類來說,如果我們使用 == 并且沒有對于這個子類的重載的話,將轉為調用這個類的-isEqual:方法,如果子類沒有實現(xiàn)-isEqual:方法,則會使用NSObject的實現(xiàn),直接比較對象的內存地址。

如果要進行對象指針的判定,在Swift中是使用另一個操作符===

Swizzle

Swizzle是Objective-C運行時的黑魔法之一。
在Swift中也可以使用它,前提要把方法聲明為@objc
比如,置換UIButton的事件發(fā)送方法,以統(tǒng)計全局點擊:

extension UIButton {
    
    class func jx_swizzleSendAction() {
        let cls: AnyClass! = UIButton.self
        let originalSelector = #selector(sendAction(_:to:for:))
        let swizzledSelector = #selector(jx_sendAction(_:to:for:))
        
        let originalMethod = class_getInstanceMethod(cls, originalSelector)
        let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
        
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }
    
    @objc func jx_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
        print("swizzle tap!")
        jx_sendAction(action, to: target, for: event)
    }
}

jx_swizzleSendAction是我們定義用來設置方法置換的代碼,由于新版本Swift已經不能重寫+load+initialize方法了,所以我們只好在一個比較早的時機手動調用它,讓置換生效。

補充一點,可能你會疑惑為什么jx_sendAction方法中又調用了jx_sendAction,這不就死循環(huán)了嗎?
不會的。在A和B方法發(fā)生置換以后,你可以想象成兩個方法的方法名與方法體已經被相互交換了,當調用A方法時,實際執(zhí)行的是B的實現(xiàn)。

輸出格式化

如果想使用像%.2f這樣的方式取得格式化字符串,可以這樣:

let n = 1.23456789
let format = String.init(format: "%0.2f", n)
print(format)
// 1.23

數(shù)組enumerate

在Swift中,可以用for循環(huán)配合enumerated()取代OC的enumerateObjectsUsingBlock了。

let arr = [1, 2, 4, 5]
var result = 0
for (idx, num) in arr.enumerated() {
    result += num
    if idx == 2 {
        break
    }
}

sizeof和sizeofValue

Swift3以后,sizeof功能由MemoryLayout類封裝。
求類型所占內存大小,使用它的計算屬性size

let stringSize = MemoryLayout<String>.size
print("string size: \(stringSize)")
print("Uint16 size: \(MemoryLayout<UInt16>.size)")
// string size: 24
// Uint16 size: 2

求值(變量)的所占內存大小,用size(ofValue value: T) -> Int方法

let numArray: [UInt16] = [1, 2, 3, 4, 5]
print("numArray size: \(MemoryLayout.size(ofValue: numArray))")
// numArray size: 8

上例的numArray被sizeofValue后,得到結果為8,這其實是64位系統(tǒng)一個引用的長度。由此可見sizeofValue所返回的是這個值實際的大小,而非其意義內容的大小。
以下對枚舉做個測試,可體會一下:

enum MyEnum: UInt16 {
    case A = 0
    case B = 65535
}
print("MyEnum size: \(MemoryLayout<MyEnum>.size)")
print("MyEnum.A size: \(MemoryLayout.size(ofValue: MyEnum.A))")
print("MyEnum.B size: \(MemoryLayout.size(ofValue: MyEnum.B))")
print("MyEnum.A.rawValue size: \(MemoryLayout.size(ofValue: MyEnum.A.rawValue))")
// MyEnum size: 1
// MyEnum.A size: 1
// MyEnum.B size: 1
// MyEnum.A.rawValue size: 2

delegate

Cocoa 開發(fā)中接口-委托 (protocol-delegate) 模式是一種常用的設計模式,它貫穿于整個 Cocoa 框架中,為代碼之間的關系清理和解耦合做出了不可磨滅的貢獻。

一般我們希望delegate引用是weak的,但在Swift中如果直接這么寫的話,編譯器會報錯:

protocol MyDelegate {
    func method()
}

class AClass {
    weak var delegate: MyDelegate?
}
// 'weak' may only be applied to class and class-bound protocol types, not 'MyDelegate'

這是因為Swift的protocol除了可以被class遵守外,還可以被struct或enum這樣的非class遵守的,它本身不通過引用計數(shù)來管理內存,所以也不能用weak修飾。

想要在Swift中使用weak delegate,我們就需要將protocol限制在class內,在聲明后加上class關鍵字以限制:

protocol MyDelegate: class {
    func method()
}

class AClass {
    weak var delegate: MyDelegate?
}

Associated Object

Swift仍然不能通過Category向已有類添加成員變量,但我們還是可以使用OC運行時,將一個對象關聯(lián)到已有的要擴展的對象上。

class MyClass: NSObject {
}

private var key: Void?

extension MyClass {
    var title: String? {
        get {
            return objc_getAssociatedObject(self, &key) as? String
        }
        set {
            objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

這樣title在使用起來就像普通的屬性一樣。

synchronized

現(xiàn)版本的Swift是沒有@synchronized的,如果我們想保護一個對象在某個作用域內不被其它線程改變,可以這么做:

var anObject: Any = ""
objc_sync_enter(anObject)

// 在 enter 和 exit 之間,anObject 不會被其它線程改變

objc_sync_exit(anObject)

當然,也可以寫個全局方法,封裝起來,這樣就和以前的@synchronized的很像了:

func synchronized(_ object: Any, closure: () -> ()) {
    objc_sync_enter(object)
    closure()
    objc_sync_exit(object)
}

synchronized(anObject) {
    // 在括號內,anObject不會被其它線程改變
}

參考

  • Swifter - 100個 Swift 必備 Tips
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378