iOS開(kāi)發(fā)必備知識(shí),讓你理解OC到Swift的都有哪些轉(zhuǎn)變

1、Selector

@selectorObjective-C 時(shí)代的一個(gè)關(guān)鍵字,它可以將一個(gè)方法轉(zhuǎn)換并賦值給一個(gè) SEL 類(lèi)型,它的表現(xiàn)很類(lèi)似一個(gè)動(dòng)態(tài)的函數(shù)指針。在 Objective-C 時(shí) selector 非常常用,從設(shè)定target-action,到自舉詢問(wèn)是否響應(yīng)某個(gè)方法,再到指定接受通知時(shí)需要調(diào)用的方法等等,都是由 selector 來(lái)負(fù)責(zé)的。在 Objective-C 里生成一個(gè) selector 的方法一般是這個(gè)樣子的

-(void) callMe {
    //...
}

-(void) callMeWithParam:(id)obj {
    //...
}

SEL someMethod = @selector(callMe);
SEL anotherMethod = @selector(callMeWithParam:);

// 或者也可以使用 NSSelectorFromString
// SEL someMethod = NSSelectorFromString(@"callMe");
// SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:"); 
復(fù)制代碼

一般為了方便,很多人會(huì)選擇使用 @selector,但是如果要追求靈活的話,可能會(huì)更愿意使用 NSSelectorFromString 的版本 -- 因?yàn)槲覀兛梢栽谶\(yùn)行時(shí)動(dòng)態(tài)生成字符串,通過(guò)方法名來(lái)調(diào)用對(duì)應(yīng)的方法

在 Swift 中沒(méi)有 @selector 了,取而代之,從 Swift 2.2 開(kāi)始我們使用 #selector 來(lái)從暴露給 Objective-C 的代碼中獲取一個(gè) selector。類(lèi)似地,在 Swift 里對(duì)應(yīng)原來(lái) SEL的類(lèi)型是一個(gè)叫做 Selector 的結(jié)構(gòu)體

@objc func callMe() {
    //...
}
@objc func callMeWithParam(obj: AnyObject!) {
    //...
}
let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam(obj:))  
復(fù)制代碼

【注】selector 其實(shí)是 Objective-C runtime 的概念。在 Swift 4 中,默認(rèn)情況下所有的 Swift 方法在 Objective-C 中都是不可見(jiàn)的,所以你需要在這類(lèi)方法前面加上 @objc 關(guān)鍵字,將這個(gè)方法暴露給 Objective-C,才能進(jìn)行使用

如果方法名字在方法所在域內(nèi)是唯一的話,我們可以簡(jiǎn)單地只是用方法的名字來(lái)作為 #selector 的內(nèi)容。相比于前面帶有冒號(hào)的完整的形式來(lái)說(shuō),這么寫(xiě)起來(lái)會(huì)方便一些

let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam)
復(fù)制代碼

如果同一個(gè)作用域里面存在同樣名字的兩個(gè)方法,但是參數(shù)不同,我們可以通過(guò)將方法強(qiáng)制轉(zhuǎn)換來(lái)使用

@objc func commonFunc() {}

@objc func commonFunc(input: Int) -> Int {
    return input
} 

let method1 = #selector(commonFunc as ()->())
let method2 = #selector(commonFunc as (Int)->Int)
復(fù)制代碼

作為一個(gè)開(kāi)發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:761407670 進(jìn)群密碼000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長(zhǎng)!

提供逆向安防、Swift、算法、架構(gòu)設(shè)計(jì)、多線程,網(wǎng)絡(luò)進(jìn)階,還有底層、音視頻、Flutter等資料


2、實(shí)例方法的動(dòng)態(tài)調(diào)用

class MyClass {
    func method(number: Int) -> Int {
        return number + 1
    }
}
復(fù)制代碼

想要調(diào)用method方法的話,最普通的使用方式是生成MyClass的實(shí)例,然后用 .method 來(lái)調(diào)用它

let cls = MyClass()
cls.method(number: 1)
復(fù)制代碼

我們還可以把剛才的方法該成下面這樣

let f = MyClass.method
let object = MyClass()
let result = f(object)(1)
復(fù)制代碼

我們觀察f類(lèi):alt+單擊

let f: (MyClass) -> (Int) -> Int
復(fù)制代碼

其實(shí)對(duì)于 Type.instanceMethod 這樣的取值語(yǔ)句,實(shí)際上剛才

let f = MyClass.method
復(fù)制代碼

做的事情類(lèi)似于下面字面量的轉(zhuǎn)換

let f = { (obj: MyClass) in obj.method }
復(fù)制代碼

3、單例

在OC中單例的公認(rèn)寫(xiě)法

@implementation MyManager
+ (id)sharedManager {
    static MyManager * staticInstance = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        staticInstance = [[self alloc] init];
    });
    return staticInstance;
}
@end 
復(fù)制代碼

使用 GCD 中的 dispatch_once_t 可以保證里面的代碼只被調(diào)用一次,以此保證單例在線程上的安全

在Swift中移出了dispatch_once,但是我們有更簡(jiǎn)單的寫(xiě)法

class MyManager  {
    static let shared = MyManager()
    private init() {}
}
復(fù)制代碼

4、條件編譯

在 C 系語(yǔ)言中,可以使用#if或者 #ifdef 之類(lèi)的編譯條件分支來(lái)控制哪些代碼需要編譯,而哪些代碼不需要。Swift 中沒(méi)有宏定義的概念,因此我們不能使用#ifdef 的方法來(lái)檢查某個(gè)符號(hào)是否經(jīng)過(guò)宏定義。但是為了控制編譯流程和內(nèi)容,Swift 還是為我們提供了幾種簡(jiǎn)單的機(jī)制來(lái)根據(jù)需求定制編譯內(nèi)容的。

首先是 #if 這一套編譯標(biāo)記還是存在的,#elseif#else 是可選的。

#if <condition>

#elseif <condition>

#else

#endif
復(fù)制代碼

但是這幾個(gè)表達(dá)式里的 condition 并不是任意的。Swift 內(nèi)建了幾種平臺(tái)和架構(gòu)的組合,來(lái)幫助我們?yōu)椴煌钠脚_(tái)編譯不同的代碼,具體地

方法 可選參數(shù)
os() macOS, iOS, tvOS, watchOS, Linux
arch() x86_64, arm, arm64, i386
swift() >= 某個(gè)版本

如果我們統(tǒng)一我們?cè)?iOS 平臺(tái)和 Mac 平臺(tái)的關(guān)于顏色的 API 的話,一種可能的方法就是配合 typealias 進(jìn)行條件編譯:

#if os(macOS)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif 

#if arch(x86_64)

#else

#endif

#if swift(>=14.0)

#else

#endif
復(fù)制代碼

對(duì)自定義符號(hào)進(jìn)行編譯

我們需要使用同一個(gè) target 完成同一個(gè) app 的收費(fèi)版和免費(fèi)版兩個(gè)版本,并且希望在點(diǎn)擊某個(gè)按鈕時(shí)收費(fèi)版本執(zhí)行功能,而免費(fèi)版本彈出提示的話,可以使用類(lèi)似下面的方法

func someButtonPressed(sender: AnyObject!) {
    #if FREE_VERSION
        // 彈出購(gòu)買(mǎi)提示,導(dǎo)航至商店等
    #else
        // 實(shí)際功能
    #endif
}
復(fù)制代碼

在這里我們用 FREE_VERSION 這個(gè)編譯符號(hào)來(lái)代表免費(fèi)版本。為了使之有效,我們需要在項(xiàng)目的編譯選項(xiàng)中進(jìn)行設(shè)置,在項(xiàng)目的 Build Settings 中,找到 Swift Compiler - Custom Flags,并在其中的Other Swift Flags加上-D FREE_VERSION 就可以了。

5、@UIApplicationMain

在 C 系語(yǔ)言中,程序的入口都是 main 函數(shù)。對(duì)于一個(gè) Objective-C 的 iOS app 項(xiàng)目,在新建項(xiàng)目時(shí), Xcode 將幫我們準(zhǔn)備好一個(gè) main.m 文件,其中就有這個(gè) main 函數(shù)

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil,
                   NSStringFromClass([AppDelegate class]));
    }
}
復(fù)制代碼

這個(gè)方法將根據(jù)第三個(gè)參數(shù)初始化一個(gè) UIApplication 或其子類(lèi)的對(duì)象并開(kāi)始接收事件 (在這個(gè)例子中傳入 nil,意味使用默認(rèn)的 UIApplication)。最后一個(gè)參數(shù)指定了 AppDelegate 類(lèi)作為應(yīng)用的委托,它被用來(lái)接收類(lèi)似 didFinishLaunching 或者 didEnterBackground 這樣的與應(yīng)用生命周期相關(guān)的委托方法。另外,雖然這個(gè)方法標(biāo)明為返回一個(gè) int,但是其實(shí)它并不會(huì)真正返回。它會(huì)一直存在于內(nèi)存中,直到用戶或者系統(tǒng)將其強(qiáng)制終止

新建一個(gè) Swift 的 iOS app 項(xiàng)目后,我們會(huì)發(fā)現(xiàn)所有文件中都沒(méi)有一個(gè)像 Objective-C 時(shí)那樣的 main 文件,也不存在 main 函數(shù)。唯一和main有關(guān)系的是在默認(rèn)的 AppDelegate 類(lèi)的聲明上方有一個(gè) @UIApplicationMain 的標(biāo)簽。

其實(shí) Swift 的 app 也是需要 main 函數(shù)的,只不過(guò)默認(rèn)情況下是 @UIApplicationMain 幫助我們自動(dòng)生成了而已。

如我們?cè)趧h除 @UIApplicationMain 后,在項(xiàng)目中添加一個(gè) main.swift 文件,然后加上這樣的代碼

UIApplicationMain(Process.argc, Process.unsafeArgv, nil,
    NSStringFromClass(AppDelegate)) 
復(fù)制代碼

現(xiàn)在編譯運(yùn)行,就不會(huì)再出現(xiàn)錯(cuò)誤了。當(dāng)然,我們還可以通過(guò)將第三個(gè)參數(shù)替換成自己的 UIApplication 子類(lèi),這樣我們就可以輕易地做一些控制整個(gè)應(yīng)用行為的事情了。比如將 main.swift 的內(nèi)容換成

UIApplicationMain(
    CommandLine.argc,
    UnsafeMutableRawPointer(CommandLine.unsafeArgv)
        .bindMemory(
            to: UnsafeMutablePointer<Int8>.self,
            capacity: Int(CommandLine.argc)),
    NSStringFromClass(MyApplication.self),
    NSStringFromClass(AppDelegate.self)
)

import UIKit
class MyApplication: UIApplication {
    override func sendEvent(_ event: UIEvent) {
        super.sendEvent(event)
        print("Event sent:\(event)")
    }
}

let cls = MyClass()
cls.mustProtocolMethod()
cls.mustProtocolMethod1()
復(fù)制代碼

這樣每次發(fā)送事件 (比如點(diǎn)擊按鈕) 時(shí),我們都可以監(jiān)聽(tīng)到這個(gè)事件了

6、可選協(xié)議和協(xié)議擴(kuò)展

Objective-C 中的 protocol 里存在 @optional 關(guān)鍵字,被這個(gè)關(guān)鍵字修飾的方法并非必須要被實(shí)現(xiàn)。我們可以通過(guò)協(xié)議定義一系列方法,然后由實(shí)現(xiàn)協(xié)議的類(lèi)選擇性地實(shí)現(xiàn)其中幾個(gè)方法。最好的例子我想應(yīng)該是 UITableViewDataSource 和 UITableViewDelegate。前者中有兩個(gè)必要方法

-tableView:numberOfRowsInSection:
-tableView:cellForRowAtIndexPath:  
復(fù)制代碼

原生的 Swift protocol 里沒(méi)有可選項(xiàng),所有定義的方法都是必須實(shí)現(xiàn)的

protocol MyProtocol {
    func mustProtocolMethod() //必須實(shí)現(xiàn)方法
    func mustProtocolMethod1() //必須實(shí)現(xiàn)方法
}

class MyClass: MyProtocol {
    func mustProtocolMethod() {
        print("MyClass-->必須實(shí)現(xiàn)方法:mustProtocolMethod")
    }

    func mustProtocolMethod1() {
        print("MyClass-->必須實(shí)現(xiàn)方法:mustProtocolMethod1")
    }

}
復(fù)制代碼

如果我們想要像 Objective-C 里那樣定義可選的協(xié)議方法,就需要將協(xié)議本身和可選方法都定義為Objective-C 的,也即在 protocol 定義之前以及協(xié)議方法之前加上 @objc。另外和 Objective-C 中的 @optional 不同,我們使用沒(méi)有 @ 符號(hào)的關(guān)鍵字 optional 來(lái)定義可選方法

@objc protocol MyProtocol1 {
    @objc optional func optionalProtocolMethod() //可選方法
    func mustProtocolMethod1() //必須實(shí)現(xiàn)方法
}

class MyClass1: MyProtocol1 {
    func mustProtocolMethod1() {
         print("MyClass1-->必須實(shí)現(xiàn)方法:mustProtocolMethod1")
    }
}

let cls1 = MyClass1()
cls1.mustProtocolMethod1()
復(fù)制代碼

一個(gè)不可避免的限制是,使用 @objc 修飾的 protocol 就只能被 class 實(shí)現(xiàn)了,也就是說(shuō),對(duì)于 struct 和 enum 類(lèi)型,我們是無(wú)法令它們所實(shí)現(xiàn)的協(xié)議中含有可選方法或者屬性的

在 Swift 2.0 中,我們有了另一種選擇,那就是使用 protocol extension。我們可以在聲明一個(gè) protocol 之后再用 extension 的方式給出部分方法默認(rèn)的實(shí)現(xiàn)。這樣這些方法在實(shí)際的類(lèi)中就是可選實(shí)現(xiàn)的了

protocol MyProtocol2 {
    func optionalProtocolMethod1() //可選方法
    func optionalProtocolMethod2() //可選方法
    func mustProtocolMethod1() //必須實(shí)現(xiàn)方法
}

extension MyProtocol2{
    func optionalProtocolMethod1(){}
    func optionalProtocolMethod2(){}
}
復(fù)制代碼

7、內(nèi)存管理,weak 和 unowned

跟OC一樣,Swift也是采用基于引用計(jì)算的ARC內(nèi)存管理方案(針對(duì)堆空間)

Swift中ARC有3種引用

  • 1、強(qiáng)引用:默認(rèn)情況下,引用都是強(qiáng)引用
  • 2、弱引用(weak):通過(guò)weak定義弱引用
    • 必須是可選類(lèi)型的var,因?yàn)閷?shí)例銷(xiāo)毀后,ARC會(huì)自動(dòng)將弱引用設(shè)置為nil
    • ARC自動(dòng)給弱引用設(shè)置nil時(shí),不會(huì)觸發(fā)屬性觀察器
  • 3、無(wú)主引用(unowned):通過(guò)unowned定義無(wú)主引用
    • 不會(huì)產(chǎn)生強(qiáng)引用,實(shí)例銷(xiāo)毀后仍然存儲(chǔ)著實(shí)例的內(nèi)存地址(類(lèi)似于OC中的unsafe_unretained
    • 試圖銷(xiāo)毀后訪問(wèn)無(wú)主引用,會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤(野指針)
    • Fatal error: Attempted to read an unowned reference but object 0x10070a460 was already deallocated
class Person {
    func eat() {
    }
    deinit {
        print("Person銷(xiāo)毀")
    }
}

unowned var p = Person()
p.eat()
復(fù)制代碼

這段代碼就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤

循環(huán)引用

weak、unowned 都能解決循環(huán)引用的問(wèn)題,unowned 要比weak 少一些性能消耗

  • 生命周期中可能被置為nil使用weak
  • 初始化賦值以后不會(huì)被置為nil使用unowned

閉包的循環(huán)引用

  • 閉包表達(dá)式默認(rèn)會(huì)對(duì)用到的外層對(duì)象產(chǎn)生額外的強(qiáng)引用(對(duì)外層進(jìn)行了retain操作)
class Person {
    var fn:(() -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("Person銷(xiāo)毀")
    }
}
func test() {
    let p = Person()
    p.fn = {
        p.run()
    }
}
test()
復(fù)制代碼

下面這段代碼就會(huì)造成循環(huán)引用,想要解決這個(gè)問(wèn)題,可以使用weak或者unowned

func test() {
    let p = Person()
    p.fn = {[weak p] in
        p?.run()
    }
}

func test() {
    let p = Person()
    p.fn = {[unowned p] in
        p.run()
    }
}
復(fù)制代碼

如果想在定義閉包屬性的同時(shí)引用self,這個(gè)閉包必須是lazy的,因?yàn)樵趯?shí)例初始化完畢后才能引用self

class Person {
    lazy var fun:(() -> ()) = {
        [weak self] in
        self?.run()
    }
    func run() {
        print("run")
    }
    deinit {
        print("Person銷(xiāo)毀")
    }
}
復(fù)制代碼

閉包fn內(nèi)部如果用到了實(shí)例成員,屬性,方法,編譯器會(huì)強(qiáng)制要求明確的寫(xiě)出self

【注】:編譯器強(qiáng)制要求明確的寫(xiě)出self的時(shí)候有可能會(huì)導(dǎo)致循環(huán)引用,需要注意的

如果lazy屬性是閉包調(diào)用的結(jié)果,那么不用考慮循環(huán)引用問(wèn)題,(因?yàn)殚]包調(diào)用后,閉包的聲明周期就結(jié)束了)

class Person {
    var age: Int = 0
    lazy var getAge: Int = {
        self.age
    }()
    deinit {
        print("Person銷(xiāo)毀")
    }
}
復(fù)制代碼

8、值類(lèi)型與引用類(lèi)型

內(nèi)存(RAM)中有兩個(gè)區(qū)域,棧區(qū)(stack)和堆區(qū)(heap)。在 Swift 中,值類(lèi)型,存放在棧區(qū);引用類(lèi)型,存放在堆區(qū)。

值類(lèi)型(Value Type)

值類(lèi)型,即每個(gè)實(shí)例保持一份數(shù)據(jù)拷貝

在 Swift 中,典型的有 struct,enum,以及 tuple 都是值類(lèi)型。而平時(shí)使用的 Int, Double,F(xiàn)loat,String,Array,Dictionary,Set 其實(shí)都是用結(jié)構(gòu)體實(shí)現(xiàn)的,也是值類(lèi)型。

Swift 中,值類(lèi)型的賦值為深拷貝(Deep Copy),值語(yǔ)義(Value Semantics)即新對(duì)象和源對(duì)象是獨(dú)立的,當(dāng)改變新對(duì)象的屬性,源對(duì)象不會(huì)受到影響,反之同理。

struct CoordinateStruct {
   var x: Double
   var y: Double
}

var coordA = CoordinateStruct(x: 0, y: 0)
var coordB = coordA

coordA.x = 100.0
print("coordA.x -> \(coordA.x)")
print("coordB.x -> \(coordB.x)")
復(fù)制代碼

如果聲明一個(gè)值類(lèi)型的常量,那么就意味著該常量是不可變的(無(wú)論內(nèi)部數(shù)據(jù)為 var/let)

let coordC = CoordinateStruct(x: 0, y: 0)
復(fù)制代碼

在 Swift 3.0 中,可以使用 withUnsafePointer(to:_:) 函數(shù)來(lái)打印值類(lèi)型變量的內(nèi)存地址,這樣就能看出兩個(gè)變量的內(nèi)存地址并不相同。

withUnsafePointer(to: &coordA) { print("\($0)") }
withUnsafePointer(to: &coordB) { print("\($0)") }

0x0000000100007670
0x0000000100007680
復(fù)制代碼

在 Swift 中,雙等號(hào)(== & !=)可以用來(lái)比較變量存儲(chǔ)的內(nèi)容是否一致,如果要讓我們的 struct 類(lèi)型支持該符號(hào),則必須遵守Equatable協(xié)議。

extension CoordinateStruct: Equatable {
    static func ==(left: CoordinateStruct, right: CoordinateStruct) -> Bool {
        return (left.x == right.x && left.y == right.y)
    }
}

if coordA != coordB {
    print("coordA != coordB")
}
復(fù)制代碼

引用類(lèi)型(Reference Type)

引用類(lèi)型,即所有實(shí)例共享一份數(shù)據(jù)拷貝

在 Swift 中,class 和閉包是引用類(lèi)型。引用類(lèi)型的賦值是淺拷貝(Shallow Copy),引用語(yǔ)義(Reference Semantics)即新對(duì)象和源對(duì)象的變量名不同,但其引用(指向的內(nèi)存空間)是一樣的,因此當(dāng)使用新對(duì)象操作其內(nèi)部數(shù)據(jù)時(shí),源對(duì)象的內(nèi)部數(shù)據(jù)也會(huì)受到影響。

class Dog {
    var height = 0.0
    var weight = 0.0
}

var dogA = Dog()
var dogB = dogA

dogA.height = 50.0
print("dogA.height -> \(dogA.height)")
print("dogB.height -> \(dogB.height)")

// dogA.height -> 50.0
// dogB.height -> 50.0
復(fù)制代碼

在 Swift 3.0 中,可以使用以下方法來(lái)打印引用類(lèi)型變量指向的內(nèi)存地址。從中即可發(fā)現(xiàn),兩個(gè)變量指向的是同一塊內(nèi)存空間。

print(Unmanaged.passUnretained(dogA).toOpaque())
print(Unmanaged.passUnretained(dogB).toOpaque())

//0x0000000100772ff0
//0x0000000100772ff0
復(fù)制代碼

在 Swift 中,三等號(hào)(=== & !==)可以用來(lái)比較引用類(lèi)型的引用(即指向的內(nèi)存地址)是否一致。也可以在遵守 Equatable 協(xié)議后,使用雙等號(hào)(== & !=)用來(lái)比較變量的內(nèi)容是否一致。

9、String 還是 NSString

簡(jiǎn)單來(lái)說(shuō):沒(méi)有特別需要,盡可能的還是使用String,有以下三個(gè)原因

  • 1、雖然 StringNSString 有著良好的互相轉(zhuǎn)換的特性,但是現(xiàn)在 Cocoa 所有的 API 都接受和返回 String類(lèi)型。我們沒(méi)有必要也不必給自己憑空添加麻煩去把框架中返回的字符串做一遍轉(zhuǎn)換
  • 2、因?yàn)樵?Swift 中 String是struct,相比起 NSObject 的 NSString 類(lèi)來(lái)說(shuō),更切合字符串的 "不變" 這一特性。通過(guò)配合常量賦值 (let) ,這種不變性在多線程編程時(shí)就非常重要了,它從原理上將程序員從內(nèi)存訪問(wèn)和操作順序的擔(dān)憂中解放出來(lái)。另外,在不觸及 NSString 特有操作和動(dòng)態(tài)特性的時(shí)候,使用 String 的方法,在性能上也會(huì)有所提升
  • 3、因?yàn)?String 實(shí)現(xiàn)了 Collection 這樣的協(xié)議,因此有些 Swift 的語(yǔ)法特性只有 String 才能使用,而 NSString 是沒(méi)有的。一個(gè)典型就是 for...in 的枚舉

10、GCD

GCD中Swift和OC都差不多,為了方便使用,我們可以簡(jiǎn)單封裝以下GCD

typealias Task = (_ cancel : Bool) -> Void

@discardableResult
func delay(_ time: TimeInterval, task: @escaping ()->()) ->  Task? {

    func dispatch_later(block: @escaping ()->()) {
        let t = DispatchTime.now() + time
        DispatchQueue.main.asyncAfter(deadline: t, execute: block)
    }

    var closure: (()->Void)? = task
    var result: Task?

    let delayedClosure: Task = {
        cancel in
        if let internalClosure = closure {
            if (cancel == false) {
                DispatchQueue.main.async(execute: internalClosure)
            }
        }
        closure = nil
        result = nil
    }

    result = delayedClosure
    dispatch_later {
        if let delayedClosure = result {
            delayedClosure(false)
        }
    }
    return result;
}
func cancel(_ task: Task?) {
    task?(true)
}
復(fù)制代碼

11、自省

向一個(gè)對(duì)象發(fā)出詢問(wèn),以確定他是不是屬于某個(gè)類(lèi),這種操作就稱為自省。

在OC中一個(gè)對(duì)象詢問(wèn)它是不是屬于某個(gè)類(lèi)。常用的方法有下面兩類(lèi)

OC方法

[obj1 isKindOfClass:[ClassA class]];
[obj2 isMemberOfClass:[ClassB class]];
復(fù)制代碼
  • 1、-isKindOfClass:判斷 obj1 是否是 ClassA 或者其子類(lèi)的實(shí)例對(duì)象;
  • 2、isMemberOfClass: 則對(duì) obj2 做出判斷,當(dāng)且僅當(dāng) obj2 的類(lèi)型為 ClassB 時(shí)返回為真

Swift方法

class ClassA: NSObject {}
class ClassB: ClassA {}

let obj1 = ClassA()
let obj2 = ClassB()

print(obj1.isKind(of: ClassA.self))
print(obj2.isMember(of: ClassA.self))

//true
//false
復(fù)制代碼

對(duì)于一個(gè)不確定的類(lèi)型,我們現(xiàn)在可以使用 is 來(lái)進(jìn)行判斷。is 在功能上相當(dāng)于原來(lái)的 isKindOfClass,可以檢查一個(gè)對(duì)象是否屬于某類(lèi)型或其子類(lèi)型。is 和原來(lái)的區(qū)別主要在于亮點(diǎn),首先它不僅可以用于 class 類(lèi)型上,也可以對(duì) Swift 的其他像是 structenum類(lèi)型進(jìn)行判斷

class ClassA { }
class ClassB: ClassA { }

let obj: AnyObject = ClassB()

if (obj is ClassA) {
    print("屬于 ClassA")
}

if (obj is ClassB) {
    print("屬于 ClassB")
}
復(fù)制代碼

12、KVO

在Swift中KVO僅限于NSObject的子類(lèi),我們還需要做額外的工作,那就是將想要觀測(cè)的對(duì)象標(biāo)記為 dynamic 和 @objc

在 Swift 4 之前的版本中,為一個(gè) NSObject 的子類(lèi)實(shí)現(xiàn) KVO 的最簡(jiǎn)單的例子看起來(lái)是這樣的


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

private var myContext = 0
class Class: NSObject {

    var myObject: MyClass!

    override init() {
        super.init()
        myObject = MyClass()
        print("初始化 MyClass,當(dāng)前日期: \(myObject.date)")
        myObject.addObserver(self,
            forKeyPath: "date",
            options: .new,
            context: &myContext)

        delay(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 newDate = change[.newKey] as? Date {
                print("MyClass 日期發(fā)生變化 \(newDate)")
            }
        }
    }
}

let obj = Class()

初始化 MyClass,當(dāng)前日期: 2020-04-08 07:26:22 +0000
MyClass 日期發(fā)生變化 2020-04-08 07:26:25 +0000
復(fù)制代碼

Swift 4 中 Apple 引入了新的 KeyPath 的表達(dá)方式,現(xiàn)在,對(duì)于類(lèi)型 Foo 中的變量 bar: Bar,對(duì)應(yīng)的 KeyPath 可以寫(xiě)為 \Foo.bar

class AnotherClass: NSObject {
    var myObject: MyClass!
    var observation: NSKeyValueObservation?
    override init() {
        super.init()
        myObject = MyClass()
        print("初始化 AnotherClass,當(dāng)前日期: \(myObject.date)")

        observation = myObject.observe(\MyClass.date, options: [.new]) { (_, change) in
            if let newDate = change.newValue {
                print("AnotherClass 日期發(fā)生變化 \(newDate)")
            }
        }

        delay(1) { self.myObject.date = Date() }
    }
} 
復(fù)制代碼

使用Swift 4.0 KeyPath的好處有很多

  • 1、設(shè)定觀察和處理觀察的代碼被放在了一起,讓代碼維護(hù)難度降低很多;
  • 2、其次在處理時(shí)我們得到的是類(lèi)型安全的結(jié)果,而不是從字典中取值;
  • 3、我們不再需要使用 context 來(lái)區(qū)分是哪一個(gè)觀察量發(fā)生了變化,而且使用 observation 來(lái)持有觀察者可以讓我們從麻煩的內(nèi)存管理中解放出來(lái),觀察者的生命周期將隨著 AnotherClass 的釋放而結(jié)束

Swift 中使用 KVO 還是有有兩個(gè)顯而易見(jiàn)的問(wèn)題

  • 1、在 Objective-C 中我們幾乎可以沒(méi)有限制地對(duì)所有滿足 KVC 的屬性進(jìn)行監(jiān)聽(tīng),而現(xiàn)在我們需要屬性有 dynamic 和 @objc 進(jìn)行修飾,有時(shí)候我們很可能也無(wú)法修改想要觀察的類(lèi)的源碼,遇到這種情況,一個(gè)可行的方案是繼承這個(gè)類(lèi),并且將需要觀察的屬性使用dynamic 和 @objc重寫(xiě)
class MyClass: NSObject {
    var date = Date()
}

class MyChildClass: MyClass {
    @objc dynamic override var date: Date {
        get { return super.date }
        set { super.date = newValue }
    }
}
復(fù)制代碼
  • 2、另一個(gè)大問(wèn)題是對(duì)于那些非 NSObject 的 Swift 類(lèi)型怎么辦。我們可以通過(guò)屬性觀察器來(lái)處理

13、局部scope

C 系語(yǔ)言中在方法內(nèi)部我們是可以任意添加成對(duì)的大括號(hào) {} 來(lái)限定代碼的作用范圍的。這么做一般來(lái)說(shuō)有兩個(gè)好處,首先是超過(guò)作用域后里面的臨時(shí)變量就將失效,這不僅可以使方法內(nèi)的命名更加容易,也使得那些不被需要的引用的回收提前進(jìn)行了,可以稍微提高一些代碼的效率;另外,在合適的位置插入括號(hào)也利于方法的梳理,對(duì)于那些不太方便提取為一個(gè)單獨(dú)方法,但是又應(yīng)該和當(dāng)前方法內(nèi)的其他部分進(jìn)行一些區(qū)分的代碼,使用大括號(hào)可以將這樣的結(jié)構(gòu)進(jìn)行一個(gè)相對(duì)自然的劃分

OC代碼

- (void)loadView {
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
    {
        UILabel *titleLabel = [[UILabel alloc]
                initWithFrame:CGRectMake(150, 30, 200, 40)];
        titleLabel.textColor = [UIColor redColor];
        titleLabel.text = @"Title";
        [view addSubview:titleLabel];
    }

    {
        UILabel *textLabel = [[UILabel alloc]
                initWithFrame:CGRectMake(150, 80, 200, 40)];
        textLabel.textColor = [UIColor redColor];
        textLabel.text = @"Text";
        [view addSubview:textLabel];
    }

    self.view = view;
}
復(fù)制代碼

Swift方法

在 Swift 中,直接使用大括號(hào)的寫(xiě)法是不支持的,因?yàn)檫@和閉包的定義產(chǎn)生了沖突。如果我們想類(lèi)似地使用局部 scope 來(lái)分隔代碼的話,一個(gè)不錯(cuò)的選擇是定義一個(gè)接受 ()->() 作為函數(shù)的全局方法,然后執(zhí)行它

override func loadView() {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
        view.backgroundColor = .white

        local {
            let titleLabel = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
            titleLabel.textColor = .red
            titleLabel.text = "Title"
            view.addSubview(titleLabel)
        }

        local {
            let textLabel = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40))
            textLabel.textColor = .red
            textLabel.text = "Text"
            view.addSubview(textLabel)
        }

        self.view = view
    }
復(fù)制代碼

我們還可以使用匿名閉包來(lái)實(shí)現(xiàn)

override func loadView() {

        let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
        view.backgroundColor = .white

        let titleLabel: UILabel = {
            let label = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
            label.textColor = .red
            label.text = "Title"
            return label
        }()
        view.addSubview(titleLabel)

        let textLabel: UILabel = {
            let label = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40))
            label.textColor = .red
            label.text = "Text"
            return label
        }()
        view.addSubview(textLabel)

        self.view = view
    }
復(fù)制代碼

14、關(guān)聯(lián)對(duì)象

我們經(jīng)常會(huì)遇到給分類(lèi)添加成員變量的問(wèn)題,對(duì)于這類(lèi)問(wèn)題,OC的寫(xiě)法大家都是耳熟能詳了。譬如給UIView添加一個(gè)viewId的成員變量

#import <objc/runtime.h>
static const void *RunTimeViewID = @"RunTimeViewID";

@implementation UIView (JHExtension)

- (NSString *)viewID{
    NSString *ID = objc_getAssociatedObject(self, &RunTimeViewID);
    return ID;
}
- (void)setViewID:(NSString *)viewID{
    objc_setAssociatedObject(self, &RunTimeViewID, viewID, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
復(fù)制代碼

Swift

在 Swift 中這樣的方法依舊有效,只不過(guò)在寫(xiě)法上可能有些不同。兩個(gè)對(duì)應(yīng)的運(yùn)行時(shí)的 get 和 set Associated Object 的 API 是這樣的

func objc_getAssociatedObject(object: AnyObject!,
                                 key: UnsafePointer<Void>
                             )  -> AnyObject!

func objc_setAssociatedObject(object: AnyObject!,
                                 key: UnsafePointer<Void>,
                               value: AnyObject!,
                              policy: objc_AssociationPolicy)  
復(fù)制代碼
struct RunTimeViewKey {
    static let RunTimeViewID = UnsafeRawPointer.init(bitPattern: "RunTimeViewID".hashValue)
}

extension UIView {
    var ViewID: String? {
        set {
            objc_setAssociatedObject(self, RunTimeViewKey.RunTimeViewID!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)

        }
        get {
            return  objc_getAssociatedObject(self, RunTimeViewKey.RunTimeViewID!) as? String
        }
    }

}
復(fù)制代碼

15、Lock

無(wú)并發(fā),不編碼。而只要一說(shuō)到多線程或者并發(fā)的代碼,我們可能就很難繞開(kāi)對(duì)于鎖的討論。簡(jiǎn)單來(lái)說(shuō),為了在不同線程中安全地訪問(wèn)同一個(gè)資源,我們需要這些訪問(wèn)順序進(jìn)行

OC方法

- (void)myMethod:(id)anObj {
    @synchronized(anObj) {
        // 在括號(hào)內(nèi)持有 anObj 鎖
    }
}
復(fù)制代碼

Swift方法

在Swift中去掉了synchronized方法,其實(shí) @synchronized 在幕后做的事情是調(diào)用了objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些異常判斷。因此,在 Swift 中,如果我們忽略掉那些異常的話,我們想要 lock 一個(gè)變量的話

//定義一個(gè)閉包
func synchronized(_ lock: AnyObject, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

func myMethodLocked(anObj: AnyObject!) {
    synchronized(anObj) {
        // 在括號(hào)內(nèi)持有 anObj 鎖
    }
} 
復(fù)制代碼

舉一個(gè)具體的使用例子,比如我們想要為某個(gè)類(lèi)實(shí)現(xiàn)一個(gè)線程安全的 setter,可以這樣進(jìn)行重寫(xiě)

class Obj {
    var _str = "123"
    var str: String {
        get {
            return _str
        }
        set {
            synchronized(self) {
                _str = newValue
            }
        }
    // 下略
    }
} 
復(fù)制代碼

16、性能方面

相比于 Objective-C,Swift 最大的改變就在于方法調(diào)用上的優(yōu)化。

OC方法調(diào)用

在 Objective-C 中,所有的對(duì)于 NSObject 的方法調(diào)用在編譯時(shí)會(huì)被轉(zhuǎn)為 objc_msgSend 方法。這個(gè)方法運(yùn)用 Objective-C 的運(yùn)行時(shí)特性,使用派發(fā)的方式在運(yùn)行時(shí)對(duì)方法進(jìn)行查找。因?yàn)?Objective-C 的類(lèi)型并不是編譯時(shí)確定的,我們?cè)诖a中所寫(xiě)的類(lèi)型不過(guò)只是向編譯器的一種“建議”,不論對(duì)于怎樣的方法,這種查找的代價(jià)基本都是同樣的

這個(gè)過(guò)程的等效的表述可能類(lèi)似這樣 (注意這只是一種表述,與實(shí)際的代碼和工作方式無(wú)關(guān))

methodToCall = findMethodInClass(class, selector);
// 這個(gè)查找一般需要遍歷類(lèi)的方法表,需要花費(fèi)一定時(shí)間

methodToCall();  // 調(diào)用  
復(fù)制代碼

Swift方法調(diào)用

Swift 因?yàn)槭褂昧烁踩蛧?yán)格的類(lèi)型,如果我們?cè)诰帉?xiě)代碼中指明了某個(gè)實(shí)際的類(lèi)型的話 (注意,需要的是實(shí)際具體的類(lèi)型,而不是像 Any 這樣的抽象的協(xié)議),我們就可以向編譯器保證在運(yùn)行時(shí)該對(duì)象一定屬于被聲明的類(lèi)型 因?yàn)橛辛烁喔鞔_的類(lèi)型信息,編譯器就可以在類(lèi)型中處理多態(tài)時(shí)建立虛函數(shù)表 (vtable),這是一個(gè)帶有索引的保存了方法所在位置的數(shù)組。在方法調(diào)用時(shí),與原來(lái)動(dòng)態(tài)派發(fā)和查找方法不同,現(xiàn)在只需要通過(guò)索引就可以直接拿到方法并進(jìn)行調(diào)用了,這是實(shí)實(shí)在在的性能提升。這個(gè)過(guò)程大概相當(dāng)于:

let methodToCall = class.vtable[methodIndex]
// 直接使用 methodIndex 獲取實(shí)現(xiàn)

methodToCall();  // 調(diào)用
復(fù)制代碼

更進(jìn)一步,在確定的情況下,編譯器對(duì) Swift 的優(yōu)化甚至可以做到將某些方法調(diào)用優(yōu)化為 inline 的形式。比如在某個(gè)方法被 final 標(biāo)記時(shí),由于不存在被重寫(xiě)的可能,vtable 中該方法的實(shí)現(xiàn)就完全固定了。對(duì)于這樣的方法,編譯器在合適的情況下可以在生成代碼的階段就將方法內(nèi)容提取到調(diào)用的地方,從而完全避免調(diào)用

總結(jié)

  • 1、文章是讀王巍 (onevcat). “Swifter - Swift 必備 Tips (第四版)總結(jié)所得
  • 2、文章中代碼的demo地址

持續(xù)更新--請(qǐng)iOS的小伙伴關(guān)注! 喜歡的話給一個(gè)贊吧!

作為一個(gè)開(kāi)發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:761407670 進(jìn)群密碼000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長(zhǎng)!

摘自網(wǎng)絡(luò):https://juejin.im/post/5e8485d16fb9a03c3d7378a6

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容