Swift枚舉底層研究

本文我們來探究Swift枚舉類型(Enum)的底層實現(xiàn)邏輯。如果不想看分析過程就珠,可以直接看最后的總結材蛛。
如果對文中的匯編知識不清楚臊泌,可以查閱ARM64匯編入門這篇文章。

枚舉內存分析

  • 枚舉的基本使用方法如下所示:
enum Direction {
    case North
    case South
    case East
    case West
}
  • 枚舉的內存大小
let size = MemoryLayout<Direction>.size              // 分配的內存大小是1字節(jié)
let stride = MemoryLayout<Direction>.stride          // 實際使用的內存大小是1字節(jié)
let alignment = MemoryLayout<Direction>.alignment    // 字節(jié)對齊是1

枚舉的內存大小是1個字節(jié)

  • 匯編分析
<!-- 測試代碼 -->
func test() {
    var direction = Direction.North
    direction = Direction.South
    direction = Direction.East
    direction = Direction.West
}

<!-- 對應的匯編 -->
JJSwift`test():
    0x100002be8 <+0>:  sub    sp, sp, #0x10   
    0x100002bec <+4>:  strb   wzr, [sp, #0xf] // 存儲的值是0
    0x100002bf0 <+8>:  mov    w8, #0x1
    0x100002bf4 <+12>: strb   w8, [sp, #0xf]  // 存儲的值是1
    0x100002bf8 <+16>: mov    w8, #0x2
    0x100002bfc <+20>: strb   w8, [sp, #0xf]  // 存儲的值是2
    0x100002c00 <+24>: mov    w8, #0x3
    0x100002c04 <+28>: strb   w8, [sp, #0xf]  // 存儲的值是3
    0x100002c08 <+32>: add    sp, sp, #0x10            
    0x100002c0c <+36>: ret    

內存中存儲的值和數(shù)組的索引類似坏平,從0開始拢操,每個元素的存儲值+1
Direction.North : 0
Direction.South : 1
Direction.South : 2
Direction.West : 3

枚舉原始值

內存分析

枚舉可以使用相同類型(包括String, Int, Float)的默認值和枚舉對應,這個默認值就是原始值(Raw Value)舶替。

  • 使用方法
enum HttpMethod: String {
    case GET                // 編譯器默認綁定"GET"
    case POST               // 編譯器默認綁定"POST"
    case PUT                // 編譯器默認綁定"PUT"
    case DELETE             // 編譯器默認綁定"DELETE"
    case PATCH = "OTHER"    // 綁定"OTHER"
}

enum Direction: Int {
    case North              // 編譯器默認綁定0
    case South              // 編譯器默認綁定1
    case East = 9           // 定9
    case West               // 編譯器默認綁定10
}

如果不指定原始值令境,編譯器會默認指定對應的原始值

  • 枚舉的內存大小
let size = MemoryLayout<Direction>.size              // 1
let stride = MemoryLayout<Direction>.stride          // 1
let alignment = MemoryLayout<Direction>.alignment    // 1

內存大小和未綁定原始值的情況一樣

  • 匯編分析
enum Direction: Int {
    case North              // 編譯器默認綁定0
    case South              // 編譯器默認綁定1
    case East = 9           // 定9
    case West               // 編譯器默認綁定10
}

<!-- 代碼 -->
func test() {
    var direction = Direction.North
    direction = Direction.South
    direction = Direction.East
    direction = Direction.West
}

<!-- 對應的匯編 -->
JJSwift`test():
    0x100002be8 <+0>:  sub    sp, sp, #0x10   
    0x100002bec <+4>:  strb   wzr, [sp, #0xf] // 存儲的值是0
    0x100002bf0 <+8>:  mov    w8, #0x1
    0x100002bf4 <+12>: strb   w8, [sp, #0xf]  // 存儲的值是1
    0x100002bf8 <+16>: mov    w8, #0x2
    0x100002bfc <+20>: strb   w8, [sp, #0xf]  // 存儲的值是2
    0x100002c00 <+24>: mov    w8, #0x3
    0x100002c04 <+28>: strb   w8, [sp, #0xf]  // 存儲的值是3
    0x100002c08 <+32>: add    sp, sp, #0x10            
    0x100002c0c <+36>: ret    

存儲值和未綁定原始值的情況一樣

獲取原始值
<!-- 代碼 -->
var raw = Direction.East.rawValue
rawValue

編譯器自動生成了一個存儲屬性rawValue

rawValue

存儲屬性的實現(xiàn)邏輯:
如果枚舉內存值為0(即Direction.North):則返回 0
如果枚舉內存值為1(即Direction.South):則返回 1
如果枚舉內存值為2(即Direction.East):則返回 9
如果枚舉內存值為3(即Direction.West):則返回 10

  • 編譯器等同于生成了以下代碼,可以獲取原始值
var rawValue: Int {
    get {
        switch self {
        case .North:
            return 0
        case .South:
            return 1
        case .East:
            return 9
        case .West:
            return 10
        }
    }
}
利用原始值初始化枚舉
<!-- 代碼 -->
var direction = Direction(rawValue: 10)
init

編譯器自動生成了一個init初始化方法

init

init的實現(xiàn)邏輯:
如果傳0顾瞪,返回枚舉Direction.North(內存值為0)
如果傳1舔庶,返回枚舉Direction.South(內存值為1)
如果傳9返劲,返回枚舉Direction.East(內存值為2)
如果傳10,返回枚舉Direction.West(內存值為3)
如果傳其他整形栖茉,返回nil(內存值為4)

  • 編譯器等同于生成了以下代碼篮绿,進行枚舉的初始化
init?(rawValue: Int) {
    switch rawValue {
    case 0: self = .North
    case 1: self = .South
    case 9: self = .East
    case 10: self = .West
    default: return nil
    }
}

枚舉可選項

init方法返回的是枚舉可選項,如果是空存儲的值是4吕漂,如果直接定義一個枚舉可選項呢亲配?

func test() {
    var dir: Direction?
}
JJSwift`test():
    0x100002bd0 <+0>:  sub    sp, sp, #0x10
    0x100002bd4 <+4>:  mov    w8, #0x4        // 直接賦值4
->  0x100002bd8 <+8>:  strb   w8, [sp, #0xf]
    0x100002bdc <+12>: add    sp, sp, #0x10 
    0x100002be0 <+16>: ret    

Direction?如果值為nil, 則內存中的值就確定是4

枚舉關聯(lián)值

每個枚舉項可以關聯(lián)一個值,這些值就是關聯(lián)值

enum Error {
    case error1(Int, Int, Int)
    case error2(Int, Int)
    case error3(Int)
    case error4
}
  • 枚舉的內存大小
let size = MemoryLayout<Direction>.size              // 25
let stride = MemoryLayout<Direction>.stride          // 32
let alignment = MemoryLayout<Direction>.alignment    // 8

只需要25個字節(jié)惶凝,實際占用了32個字節(jié)吼虎,8字節(jié)對齊

  • 匯編和內存分析
<!-- 代碼 -->
func test() {
    var error = Error.error1(1, 2, 3)
    error = Error.error2(4, 5)
    error = Error.error3(6)
    error = Error.error4
}

<!-- 對應的匯編 -->

JJSwift`test():
    0x100002608 <+0>:   sub    sp, sp, #0x20        // 壓棧   
    0x10000260c <+4>:   mov    w8, #0x1             
    0x100002610 <+8>:   str    x8, [sp]             // 將1存入sp內存地址開始的8字節(jié)
    0x100002614 <+12>:  mov    w8, #0x2
    0x100002618 <+16>:  str    x8, [sp, #0x8]       // 將2存入sp+0x8內存地址開始的8字節(jié)
    0x10000261c <+20>:  mov    w8, #0x3
    0x100002620 <+24>:  str    x8, [sp, #0x10]      // 將3存入sp+0x10內存地址開始的8字節(jié)
    0x100002624 <+28>:  strb   wzr, [sp, #0x18]     // 將0存入sp+0x18內存地址開始的1字節(jié) (第一個變量內存賦值完成)
    0x100002628 <+32>:  mov    w8, #0x4
    0x10000262c <+36>:  str    x8, [sp]
    0x100002630 <+40>:  mov    w8, #0x5
    0x100002634 <+44>:  str    x8, [sp, #0x8]
    0x100002638 <+48>:  str    xzr, [sp, #0x10]
    0x10000263c <+52>:  mov    w8, #0x1
    0x100002640 <+56>:  strb   w8, [sp, #0x18]
    0x100002644 <+60>:  mov    w8, #0x6
    0x100002648 <+64>:  str    x8, [sp]
    0x10000264c <+68>:  str    xzr, [sp, #0x8]
    0x100002650 <+72>:  str    xzr, [sp, #0x10]
    0x100002654 <+76>:  mov    w8, #0x2
    0x100002658 <+80>:  strb   w8, [sp, #0x18]
    0x10000265c <+84>:  str    xzr, [sp]
    0x100002660 <+88>:  str    xzr, [sp, #0x8]
    0x100002664 <+92>:  str    xzr, [sp, #0x10]
    0x100002668 <+96>:  mov    w8, #0x3
    0x10000266c <+100>: strb   w8, [sp, #0x18]
    0x100002670 <+104>: add    sp, sp, #0x20
    0x100002674 <+108>: ret    

我只分析了第一個變量的賦值:Error.error1(1, 2, 3)在內存中是1,2苍鲜,3思灰,0,其他的類似混滔。

我們也可以直接查看下Error.error1(1, 2, 3)內存的值:

(lldb) register read sp
      sp = 0x000000016fdfdae0
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000001 0x0000000000000002
0x16fdfdaf0: 0x0000000000000003 0x00000001000d8000

內存中確實也是存儲的 1 2 3 0 (注意:0x00000001000d8000 只需要看最后一個字節(jié)洒疚,因為數(shù)據(jù)只存了一個字節(jié),其他的數(shù)據(jù)是臟數(shù)據(jù), 所以只占用了25個字節(jié))

<!-- Error.error1(1, 2, 3) -->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000001 0x0000000000000002
0x16fdfdaf0: 0x0000000000000003 0x00000001000d8000

<!--Error.error2(4, 5)-->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000004 0x0000000000000005
0x16fdfdaf0: 0x0000000000000000 0x00000001000d8001

<!--Error.error3(6)-->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000006 0x0000000000000000
0x16fdfdaf0: 0x0000000000000000 0x00000001000d8002

<!--error4-->
(lldb) x/4gx 0x000000016fdfdae0
0x16fdfdae0: 0x0000000000000000 0x0000000000000000
0x16fdfdaf0: 0x0000000000000000 0x00000001000d8003

枚舉的方法

Swift的枚舉可以定義方法坯屿,我們看到枚舉變量的內存和方法是不相關的油湖,那枚舉變量是如何和方法關聯(lián)起來的呢?

enum Direction: Int {
    case North
    case South
    case East = 9
    case West
    // 定義方法
    func dirMethod() -> Int {
        return 1
    }
}

我們來看看方法調用的實現(xiàn):

func test() {
    let d = Direction(rawValue: 9)
    d?.dirMethod()
}
method

如果枚舉變量dnil领跛,函數(shù)dirMethod不會調用
如果枚舉變量d不為nil乏德,調用函數(shù)dirMethod前會將枚舉變量d作為參數(shù)傳入,從而實現(xiàn)了枚舉變量和函數(shù)的關聯(lián)

總結

  • 枚舉變量只占一個字節(jié)吠昭,如果超過255種情況不應該使用枚舉
  • 枚舉的內存中存儲的值依次是 0喊括,1,... n-1 整數(shù)(n為枚舉成員的數(shù)量)
  • 如果枚舉變量為nil, 則內存中存儲的值是n(n為枚舉成員的數(shù)量)
  • 如果枚舉的原始值矢棚,則編譯器會自動生成計算屬性rawValue和初始化方法init?(rawValue: T) -> ()
  • 枚舉的關聯(lián)值放在內存的前面郑什,和枚舉類型值放在一起存儲(關聯(lián)值的存儲長度由最長的那個枚舉成員決定)
  • 枚舉的方法調用會將枚舉變量作為參數(shù)傳入,實現(xiàn)枚舉和方法的關聯(lián)
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末幻妓,一起剝皮案震驚了整個濱河市蹦误,隨后出現(xiàn)的幾起案子劫拢,更是在濱河造成了極大的恐慌肉津,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舱沧,死亡現(xiàn)場離奇詭異妹沙,居然都是意外死亡,警方通過查閱死者的電腦和手機熟吏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門距糖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玄窝,“玉大人,你說我怎么就攤上這事悍引《髦” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵趣斤,是天一觀的道長俩块。 經常有香客問我,道長浓领,這世上最難降的妖魔是什么玉凯? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮联贩,結果婚禮上漫仆,老公的妹妹穿的比我還像新娘。我一直安慰自己泪幌,他們只是感情好盲厌,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著祸泪,像睡著了一般狸眼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浴滴,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天拓萌,我揣著相機與錄音,去河邊找鬼升略。 笑死微王,一個胖子當著我的面吹牛,可吹牛的內容都是我干的品嚣。 我是一名探鬼主播炕倘,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翰撑!你這毒婦竟也來了罩旋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤眶诈,失蹤者是張志新(化名)和其女友劉穎涨醋,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逝撬,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡浴骂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了宪潮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溯警。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡趣苏,死狀恐怖,靈堂內的尸體忽然破棺而出梯轻,到底是詐尸還是另有隱情食磕,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布喳挑,位于F島的核電站芬为,受9級特大地震影響,放射性物質發(fā)生泄漏蟀悦。R本人自食惡果不足惜媚朦,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望日戈。 院中可真熱鬧询张,春花似錦、人聲如沸浙炼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弯屈。三九已至蜗帜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間资厉,已是汗流浹背厅缺。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宴偿,地道東北人湘捎。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像窄刘,于是被迫代替她去往敵國和親窥妇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容