Swift 協(xié)議介紹

一、協(xié)議與繼承

class CXTeacher {
    var age = 10
    var name = "chenxi"
}

class Dog {
    var name = "糯米"
    var type = "泰迪"
}

例如如上代碼,這個(gè)時(shí)候我們有一個(gè)需求,要為這兩個(gè)類添加一個(gè) debug 函數(shù)來打印當(dāng)前 類的信息。從繼承的?度來說,我們可能會(huì)想到抽取一個(gè)公共的基類,當(dāng)然這里兩個(gè)類都是動(dòng)物,人也是動(dòng)物。但是從業(yè)務(wù)邏輯上來說,這么處理不太合理。所以最直觀的辦法是對于每一個(gè)類都寫一個(gè)單獨(dú)的 debug 函數(shù)。

class CXTeacher {
    var age = 10
    var name = "chenxi"
    
    func debug(){
        print("")
    }
}

class Dog {
    var name = "糯米"
    var type = "泰迪"
    
    func debug(){
        print("")
    }
}

如果我們對當(dāng)前代碼中的每個(gè)類都需要 debug,那上面這種方法顯然是行不通的,于是我們有 了下面的代碼:

func debug(subject: Any){
    print("")
}

看到這里可能大家也會(huì)覺得沒有問題,但是如果我們要具體的描述當(dāng)前類的具體信息,這個(gè)時(shí)候 我們還需要引入一個(gè)公共的基類,同時(shí)我們還需要有一個(gè)公共的屬性 description 來讓子類重 載,這無疑對我們的代碼是很強(qiáng)的入侵。

所以這個(gè)時(shí)候我們通過一個(gè)協(xié)議來描述當(dāng)前類的共同行為,并通過 extension 的方式來對我們的類進(jìn)行擴(kuò)展,這樣來處理的話就會(huì)好很多。

extension CXTeacher : CustomStringConvertible {
    var description : String { get { return "LGTeacher: \(age)\(name)" } }
}
extension Dog : CustomStringConvertible {
    var description : String { get { return "Dog: \(name)\(type)" } }
}

func print(subject: CustomStringConvertible) {
    let string = subject.description
    print(string)
}

這里我們可以稍微的總結(jié)一下:

  • class 本質(zhì)上定義了一個(gè)對象是什么
  • protocol 本質(zhì)上定義了一個(gè)對象有哪些行為

二、協(xié)議的基本語法

  • 協(xié)議要求一個(gè)屬性必須明確是 get 或 get 和 set
protocol MyProtocol {
    var age: Int{ get set } 
    var name: String{ get }
}

這里需要注意的一點(diǎn)是:并不是說當(dāng)前聲明 get 的屬性一定是計(jì)算屬性

class LGTeacher: MyProtocol{ 
    var age: Int = 18
    var name: String
    init(_ name: String) {
        self.name = name
    }
}
  • 協(xié)議中的異變方法,表示在該方法可以改變其所屬的實(shí)例,以及該實(shí)例的所有屬性(用于枚 舉和結(jié)構(gòu)體),在為類實(shí)現(xiàn)該方法的時(shí)候不需要寫 mutating 關(guān)鍵字
protocol Togglable {
    mutating func toggle()
}

class CXTeacher: Togglable {
    func toggle() {
        print(#function)
    }
}

struct CXPerson: Togglable {
    mutating func toggle() {
        print(#function)
    }
}
  • 類在實(shí)現(xiàn)協(xié)議中的初始化器,必須使用 required 關(guān)鍵字修飾初始化器的實(shí)現(xiàn)(類的初始化器前添加 required 修飾符來表明所有該類的子類都必須實(shí)現(xiàn)該初始化器)
protocol MyProtocol {
    init(_ age: Int)
}

class CXPerson: MyProtocol {
    var age = 10
    required init(_ age: Int) {
        self.age = age
    }
}

這里有一種特殊情況,如果 CXPerson 是不可被繼承的,那么不加 required 修飾也沒問題。

protocol MyProtocol {
    init(_ age: Int)
}

final class CXPerson: MyProtocol {
    var age = 10
    init(_ age: Int) {
        self.age = age
    }
}
  • 類專用協(xié)議(通過添加 AnyObject 關(guān)鍵字到協(xié)議的繼承列表,你就可以限制協(xié)議只能被類 類型采納)
  • 可選協(xié)議:如果我們不想強(qiáng)制讓遵循協(xié)議的類類型實(shí)現(xiàn),可以使用 optional 作為前綴 放在協(xié)議的定義。
@objc protocol Incrementable {
     @objc optional func increment(by: Int) -> Int
}

class CXPerson: Incrementable {

}

let p: Incrementable = CXPerson()
//這里CXPerson沒實(shí)現(xiàn)increment方法這樣調(diào)用的話也不會(huì)出錯(cuò)
p.increment?(by: 10)

三、協(xié)議原理探究

SIL 代碼分析協(xié)議調(diào)度方式

在前面的文章中我們已經(jīng)了解到類的方法調(diào)用是通過 V-table(函數(shù)表)來調(diào)度的,那么讓類來實(shí)現(xiàn)協(xié)議中的方法的時(shí)候調(diào)度方式是怎么樣的呢?這里我們將 swift 代碼編譯 成 SIL 代碼來看一下。

  • swift 代碼
protocol Incrementable {
    func increment(by: Int)
}

class CXPerson: Incrementable {
    func increment(by: Int) {
        print(by)
    }
}

let p: CXPerson = CXPerson()
p.increment(by: 10)
  • SIL 代碼分析

第一步我們先找到 main 函數(shù),在這里可以看到 increment 函數(shù)是通過 class_method 方式進(jìn)行調(diào)度的,下面我們在官方文檔看一下對 class_method 的解釋。

通過文檔介紹可以知道 class_method 是通過 v-table 的方式進(jìn)行調(diào)度的。

這里我們搜索 s4main8CXPersonC9increment2byySi_tF。

這里可以看到 increment 被聲明在了 v-table 中。下面我們對 swift 代碼做下修改,將 p 聲明成 Incrementable 類型再來看一下調(diào)度方式是否一樣。

  • swift 代碼
protocol Incrementable {
    func increment(by: Int)
}

class CXPerson: Incrementable {
    func increment(by: Int) {
        print(by)
    }
}

let p: Incrementable = CXPerson()
p.increment(by: 10)
  • SIL 代碼分析

這里在 main 函數(shù)中可以看到 increment 函數(shù)的調(diào)度變成了 _method,所以我們先在官方文檔看一下 witness_method 的介紹。

文檔的大致意思是講會(huì)通過查到當(dāng)前類的 witness-table 來找到當(dāng)前方法的實(shí)現(xiàn)。這里 witness-table 叫作協(xié)議見證表,當(dāng)一個(gè)類準(zhǔn)了一個(gè)協(xié)議并且實(shí)現(xiàn)了協(xié)議中的方法,編譯器就會(huì)為當(dāng)前類創(chuàng)業(yè)一個(gè) witness-table, witness-table 就記錄了當(dāng)前類實(shí)現(xiàn)協(xié)議方法的編碼信息。

SIL 代碼中我們確實(shí)看到了 sil_witness_table,這里我們通過搜索 s4main8CXPersonCAA13IncrementableA2aDP9increment2byySi_tFTW 來看一下協(xié)議方法是如何調(diào)度的。

這里會(huì)通過 s4main8CXPersonCAA13IncrementableA2aDP9increment2byySi_tFTW 來查找具體的方法類型,也就是我們在 CXPerson 類中實(shí)現(xiàn) increment 方法 。也就是說當(dāng)變量被聲明成協(xié)議類型的時(shí)候,編譯器會(huì)通過 witness-table 做一層橋接, 最終找到變量的具體實(shí)際類型及具體實(shí)現(xiàn),并完成方法的調(diào)度。

通過匯編代碼分析協(xié)議方法的調(diào)度

  • swift 代碼
protocol Incrementable {
    func increment(by: Int)
}

class CXPerson: Incrementable {
    func increment(by: Int) {
        print(by)
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let p: Incrementable = CXPerson()
        p.increment(by: 10)
    }
}
  • 匯編代碼分析

這里可以看到 x2 存儲(chǔ)的就是 witness-table,x2 偏移 0x8 得到 x8,也就是 increment 函數(shù)地址。在 blr x8 斷點(diǎn)這里操作 fn + control + F7 快捷鍵,也就是指令單步執(zhí)行,進(jìn)入到 increment 函數(shù)的具體實(shí)現(xiàn)。

這里可以看到,有經(jīng)過了一層跳轉(zhuǎn),通過 x8 偏移 0x50,這個(gè)時(shí)候才是 increment 函數(shù)的真正執(zhí)行地址。通過 fn + control + F7 快捷鍵也可以看到執(zhí)行了 CXPersonincrement 方法。

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

相關(guān)閱讀更多精彩內(nèi)容

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