Swift匯編分析結(jié)構(gòu)體和類的本質(zhì)02

1平酿、結(jié)構(gòu)體

結(jié)構(gòu)體都有一個編譯器自動生成的初始化器塞椎。
根據(jù)情況可能會生成多個初始化器,保證所有的成員(存儲屬性喜庞、Stored Property)都有初始值。

struct Person {
    var name: String
    var age: Int
}
//正確
var person = Person(name: "wang", age: 18)
//錯誤
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()

可以看到我們在確保所有的儲存屬性有值的情況下去初始化才是正確的棋返。
那我們也可以這樣去定義屬性

struct Person {
    var name: String = ""
    var age: Int = 0
}

var person = Person(name: "wang", age: 18)
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()

這種情況下下面的即中初始化方法均可男摧,因為我們已經(jīng)給我們的存儲屬性賦了初值了。
那同樣吹零,我們也可以給定其初始值為nil柠新,這樣也可以使用下面的初始化方法。

當然射沟,我們也可以自定義初始化器殊者。

struct Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

同樣我們也可以在自定義的初始化器中給定其默認值。

struct Person {
    var age: Int?
    
    init() {
        self.age = 10
    }
}
var person = Person()

此時我們通過匯編進入看一下验夯,可以看到其調(diào)用了init函數(shù)

image.png

通過命令si進入該函數(shù)
image.png

可以看到在第8行處將10賦值到一處內(nèi)存中猖吴。

但如果我們沒有自定義初始化器呢?也就是直接給定其一個默認值的情況下挥转,其內(nèi)部是怎么調(diào)用的呢海蔽?

struct Person {
    var age: Int = 10
}
var person = Person()

通過斷點走入?yún)R編代碼共屈。可以看到與上面相比基本是一模一樣的党窜,甚至連調(diào)用方法的地址都是一致的拗引。

image.png

可以看到仍然是調(diào)用了init方法,同樣進入該方法中,可以看到也是有賦值操作的刑然。
image.png

也就是說寺擂,不管我們是通過自定義初始化器給定屬性默認值還是直接定義屬性時給定其默認值都是在init方法中將數(shù)據(jù)寫入屬性中去的。

至于結(jié)構(gòu)體在內(nèi)存中的布局其實與上一期我們講到的枚舉是一致的泼掠。
比如上面我們實例化的person對象怔软,其有一個屬性為age,該屬性在內(nèi)存中占8個字節(jié)择镇,又因為其內(nèi)存對齊字節(jié)為8挡逼,因此其內(nèi)存中的布局為0x000000000000000a

2腻豌、類

相比結(jié)構(gòu)體而言類是沒有可以傳入成員值的初始化器的家坎。
同樣,當我們定義了類以后沒有給其成員默認值的情況下也是會有錯誤提示的


image.png

但是類因為沒有默認的成員初始化器吝梅,因此我們直接在初始化時傳入成員屬性時不可行的:


image.png

此時需要我們手動去給其實現(xiàn)初始化器虱疏。

3、區(qū)別

其實結(jié)構(gòu)體和類的本質(zhì)區(qū)別在于結(jié)構(gòu)體是值類型而類是引用類型苏携。
也就是說結(jié)構(gòu)體對象其位于內(nèi)存中即椬龅桑空間中,而引用類型其真正的內(nèi)存空間在堆空間中右冻。
但嚴格來說我們并不能說結(jié)構(gòu)體內(nèi)存一定是在椬芭睿空間,其內(nèi)存分配在哪兒取決于它定義的位置纱扭,如果其在函數(shù)內(nèi)定義的那么其內(nèi)存在楇怪悖空間內(nèi)。如果其在外部定義乳蛾,那么其內(nèi)存在數(shù)據(jù)段內(nèi)(全局區(qū))暗赶。如果其作為一個類的屬性,那么其內(nèi)存自然是會分配在堆空間屡久。
至于類的話不管在哪兒定義其內(nèi)存一定是在堆空間忆首,在不同的地方定義改變的只是其指針的內(nèi)存位置。

問題:那如果結(jié)構(gòu)體定義在一個類的方法里邊其內(nèi)存會在哪兒被环?

3.1 內(nèi)存分配

那么我們怎么去區(qū)分一個對象是在棧空間還是在堆空間详幽?
其實如果對象是在堆空間時其初始化時會牽涉到alloc或者malloc筛欢。

此時我們定義一個類對象并實例化浸锨,此時加上斷點

image.png

匯編開啟后會看到
image.png

此時可以看到在第14行處明顯的有調(diào)用allocating_init,即向堆空間申請內(nèi)存版姑,此時通過命令si進入
image.png

可以看到在第16行處有allocObject,此時進入柱搜,然后一直通過si命令進入。
最終到這個地方:
image.png

可以看到在這里調(diào)用了一個slowAlloc方法剥险。
進入后可以看到在這里會調(diào)用系統(tǒng)級別的malloc方法分配內(nèi)存
image.png

而同樣的我們可以看到結(jié)構(gòu)體會調(diào)用init聪蘸,也就是直接分配在棧空間的表制。

//類對象堆空間申請內(nèi)存過程

接下來我們看看structclass在內(nèi)存中的地址健爬。定義一個結(jié)構(gòu)體以及類,并實例化

struct StructObject {
    var age: Int = 10
    var count: Int = 11
}


class ClassObject {
    var age: Int = 10
    var count: Int = 20
}

var strutObject = StructObject()
var classObject = ClassObject()



print("結(jié)構(gòu)體變量的地址:",Mems.ptr(ofVal: &strutObject))
print("結(jié)構(gòu)體變量的內(nèi)容:",Mems.memStr(ofVal: &strutObject))

print("----------------")

print("類變量指針的地址:",Mems.ptr(ofVal: &classObject))
print("類變量指針的內(nèi)容:",Mems.memStr(ofVal: &classObject))

此時我們查看打印結(jié)果:

結(jié)構(gòu)體變量的地址: 0x00000001000083c8
結(jié)構(gòu)體變量的內(nèi)容: 0x000000000000000a 0x000000000000000b
----------------
類變量指針的地址: 0x00000001000083d8
類變量指針的內(nèi)容: 0x000000010079c950

其中0x00000001000083c80x00000001000083d8分別是兩個對象的地址么介。我們查看內(nèi)存中的值娜遵,可以看到前8個字節(jié)是Person.age的值,后面8個字節(jié)是Person.count的值

image.png

而類對應(yīng)的內(nèi)存中只是存放了另一個地址壤短,而這個地址實際上指向的就是堆空間设拟。

image.png

可是我們根據(jù)該地址去查看內(nèi)存中的數(shù)據(jù)時發(fā)現(xiàn)前8個字節(jié)以及第2個8個字節(jié)中放的數(shù)據(jù)不知道是啥,而從第3個把個字節(jié)才看到是對應(yīng)的屬性的值久脯。
其實類對應(yīng)的存儲空間中纳胧,前8個字節(jié)指向的是內(nèi)存信息,第2個8字節(jié)是引用計數(shù)信息帘撰,后面就是成員信息了跑慕。
我們通過計算類變量指針地址與結(jié)構(gòu)體變量的指針地址可以獲知其差了0x10,也就是16個字節(jié),那么0x00000001000083c8這個地址存放0x000000000000000a骡和,接下來8個字節(jié)存放0x000000000000000b相赁,接下來的地址0x00000001000083d8存放的就是0x000000010079c950內(nèi)容,也就是這三個挨著的地址前兩個放著結(jié)構(gòu)體的內(nèi)容慰于,后一個存放著類對象的地址钮科。

我們也可以打印出類對象對應(yīng)的信息:

print("類變量指針所指向的內(nèi)容的地址:",Mems.ptr(ofRef: classObject))
print("類變量指針所指向的內(nèi)容的內(nèi)容:",Mems.memStr(ofRef: classObject))

類變量指針所指向的內(nèi)容的地址: 0x00000001006086f0
類變量指針所指向的內(nèi)容的內(nèi)容: 0x00000001000082c8 0x0000000200000002 0x000000000000000a 0x0000000000000014

可以看到指針中存放的就是類對象的地址,類對象的4個8字節(jié)即是前面所說的一些信息婆赠。

我們也可以分析結(jié)構(gòu)體對象和類對象在內(nèi)存中的大小绵脯。
上述我們定義的結(jié)構(gòu)體對象有2個屬性,那么其在內(nèi)存中的大小為16字節(jié)休里。
類對象同樣也有兩個屬性蛆挫,但是指針僅占8字節(jié),而這8個字節(jié)中存的是類對象的地址妙黍,而真正的類對象是占用32個字節(jié)的悴侵。
上述結(jié)論可通過MemoryLayout打印結(jié)果可知。

print("結(jié)構(gòu)體內(nèi)存大小",MemoryLayout<StructObject>.stride)
print("類對象指針內(nèi)存大小",MemoryLayout<ClassObject>.stride)

結(jié)構(gòu)體內(nèi)存大小 16
類對象指針內(nèi)存大小 8
3.2 值類型匯編分析

值類型為深拷貝(deep copy) 拭嫁,引用類型為淺拷貝(shallow copy)
深拷貝也就是賦值一份副本可免,即重新分配一塊內(nèi)存而淺拷貝僅僅是定義一個指針指向相同的內(nèi)存抓于。

    func test() {

    struct Auction {
        var id: Int
        var type: Int
    }

    let auction = Auction(id: 10, type: 20)
    var p = auction
    p.id = 11
    p.type = 22
    print("qeqwwq")

}
test()

同樣斷點分析匯編

image.png

直接找立即數(shù),可以看到在第8行和第9行處將10和20分別放入ediesi中,而實際上ediesi內(nèi)容在rdirsi中可取得浇借,地市我們進入init方法中:
image.png

可以看到分別從rdirsi中取值放入raxrdx中捉撮,這個時候raxrdx分別存放的就是1020,此時再看第一張圖片中call指令下面可以看到分別將raxrdx中的值放入了-0x10(%rbp)-0x8(%rbp)中,同時也放入了-0x20(%rbp)-0x18(%rbp)中妇垢,其實這也就對應(yīng)了代碼的實例化auction以及p = auction 巾遭,第15行和16行又將1122放入到p對應(yīng)的內(nèi)存中,也就是重新給p賦值操作闯估。
此時我們是將結(jié)構(gòu)體定義在函數(shù)內(nèi)灼舍,實際上我們將結(jié)構(gòu)體定義在外部也是一樣,只是稍顯復(fù)雜而已睬愤,但是其邏輯完全相同片仿。

在swift標準庫中字符串、數(shù)組尤辱、字典其底層都是結(jié)構(gòu)體砂豌,因此其也都是值類型。
而swift中為了提升性能它們采用 Copy On Write機制光督。也就是修改的時候才去copy阳距,如果不進行修改的時候仍然是同一份內(nèi)存。

當對同一個結(jié)構(gòu)體對象進行第二次實例化的時候其只是對原地址中的數(shù)據(jù)進行重新賦值结借,其地址不會改變筐摘。

var auction = Auction(id: 10, type: 20)
auction = Auction(id: 11, type: 22)

image.png

查看匯編,直接找到立即數(shù)1020可以看到分別賦值給ediesi船老,也就是rdirsi咖熟。call調(diào)用可函數(shù),進入可以看到柳畔,從rdirsi中取出內(nèi)容又給了raxrdx馍管。

SwiftStudy`init(id:type:) in Auction #1 in test():
->  0x1000010e0 <+0>:  pushq  %rbp
    0x1000010e1 <+1>:  movq   %rsp, %rbp
    0x1000010e4 <+4>:  movq   %rdi, %rax
    0x1000010e7 <+7>:  movq   %rsi, %rdx
    0x1000010ea <+10>: popq   %rbp
    0x1000010eb <+11>: retq   

此時我們看匯編斷點的截圖,函數(shù)調(diào)用完成后又把raxrdx中的值給了-0x10(%rbp)-0x8(%rbp)薪韩,此時我們查看對應(yīng)的內(nèi)存空間:

image.png

可以看到對應(yīng)的正是1020确沸,那么對應(yīng)的地址0x7FFEEFBFF530則是通過拿到rbp的地址后計算得到對應(yīng)的地址。
可以看到后面又重新走了賦值俘陷,調(diào)用call方法然后再賦值的操作罗捎。可以看到后面的賦值和前面的賦值的地址是一致的拉盾。

3.3 引用類型匯編分析

定義一個類

func testClass() {
    
    class Person {
        var age: Int
        var height: Int

        init(age: Int, height: Int) {
            self.age = age
            self.height = height
        }
    }

    var person = Person(age: 10, height: 20)
    var person1 = person
    person1.age = 11
    person1.height = 22
}

同樣桨菜,進入?yún)R編斷點后直接找對應(yīng)的立即數(shù),可以看待第13捉偏、14行對應(yīng)的分別是1020雷激,分別放入寄存器ediesi替蔬,然后執(zhí)行call指令告私。

image.png

接下來調(diào)用了allocating_init屎暇,進入該函數(shù)可以看到從對應(yīng)的寄存器中取值后給到了相鄰的兩個地址中。

image.png

結(jié)束函數(shù)調(diào)用驻粟,回到執(zhí)行call

image.png

可以知道根悼,函數(shù)調(diào)用結(jié)束后將rax中的值給了-0x10(%rbp)-0x60(%rbp)
而根據(jù)我們的代碼可知蜀撑,在實例化person后會將person賦值給person1挤巡。那么和這里的匯編代碼吻合了,也就是rax存放的就是實例化Person的地址酷麦,該地址分別給放在了對應(yīng)的兩個地址中矿卑。

  • rax一般存放函數(shù)調(diào)用的返回值
  • -0x60(%rbp) 類似于這種rbp-地址值的類型一般都是局部變量

獲取rax存放的地址值,直接去查看改地址對應(yīng)的內(nèi)存

(lldb) register read rax
     rax = 0x00000001005388f0
(lldb) memory read 0x00000001005388f0
0x1005388f0: c0 72 00 00 01 00 00 00 02 00 00 00 00 00 00 00  .r..............
0x100538900: 0a 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00  ................
(lldb) 
image.png

可以看到沃饶,存放數(shù)據(jù)的共有32個字節(jié)母廷,第一個8字節(jié)存放的是類信息,第2個8字節(jié)存放的是引用計數(shù)信息糊肤,之后便是其兩個屬性數(shù)據(jù)了琴昆。
接下來我們?nèi)タ葱薷?code>person1的屬性值。同樣我們?nèi)フ覍?yīng)的立即數(shù)

image.png

可知馆揉,將兩個立即數(shù)放入到了對應(yīng)的兩個對應(yīng)的空間业舍。此時斷點在37行,獲取rax的地址升酣,此時的地址既是對應(yīng)的personperson1內(nèi)存放的地址舷暮。

(lldb) register read rax
     rax = 0x00000001005388f0

而又分別在0x00000001005388f0的基礎(chǔ)上移動了16個字節(jié)(0x100538900)和24個字節(jié)(0x100538908)對應(yīng)的不就是兩個屬性數(shù)據(jù)的地址,也就是這里直接是修改了屬性的值了噩茄,從而personperson1指向的堆空間的數(shù)據(jù)也改變了下面。

  • 類似rax+地址值 一般都是堆空間的地址
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市巢墅,隨后出現(xiàn)的幾起案子诸狭,更是在濱河造成了極大的恐慌,老刑警劉巖君纫,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驯遇,死亡現(xiàn)場離奇詭異,居然都是意外死亡蓄髓,警方通過查閱死者的電腦和手機叉庐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來会喝,“玉大人陡叠,你說我怎么就攤上這事玩郊。” “怎么了枉阵?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵译红,是天一觀的道長。 經(jīng)常有香客問我兴溜,道長侦厚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任拙徽,我火速辦了婚禮刨沦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘膘怕。我一直安慰自己想诅,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布岛心。 她就那樣靜靜地躺著来破,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鹉梨。 梳的紋絲不亂的頭發(fā)上讳癌,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音存皂,去河邊找鬼晌坤。 笑死,一個胖子當著我的面吹牛旦袋,可吹牛的內(nèi)容都是我干的骤菠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼疤孕,長吁一口氣:“原來是場噩夢啊……” “哼商乎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祭阀,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鹉戚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后专控,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抹凳,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年伦腐,在試婚紗的時候發(fā)現(xiàn)自己被綠了赢底。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖幸冻,靈堂內(nèi)的尸體忽然破棺而出粹庞,到底是詐尸還是另有隱情,我是刑警寧澤洽损,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布庞溜,位于F島的核電站,受9級特大地震影響趁啸,放射性物質(zhì)發(fā)生泄漏强缘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一不傅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧赏胚,春花似錦访娶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至典勇,卻和暖如春劫哼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背割笙。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工权烧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伤溉。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓般码,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乱顾。 傳聞我的和親對象是個殘疾皇子板祝,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355