本文主要介紹為什么結(jié)構(gòu)體是值類型,類是引用類型
值類型
前提:需要了解內(nèi)存五大區(qū),,如下所示
棧區(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
- 獲取age的棧區(qū)地址:
- 查看
age2
的情況节值,從下圖中可以看出,age2
的賦值相當(dāng)于將age
中的值拿出來(lái),賦值給了age2
。其中age
與age2
的地址 相差了8
字節(jié)逻澳,從這里可以說(shuō)明椡謇浚空間是連續(xù)
的、且是從高到低
的
所以,從上面可以說(shuō)明,age就是值類型
值類型 特點(diǎn)
1、地址中存儲(chǔ)的是
值
2、值類型的傳遞過(guò)程中,相當(dāng)于
傳遞
了一個(gè)副本
,也就是所謂的深拷貝
3、值傳遞過(guò)程中,并不共享狀態(tài)
結(jié)構(gòu)體
結(jié)構(gòu)體的常用寫法
//***** 寫法一 *****
struct CJLTeacher {
var age: Int = 18
func teach(){
print("teach")
}
}
var t = CJLTeacher()
//***** 寫法二 *****
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)體中屬性可以賦值捣作,也可以不賦值
init
方法可以重寫掉盅,也可以使用系統(tǒng)默認(rèn)的
結(jié)構(gòu)體的SIL分析
-
如果
沒(méi)有init
,系統(tǒng)會(huì)提供不同的默認(rèn)初始化方法 -
如果
提供了自定義的init
侣集,就只有自定義的
為什么結(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)的信息 -
獲取t的內(nèi)存地址嘉汰,并查看其內(nèi)存情況
獲取地址:
po withUnsafePointer(to: &t){print($0)}
查看內(nèi)存情況:
x/8g 0x0000000100008158
問(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)存空間的
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ū)的分配
總結(jié)
作為一個(gè)開(kāi)發(fā)者毅待,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS開(kāi)發(fā)交流群:130 595 548归榕,不管你是小白還是大牛都?xì)g迎入駐 尸红,讓我們一起進(jìn)步,共同發(fā)展!(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔M饫铩)
結(jié)構(gòu)體是值類型
怎爵,且結(jié)構(gòu)體的地址就是第一個(gè)成員的內(nèi)存地址-
值類型
在內(nèi)存中直接
存儲(chǔ)值
值類型的賦值,是一個(gè)
值傳遞
的過(guò)程盅蝗,即相當(dāng)于拷貝了一個(gè)副本鳖链,存入不同的內(nèi)存空間,兩個(gè)空間彼此間并不共享狀態(tài)
值傳遞
其實(shí)就是深拷貝
引用類型
類
**類的常用寫法 **
//****** 寫法一 *******
class CJLTeacher {
var age: Int = 18
func teach(){
print("teach")
}
init(_ age: Int) {
self.age = age
}
}
var t = CJLTeacher.init(20)
//****** 寫法二 *******
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ò)
需要自己實(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ǔ)的是值
-
獲取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
- 獲取
引用類型 特點(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一起修改,即淺拷貝
問(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
注意:在編寫代碼過(guò)程中傻盟,應(yīng)該盡量避免值類型包含引用類型
查看當(dāng)前的SIL
文件速蕊,盡管CJLTeacher1
是放在值類型中的,在傳遞的過(guò)程中娘赴,不管是傳遞還是賦值规哲,teacher
都是按照引用計(jì)數(shù)
進(jìn)行管理的
可以通過(guò)打印teacher
的引用計(jì)數(shù)來(lái)驗(yàn)證我們的說(shuō)法,其中teacher的引用計(jì)數(shù)為3
主要是是因?yàn)椋?/p>
main
中retain
一次teacher.getter
方法中retain一次-
teacher.setter
方法中retain一次
mutating
通過(guò)結(jié)構(gòu)體
定義一個(gè)棧
诽表,主要有push唉锌、pop方法,此時(shí)我們需要?jiǎng)討B(tài)修改棧中的數(shù)組
-
如果是以下這種寫法竿奏,會(huì)直接報(bào)錯(cuò)袄简,原因是
值類型本身是不允許修改屬性
的 將push方法改成下面的方式,查看
SIL
文件中的push
函數(shù)
struct CJLStack {
var items: [Int] = []
func push(_ item: Int){
print(item)
}
}
從圖中可以看出泛啸,push
函數(shù)除了item
绿语,還有一個(gè)默認(rèn)參數(shù)self
,self
是let
類型候址,表示不允許修改
- 嘗試1:如果將push函數(shù)修改成下面這樣吕粹,可以添加進(jìn)去嗎?
struct CJLStack {
var items: [Int] = []
func push(_ item: Int){
var s = self
s.items.append(item)
}
}
打印結(jié)果如下
可以得出上面的代碼并不能將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ò)程中损趋,傳遞
的是引用
(即地址)
inout關(guān)鍵字
一般情況下,在函數(shù)的聲明中椅寺,默認(rèn)的參數(shù)都是不可變
的浑槽,如果想要直接修改,需要給參數(shù)加上inout
關(guān)鍵字
-
未加
inout
關(guān)鍵字返帕,給參數(shù)賦值桐玻,編譯報(bào)錯(cuò) -
添加
inout
關(guān)鍵字,可以給參數(shù)賦值
總結(jié)
作為一個(gè)開(kāi)發(fā)者荆萤,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要镊靴,這是一個(gè)我的iOS開(kāi)發(fā)交流群:130 595 548铣卡,不管你是小白還是大牛都?xì)g迎入駐 ,讓我們一起進(jìn)步偏竟,共同發(fā)展V舐洹(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔!)
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)鍵字,將self
從let
常量改成了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)試如下所示
-
打開(kāi)打開(kāi)demo的
Mach-O
可執(zhí)行文件耕餐,其中的__text
段凡傅,就是所謂的代碼段,需要執(zhí)行的匯編指令都在這里
對(duì)于上面的分析肠缔,還有個(gè)疑問(wèn):直接地址調(diào)用后面是符號(hào)
夏跷,這個(gè)符號(hào)哪里來(lái)的哼转?
是從Mach-O
文件中的符號(hào)表Symbol Tables
,但是符號(hào)表中并不存儲(chǔ)字符串
拓春,字符串存儲(chǔ)在String Table(字符串表释簿,存放了所有的變量名和函數(shù)名,以字符串形式存儲(chǔ))
硼莽,然后根據(jù)符號(hào)表中的偏移值到字符串中查找對(duì)應(yīng)的字符,然后進(jìn)行命名重整:工程名+類名+函數(shù)名
煮纵,如下所示
-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)
如果將
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)解的擂涛,歡迎留言指出)
函數(shù)符號(hào)命名規(guī)則
- 對(duì)于
C函數(shù)
來(lái)說(shuō)读串,命名的重整規(guī)則就是在函數(shù)名之前加_
(注意:C中不允許函數(shù)重載,因?yàn)闆](méi)有辦法區(qū)分)
#include <stdio.h>
void test(){ }
-
對(duì)于OC來(lái)說(shuō)歼指,也不支持函數(shù)重載爹土,其符號(hào)命名規(guī)則是
-[類名 函數(shù)名]
對(duì)于Swift來(lái)說(shuō),是云溪函數(shù)重載踩身,主要是因?yàn)閟wift中的重整命名規(guī)則比較復(fù)雜胀茵,可以確保函數(shù)符號(hào)的唯一性
補(bǔ)充:ASLR
關(guān)于ASLR
的詳細(xì)說(shuō)明這里目前不解釋了,下面是針對(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) -
可以通過(guò)
image list
查看峭弟,其中0x0000000100000000
是程序運(yùn)行的首地址,后8位是隨機(jī)偏移00000000
(即ASLR) 將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
:將寄存器中的值寫入到內(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(){}
}
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)印證趾浅,-
register read x0
愕提,此時(shí)的地址和 實(shí)例對(duì)象的地址是相同
的,其中x8
實(shí)例對(duì)象地址皿哨,即首地址
-
觀察這幾個(gè)方法的偏移地址浅侨,可以發(fā)現(xiàn)方法是連續(xù)存放的,正好對(duì)應(yīng)V-Table
函數(shù)表中的排放順序
往史,即是按照定義順序排放在函數(shù)表中
函數(shù)表源碼探索
下面來(lái)進(jìn)行函數(shù)表底層
的源碼探索
-
源碼中搜索
initClassVTable
,并加上斷點(diǎn)仗颈,然后寫上源碼進(jìn)行調(diào)試其內(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ù)
其原因是因?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):
- 繼承方法和屬性,不能寫extension中谦秧。
- 而extension中創(chuàng)建的函數(shù)竟纳,一定是只屬于自己類,但是其
子類也有其訪問(wèn)權(quán)限
油够,只是不能繼承和重寫
蚁袭,如下所示
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)限,只是不能繼承和重寫
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(){}
}
@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)度
【小技巧】:混編
頭文件查看方式:查看項(xiàng)目名-Swift.h
頭文件
!](//upload-images.jianshu.io/upload_images/2251862-dc95c85d154fc67c.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1172/format/webp)
- 如果只是通過(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ù))
即在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ā)
場(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
-
如果teach沒(méi)有實(shí)現(xiàn) / 如果去掉
dynamic
修飾符靶剑,會(huì)報(bào)錯(cuò)
總結(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()
從結(jié)果中可以看出手幢,是在堆區(qū)捷凄,即heap pointer
棧區(qū)
查看以下代碼的內(nèi)存地址位于哪個(gè)區(qū)?
func test(){
var age: Int = 10
print(age)
}
從結(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)存地址
其中
__DATA.__data
表示segment.section
,這里的位置和全局區(qū)并不沖突监透,因?yàn)橐粋€(gè)是人為的內(nèi)存分配(內(nèi)存布局分區(qū))
桶错,一個(gè)是Mach-O的segment.section
段中,是文件的格式劃分 -
查看age(全局未初始化變量)的內(nèi)存地址
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ò)) -
觀察3個(gè)變量的地址,其地址都是相鄰的,因?yàn)樵趦?nèi)存中都放在了全局區(qū)再榄,觀察其內(nèi)存地址狡刘,可以發(fā)現(xiàn),在
全局區(qū)
中困鸥,未初始化變量地址 比 已初始化變量地址 高
-
如果定義了一個(gè)
char *p = "CJLTeacher"
,查看*p
嗅蔬,存儲(chǔ)在__TEXT.cstring
段,內(nèi)存中存儲(chǔ)在常量區(qū)
-
如果是
const
修飾的變量呢疾就?存放在Mach-O文件中的__TEXT.__const
段 如果使用
static + const
修飾變量澜术,此時(shí)變量在哪?**
static const int age3 = 40;
-
查看
age3
的內(nèi)存地址虐译,地址特別大瘪板,而且使用cat
查看不了,因?yàn)閙ach-o沒(méi)有記錄漆诽,age3 就是30侮攀,即使用static+const
修飾的變量就相當(dāng)于直接替換
對(duì)于swift的分析
let age = 10
由于是不可變所以不能通過(guò)po+cat
查看內(nèi)存,通過(guò)匯編 首地址+偏移
來(lái)獲取age的內(nèi)存,發(fā)現(xiàn)是在Mach-O的__DATA.__common
段
從這里可以發(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
總結(jié)
作為一個(gè)開(kāi)發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要薄坏,這是一個(gè)我的iOS開(kāi)發(fā)交流群:130 595 548趋厉,不管你是小白還是大牛都?xì)g迎入駐 ,讓我們一起進(jìn)步胶坠,共同發(fā)展>恕(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔!)
- 對(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)有記錄