第四節(jié)課 類的原理分析(上)
isa分析到元類
p/x p //拿到當(dāng)前指針地址
我們通過
x p
指令可以驗(yàn)證下唱矛,看到打印出的地址與我們拿到的地址一模一樣。
接下來我們
x/4gx 0x000000010292f480
0x001d800100008365
就是我們的對象的isa
p/x 0x001d800100008365 & 0x00007ffffffffff8 po 0x0000000100008360 //就是我們當(dāng)前的類
其實(shí)最后還是這么個(gè)地址缸榛,有那么一丟丟好奇朱沃,再次x/4g
會得到什么,干就完了~
可以看到耍铜,打印出來的結(jié)果里跟上面的指針對象擁有同樣的內(nèi)存結(jié)構(gòu)塞赂,那第一個(gè)地址是啥泪勒?還是
isa
嘛?又一次得到
LGPerson
,0x0000000100008360
是LGPerson
,0x0000000100008338
還是LGPerson
圆存,小朋友叼旋,你是否有很多問號?沦辙?猜想:類會和我們的對象無限開辟夫植,內(nèi)存不止有一個(gè)類
接下來就來驗(yàn)證下我們的猜想
Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p \n%p \n%p \n%p",class1,class2,class3,class4);
<----輸出結(jié)果---->
0x100008360
0x100008360
0x100008360
0x100008360
我們可以看到最終輸出的類都是0x100008360
,那就證明之前的0x0000000100008338
它不是類油讯,那么它是個(gè)什么详民?接下來我們就需要使用爛蘋果來分析了
在這里我們看到了熟悉的東西,但是并沒有看到
0x0000000100008338
這個(gè)東西
當(dāng)我們看到符號表的時(shí)候看到了一個(gè)奇奇怪怪的東西
這個(gè)METACLASS
實(shí)際上并不是我們通過代碼創(chuàng)建的撞羽,這個(gè)是系統(tǒng)或者編譯器幫我們生成好的阐斜,這個(gè)東西就叫做元類
。
我們得到了一個(gè)大概的流程:對象isa -> 類isa -> 元類
isa走位圖和繼承鏈
isa走位圖
上面我們通過isa分析出了一個(gè)元類诀紊,那我們就想,元類之后會不會還有東西隅俘?
繼續(xù)用上面元類的isa
po打印輸出的是NSObject邻奠,&上我們的掩碼,又是他本身为居,這個(gè)東西就是我們的根元類
對象isa -> 類isa -> 元類 -> 根元類
我們看到了NSObject有點(diǎn)奇怪碌宴,那我們就跟直接打印的對比下
我們發(fā)現(xiàn),通過
NSObject.class
獲取到的地址與剛才po
的地址并不相同蒙畴,但是我們繼續(xù)往下x/4g
的時(shí)候發(fā)現(xiàn)isa
的位置又與po
的地址相同了贰镣。就相當(dāng)于是NSObject.class的isa -> NSObject的元類
,只有兩層根類 isa -> 根元類 isa
根據(jù)上面我們分析的過程可以看到
isa
的鏈路如上圖虛線部分膳凝。同時(shí)我們也可以使用代碼來進(jìn)行驗(yàn)證一下
NSObject *object1 = [NSObject alloc];
// NSObject類
Class class = object_getClass(object1);
// NSObject元類
Class metaClass = object_getClass(class);
// NSObject根元類
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元類
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 實(shí)例對象\n%p 類\n%p 元類\n%p 根元類\n%p 根根元類",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// LGPerson元類
Class pMetaClass = object_getClass(LGPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);
<----輸出結(jié)果---->
0x100547790 實(shí)例對象
0x7fff9103a118 類
0x7fff9103a0f0 元類
0x7fff9103a0f0 根元類
0x7fff9103a0f0 根根元類
NSObject - 0x7fff9103a0f0
繼承鏈
根據(jù)上面的打印結(jié)果碑隆,我們發(fā)現(xiàn)任何對象,元類的父類都是根元類蹬音,有點(diǎn)燒腦了吧上煤?
接下來我們寫了一個(gè)LGTeacher繼承自LGPerson,再次通過上面代碼進(jìn)行打印
// LGTeacher -> LGPerson -> NSObject
// 元類也有一條繼承鏈
Class tMetaClass = object_getClass(LGTeacher.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"%@ - %p",tsuperClass,tsuperClass);
<----輸出結(jié)果---->
LGPerson - 0x100008338
元類的繼承鏈如下圖實(shí)線部分
[圖片上傳失敗...(image-743b09-1624451069459)]
我們繼續(xù)分析NSObject特殊情況
// NSObject 根類特殊情況
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
// 根元類 -> NSObject
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
<----輸出結(jié)果---->
(null) - 0x0
NSObject - 0x7fff9103a118
根據(jù)上述結(jié)果著淆,我們推出了圖中RootClass(class) -> nil
的這一步
以及RootClass(meta) -> RootClass(class)
這一步
小結(jié):
-
NSOject
對象的元類
與根元類
是同一個(gè)
-
元類間
也存在著繼承的關(guān)系
劫狠,跟類是一樣的 根元類的父類指向了NSObject
-
NSObject的父類是(null)
,地址為0x0永部,即NSObject沒有父類
指針和內(nèi)存平移
如果要想獲取對象內(nèi)存中的變量独泞,底層實(shí)現(xiàn)方式是對象的首地址+偏移值。下面探究下內(nèi)存偏移
我們平時(shí)遇到的指針分為三種:普通指針苔埋、對象指針懦砂、數(shù)組指針
普通指針
int a = 10;
int b = 10;
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
<----輸出結(jié)果---->
10 -- 0x7ffeefbff41c
10 -- 0x7ffeefbff418
- 我們發(fā)現(xiàn),
值是一樣的
,但是指針地址卻是不同的
孕惜,這就是我們所說的copy:值拷貝
-
a
的地址是0x7ffeefbff41c
愧薛,b
的地址是0x7ffeefbff418
,相差4字節(jié)
衫画,主要取決于a的類型
對象指針
HZMPerson *p1 = [HZMPerson alloc];
HZMPerson *p2 = [HZMPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
<----輸出結(jié)果---->
<HZMPerson: 0x1007049f0> -- 0x7ffeefbff410
<HZMPerson: 0x100704650> -- 0x7ffeefbff408
我們發(fā)現(xiàn)毫炉,對象的地址不同
,地址指向的空間也不同
數(shù)組指針
int c[4] = {1,2,3,4};
int *d = c;
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
<----輸出結(jié)果---->
0x7ffeefbff430 - 0x7ffeefbff430 - 0x7ffeefbff434
0x7ffeefbff430 - 0x7ffeefbff434 - 0x7ffeefbff438
-
數(shù)組的地址
就是數(shù)組元素中的首地址
削罩,即&c
和&c[0]
都是首地址
- 數(shù)組中每個(gè)元素之間的
地址間隔
瞄勾,由當(dāng)前元素的數(shù)據(jù)類型決定
的 - 數(shù)組的
元素地址
可以通過首地址+n*類型大小
方式,這種方式是數(shù)組中的元素類型必須相同
弥激。 - 數(shù)組元素不相同用
首地址+偏移量
方式进陡,根據(jù)當(dāng)前變量的偏移值(需要前面類型大小相加)
小結(jié):
- 內(nèi)存地址就是內(nèi)存元素的首地址
- 內(nèi)存偏移可以根據(jù)首地址+ 偏移值方法獲取相對應(yīng)變量的地址
源碼分析類的結(jié)構(gòu)
在之前的探索時(shí)我們發(fā)現(xiàn)isa
是Class
類型的。Class
類型是objc_class *
微服,objc_class
是一個(gè)結(jié)構(gòu)體趾疚。 所有的Class
底層實(shí)現(xiàn)都是objc_class
。又來到了我們的源碼分析部分以蕴,在 objc4-818.2
中全局搜索objc_class
代碼如下
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// 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 getSuperclass() const {
#if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
if (superclass == Nil)
return Nil;
//省略部分源碼
}
objc_class
這個(gè)結(jié)構(gòu)體內(nèi)部存儲了幾個(gè)成員變量糙麦,怎么去探究它們呢?類的地址是知道的丛肮,那么就根據(jù)上面探究過的首地址+偏移值
來獲取里面的成員變量的地址
赡磅,然后獲取值
。但是偏移值需要知道當(dāng)前變量之前的所有成員變量的大小
- Class ISA://結(jié)構(gòu)體指針占
8字節(jié)
繼承自objc_object - Class superclass:結(jié)構(gòu)體指針占
8字節(jié)
- cache_t cache:
宝与?焚廊?母雞
- class_data_bits_t bits;
計(jì)算 cache 類的內(nèi)存大小
接下來我們就先看cache
的內(nèi)存大小,查看cache_t
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
};
//下面是一些方法省略
};
cache_t
是結(jié)構(gòu)體類型
习劫,有兩個(gè)成員變量_bucketsAndMaybeMask
和一個(gè)聯(lián)合體
_bucketsAndMaybeMask
是 uintptr_t
無符長整型占8字節(jié)
聯(lián)合體里面有兩個(gè)成員變量結(jié)構(gòu)體
和 _originalPreoptCache
咆瘟,聯(lián)合體的內(nèi)存大小由成員變量中的最大變量類型決定
_originalPreoptCache
是結(jié)構(gòu)體指針占8字節(jié)
結(jié)構(gòu)體中有_maybeMask
、_flags
榜聂、_occupied
搞疗。 _maybeMask
是uint32_t
占 4字節(jié)
,_flags
和_occupied
是uint16_t
各占2字節(jié)
须肆,結(jié)構(gòu)體大小是8字節(jié)
cache_t
的內(nèi)存大小是 8+8
或者是8+4+2+2
都是16字節(jié)
匿乃,所以cache
就是16字節(jié)
*isa
的內(nèi)存地址是首地址,前面已經(jīng)探究了
-
superclass
的內(nèi)存地址是首地址+0x8 -
cache
的內(nèi)存地址是首地址+0x10 -
bits
的內(nèi)存地址是首地址+0x20
具體cache
里面的內(nèi)容我們后續(xù)再進(jìn)行講解豌汇,我們先看看bits
這個(gè)東西
獲取bits
所以有上述計(jì)算可知幢炸,想要獲取bits
的中的內(nèi)容,只需通過類
的首地址平移32字節(jié)
即可
以下是通過lldb命令調(diào)試的過程
ps:獲取類的首地址有兩種方式
- 通過
p/x LGPerson.class
直接獲取首地址 - 通過
x/4gx LGPerson.class
拒贱,打印內(nèi)存信息獲取
從$2
指針的打印結(jié)果中可以看出bits
中存儲的信息宛徊,其類型是class_rw_t
佛嬉,也是一個(gè)結(jié)構(gòu)體類型。但我們還是沒有看到屬性列表闸天、方法列表等
暖呕,需要繼續(xù)往下探索
探索 屬性列表,即 property_list
通過查看class_rw_t
定義的源碼發(fā)現(xiàn)苞氮,結(jié)構(gòu)體中有提供相應(yīng)的方法去獲取 屬性列表湾揽、方法列表等,如下所示
在獲取bits
并打印bits
信息的基礎(chǔ)上笼吟,通過class_rw_t
提供的方法库物,繼續(xù)探索 bits
中的屬性列表
,以下是lldb探索的過程圖示
p $3.properties()
命令中的propertoes
方法是由class_rw_t
提供的,方法中返回的實(shí)際類型為property_array_t
由于
list
的類型是property_list_t
贷帮,是一個(gè)指針戚揭,所以通過p *$6
獲取內(nèi)存中的信息,同時(shí)也證明bits
中存儲了property_list
撵枢,即屬性列表p $7.get(2)
民晒,想要獲取LGPerson
中的下一個(gè)成員變量,發(fā)現(xiàn)提示數(shù)組越界了
锄禽,說明 property_list 中只有兩個(gè)屬性
目前我們的LGPerson中只有兩個(gè)屬性镀虐,那么我們添加一些其他的東西后,property_list
會有什么變化呢沟绪?
我們新增了一個(gè)成員變量,重復(fù)上面步驟結(jié)果發(fā)現(xiàn)空猜,還是只能取出兩個(gè)屬性绽慈,由此可得出property_list
中只有屬性,沒有成員變量與方法辈毯。那成員變量哪去了坝疼?
通過查看objc_class
中bits
屬性中存儲數(shù)據(jù)的類class_rw_t
的定義發(fā)現(xiàn),除了methods谆沃、properties钝凶、protocols
方法,還有一個(gè)ro
方法唁影,其返回類型是class_ro_t
耕陷,通過查看其定義,發(fā)現(xiàn)其中有一個(gè)ivars
屬性据沈,我們大膽猜測:是否成員變量就存儲在這個(gè)ivar_list_t
類型的ivars
屬性中呢哟沫?下圖是lldb命令的調(diào)試流程
通過{}定義的成員變量,會存儲在類的bits
屬性中锌介,通過bits -> data() ->ro() -> ivars
獲取成員變量列表嗜诀,除了包括成員變量猾警,還包括屬性定義的成員變量
通過@property
定義的屬性,也會存儲在bits
屬性中隆敢,通過bits -> data() -> properties() -> list
獲取屬性列表发皿,其中只包含屬性
探索methods_list
上面我們新增了成員變量,現(xiàn)在我們添加兩個(gè)方法來看看
繼續(xù)上面的lldb調(diào)試
但是我們發(fā)現(xiàn)直到我們?nèi)〉皆浇绶餍蛴〕鰜淼姆椒ǘ际强盏姆椒ㄑㄊ@是為啥?
這個(gè)其實(shí)是因?yàn)?br>
property_t
的底層是存在兩個(gè)成員變量的
struct property_t {
const char *name;
const char *attributes;
};
而我們的method_t
則沒有
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
但是我們可以看見 有一個(gè)結(jié)構(gòu)體big
匣屡,里面則存在著成員變量name
,我們?nèi)∫幌略囋?br>
可以取出來了封救,但是發(fā)現(xiàn)并沒有類方法。在之前捣作,我們曾提及了元類誉结,類對象
的isa指向就是元類
,元類
是用來存儲類的相關(guān)信息
的券躁,所以我們大膽的猜一下:是否類方法存儲在元類的bits中呢惩坑?可以通過lldb命令來驗(yàn)證我們的猜測。下圖是lldb命令的調(diào)試流程
通過打印也拜,我們看到了之前的類方法以舒,證明了我們的猜想成立
類
的實(shí)例方法存儲在類的bits屬性中
,類方法存儲在元類的bits屬性中
慢哈。通過bits/元類bits -> methods() -> list
獲取實(shí)例方法列表