原文鏈接: http://draveness.me/method-struct/
關(guān)注倉庫憔狞,及時(shí)獲得更新:iOS-Source-Code-Analyze
因?yàn)?ObjC 的 runtime 只能在 Mac OS 下才能編譯鼻忠,所以文章中的代碼都是在 Mac OS露乏,也就是
x86_64
架構(gòu)下運(yùn)行的,對于在 arm64 中運(yùn)行的代碼會(huì)特別說明匈庭。
在上一篇分析 isa
的文章從 NSObject 的初始化了解 isa中曾經(jīng)說到過實(shí)例方法被調(diào)用時(shí)污桦,會(huì)通過其持有 isa
指針尋找對應(yīng)的類,然后在其中的 class_data_bits_t
中查找對應(yīng)的方法饼记,在這一篇文章中會(huì)介紹方法在 ObjC 中是如何存儲(chǔ)方法的。
這篇文章的首先會(huì)根據(jù) ObjC 源代碼來分析方法在內(nèi)存中的存儲(chǔ)結(jié)構(gòu)慰枕,然后在 lldb 調(diào)試器中一步一步驗(yàn)證分析的正確性具则。
方法在內(nèi)存中的位置
先來了解一下 ObjC 中類的結(jié)構(gòu)圖:
-
isa
是指向元類的指針,不了解元類的可以看 Classes and Metaclasses -
super_class
指向當(dāng)前類的父類 -
cache
用于緩存指針和vtable
具帮,加速方法的調(diào)用 -
bits
就是存儲(chǔ)類的方法博肋、屬性低斋、遵循的協(xié)議等信息的地方
class_data_bits_t
結(jié)構(gòu)體
這一小結(jié)會(huì)分析類結(jié)構(gòu)體中的 class_data_bits_t bits
。
下面就是 ObjC 中 class_data_bits_t
的結(jié)構(gòu)體匪凡,其中只含有一個(gè) 64 位的 bits
用于存儲(chǔ)與類有關(guān)的信息:
在 objc_class
結(jié)構(gòu)體中的注釋寫到 class_data_bits_t
相當(dāng)于 class_rw_t
指針加上 rr/alloc 的標(biāo)志膊畴。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
它為我們提供了便捷方法用于返回其中的 class_rw_t *
指針:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
將 bits
與 FAST_DATA_MASK
進(jìn)行位運(yùn)算,只取其中的 [3, 47]
位轉(zhuǎn)換成 class_rw_t *
返回病游。
在 x86_64 架構(gòu)上唇跨,Mac OS 只使用了其中的 47 位來為對象分配地址。而且由于地址要按字節(jié)在內(nèi)存中按字節(jié)對齊衬衬,所以掩碼的后三位都是 0买猖。
因?yàn)?class_rw_t *
指針只存于第 [3, 47]
位,所以可以使用最后三位來存儲(chǔ)關(guān)于當(dāng)前類的其他信息:
#define FAST_IS_SWIFT (1UL<<0)
#define FAST_HAS_DEFAULT_RR (1UL<<1)
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
#define FAST_DATA_MASK 0x00007ffffffffff8UL
-
isSwift()
-
FAST_IS_SWIFT
用于判斷 Swift 類
-
-
hasDefaultRR()
-
FAST_HAS_DEFAULT_RR
當(dāng)前類或者父類含有默認(rèn)的retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
方法
-
-
requiresRawIsa()
-
FAST_REQUIRES_RAW_ISA
當(dāng)前類的實(shí)例需要 rawisa
-
執(zhí)行 class_data_bits_t
結(jié)構(gòu)體中的 data()
方法或者調(diào)用 objc_class
中的 data()
方法會(huì)返回同一個(gè) class_rw_t *
指針滋尉,因?yàn)?objc_class
中的方法只是對 class_data_bits_t
中對應(yīng)方法的封裝玉控。
// objc_class 中的 data() 方法
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
// class_data_bits_t 中的 data() 方法
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
class_rw_t
和 class_ro_t
ObjC 類中的屬性、方法還有遵循的協(xié)議等信息都保存在 class_rw_t
中:
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
};
其中還有一個(gè)指向常量的指針 ro
狮惜,其中存儲(chǔ)了當(dāng)前類在編譯期就已經(jīng)確定的屬性高诺、方法以及遵循的協(xié)議。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
在編譯期間類的結(jié)構(gòu)中的 class_data_bits_t *data
指向的是一個(gè) class_ro_t *
指針:
然后在加載 ObjC 運(yùn)行時(shí)的過程中在 realizeClass
方法中:
- 從
class_data_bits_t
調(diào)用data
方法讽挟,將結(jié)果從class_rw_t
強(qiáng)制轉(zhuǎn)換為class_ro_t
指針 - 初始化一個(gè)
class_rw_t
結(jié)構(gòu)體 - 設(shè)置結(jié)構(gòu)體
ro
的值以及flag
- 最后設(shè)置正確的
data
懒叛。
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
下圖是 realizeClass
方法執(zhí)行過后的類所占用內(nèi)存的布局,你可以與上面調(diào)用方法前的內(nèi)存布局對比以下耽梅,看有哪些更改:
但是薛窥,在這段代碼運(yùn)行之后 class_rw_t
中的方法,屬性以及協(xié)議列表均為空眼姐。這時(shí)需要 realizeClass
調(diào)用 methodizeClass
方法來將類自己實(shí)現(xiàn)的方法(包括分類)诅迷、屬性和遵循的協(xié)議加載到 methods
、 properties
和 protocols
列表中众旗。
XXObject
下面罢杉,我們將分析一個(gè)類 XXObject
在運(yùn)行時(shí)初始化過程中內(nèi)存的更改,這是 XXObject
的接口與實(shí)現(xiàn):
// XXObject.h 文件
#import <Foundation/Foundation.h>
@interface XXObject : NSObject
- (void)hello;
@end
// XXObject.m 文件
#import "XXObject.h"
@implementation XXObject
- (void)hello {
NSLog(@"Hello");
}
@end
這段代碼是運(yùn)行在 Mac OS X 10.11.3 (x86_64)版本中贡歧,而不是運(yùn)行在 iPhone 模擬器或者真機(jī)上的滩租,如果你在 iPhone 或者真機(jī)上運(yùn)行,可能有一定差別利朵。
這是主程序的代碼:
#import <Foundation/Foundation.h>
#import "XXObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class cls = [XXObject class];
NSLog(@"%p", cls);
}
return 0;
}
編譯后內(nèi)存中類的結(jié)構(gòu)
因?yàn)?strong>類在內(nèi)存中的位置是編譯期就確定的律想,先運(yùn)行一次代碼獲取 XXObject
在內(nèi)存中的地址。
0x100001168
接下來绍弟,在整個(gè) ObjC 運(yùn)行時(shí)初始化之前技即,也就是 _objc_init
方法中加入一個(gè)斷點(diǎn):
然后在 lldb 中輸入以下命令:
(lldb) p (objc_class *)0x100001168
(objc_class *) $0 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $1 = 0x0000000100001188
(lldb) p $1->data()
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
(class_rw_t *) $2 = 0x00000001000010e8
(lldb) p (class_ro_t *)$2 // 將 class_rw_t 強(qiáng)制轉(zhuǎn)化為 class_ro_t
(class_ro_t *) $3 = 0x00000001000010e8
(lldb) p *$3
(class_ro_t) $4 = {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "XXObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
現(xiàn)在我們獲取了類經(jīng)過編譯器處理后的只讀屬性 class_ro_t
:
(class_ro_t) $4 = {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "XXObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
可以看到這里面只有 baseMethodList
和 name
是有值的,其它的 ivarLayout
樟遣、 baseProtocols
而叼、 ivars
身笤、weakIvarLayout
和 baseProperties
都指向了空指針,因?yàn)轭愔袥]有實(shí)例變量葵陵,協(xié)議以及屬性液荸。所以這里的結(jié)構(gòu)體符合我們的預(yù)期。
通過下面的命令查看 baseMethodList
中的內(nèi)容:
(lldb) p $4.baseMethodList
(method_list_t *) $5 = 0x00000001000010c8
(lldb) p $5->get(0)
(method_t) $6 = {
name = "hello"
types = 0x0000000100000fa4 "v16@0:8"
imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13)
}
(lldb) p $5->get(1)
Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runtime/runtime/objc-runtime-new.h, line 110.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb)
使用 $5->get(0)
時(shí)埃难,成功獲取到了 -[XXObject hello]
方法的結(jié)構(gòu)體 method_t
莹弊。而嘗試獲取下一個(gè)方法時(shí),斷言提示我們當(dāng)前類只有一個(gè)方法涡尘。
realizeClass
這篇文章中不會(huì)對 realizeClass
進(jìn)行詳細(xì)的分析忍弛,該方法的主要作用是對類進(jìn)行第一次初始化,其中包括:
- 分配可讀寫數(shù)據(jù)空間
- 返回真正的類結(jié)構(gòu)
static Class realizeClass(Class cls)
上面就是這個(gè)方法的簽名考抄,我們需要在這個(gè)方法中打一個(gè)條件斷點(diǎn)细疚,來判斷當(dāng)前類是否為 XXObject
:
這里直接判斷兩個(gè)指針是否相等,而不使用 [NSStringFromClass(cls) isEqualToString:@"XXObject"]
是因?yàn)樵谶@個(gè)時(shí)間點(diǎn)川梅,這些方法都不能調(diào)用疯兼,在 ObjC 中沒有這些方法,所以只能通過判斷類指針是否相等的方式來確認(rèn)當(dāng)前類是 XXObject
贫途。
直接與指針比較是因?yàn)轭愒趦?nèi)存中的位置是編譯期確定的吧彪,只要代碼不改變,類在內(nèi)存中的位置就會(huì)不變(已經(jīng)說過很多遍了)丢早。
這個(gè)斷點(diǎn)就設(shè)置在這里姨裸,因?yàn)?XXObject
是一個(gè)正常的類,所以會(huì)走 else
分支分配可寫的類數(shù)據(jù)怨酝。
運(yùn)行代碼時(shí)傀缩,因?yàn)槊看味紩?huì)判斷當(dāng)前類指針是不是指向的
XXObject
,所以會(huì)等一會(huì)才會(huì)進(jìn)入斷點(diǎn)农猬。
在這時(shí)打印類結(jié)構(gòu)體中的 data
的值赡艰,發(fā)現(xiàn)其中的布局依舊是這樣的:
在運(yùn)行完這段代碼之后:
我們再來打印類的結(jié)構(gòu):
(lldb) p (objc_class *)cls // 打印類指針
(objc_class *) $262 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x0000000100001188 // 在類指針上加 32 的 offset 打印 class_data_bits_t 指針
(class_data_bits_t *) $263 = 0x0000000100001188
(lldb) p *$263 // 訪問 class_data_bits_t 指針的內(nèi)容
(class_data_bits_t) $264 = (bits = 4302315312)
(lldb) p $264.data() // 獲取 class_rw_t
(class_rw_t *) $265 = 0x0000000100701f30
(lldb) p *$265 // 訪問 class_rw_t 指針的內(nèi)容,發(fā)現(xiàn)它的 ro 已經(jīng)設(shè)置好了
(class_rw_t) $266 = {
flags = 2148007936
version = 0
ro = 0x00000001000010e8
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = nil
demangledName = 0x0000000000000000 <no value available>
}
(lldb) p $266.ro // 獲取 class_ro_t 指針
(const class_ro_t *) $267 = 0x00000001000010e8
(lldb) p *$267 // 訪問 class_ro_t 指針的內(nèi)容
(const class_ro_t) $268 = {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "XXObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
(lldb) p $268.baseMethodList // 獲取基本方法列表
(method_list_t *const) $269 = 0x00000001000010c8
(lldb) p $269->get(0) // 訪問第一個(gè)方法
(method_t) $270 = {
name = "hello"
types = 0x0000000100000fa4 "v16@0:8"
imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13)
}
(lldb) p $269->get(1) // 嘗試訪問第二個(gè)方法斤葱,越界
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runtime/runtime/objc-runtime-new.h, line 110.
(lldb)
最后一個(gè)操作實(shí)在是截取不到了
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
在上述的代碼運(yùn)行之后慷垮,類的只讀指針 class_ro_t
以及可讀寫指針 class_rw_t
都被正確的設(shè)置了。但是到這里揍堕,其 class_rw_t
部分的方法等成員都指針均為空料身,這些會(huì)在 methodizeClass
中進(jìn)行設(shè)置:
在這里調(diào)用了 method_array_t
的 attachLists
方法,將 baseMethods
中的方法添加到 methods
數(shù)組之后鹤啡。我們訪問 methods
才會(huì)獲取當(dāng)前類的實(shí)例方法。
方法的結(jié)構(gòu)
說了這么多蹲嚣,到現(xiàn)在我們可以簡單看一下方法的結(jié)構(gòu)递瑰,與類和對象一樣祟牲,方法在內(nèi)存中也是一個(gè)結(jié)構(gòu)體。
struct method_t {
SEL name;
const char *types;
IMP imp;
};
其中包含方法名抖部,類型還有方法的實(shí)現(xiàn)指針 IMP
:
上面的 -[XXObject hello]
方法的結(jié)構(gòu)體是這樣的:
name = "hello"
types = 0x0000000100000fa4 "v16@0:8"
imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13
方法的名字在這里沒有什么好說的说贝。其中,方法的類型是一個(gè)非常奇怪的字符串 "v16@0:8"
這在 ObjC 中叫做類型編碼(Type Encoding)慎颗,你可以看這篇官方文檔了解與類型編碼相關(guān)的信息乡恕。
對于方法的實(shí)現(xiàn),lldb 為我們標(biāo)注了方法在文件中實(shí)現(xiàn)的位置俯萎。
小結(jié)
在分析方法在內(nèi)存中的位置時(shí)傲宜,筆者最開始一直在嘗試尋找只讀結(jié)構(gòu)體 class_ro_t
中的 baseMethods
第一次設(shè)置的位置(了解類的方法是如何被加載的)。嘗試從 methodizeClass
方法一直向上找夫啊,直到 _obj_init
方法也沒有找到設(shè)置只讀區(qū)域的 baseMethods
的方法函卒。
而且在 runtime 初始化之后,realizeClass
之前撇眯,從 class_data_bits_t
結(jié)構(gòu)體中獲取的 class_rw_t
一直都是錯(cuò)誤的报嵌,這個(gè)問題在最開始非常讓我困惑,直到后來在 realizeClass
中發(fā)現(xiàn)原來在這時(shí)并不是 class_rw_t
結(jié)構(gòu)體熊榛,而是class_ro_t
锚国,才明白錯(cuò)誤的原因。
后來突然想到類的一些方法玄坦、屬性和協(xié)議實(shí)在編譯期決定的(baseMethods
等成員以及類在內(nèi)存中的位置都是編譯期決定的)血筑,才感覺到豁然開朗。
- 類在內(nèi)存中的位置是在編譯期間決定的营搅,在之后修改代碼云挟,也不會(huì)改變內(nèi)存中的位置。
- 類的方法转质、屬性以及協(xié)議在編譯期間存放到了“錯(cuò)誤”的位置园欣,直到
realizeClass
執(zhí)行之后,才放到了class_rw_t
指向的只讀區(qū)域class_ro_t
休蟹,這樣我們即可以在運(yùn)行時(shí)為class_rw_t
添加方法沸枯,也不會(huì)影響類的只讀結(jié)構(gòu)。 - 在
class_ro_t
中的屬性在運(yùn)行期間就不能改變了赂弓,再添加方法時(shí)绑榴,會(huì)修改class_rw_t
中的methods
列表,而不是class_ro_t
中的baseMethods
盈魁,對于方法的添加會(huì)在之后的文章中分析翔怎。
參考資料
關(guān)注倉庫,及時(shí)獲得更新:iOS-Source-Code-Analyze
轉(zhuǎn)載請注明 Blog: Draveness
Follow: @Draveness·Github