一、類的本質(zhì)
在《OC底層原理04-對象的本質(zhì)》那篇文章中,我們講到了如何將.m文件編譯成.cpp文件查看底層結(jié)構(gòu),這里就不作過多贅述
1.1 在cpp文件找查找Class的定義
typedef struct objc_class *Class;
- 在
main.cpp中,找到了底層關(guān)于Class的定義,類是一個objc_class類型的結(jié)構(gòu)體
1.2 接著進(jìn)入objc4源碼查找objc_class,源碼相關(guān)可以查閱《OC底層原理02-iOS_objc4-781.2 最新源碼編譯調(diào)試》
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
//: 由于代碼量過大,這里只展示關(guān)鍵代碼,源碼請自行查閱
}
-
objc_class是一個結(jié)構(gòu)體,所以進(jìn)一步再次說明類也是一個結(jié)構(gòu)體 -
objc_class繼承于objc_object,類也是對象,萬物皆對象 -
objc_class繼承了objc_object的isa屬性,所以類的地址第一位仍然存的是isa -
superclass:objc_object類型的結(jié)構(gòu)體指針 -
cache:緩存 -
bits:存儲屬性,實例方法,協(xié)議
總結(jié):類的本質(zhì)是一個
objc_class類型的結(jié)構(gòu)體
二、 探索objc_class中的bits
類不能像對象那樣直接斷點調(diào)試打印,只有先從源碼入手,分析源碼,并且引入二種分析方法:
-
地址平移:通過首地址+前面屬性所占內(nèi)存,平移到我們需要的存儲屬性(bits)的內(nèi)存地址處 (文章結(jié)尾拓展有講到) -
*():輸出指針類型的對象
2.1 通過閱讀源碼定位bits內(nèi)存地址
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
//: 由于代碼量過大,這里只展示關(guān)鍵代碼,源碼請自行查閱
}
2.1.1 第一位屬性:isa 的內(nèi)存大小
- 由于
objc_class繼承于objc_object,繼承屬性isa,占8字節(jié)
2.1.2 第二位屬性:superclass 的內(nèi)存大小
typedef struct objc_class *Class;
-
Class是一個objc_class的結(jié)構(gòu)體,superclass是Class的指針,結(jié)構(gòu)體指針大小為8,指向NSObject類,占8字節(jié)
2.1.3 第三位屬性:cache 的內(nèi)存大小
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
//: 由于代碼量過大,這里只展示關(guān)鍵代碼,源碼請自行查閱
-
cache_t結(jié)構(gòu)體中有大量static類型的方法和變量,還有大量其他方法。 -
static申明的變量和方法不計入結(jié)構(gòu)體內(nèi)存,不會存在結(jié)構(gòu)體中。 -
方法也不會存在結(jié)構(gòu)體中,也不計入結(jié)構(gòu)體內(nèi)存 -
cache_t大小由_buckets、_mask、_flags、_occupied來決定 -
_buckets是一個結(jié)構(gòu)體指針<struct bucket_t *>,占8字節(jié) -
_mask源碼聲明:typedef uint32_t mask_t,uint32_t源碼聲明:typedef unsigned int uint32_t,占4字節(jié) -
_flags和_occupied都是uint16_t,uint16_t源碼聲明:typedef unsigned short uint16_t,各占2字節(jié) - 所以計算出
cache占16字節(jié),8+4+2+2
2.1.4 結(jié)論
- 只要把
GomuPerson首地址平移32位(isa[8位]+superclasss[8位]+ cache[16位]),就能拿到研究對象bits的指針地址
2.2 通過lldb調(diào)試拿到bits中的方法、屬性、協(xié)議
2.2.2 準(zhǔn)備工作
GomuPerson.h
@protocol TestProtocol <NSObject>
@end
@interface GomuPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *sex;
- (void)sayNO;
+ (void)sayLove;
GomuPerson.m
- (void)sayNO{}
+ (void)sayLove{}
- 在
objc4工程中創(chuàng)建類GomuPerson - 在
GomuPerson.h中創(chuàng)建屬性name,sex,協(xié)議TestProtocol,實例屬性hobby,實例方法sayNO,類方法sayLove - 在
GomuPerson.m中實現(xiàn)實例方法sayNO、類方法sayLove
2.2.2 獲取類GomuPerson的首地址
//: 方法一
(lldb) p/x GomuPerson.class
(Class) $0 = 0x0000000100002320 GomuPerson
//: 得到地址 `0x0000000100002488 `
//: 方法二
(lldb) x/4gx person
0x1010297b0: 0x001d800100002325 0x0000000000000000
0x1010297c0: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x001d800100002325 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x0000000100002320
//: 得到地址 `0x0000000100002320 `
- 得到
GomuPerson的首地址0x0000000100002320
2.2.2 把GomuPerson的首地址平移32位
(lldb) p/x 0x0000000100002320 + 32
(long) $3 = 0x0000000100002340
- 得到
bits的地址0x0000000100002340
2.2.3 根據(jù)源碼拿到bits.data()
//: 源碼
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
//: lldb拿data()
//: 強(qiáng)轉(zhuǎn)(class_data_bits_t *)類型
(lldb) p (class_data_bits_t *)$3
(class_data_bits_t *) $4 = 0x0000000100002340
//: 取出data(),指針取值用->
(lldb) p $4->data()
(class_rw_t *) $5 = 0x00000001006b0460
//: 去指針化
(lldb) p *($5)
(class_rw_t) $6 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294975768
}
firstSubclass = nil //: 子類
nextSiblingClass = NSUUID
}
2.2.4 在源碼中查看class_rw_t
struct class_rw_t {
//: 由于代碼量過大,這里只展示關(guān)鍵代碼,源碼請自行查閱
//: 獲取ro()
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>()->ro;
}
return v.get<const class_ro_t *>();
}
//: 獲取方法
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
// 獲取屬性
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->baseProperties};
}
}
// 獲取協(xié)議
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
}
}
}
- 在
class_rw_t源碼中,找到methods()、properties()、protocols()
2.2.5 獲取方法列表methods()中存的方法
//: 獲取methods()
(lldb) p $6.methods()
(const method_array_t) $7 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100002160
arrayAndFlag = 4294975840
}
}
}
//: 獲取methods()中的list
(lldb) p $7.list
(method_list_t *const) $8 = 0x0000000100002160
//: 去指針化
(lldb) p *($8)
(method_list_t) $9 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 6
first = {
name = "sayNO"
types = 0x0000000100000f4c "v16@0:8"
imp = 0x0000000100000da0 (GomuTest`-[GomuPerson sayNO])
}
}
}
// count = 6 ,我們打印一下這6個元素
(lldb) p $9.get(0)
(method_t) $10 = {
name = "sayNO"
types = 0x0000000100000f4c "v16@0:8"
//: 拿到 sayNO
imp = 0x0000000100000da0 (GomuTest`-[GomuPerson sayNO])
}
(lldb) p $9.get(1)
(method_t) $11 = {
name = "sex"
types = 0x0000000100000f60 "@16@0:8"
//: 系統(tǒng)自動生成get方法
imp = 0x0000000100000e00 (GomuTest`-[GomuPerson sex])
}
(lldb) p $9.get(2)
(method_t) $12 = {
name = "setSex:"
types = 0x0000000100000f68 "v24@0:8@16"
//: 系統(tǒng)自動生成set方法
imp = 0x0000000100000e20 (GomuTest`-[GomuPerson setSex:])
}
(lldb) p $9.get(3)
(method_t) $13 = {
name = ".cxx_destruct"
types = 0x0000000100000f4c "v16@0:8"
//: 系統(tǒng)自動生成c++函數(shù)
imp = 0x0000000100000e50 (GomuTest`-[GomuPerson .cxx_destruct])
}
(lldb) p $9.get(4)
(method_t) $14 = {
name = "name"
types = 0x0000000100000f60 "@16@0:8"
//: 系統(tǒng)自動生成get方法
imp = 0x0000000100000db0 (GomuTest`-[GomuPerson name])
}
(lldb) p $9.get(5)
(method_t) $15 = {
name = "setName:"
types = 0x0000000100000f68 "v24@0:8@16"
//: 系統(tǒng)自動生成set方法
imp = 0x0000000100000dd0 (GomuTest`-[GomuPerson setName:])
}
-
實例方法(sayNo)確定存在bits里面 - 除了我們自定義的
實例方法,系統(tǒng)在編譯中自動幫我們實現(xiàn)了屬性的get、set方法([GomuPerson sex]、[GomuPerson setSex:]、[GomuPerson name]、[GomuPerson name]) - 除此之外,系統(tǒng)還實現(xiàn)了
.cxx_destruct,c++的方法,因為OC是底層是封裝的c++,所以會默認(rèn)添加 - 類方法
sayLove沒有存在bits里 - 系統(tǒng)在編譯中沒有給
實例屬性hobby生成get、set方法
2.2.6 獲取屬性列表properties()中存的屬性
//: 獲取properties()
(lldb) p $6.properties()
(const property_array_t) $16 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100002260
arrayAndFlag = 4294976096
}
}
}
//: 獲取properties()中的list
(lldb) p $16.list
(property_list_t *const) $17 = 0x0000000100002260
//: 去指針化
(lldb) p *($17)
(property_list_t) $18 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 2
first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
}
}
// count = 2 ,我們打印一下這2個元素
(lldb) p $18.get(0)
(property_t) $19 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
(lldb) p $18.get(1)
(property_t) $20 = (name = "sex", attributes = "T@\"NSString\",&,N,V_sex")
(lldb)
-
properties()中只存了屬性name,sex。 - 實例屬性
hobby沒有存在properties()中
2.2.7 獲取協(xié)議列表protocols()中存的協(xié)議
(lldb) p $6.protocols()
(const protocol_array_t) $7 = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
(lldb) p $7.list
(protocol_list_t *const) $6 = 0x0000000000000000
//: $6 是個空,全為0
-
協(xié)議也沒有存在protocols中
2.3 探索實例屬性、類方法、協(xié)議存在哪
2.3.1 實例屬性存儲探索
//: 拿到`GomuPerson`首地址
(lldb) p/x GomuPerson.class
(Class) $0 = 0x0000000100002320 GomuPerson
//: 地址平移32位
(lldb) p/x 0x0000000100002320 + 32
(long) $1 = 0x0000000100002340
//: 強(qiáng)轉(zhuǎn)成(class_data_bits_t *)類型
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x0000000100002340
//: 取data()
(lldb) p $2->data()
(class_rw_t *) $3 = 0x00000001007646a0
// 去指針化
(lldb) p *($3)
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294975768
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
// 拿到ro(),同上面的`methods()`
(lldb) p $4.ro()
(const class_ro_t *) $5 = 0x0000000100002118
// 去指針化
(lldb) p *$5
(const class_ro_t) $6 = {
flags = 388
instanceStart = 8
instanceSize = 32
reserved = 0
ivarLayout = 0x0000000100000f03 "\x03"
name = 0x0000000100000ef8 "GomuPerson"
baseMethodList = 0x0000000100002160
baseProtocols = 0x0000000000000000
ivars = 0x00000001000021f8
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100002260
_swiftMetadataInitializer_NEVER_USE = {}
}
//: 拿到ivars
(lldb) p $6.ivars
(const ivar_list_t *const) $7 = 0x00000001000021f8
//: 去指針化
(lldb) p *$7
(const ivar_list_t) $8 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 3
first = {
offset = 0x0000000100002290
name = 0x0000000100000f0d "hobby"
type = 0x0000000100000f54 "@\"NSString\""
alignment_raw = 3
size = 8
}
}
}
// 由于 ivar_list_t 是一個數(shù)組,所以直接get
(lldb) p $8.get(0)
(ivar_t) $9 = {
offset = 0x0000000100002290
//: 找到實例屬性`hobby `
name = 0x0000000100000f0d "hobby"
type = 0x0000000100000f54 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $8.get(1)
(ivar_t) $10 = {
offset = 0x0000000100002298
//: 系統(tǒng)自動給屬性生成`_name `實例屬性
name = 0x0000000100000f13 "_name"
type = 0x0000000100000f54 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $8.get(2)
(ivar_t) $11 = {
offset = 0x00000001000022a0
//: 系統(tǒng)自動給屬性生成`_sex`實例屬性
name = 0x0000000100000f19 "_sex"
type = 0x0000000100000f54 "@\"NSString\""
alignment_raw = 3
size = 8
}
- 實例屬性
hobby存在ro中 - 系統(tǒng)在編譯中還是給屬性自動生成實例屬性
_name、_sex
2.3.2 類方法存儲探索
2.3.2.1 查看源碼
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
-
instanceMethods和classMethods都是method_list_t類型 - 發(fā)現(xiàn)
methodsForMeta這個方法,如果是元類,則返回類方法列表,如果不是元類,則返回實例方法列表 - 猜想,
類方法存在元類中
2.3.2.1 通過lldb調(diào)試元類,驗證猜想
//: 拿到`GomuPerson`內(nèi)存地址
(lldb) x/4gx GomuPerson.class
0x100002320: 0x00000001000022f8 0x0000000100334140
0x100002330: 0x000000010032e410 0x0000802c00000000
//: isa & mask
(lldb) p/x 0x00000001000022f8 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x00000001000022f8
//: 打印$1,GomuPerson的元類還是GomuPerson類型
(lldb) po $1
GomuPerson
//: 平移32位
(lldb) p/x $1 + 32
(unsigned long long) $2 = 0x0000000100002318
//: 強(qiáng)轉(zhuǎn)成(class_data_bits_t *)類型
(lldb) p (class_data_bits_t *)$2
(class_data_bits_t *) $3 = 0x0000000100002318
//: 取data()
(lldb) p $3->data()
(class_rw_t *) $4 = 0x000000010104bee0
//: 去指針化
(lldb) p *$4
(class_rw_t) $5 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294975664
}
firstSubclass = nil
nextSiblingClass = 0x00007fff8c84bc60
}
//: 拿到methods()
(lldb) p $5.methods()
(const method_array_t) $6 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000020f8
arrayAndFlag = 4294975736
}
}
}
//: 取出methods()里面的list
(lldb) p $6.list
(method_list_t *const) $7 = 0x00000001000020f8
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayLove"
types = 0x0000000100000f4c "v16@0:8"
//: 找到類方法sayLove
imp = 0x0000000100000d90 (GomuTest`+[GomuPerson sayLove])
}
}
}
2.3.3 協(xié)議的存儲探索
暫時沒探索到,后面找機(jī)會補起
三、拓展知識
3.1 內(nèi)存平移
3.1.1 普通指針
int num1 = 20;
int num2 = 20;
int num3 = 20;
NSLog(@"%d---%p",num1,&num1);
NSLog(@"%d---%p",num2,&num2);
NSLog(@"%d---%p",num3,&num3);
//: 打印
20---0x7ffeefbff57c
20---0x7ffeefbff580
20---0x7ffeefbff584
-
num1、num2、num3都指向10,這個10系統(tǒng)編譯中就已經(jīng)存到了某段內(nèi)存中,num1、num2、num3的地址卻不一樣,這就叫值拷貝,也叫淺拷貝 -
num1、num2、num3地址之間相差4字節(jié),內(nèi)存連續(xù) -
如下圖
image.png
3.1.2 對象指針
GomuPerson *person1 = [GomuPerson alloc];
GomuPerson *person2 = [GomuPerson alloc];
GomuPerson *person3 = [GomuPerson alloc];
NSLog(@"%p---%p",person1,&person1);
NSLog(@"%p---%p",person2,&person2);
NSLog(@"%p---%p",person3,&person3);
//: 打印
0x102230b50---0x7ffeefbff570
0x102234b00---0x7ffeefbff578
0x102234b20---0x7ffeefbff580
-
person1、person2、person3是指針,指向各自[GomuPerson alloc]開辟的內(nèi)存,&person1、&person2、&person3是指向person1、person2、person3對象指針的地址,這個指針就是二級指針 -
如下圖
image.png
3.1.3 數(shù)組指針
int arr[4] = {1,2,3,4};
int *d = arr;
NSLog(@"%p -- %p - %p", &arr, &arr[0], &arr[1]);
NSLog(@"%p -- %p - %p", d, d+1, d+2);
//: 打印
0x7ffeefbff570 -- 0x7ffeefbff570 - 0x7ffeefbff574
0x7ffeefbff570 -- 0x7ffeefbff574 - 0x7ffeefbff578
-
&arr、&arr[0]、d都是取的第一個地址,說明數(shù)組的首地址就是第一個元素的地址 - 通過地址平移
d+1,我們?nèi)〉搅?code>arr[1],數(shù)組指針地址平移,按照數(shù)組下標(biāo)平移,內(nèi)存中就是按照元素類型所占內(nèi)存進(jìn)行平移,0x7ffeefbff574->0x7ffeefbff578,因為是int類型所以平移4 - 依次類推,結(jié)構(gòu)體中也可以用
地址平移的方式去拿不能直接拿到的屬性

