Swift-進(jìn)階 03:值類型 & 引用類型

Swift 進(jìn)階之路 文章匯總

本文主要介紹為什么結(jié)構(gòu)體是值類型,類是引用類型

值類型

前提:需要了解內(nèi)存五大區(qū),內(nèi)存五大區(qū)可以參考這篇文章iOS-底層原理 24:內(nèi)存五大區(qū),如下所示

值類型-1

  • 棧區(qū)的地址 比 堆區(qū)的地址 大

  • 棧是從高地址->低地址,向下延伸,由系統(tǒng)自動(dòng)管理,是一片連續(xù)的內(nèi)存空間

  • 堆是從低地址->高地址,向上延伸,由程序員管理,堆空間結(jié)構(gòu)類似于鏈表,是不連續(xù)的

  • 日常開(kāi)發(fā)中的溢出是指堆棧溢出,可以理解為棧區(qū)與堆區(qū)邊界碰撞的情況

  • 全局區(qū)、常量區(qū)都存儲(chǔ)在Mach-O中的__TEXT cString

我們通過(guò)一個(gè)例子來(lái)引入什么是值類型

func test(){
    //棧區(qū)聲明一個(gè)地址,用來(lái)存儲(chǔ)age變量
    var age = 18
    //傳遞的值
    var age2 = age
    //age、age2是修改獨(dú)立內(nèi)存中的值
    age = 30
    age2 = 45
    
    print("age=\(age),age2=\(age2)")
}
test()

從例子中可以得出,age存儲(chǔ)在棧區(qū)

  • 查看age的內(nèi)存情況,從圖中可以看出,棧區(qū)直接存儲(chǔ)的是
    • 獲取age的棧區(qū)地址:po withUnsafePointer(to: &age){print($0)}
    • 查看age內(nèi)存情況:x/8g 0x00007ffeefbff3e0
值類型-2
  • 查看age2的情況,從下圖中可以看出,age2的賦值相當(dāng)于將age中的值拿出來(lái),賦值給了age2。其中ageage2 的地址 相差了8字節(jié),從這里可以說(shuō)明??臻g是連續(xù)的、且是從高到低
值類型-3

所以,從上面可以說(shuō)明,age就是值類型

值類型 特點(diǎn)

  • 1、地址中存儲(chǔ)的是

  • 2、值類型的傳遞過(guò)程中,相當(dāng)于傳遞了一個(gè)副本,也就是所謂的深拷貝

  • 3、值傳遞過(guò)程中,并不共享狀態(tài)

結(jié)構(gòu)體

結(jié)構(gòu)體的常用寫(xiě)法

//***** 寫(xiě)法一 *****
struct CJLTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
}
var t = CJLTeacher()

//***** 寫(xiě)法二 *****
struct CJLTeacher {
    var age: Int
    
    func teach(){
        print("teach")
    }
}
var t = CJLTeacher(age: 18)
  • 在結(jié)構(gòu)體中,如果不給屬性默認(rèn)值,編譯是不會(huì)報(bào)錯(cuò)的。即在結(jié)構(gòu)體中屬性可以賦值,也可以不賦值


    值類型-4
  • init方法可以重寫(xiě),也可以使用系統(tǒng)默認(rèn)的

結(jié)構(gòu)體的SIL分析

  • 如果沒(méi)有init,系統(tǒng)會(huì)提供不同的默認(rèn)初始化方法

    值類型-5

  • 如果提供了自定義的init,就只有自定義的

    值類型-6

為什么結(jié)構(gòu)體是值類型?

定義一個(gè)結(jié)構(gòu)體,并進(jìn)行分析

struct CJLTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var  t = CJLTeacher()
print("end")
  • 打印t:po t,從下圖中可以發(fā)現(xiàn),t的打印直接就是值,沒(méi)有任何與地址有關(guān)的信息

    值類型-7

  • 獲取t的內(nèi)存地址,并查看其內(nèi)存情況

    • 獲取地址:po withUnsafePointer(to: &t){print($0)}

    • 查看內(nèi)存情況:x/8g 0x0000000100008158

值類型-8

問(wèn)題:此時(shí)將t賦值給t1,如果修改了t1,t會(huì)發(fā)生改變嗎?

  • 直接打印t及t1,可以發(fā)現(xiàn)t并沒(méi)有因?yàn)閠1的改變而改變,主要是因?yàn)橐驗(yàn)?code>t1和t之間是值傳遞,即t1和t是不同內(nèi)存空間,是直接將t中的值拷貝至t1中。t1修改的內(nèi)存空間,是不會(huì)影響t的內(nèi)存空間的
    值類型-9

SIL驗(yàn)證

同樣的,我們也可以通過(guò)分析SIL來(lái)驗(yàn)證結(jié)構(gòu)體是值類型

  • SIL文件中,我們查看結(jié)構(gòu)體的初始化方法,可以發(fā)現(xiàn)只有init,而沒(méi)有malloc,在其中看不到任何關(guān)于堆區(qū)的分配
    值類型-10

總結(jié)

  • 結(jié)構(gòu)體是值類型,且結(jié)構(gòu)體的地址就是第一個(gè)成員的內(nèi)存地址

  • 值類型

    • 在內(nèi)存中直接存儲(chǔ)值

    • 值類型的賦值,是一個(gè)值傳遞的過(guò)程,即相當(dāng)于拷貝了一個(gè)副本,存入不同的內(nèi)存空間,兩個(gè)空間彼此間并不共享狀態(tài)

    • 值傳遞其實(shí)就是深拷貝

引用類型

**類的常用寫(xiě)法 **

//****** 寫(xiě)法一 *******
class CJLTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
}
var t = CJLTeacher.init(20)

//****** 寫(xiě)法二 *******
class CJLTeacher {
    var age: Int?
    
    func teach(){
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
}
var t = CJLTeacher.init(20)
  • 在類中,如果屬性沒(méi)有賦值,也不是可選項(xiàng),編譯會(huì)報(bào)錯(cuò)


    引用類型-1
  • 需要自己實(shí)現(xiàn)init方法

為什么類是引用類型?

定義一個(gè)類,通過(guò)一個(gè)例子來(lái)說(shuō)明

class CJLTeacher1 {
    var age: Int = 18
    var age2: Int = 20
}
var t1 = CJLTeacher1()

類初始化的對(duì)象t1,存儲(chǔ)在全局區(qū)

  • 打印t1、t:po t1,從圖中可以看出,t1內(nèi)存空間中存放的是地址,t中存儲(chǔ)的是
    引用類型-2
  • 獲取t1變量的地址,并查看其內(nèi)存情況
    • 獲取t1指針地址:po withUnsafePointer(to: &t1){print($0)}
    • 查看t1全局區(qū)地址內(nèi)存情況:x/8g 0x0000000100008218
    • 查看t1地址中存儲(chǔ)的堆區(qū)地址內(nèi)存情況:x/8g 0x00000001040088f0
引用類型-4

引用類型 特點(diǎn)

  • 1、地址中存儲(chǔ)的是堆區(qū)地址

  • 2、堆區(qū)地址中存儲(chǔ)的是

問(wèn)題1:此時(shí)將t1賦值給t2,如果修改了t2,會(huì)導(dǎo)致t1修改嗎?

  • 通過(guò)lldb調(diào)試得知,修改了t2,會(huì)導(dǎo)致t1改變,主要是因?yàn)?code>t2、t1地址中都存儲(chǔ)的是 同一個(gè)堆區(qū)地址,如果修改,修改是同一個(gè)堆區(qū)地址,所以修改t2會(huì)導(dǎo)致t1一起修改,即淺拷貝
    引用類型-5

問(wèn)題2:如果結(jié)構(gòu)體中包含類對(duì)象,此時(shí)如果修改t1中的實(shí)例對(duì)象屬性,t會(huì)改變嗎?

代碼如下所示

class CJLTeacher1 {
    var age: Int = 18
    var age2: Int = 20
}

struct CJLTeacher {
    var age: Int = 18
    var age2: Int = 20
    var teacher: CJLTeacher1 = CJLTeacher1()
}

var  t = CJLTeacher()

var t1 = t
t1.teacher.age = 30

//分別打印t1和t中teacher.age,結(jié)果如下
t1.teacher.age = 30 
t.teacher.age = 30

從打印結(jié)果中可以看出,如果修改t1中的實(shí)例對(duì)象屬性,會(huì)導(dǎo)致t中實(shí)例對(duì)象屬性的改變。雖然在結(jié)構(gòu)體中是值傳遞,但是對(duì)于teacher,由于是引用類型,所以傳遞的依然是地址

同樣可以通過(guò)lldb調(diào)試驗(yàn)證

  • 打印t的地址:po withUnsafePointer(to: &t){print($0)}
  • 打印t的內(nèi)存情況: x/8g 0x0000000100008238
  • 打印t中teacher地址的內(nèi)存情況:x/8g 0x000000010070e4a0
引用類型-6

注意:在編寫(xiě)代碼過(guò)程中,應(yīng)該盡量避免值類型包含引用類型

查看當(dāng)前的SIL文件,盡管CJLTeacher1是放在值類型中的,在傳遞的過(guò)程中,不管是傳遞還是賦值,teacher都是按照引用計(jì)數(shù)進(jìn)行管理的

引用類型-7

可以通過(guò)打印teacher的引用計(jì)數(shù)來(lái)驗(yàn)證我們的說(shuō)法,其中teacher的引用計(jì)數(shù)為3
引用類型-8

主要是是因?yàn)椋?p>

  • mainretain一次

  • teacher.getter方法中retain一次

  • teacher.setter方法中retain一次

    引用類型-9

mutating

通過(guò)結(jié)構(gòu)體定義一個(gè),主要有push、pop方法,此時(shí)我們需要?jiǎng)討B(tài)修改棧中的數(shù)組

  • 如果是以下這種寫(xiě)法,會(huì)直接報(bào)錯(cuò),原因是值類型本身是不允許修改屬性

    引用類型-10

  • 將push方法改成下面的方式,查看SIL文件中的push函數(shù)

struct CJLStack {
    var items: [Int] = []
    func push(_ item: Int){
        print(item)
    }
}

引用類型-11

從圖中可以看出,push函數(shù)除了item,還有一個(gè)默認(rèn)參數(shù)self,selflet類型,表示不允許修改

  • 嘗試1:如果將push函數(shù)修改成下面這樣,可以添加進(jìn)去嗎?
struct CJLStack {
    var items: [Int] = []
    func push(_ item: Int){
        var s = self
        s.items.append(item)
    }
}

打印結(jié)果如下

引用類型-12

可以得出上面的代碼并不能將item添加進(jìn)去,因?yàn)?code>s是另一個(gè)結(jié)構(gòu)體對(duì)象,相當(dāng)于值拷貝,此時(shí)調(diào)用push是將item添加到s的數(shù)組中了

  • 根據(jù)前文中的錯(cuò)誤提示,給push添加mutating,發(fā)現(xiàn)可以添加到數(shù)組了
struct CJLStack {
    var items: [Int] = []
    mutating func push(_ item: Int){
        items.append(item)
    }
}

查看其SIL文件,找到push函數(shù),發(fā)現(xiàn)與之前有所不同,push添加mutating(只用于值類型)后,本質(zhì)上是給值類型函數(shù)添加了inout關(guān)鍵字,相當(dāng)于在值傳遞的過(guò)程中,傳遞的是引用(即地址)

引用類型-13

inout關(guān)鍵字

一般情況下,在函數(shù)的聲明中,默認(rèn)的參數(shù)都是不可變的,如果想要直接修改,需要給參數(shù)加上inout關(guān)鍵字

  • 未加inout關(guān)鍵字,給參數(shù)賦值,編譯報(bào)錯(cuò)
    引用類型-14
  • 添加inout關(guān)鍵字,可以給參數(shù)賦值
    引用類型-15

總結(jié)

  • 1、結(jié)構(gòu)體中的函數(shù)如果想修改其中的屬性,需要在函數(shù)前加上mutating,而類則不用

  • 2、mutating本質(zhì)也是加一個(gè) inout修飾的self

  • 3、Inout相當(dāng)于取地址,可以理解為地址傳遞,即引用

  • 4、mutating修飾方法,而inout 修飾參數(shù)

總結(jié)

通過(guò)上述LLDB查看結(jié)構(gòu)體 & 類的內(nèi)存模型,有以下總結(jié):

  • 類型,相當(dāng)于一個(gè)本地excel,當(dāng)我們通過(guò)QQ傳給你一個(gè)excel時(shí),就相當(dāng)于一個(gè)值類型,你修改了什么我們這邊是不知道的

  • 引用類型,相當(dāng)于一個(gè)在線表格,當(dāng)我們和你共同編輯一個(gè)在先表格時(shí),就相當(dāng)于一個(gè)引用類型,兩邊都會(huì)看到修改的內(nèi)容

  • 結(jié)構(gòu)體函數(shù)修改屬性, 需要在函數(shù)前添加mutating關(guān)鍵字,本質(zhì)是給函數(shù)的默認(rèn)參數(shù)self添加了inout關(guān)鍵字,將selflet常量改成了var變量

方法調(diào)度

通過(guò)上面的分析,我們有以下疑問(wèn):結(jié)構(gòu)體和類的方法存儲(chǔ)在哪里?下面來(lái)一一進(jìn)行分析

靜態(tài)派發(fā)

值類型對(duì)象的函數(shù)的調(diào)用方式是靜態(tài)調(diào)用,即直接地址調(diào)用,調(diào)用函數(shù)指針,這個(gè)函數(shù)指針在編譯、鏈接完成后就已經(jīng)確定了,存放在代碼段,而結(jié)構(gòu)體內(nèi)部并不存放方法。因此可以直接通過(guò)地址直接調(diào)用

  • 結(jié)構(gòu)體函數(shù)調(diào)試如下所示


    image
  • 打開(kāi)打開(kāi)demo的Mach-O可執(zhí)行文件,其中的__text段,就是所謂的代碼段,需要執(zhí)行的匯編指令都在這里

    image

對(duì)于上面的分析,還有個(gè)疑問(wèn):直接地址調(diào)用后面是符號(hào),這個(gè)符號(hào)哪里來(lái)的?

image

是從Mach-O文件中的符號(hào)表Symbol Tables,但是符號(hào)表中并不存儲(chǔ)字符串,字符串存儲(chǔ)在String Table(字符串表,存放了所有的變量名和函數(shù)名,以字符串形式存儲(chǔ)),然后根據(jù)符號(hào)表中的偏移值到字符串中查找對(duì)應(yīng)的字符,然后進(jìn)行命名重整:工程名+類名+函數(shù)名,如下所示
image

-Symbol Table:存儲(chǔ)符號(hào)位于字符串表的位置

  • Dynamic Symbol Table動(dòng)態(tài)庫(kù)函數(shù)位于符號(hào)表的偏移信息

還可以通過(guò)終端命令nm,獲取項(xiàng)目中的符號(hào)表

  • 查看符號(hào)表:nm mach-o文件路徑

  • 通過(guò)命令還原符號(hào)名稱:xcrun swift-demangle 符號(hào)

    image

  • 如果將edit scheme -> run中的debug改成release,編譯后查看,在可執(zhí)行文件目錄下,多一個(gè)后綴為dSYM的文件,此時(shí),再去Mach-O文件中查找teach,發(fā)現(xiàn)是找不到,其主要原因是因?yàn)?code>靜態(tài)鏈接的函數(shù),實(shí)際上是不需要符號(hào)的,一旦編譯完成,其地址確定后,當(dāng)前的符號(hào)表就會(huì)刪除當(dāng)前函數(shù)對(duì)應(yīng)的符號(hào),在release環(huán)境下,符號(hào)表中存儲(chǔ)的只是不能確定地址的符號(hào)

  • 對(duì)于不能確定地址的符號(hào),是在運(yùn)行時(shí)確定的,即函數(shù)第一次調(diào)用時(shí)(相當(dāng)于懶加載),例如print,是通過(guò)dyld_stub_bind確定地址的(這個(gè)在最新版的12.2中通過(guò)斷點(diǎn)調(diào)試并未找到,后續(xù)待繼續(xù)驗(yàn)證,有不同見(jiàn)解的,歡迎留言指出)

    image

函數(shù)符號(hào)命名規(guī)則

  • 對(duì)于C函數(shù)來(lái)說(shuō),命名的重整規(guī)則就是在函數(shù)名之前加_(注意:C中不允許函數(shù)重載,因?yàn)闆](méi)有辦法區(qū)分)
#include <stdio.h>
void test(){    }
image
  • 對(duì)于OC來(lái)說(shuō),也不支持函數(shù)重載,其符號(hào)命名規(guī)則是-[類名 函數(shù)名]

    image

  • 對(duì)于Swift來(lái)說(shuō),是云溪函數(shù)重載,主要是因?yàn)閟wift中的重整命名規(guī)則比較復(fù)雜,可以確保函數(shù)符號(hào)的唯一性

補(bǔ)充:ASLR

關(guān)于ASLR的詳細(xì)說(shuō)明參考iOS-底層原理 32:?jiǎn)?dòng)優(yōu)化(一)基本概念中對(duì)于ASLR的解釋,下面是針對(duì)函數(shù)地址的一個(gè)驗(yàn)證

  • 通過(guò)運(yùn)行發(fā)現(xiàn),Mach-O中的地址與調(diào)試時(shí)直接獲取的地址是由一定偏差的,其主要原因是實(shí)際調(diào)用時(shí)地址多了一個(gè)ASLR(地址空間布局隨機(jī)化 address space layout randomizes)

    image

  • 可以通過(guò)image list查看,其中0x0000000100000000是程序運(yùn)行的首地址,后8位是隨機(jī)偏移00000000(即ASLR)

    image

  • 將Mach-O中的文件地址0x0000000100003D50 + 0x00000000 = 0x100003D50,正好對(duì)應(yīng)上面調(diào)用的地址

動(dòng)態(tài)派發(fā)

匯編指令補(bǔ)充

  • blr:帶返回的跳轉(zhuǎn)指令,跳轉(zhuǎn)到指令后邊跟隨寄存器中保存的地址
  • mov:將某一寄存器的值復(fù)制到另一寄存器(只能用于寄存器與起存起或者 寄存器與常量之間 傳值,不能用于內(nèi)存地址)
    • mov x1, x0 將寄存器x0的值復(fù)制到寄存器x1中
  • ldr:將內(nèi)存中的值讀取到寄存器中
    • ldr x0, [x1, x2] 將寄存器x1和寄存器x2 相加作為地址,取該內(nèi)存地址的值翻入寄存器x0中
  • str:將寄存器中的值寫(xiě)入到內(nèi)存中
    • str x0, [x0, x8] 將寄存器x0的值保存到內(nèi)存[x0 + x8]處
  • bl:跳轉(zhuǎn)到某地址

探索class的調(diào)度方式

首先介紹下V_Table在SIL文件中的格式

//聲明sil vtable關(guān)鍵字
decl ::= sil-vtable
//sil vtable中包含 關(guān)鍵字、標(biāo)識(shí)(即類名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數(shù)名稱
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na
me

例如,以CJLTacher為例,其SIL中的v-table如下所示

class CJLTeacher{
    func teach(){}
    func teach2(){}
    func teach3(){}
    func teach4(){}
    @objc deinit{}
    init(){}
}
image
  • sil_vtable:關(guān)鍵字

  • CJLTeacher:表示是CJLTeacher類的函數(shù)表

  • 其次就是當(dāng)前方法的聲明對(duì)應(yīng)著方法的名稱

  • 函數(shù)表 可以理解為 數(shù)組,聲明在 class內(nèi)部的方法在不加任何關(guān)鍵字修飾的過(guò)程中,是連續(xù)存放在我們當(dāng)前的地址空間中的。這一點(diǎn),可以通過(guò)斷點(diǎn)來(lái)印證,

    image

    • register read x0,此時(shí)的地址和 實(shí)例對(duì)象的地址是相同的,其中x8 實(shí)例對(duì)象地址,即首地址
      image

觀察這幾個(gè)方法的偏移地址,可以發(fā)現(xiàn)方法是連續(xù)存放的,正好對(duì)應(yīng)V-Table函數(shù)表中的排放順序,即是按照定義順序排放在函數(shù)表中

image

函數(shù)表源碼探索

下面來(lái)進(jìn)行函數(shù)表底層的源碼探索

  • 源碼中搜索initClassVTable,并加上斷點(diǎn),然后寫(xiě)上源碼進(jìn)行調(diào)試
    image

    其內(nèi)部是通過(guò)for循環(huán)編碼,然后offset+index偏移,然后獲取method,將其存入到偏移后的內(nèi)存中,從這里可以印證函數(shù)是連續(xù)存放的

對(duì)于class中函數(shù)來(lái)說(shuō),類的方法調(diào)度是通過(guò)V-Taable,其本質(zhì)就是一個(gè)連續(xù)的內(nèi)存空間(數(shù)組結(jié)構(gòu))。

問(wèn)題:如果更改方法聲明的位置呢?例如extension中的函數(shù),此時(shí)的函數(shù)調(diào)度方式還是函數(shù)表調(diào)度嗎?

通過(guò)以下代碼驗(yàn)證

  • 定義一個(gè)CJLTeacher的extension
extension CJLTeacher{
    func teach5(){ print("teach5") }
}
  • 在定義一個(gè)子類CJLStudent繼承自CJLTeacher,查看SIL中的V-Table
class CJLStudent: CJLTeacher{}
  • 查看SIL文件,發(fā)現(xiàn)子類只繼承了class中定義的函數(shù),即函數(shù)表中的函數(shù)
    image

    其原因是因?yàn)?code>子類將父類的函數(shù)表全部繼承了,如果此時(shí)子類增加函數(shù),會(huì)繼續(xù)在連續(xù)的地址中插入,假設(shè)extension函數(shù)也是在函數(shù)表中,則意味著子類也有,但是子類無(wú)法并沒(méi)有相關(guān)的指針記錄函數(shù) 是父類方法 還是 子類方法,所以不知道方法該從哪里插入,導(dǎo)致extension中的函數(shù)無(wú)法安全的放入子類中。所以在這里可以側(cè)面證明extension中的方法是直接調(diào)用的,且只屬于類,子類是無(wú)法繼承的

開(kāi)發(fā)注意點(diǎn):

  • 繼承方法和屬性,不能寫(xiě)extension中。
  • 而extension中創(chuàng)建的函數(shù),一定是只屬于自己類,但是其子類也有其訪問(wèn)權(quán)限,只是不能繼承和重寫(xiě),如下所示
extension CJLTeacher{
    var age: Int{
        get{
            return 18
        }
    }
    func teach(){
        print("teach")
    }
}

class CJLMiddleTeacher: CJLTeacher{
    override func study() {
        print("CJLMiddleTeacher study")
    }
}

var t = CJLMiddleTeacher()
//子類有父類extension中方法的訪問(wèn)權(quán)限,只是不能繼承和重寫(xiě)
t.teach()
t.study()
print(t.age)

<!--運(yùn)行結(jié)果-->
teach
CJLMiddleTeacher study
18

final、@objc、dynamic修飾函數(shù)

final 修飾

  • final 修飾的方法是 直接調(diào)度的,可以通過(guò)SIL驗(yàn)證 + 斷點(diǎn)驗(yàn)證
class CJLTeacher {
    final func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}
image

@objc 修飾

使用@objc關(guān)鍵字是將swift中的方法暴露給OC

class CJLTeacher{
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

通過(guò)SIL+斷點(diǎn)調(diào)試,發(fā)現(xiàn)@objc修飾的方法是 函數(shù)表調(diào)度

image

【小技巧】:混編頭文件查看方式:查看項(xiàng)目名-Swift.h頭文件

image

  • 如果只是通過(guò)@objc修飾函數(shù),OC還是無(wú)法調(diào)用swift方法的,因此如果想要OC訪問(wèn)swift,class需要繼承NSObject
<!--swift類-->
class CJLTeacher: NSObject {
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

<!--橋接文件中的聲明-->
SWIFT_CLASS("_TtC9_3_指針10CJLTeacher")
@interface CJLTeacher : NSObject
- (void)teach;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

<!--OC調(diào)用-->
//1、導(dǎo)入swift頭文件
#import "CJLOCTest-Swift.h"
//2、調(diào)用
CJLTeacher *t = [[CJLTeacher alloc] init];
[t teach];

查看SIL文件發(fā)現(xiàn)被@objc修飾的函數(shù)聲明有兩個(gè):swift + OC(內(nèi)部調(diào)用的swift中的teach函數(shù))

image

即在SIL文件中生成了兩個(gè)方法

  • swift原有的函數(shù)
  • @objc標(biāo)記暴露給OC來(lái)使用的函數(shù): 內(nèi)部調(diào)用swift的

dynamic 修飾

以下面代碼為例,查看dynamic修飾的函數(shù)的調(diào)度方式

class CJLTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

其中teach函數(shù)的調(diào)度還是 函數(shù)表調(diào)度,可以通過(guò)斷點(diǎn)調(diào)試驗(yàn)證,使用dynamic的意思是可以動(dòng)態(tài)修改,意味著當(dāng)類繼承自NSObject時(shí),可以使用method-swizzling

@objc + dynamic

class CJLTeacher{
    @objc dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

通過(guò)斷點(diǎn)調(diào)試,走的是objc_msgSend流程,即 動(dòng)態(tài)消息轉(zhuǎn)發(fā)

image

場(chǎng)景:swift中實(shí)現(xiàn)方法交換

在swift中的需要交換的函數(shù)前,使用dynamic修飾,然后通過(guò):@_dynamicReplacement(for: 函數(shù)符號(hào))進(jìn)行交換,如下所示

class CJLTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

extension CJLTeacher{
    @_dynamicReplacement(for: teach)
    func teach5(){
        print("teach5")
    }
}

將teach方法替換成了teach5


image
  • 如果teach沒(méi)有實(shí)現(xiàn) / 如果去掉dynamic修飾符,會(huì)報(bào)錯(cuò)
    image

總結(jié)

  • struct類型,其中函數(shù)的調(diào)度屬于直接調(diào)用地址,即靜態(tài)調(diào)度

  • class引用類型,其中函數(shù)的調(diào)度是通過(guò)V-Table函數(shù)表來(lái)進(jìn)行調(diào)度的,即動(dòng)態(tài)調(diào)度

  • extension中的函數(shù)調(diào)度方式是直接調(diào)度

  • final修飾的函數(shù)調(diào)度方式是直接調(diào)度

  • @objc修飾的函數(shù)調(diào)度方式是函數(shù)表調(diào)度,如果OC中需要使用,class還必須繼承NSObject

  • dynamic修飾的函數(shù)的調(diào)度方式是函數(shù)表調(diào)度,使函數(shù)具有動(dòng)態(tài)性

  • @objc + dynamic 組合修飾的函數(shù)調(diào)度,是執(zhí)行的是objc_msgSend流程,即 動(dòng)態(tài)消息轉(zhuǎn)發(fā)

補(bǔ)充:內(nèi)存插件

主要補(bǔ)充內(nèi)存插件libfooplugin.dylib安裝及使用

安裝 & 使用

  • 在跟目下創(chuàng)建.lldbinit文件 vim /.lldbinit

  • 然后輸入 plugin load libfooplugin.dylib路徑

  • 使用:在lldb 調(diào)試中輸入 -- cat address 地址

可以在這里下載插件文件,密碼: go4q

內(nèi)存分區(qū)實(shí)踐

堆區(qū)

有以下代碼,通過(guò)cat查看t屬于哪個(gè)區(qū)

class CJLTeacher{
    func teach(){
        
    }
}
let t = CJLTeacher()

image

從結(jié)果中可以看出,是在堆區(qū),即heap pointer

棧區(qū)

查看以下代碼的內(nèi)存地址位于哪個(gè)區(qū)?

func test(){
    var age: Int = 10
    print(age)
}

image

從結(jié)果來(lái)看,位于棧區(qū),即stack pointer

全局區(qū)

對(duì)于C的分析

下面是C語(yǔ)言的部分代碼,查看其變量的內(nèi)存地址

//全局已初始化變量
int a = 10;
//全局未初始化變量
int age;

//全局靜態(tài)變量
static int age2 = 30;

int main(int argc, const char * argv[]) {
    
    char *p = "CJLTeacher";
    printf("%d", a);
    printf("%d", age2);
    return  0;
}
  • 查看a(全局已初始化變量)的內(nèi)存地址

    image

    其中__DATA.__data表示segment.section,這里的位置和全局區(qū)并不沖突,因?yàn)橐粋€(gè)是人為的內(nèi)存分配(內(nèi)存布局分區(qū)),一個(gè)是Mach-O的segment.section段中,是文件的格式劃分
    image

  • 查看age(全局未初始化變量)的內(nèi)存地址

    image

    age在Mach-O文件中,放在了__DATA.__common段,主要放的就是未初始化的符號(hào)聲明(mach-o相比內(nèi)存劃分更細(xì),主要是為了更好的定位符號(hào)),當(dāng)然此時(shí)的age 在內(nèi)存中依然在全局區(qū)

  • 查看age2(全局已初始化靜態(tài)變量)的內(nèi)存地址(其中需要注意:age2必須使用才能找到,否則會(huì)報(bào)錯(cuò))

    image

  • 觀察3個(gè)變量的地址,其地址都是相鄰的,因?yàn)樵趦?nèi)存中都放在了全局區(qū),觀察其內(nèi)存地址,可以發(fā)現(xiàn),在全局區(qū)中,未初始化變量地址 比 已初始化變量地址 高

    image

  • 如果定義了一個(gè)char *p = "CJLTeacher",查看*p,存儲(chǔ)在__TEXT.cstring段,內(nèi)存中存儲(chǔ)在常量區(qū)

    image

  • 如果是const修飾的變量呢?存放在Mach-O文件中的__TEXT.__const

    image

  • 如果使用static + const修飾變量,此時(shí)變量在哪?**

static const int age3 = 40;
  • 查看age3的內(nèi)存地址,地址特別大,而且使用cat查看不了,因?yàn)閙ach-o沒(méi)有記錄,age3 就是30,即使用static+const修飾的變量就相當(dāng)于直接替換
    image

對(duì)于swift的分析

let age = 10

由于是不可變所以不能通過(guò)po+cat查看內(nèi)存,通過(guò)匯編 首地址+偏移 來(lái)獲取age的內(nèi)存,發(fā)現(xiàn)是在Mach-O的__DATA.__common

image

從這里可以發(fā)現(xiàn),這與C中是有所區(qū)別的。swift的不同之處:已經(jīng)初始化的全局變量放在__DATA.__common段,猜測(cè)是因?yàn)?age開(kāi)始是被標(biāo)記為未初始化的,當(dāng)我們執(zhí)行代碼之后才將10存儲(chǔ)到對(duì)應(yīng)的內(nèi)存地址中

  • 如果是var修飾的變量呢?可以發(fā)現(xiàn)與let是一致的,還是__DATA.__common
var age2 = 10
image

總結(jié)

  • 對(duì)于C語(yǔ)言中全局變量,根據(jù)是否已經(jīng)初始化,存儲(chǔ)在Mach-O中存儲(chǔ)位置是不同的
    • 初始化的全局變量:__DATA.__data

    • 初始化的全局變量:__DATA.__common

    • 初始化的全局靜態(tài)變量,即static修飾:__DATA.__data

    • 對(duì)于char *p類型的字符:__TEXT.cstring

    • const修飾的全局變量:__TEXT.__const

    • static+const修飾的全局變量:Mach-O中沒(méi)有記錄

最后編輯于
?著作權(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ù)。

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

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