Swift中結構體的方法調度&內存分區

函數方法調度

  • 結構體的方法調度

如下結構體

struct YYTeacher {
    func teach() {
        print("teach")
    }
}

var t = YYTeacher()
# 此處添加斷點
t.teach()

匯編模式下,可知結構體函數調用方式是靜態調用(直接調用):

通過在MachOView中打開可執行文件

  • __Text,__text:代碼段
    編譯時,每一個swift文件都會經過編譯匯編形成.o文件(目標文件),所有的.o文件,最終會合成一個文件,當前代碼會根據鏈接順序依次在.o中排列好統一放在text字段里。

通過上圖可知:在調用函數時,不用再去其他地方查找teach的函數地址,編譯鏈接完成之后,地址就已經確定放在text字段里;所以說結構體的函數調度方式是靜態調度,意味著結構體存儲其中的函數,執行效率非常。

  • Symbol Table:符號表,用于調試過程

存儲的是符號位于(String Table)字符串表中的位置,直接存儲符號。

  • String Table:字符串表
    所有的變量名函數名字符串的形式存放在字符串表中。

符號經過swift命令重整(nm)變成了符號表中存放的內容。
所以也可以通過以下命令在終端拿到符號表

nm  path:拿到符號表
nm  path | grep addr:在符號表中通過指定函數地址搜索指定符號

其中:
path-->可執行文件的地址
addr-->指定函數地址
如下圖:

Release模式下,會多生成一個.dsYM文件用于捕獲崩潰、查找debug信息,在線上使用該文件。 符號表保留那些靜態鏈接的函數符號(在字符串表中的位置信息),因為一旦編譯完成就能確定地址,這時符號表精簡很多,不占用macho文件大小,保留的是那些不能確定地址的符號(在字符串表中的位置)。

總結:靜態調度的函數一旦編譯完成就能確定地址,再通過地址調用函數,只是在debug模式下為了方便調試才將該地址的符號信息以字符串形式存儲字符串表中,在字符串表中的位置信息存儲符號表中,并是通過符號表中去查找到函數地址再進行調度,要注意先后順序。

  • Dynamic Symbol Table動態庫函數位于符號表偏移信息

  • 命令重整規則

  • 對于C的函數來說, 其命令重整就是直接在函數前加“_”,所以如果在C里面定義兩個同名的函數,即使參數和返回值不同,也是不被允許的。

  • 對于OC的函數來說,其命令重整則是 -[YYTeacher test:],所以定義兩個相同名稱、相同參數個數的函數,即使返回值不一樣,也是不被允許的。因為調用函數是通過classselector去查找的,它只根據函數名參數個數去查找,如果函數名和參數個數都一樣,查找出來多個就不知道調用哪個函數。

  • 對于Swift的函數,命令重整就比較復雜,確保符號的唯一性。這樣就使得Swift中可以定義多個名稱相同參數類型不同的函數。

  • 疑問每一次運行靜態調用的函數地址都是一樣的嗎?
    答:每一次運行函數地址是絕對一樣的,因為它取決于偏移地址ASLR地址隨機化)。

首先需了解:
程序的靜態基地址:在Load Commands__TEXT字段里,VM Address就是靜態基地址。

程序運行首地址:在lldb中通過image list命令來查看首地址。

隨機偏移地址:在可執行程序隨機裝載到內存中時的隨機地址,就是我們當前這application偏移的地址??赏ㄟ^程序運行首地址 - 程序的靜態基地址得到。

最終:靜態函數的地址 = 符號表中函數地址 + 隨機偏移地址

通過上圖可知:
偏移地址 = 程序運行首地址 - 程序的靜態基地址即0x5a47000

計算一下:靜態函數的地址 = 符號表中函數地址 + 隨機偏移地址 即
0x105a48db0 = 0x100001DB0 + 0x5a47000

  • 類的方法調度
  • 一般情況下,類中的方法是通過V-Table來進行調度。
    首先了解V-TableSIL中怎樣表示的,如下圖:

這張表的本質其實就類似我們理解的數組,聲明在class內部的方法在加任何關鍵字修飾的過程中,連續存放在我們當前的地址空間中。

首先了解ARM64下的幾個匯編指令

blr:    帶返回的跳轉指令,跳轉到指令后邊跟隨寄存器中保存的地址。
mov:    將某一寄存器的值復制到另一寄存器(只能用于寄存器和寄存器或者寄存器與常量之間傳值,不能用于內存地址)如
         mov x1 , x0 : 將寄存器 x0 的值復制到寄存器 x1 中
ldr:    將內存中的值讀取到寄存器中
         ldr x0, [x1, x2] : 將內存 [x1 + x2] 處的值放入寄存器 x0 中
str:    將寄存器中的值寫入到內存中
         str x0, [x0, x8] : 將寄存器 x0 的值保存在內存 [x0 + x8] 處
bl:跳轉到某地址

通過以下例子在匯編模式下:

class YYTeacher {
    func teach() {print("teach")}
    func teach1() {print("teach1")}
    func teach2() {print("teach2")}
    func teach3() {print("teach3")}
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var t = YYTeacher()
        t.teach()
        t.teach1()
        t.teach2()
        t.teach3()
    }
}

可以看出上面的函數都是按順序放在函數表中。

接下來通過SIL中查源碼斷點來看一下:

可看出V-Table就是一個數組結構。

  • extension聲明的方法

如果更改方法聲明的位置,將方法放在extension中聲明:

extension YYTeacher {
    func teach4() {
        print("teach4")
    }
}

匯編模式下可看出:如果方法聲明放在extension中,則是直接地址調用。為什么呢?舉個例子:在Swift中,一個類有子類,有extension,extension可以寫在任意Swift文件中,如果子類所在文件優extension所在文件加載,子類的函數表會首先繼承父類的函數表,其次是自己的函數列表,當加載到extension時發現有函數,這時子類中沒有指針記錄哪些是父類方法哪些是自己的方法,就沒法將extension中的方法按順序的插入自己的函數表中。

擴展:OC中分類方法的調用

  • final關鍵字:意味著當前方法能被子類繼承只能調用,該方法也不會加入V-Table中,聲明之后直接調用
class YYTeacher {
    final func teach() { print("teach")  }
    func teach1() { print("teach1") }
}

var t = YYTeacher()
t.teach()
t.teach1()

匯編模式下直接地址調用

SILV-Table中也沒有加入final修飾的teach函數:

  • @objc關鍵字:暴露頭文件和當前方法OC調用,在匯編模式下可知其方法還是通過V-Table進行調度。
    SIL中可看出:編譯后生成了兩個函數YYTeacher.teach()@objc YYTeacher.teach(),而在@objc YYTeacher.teach()函數內部又調用YYTeacher.teach()函數。

OC-Swift 橋接演示:
OC項目中新建Swift文件并選擇Create Bridging Header,Swift中:

class YYTeacher: NSObject {
    @objc func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
}

要在OC中使用Swift文件,就需要導入頭文件,頭文件查看方式如下:

如果YYTeacher不繼承NSObject,該頭文件中則沒有與YYTeacher相關的類信息,就不能訪問到YYTeacher這個類。
繼承NSObject后頭文件中才有下列信息:

接下來在OC文件中:

OC中,只能訪問到有@objc修飾的teach函數,而沒有@objc修飾的teach1則不能被訪問到。

  • dynamic關鍵字
    匯編模式下可看出:dynamic修飾的函數依然是通過V-Table函數表調用,表示可以動態修改。
  • Swift 中的函數可以是靜態調用,靜態調用會更快。Swift的代碼直接被編譯優化成靜態調用的時候,就不能從Objective-C 中的SEL字符串來查找到對應的IMP了。這樣就需要在 Swift 中添加一個關鍵字 dynamic,告訴編譯器這個方法是可能被動態調用的,需要將其添加到查找表中。

  • 繼承自NSObjectSwift類,其繼承自父類的方法具有動態性,其他自定義方法、屬性需要加dynamic修飾才可以獲得動態性。

  • 如果方法的參數、屬性類型為Swift特有、無法映射到Objective-C的類型(如Character、Tuple),則此方法、屬性無法添加dynamic修飾, 一旦添加就會編譯報錯。

  • 通過dynamic修飾的方法可以被動態替換

class YYTeacher {
    dynamic func teach() {print("teach")}
}

extension YYTeacher {
    @_dynamicReplacement(for: teach)
    func teach1() {
        print("teach1")
    }
}

var t = YYTeacher()
t.teach()

這時調用t.teach()打印的則是teach1@_dynamicReplacement(for:teach)extension中將teach()動態替換成teach1()。

  • @objc + dynamic
    在匯編模式下可知,被@objc + dynamic修飾的方法變成了動態消息轉發:

內存分區

內存分區模型如下圖:

  • 棧區(Stack):存放的是函數內部聲明的局部變量和函數運行過程中的上下文。
func test() {
    var age : Int = 10
    print(age)
}

上面例子中的age就存放在內存中。

  • 堆區(Heap):存放的是通過new & malloc關鍵字來申請的內存空間,不連續,類似鏈表的結構,最直觀就是對象。
class YYTeacher {
    var age : Int = 10
}
var t = YYTeacher()

上面例子中的t里面存放的地址就是在堆區地址。

  • 全局區
int a = 10;
int age;
 
static int age2 = 30;

int main(int argc, const char * argv[]) {
    char *p = "YYTeacher";
    printf("%d", a);
    printf("%d", age2); // 如果不訪問age2,直接在lldb中獲取age2的地址,是獲取不到的,因為不使用則不記錄。
    return 0;
}

在上面例子中,

注意:SEGMENTSECTIONMacho文件對格式的劃分,而內存分區是人為對內存布局的分區,所以對于上面例子中a存放在全局區和在Macho文件中存放__DATA.__data里面互不沖突。

從上面圖片中可以看出,全局已初始化變量a和age2的地址比較接近,而且比全局未初始化變量的地址,可以更詳細的對全局區進行分區:

如果例子中加入全局已初始化靜態常量

int a = 10;
int age;
 
static int age2 = 30;
static const int age3 = 30;

int main(int argc, const char * argv[]) {
    char *p = "YYTeacher";
    printf("%d", a);
    printf("%d", age2);
    int b = age3;
    return 0;
}

因為age3靜態不可修改的,macho文件直接會記錄age3符號信息,賦值過程中對于編譯器來說age3這個符號根本不存在,就是一個值30,這里的int b = age3就相當于int b = 30

對于Swift來說,let age = 10
這種情況下,因為age是不可變的,所以不允許通過po withUnsafePointer(to: &age){print($0)}這種方式來獲取age的地址。

可以通過以下方式在匯編模式下來獲取age的地址為0x100008028

可知age的符號信息在macho文件中存放在__DATA.__common里面.
綜上可知:和C/OC相比,Swift對于全局變量在Macho文件中的劃分規則不一樣的.

  • 常量區
    例子中p的符號信息在Macho文件中位于__TEXT.__cstring(常量字符串)里,內存分區中位于常量區
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容