Swift底層探索(二):類型屬性枝哄、值類型&引用類型

延遲存儲(chǔ)屬性

class HotpotCat {
    lazy var age: Int = 20
}

var hotpot = HotpotCat()
hotpot.age = 30

延遲屬性賦值&大小

在hotpot.age賦值前和賦值后各打個(gè)斷點(diǎn),x/8g看下內(nèi)存地址闰非,可以看到賦值前是0x0膘格,沒有值峭范。


image.png

我們知道不加lazy的情況下內(nèi)存大小為24财松,那么加了lazy后呢?

print(class_getInstanceSize(HotpotCat.self))
32
(lldb) 

發(fā)現(xiàn)變?yōu)?2了,多了8字節(jié)辆毡。那么分析下SIL:
這里有個(gè)小細(xì)節(jié)是加xcrun swift-demangle菜秦,會(huì)還原sil中swift的符號(hào),方便閱讀

 swiftc -emit-sil main.swift | xcrun swift-demangle  >> ./main.sil && open main.sil

我們可以看到在main函數(shù)中舶掖,會(huì)給age賦值球昨,調(diào)用了agesetter方法

  %14 = class_method %9 : $HotpotCat, #HotpotCat.age!setter : (HotpotCat) -> (Int) -> (), $@convention(method) (Int, @guaranteed HotpotCat) -> () // user: %15

agesetter方法中,可以看到是個(gè)可選值眨攘。

image.png

getter同理缝驳,有值走bb1欣除,沒有值(Optional.none)走bb2賦默認(rèn)值。
image.png

那么延遲加載屬性本質(zhì)上是可選類型,在沒有被訪問前咏连,默認(rèn)值是nil(0x0) 。在getter方法中枚舉值分支進(jìn)行賦值操作堪藐。
那這里就解釋了為什么這里存儲(chǔ)屬性加上lazy就變成32字節(jié)了滋恬。因?yàn)橐鎯?chǔ)枚舉信息,實(shí)際大小為9字節(jié)秦效,字節(jié)對(duì)齊16字節(jié)雏蛮。

print(MemoryLayout<Optional<Int>>.size)
print(MemoryLayout<Optional<Int>>.stride)
9
16

swift enum默認(rèn)大小為Int8類型,編譯器會(huì)根據(jù)枚舉數(shù)量調(diào)整(Int8阱州、Int16挑秉、Int32Int64)苔货。如果只有一個(gè)枚舉值系統(tǒng)也會(huì)優(yōu)化size0衷模,stride1
獲取實(shí)例的大小可以通過MemoryLayout.size(ofValue:)獲取蒲赂。

延遲存儲(chǔ)屬性不能保證線程安全

上面我們分析了在getter方法中阱冶,會(huì)有bb1bb2兩個(gè)分支,假設(shè)此時(shí)線程1在訪問bb2并且沒有結(jié)束滥嘴,線程2也調(diào)用getter那么也可能會(huì)走bb2分支木蹬。這里就不能保證線程安全。

image.png

  • 用關(guān)鍵字lazy來標(biāo)識(shí)一個(gè)延遲存儲(chǔ)屬性若皱。
  • 延遲存儲(chǔ)屬性必須有一個(gè)默認(rèn)的初始值镊叁。
  • 延遲存儲(chǔ)屬性在第一次訪問的時(shí)候才被賦值。
  • 延遲存儲(chǔ)屬性并不能保證線程安全走触。
  • 延遲存儲(chǔ)屬性對(duì)實(shí)例對(duì)象大小有影響晦譬。

類型屬性

class HotpotCat {
    static var age: Int = 20
}

var age = HotpotCat.age
// globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
sil_global private @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $Builtin.Word
// static HotpotCat.age
sil_global hidden @static main.HotpotCat.age : Swift.Int : $Int

生成的age變成了全局的變量sil_global

image.png

ageHotpotCat.age.unsafeMutableAddressor生成的。

// HotpotCat.age.unsafeMutableAddressor
sil hidden [global_init] @main.HotpotCat.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  %0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
  %2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  %4 = global_addr @static main.HotpotCat.age : Swift.Int : $*Int // user: %5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function 'main.HotpotCat.age.unsafeMutableAddressor : Swift.Int'

這個(gè)方法執(zhí)行了全局函數(shù)globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC,并且這里有個(gè)builtin "once"互广。

// globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
sil private @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () {
bb0:
  alloc_global @static main.HotpotCat.age : Swift.Int       // id: %0
  %1 = global_addr @static main.HotpotCat.age : Swift.Int : $*Int // user: %4
  %2 = integer_literal $Builtin.Int64, 20         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function 'globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0'

這個(gè)函數(shù)將初始值賦值給了全局變量age敛腌。
前面說到的once就是swift_once卧土,我們?cè)创a里面搜索一下swift_once

image.png

可以看到dispatch_once_f,這也就是我們的GCD像樊,所以static類型屬性只會(huì)被初始化一次尤莺。

  • 用static聲明類型屬性
  • 類型屬性屬于類本身,不論有多少個(gè)實(shí)例生棍,類型屬性只有一份
  • 全局的
  • 線程安全的

單例

首先回顧下OC單類的寫法颤霎,需要dispatch_once確保線程安全并初始化一次,并且為了保證安全還要重寫allocWithZone

+ (instancetype)sharedInstance {
    static HotpotCat *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[HotpotCat alloc] init];
    });
    return instance;
}

swift 2.0的時(shí)候swift的單類也是翻譯的oc的寫法,現(xiàn)在已經(jīng)不用了涂滴。

swift單類只需要兩點(diǎn):

  • static修飾一個(gè)常量友酱。
  • init添加一個(gè)訪問控制權(quán)限。
class HotpotCat {
    static let sharedInstance = HotpotCat()
    private init(){
    }
}
var hotpot = HotpotCat.sharedInstance

值類型

struct

struct HotpotCatStruct {
    var age: Int
    var name: String
    
}

class HotpotCatClass {
    var age: Int
    var name: String
}

image.png

對(duì)于HotpotCatStruct而言柔纵,已經(jīng)有自動(dòng)合成的初始化方法粹污,對(duì)于HotpotCatClass而言"Class 'HotpotCatClass' has no initializers"。
通過SIL查看下

struct HotpotCatStruct {
  @_hasStorage var age: Int { get set }
  @_hasStorage var name: String { get set }
  init(age: Int, name: String)
}

已經(jīng)有了init(age: Int, name: String)方法首量。
那么我們給age一個(gè)默認(rèn)值呢?

struct HotpotCatStruct {
    var age: Int = 1
}
//sil
struct HotpotCatStruct {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  init(age: Int = 1)
  init()
}

可以看到既有默認(rèn)的初始化方法壮吩,也有age初始化方法。
那么我們自己實(shí)現(xiàn)了init方法呢?

struct HotpotCatStruct {
    var age: Int = 1
    
    init(age: Int) {
        self.age = age
    }
}
//sil
struct HotpotCatStruct {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  init(age: Int)
}

可以看到編譯器不會(huì)幫我們生成了加缘。

  • 結(jié)構(gòu)體不需要自定義初始化方法鸭叙,系統(tǒng)自動(dòng)合成。
  • 如果屬性有默認(rèn)值拣宏,系統(tǒng)會(huì)提供不同的默認(rèn)初始化方法沈贝。
  • 如果自定義初始化方法,系統(tǒng)不會(huì)幫我們生成勋乾。

struct值類型

首先明白下基本概念

  • 地址存儲(chǔ)的就是值
  • 傳遞過程中傳遞副本
struct HotpotCat {
    var age: Int = 1
    var age2: Int = 18
}
var hotpot = HotpotCat()
//lldb
(lldb) po hotpot
? HotpotCat
  - age : 1
  - age2 : 18

po直接打印出來是值宋下,不是地址。說明是值類型辑莫,我們?cè)俅蛴∫幌聝?nèi)存地址看下存儲(chǔ)的內(nèi)容

(lldb) po withUnsafeMutablePointer(to: &hotpot){print($0)}
0x0000000100008028
0 elements

image.png

可以看到地址存儲(chǔ)的確實(shí)是ageage2学歧。
再將hotpot賦值給hotpot2
image.png

再看下SIL,發(fā)現(xiàn)直接調(diào)用了init各吨,沒有alloc
image.png

我們?cè)倏聪?code>init
image.png

可以看到默認(rèn)alloc_stack自身枝笨,在ageage2賦值的時(shí)候是從self往下找的。init返回分配在satck的自身揭蜒。所以他是一個(gè)值類型横浑。

引用類型

那么如果是class呢?

class HotpotClass {
    var age: Int = 1
    var age2: Int = 18
}

var hotpotClass = HotpotClass()

(lldb) po hotpotClass
<HotpotClass: 0x100634610>

po直接顯示地址屉更。

image.png

對(duì)于引用類型存儲(chǔ)的是地址徙融。
如果值類型中有引用類型呢?

struct HotpotStruct {
    var age: Int = 1
    var age2: Int = 18
    var hotpotClass: HotpotClass = HotpotClass()
}

class HotpotClass {
    var age: Int = 1
    var age2: Int = 18
}

var hotpotStruct = HotpotStruct()
var hotpotStruct2 = hotpotStruct
image.png

可以看出依然傳遞的是地址瑰谜,所以應(yīng)當(dāng)避免值類型中包含引用類型欺冀。
我們?cè)偻ㄟ^SIL看一下

struct HotpotStruct {
    var age: Int = 1
    var age2: Int = 18
    var hotpotClass: HotpotClass
}

class HotpotClass {
    var name: String = "HotpotCat"
}

var hotpotClass = HotpotClass()
var hotpotStruct = HotpotStruct(hotpotClass: hotpotClass)
var hotpotStruct2 = hotpotStruct

看到這里有引用計(jì)數(shù)了,strong_retain搜索查看下發(fā)現(xiàn)一共有3處树绩。

image.png

我們打印一下看看

(lldb) po CFGetRetainCount(hotpotStruct.hotpotClass)
3

mutating

image.png
struct HotpotStruct {
    var count: Int = 0
    init() {
        self.count = 10
    }
    mutating func clear() {
        self.count = 0
    }
}

值類型本身創(chuàng)建后不允許修改,要修改需要加上mutating關(guān)鍵字
那么mutating到底干了什么呢脚猾?
先看個(gè)正常訪問變量的例子

struct HotpotStruct {
    var count: Int = 0
    func clear() {
        print(count)
    }
}

對(duì)應(yīng)的SILclear實(shí)現(xiàn)如下,可以看到隱藏參數(shù)selflet修飾砚哗,修改count就為修改地址也就是修改值類型self本身龙助,所以方法中不能修改count

image.png

init中為什么能修改呢蛛芥?initalloc_stackselfvar提鸟。
image.png

那如果self我們用var接收再修改呢?修改是可以修改仅淑,只不過修改的是self的另外一份拷貝(值傳遞)称勋。

    func clear() {
        var s = self
        s.count = 10
    }

加了mutaing進(jìn)行了什么操作呢?可以看到self是用var修飾的添加了inout關(guān)鍵字,debug_value也變成了debug_value_addr

image.png

mutaing本質(zhì)就是給參數(shù)添加了inout關(guān)鍵字傳遞地址涯竟,只用于值類型赡鲜,因?yàn)橐妙愋捅旧砭褪菍?duì)地址的操作
再用&操作驗(yàn)證下

func swap(_ a:inout Int, _ b:inout Int) {
    a = a ^ b
    b = a ^ b
    a = a ^ b
}

var valueA = 1
var valueB = 2
swap(&valueA, &valueB)
image.png

struct函數(shù)調(diào)用方式

struct HotpotStruct {
    func clear() {
       print("clear")
    }
}

var hotpotStruct =  HotpotStruct()
hotpotStruct.clear()

在函數(shù)調(diào)用處打個(gè)斷點(diǎn),直接看下匯編代碼看到是直接調(diào)用的函數(shù)地址(0x100003da0)

image.png

control +step into跟進(jìn)去可以看到執(zhí)行的就是HotpotStruct.clear()庐船。
接著我們直接分析下可執(zhí)行文件
image.png

可以確定編譯鏈接完成后银酬,可執(zhí)行文件調(diào)用的函數(shù)地址就已經(jīng)確定了。
結(jié)構(gòu)體中的方法是靜態(tài)調(diào)度(編譯筐钟、鏈接完成后函數(shù)地址就已經(jīng)確定存放在了代碼段)
剛才可執(zhí)行程序分析的時(shí)候并沒有對(duì)應(yīng)的符號(hào)SwiftStructMutating.HotpotStruct.clear()揩瞪,符號(hào)從哪里來的呢?符號(hào)表Symbol Table 存儲(chǔ)符號(hào)位于字符串表的偏移)篓冲。clear在字符串表偏移0x2李破。靜態(tài)函數(shù)只在debug下有符號(hào),release下會(huì)生成dsym文件壹将。不能確定地址的當(dāng)然還會(huì)有嗤攻。Debug模式下只是方便我們調(diào)試
image.png

符號(hào)表不直接存放字符串,字符串存放在字符串表(String Table,存放了所有的變量名诽俯、函數(shù)名)屯曹。
image.png

偏移兩個(gè)字節(jié),從005F開始~005F結(jié)束惊畏,都是clear的符號(hào)恶耽。這里的字符串經(jīng)過了命名重整。
我們直接在終端nm 可執(zhí)行文件路徑 dump下符號(hào)

0000000100003da0 T _$s19SwiftStructMutating06HotpotB0V5clearyyF
0000000100003e90 T _$s19SwiftStructMutating06HotpotB0VACycfC
0000000100003f64 s _$s19SwiftStructMutating06HotpotB0VMF
0000000100003ea0 T _$s19SwiftStructMutating06HotpotB0VMa
0000000100004018 s _$s19SwiftStructMutating06HotpotB0VMf
0000000100003f40 S _$s19SwiftStructMutating06HotpotB0VMn
0000000100004020 S _$s19SwiftStructMutating06HotpotB0VN
0000000100003f24 s _$s19SwiftStructMutatingMXM

還原一下符號(hào)名稱

xcrun swift-demangle s19SwiftStructMutating06HotpotB0V5clearyyF
$s19SwiftStructMutating06HotpotB0V5clearyyF ---> SwiftStructMutating.HotpotStruct.clear() -> ()

搜索符號(hào)nm 可執(zhí)行文件路徑 | grep 地址這里地址不帶0x

nm /Users/***/Library/Developer/Xcode/DerivedData/SwiftStructMutating-bdtkkopulnojomaeuxaauwsnscxz/Build/Products/Debug/SwiftStructMutating | grep 0100003da0
0000000100003da0 T _$s19SwiftStructMutating06HotpotB0V5clearyyF

與之對(duì)應(yīng)的我們看一下print的調(diào)度方式

image.png

image.png

c/oc中方法的調(diào)度

void test(){
    
}

int main(int argc, const char * argv[]) {
    test();
    return 0;
}

可以看到c語言中符號(hào)直接就是_test,所以這也是c語言不允許函數(shù)重載的原因颜启。OC同理-[Class selector]偷俭。

image.png

這也就解釋了Swift命名重整規(guī)則復(fù)雜的原因(確保符號(hào)的唯一)。

struct HotpotCat {
    func pot123() {
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var hotpot = HotpotCat()
        hotpot.pot123()
    }
}

打下斷點(diǎn)看下函數(shù)地址0x10ff42bb0

image.png

image.png

可以看到兩個(gè)地址有偏差缰盏,這里偏差的就是ALSR(隨機(jī)地址偏移)
可以通過image list查看,這里首地址0x0ff3f000就是ALSR涌萤。

[  0] A3D54669-3D78-3CA1-A48F-958F7718E05B 0x000000010ff3f000

VM Address靜態(tài)基地址淹遵,首地址就是根據(jù)靜態(tài)及地址+偏移量確定的。

image.png

pot123的地址0000000100003BB0+隨機(jī)偏移地址(0x0ff3f000)=運(yùn)行時(shí)地址(0x10ff42bb0)
image.png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末负溪,一起剝皮案震驚了整個(gè)濱河市透揣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌川抡,老刑警劉巖辐真,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異崖堤,居然都是意外死亡侍咱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門密幔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來楔脯,“玉大人,你說我怎么就攤上這事胯甩∶镣ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵偎箫,是天一觀的道長(zhǎng)麸粮。 經(jīng)常有香客問我,道長(zhǎng)镜廉,這世上最難降的妖魔是什么弄诲? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮娇唯,結(jié)果婚禮上齐遵,老公的妹妹穿的比我還像新娘。我一直安慰自己塔插,他們只是感情好梗摇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著想许,像睡著了一般伶授。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上流纹,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天糜烹,我揣著相機(jī)與錄音,去河邊找鬼漱凝。 笑死疮蹦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茸炒。 我是一名探鬼主播愕乎,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼阵苇,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了感论?” 一聲冷哼從身側(cè)響起绅项,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎比肄,沒想到半個(gè)月后快耿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薪前,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年润努,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了关斜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片示括。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痢畜,靈堂內(nèi)的尸體忽然破棺而出垛膝,到底是詐尸還是另有隱情,我是刑警寧澤丁稀,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布吼拥,位于F島的核電站,受9級(jí)特大地震影響线衫,放射性物質(zhì)發(fā)生泄漏凿可。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一授账、第九天 我趴在偏房一處隱蔽的房頂上張望枯跑。 院中可真熱鬧,春花似錦白热、人聲如沸敛助。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纳击。三九已至,卻和暖如春攻臀,著一層夾襖步出監(jiān)牢的瞬間焕数,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工刨啸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留百匆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓呜投,卻偏偏與公主長(zhǎng)得像加匈,于是被迫代替她去往敵國(guó)和親存璃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容