歡迎閱讀iOS探索系列(按序閱讀食用效果更加)
寫在前面
iOS探索系列
前面幾篇講到了物件的初始化、物件的記憶體分佈、物件的isa初始化以及指向分析,本文就來講講例項出例項物件的類物件——類
一、類的本質
1.類的本質
objc原始碼下準備程式碼
#import <objc/runtime.h>
@interface FXPerson : NSObject
@end
@implementation FXPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *p = [FXPerson alloc];
Class cls = object_getClass(p);
}
return 0;
}
複製程式碼
利用clang將OC檔案輸出cpp檔案(彷彿開啟了新世界的大門)
發現類在底層用Class
接收
然後開始大海撈針(開天眼模式)找到了Class
的定義
typedef struct objc_class *Class;
複製程式碼
想要找到objc_class
就搜不到了,但是總覺得它似曾相似,或許能在objc原始碼找到靈感
在原始碼中搜尋程式碼的經驗 "objc_class :"、 "objc_class {"
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() {
return bits.data();
}
...
}
複製程式碼
objc_class
繼承於objc_object
struct objc_object {
private:
isa_t isa;
public:
...
}
複製程式碼
結論:類的本質是objc_class
型別的結構體,objc_class
繼承於objc_object
,所以滿足萬物皆物件
以後不要再在面試的時候回答一句 萬物皆物件 就完事了
2.objc_object和NSObject的關係
等等,為什麼繼承objc_object
就滿足萬物皆物件
了???
看過NSObject
的定義就知道了
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製程式碼
仔細比較的話就能看出NSObject
和objc_object
有著說不清道不明的關係
- 其實
NSObject
是objc_object
的仿寫,和objc_object
的定義是一樣的,在底層會編譯成objc_object
- 同理
NSObject類
是OC版本的objc_class
3.Class isa??
isa
明明是isa_t
型別的,為什麼註釋了一句Class ISA
?
- 萬物皆物件,用繼承於objc_object的Class接收是沒問題的
- 強轉,方便isa走位時返回類的型別
二、類的結構
從objc_class
的定義可以得出,類有4個屬性:isa、superclass、cache、bits
1.Class ISA
不但例項物件
中有isa指標
,類物件
中也有isa指標
關聯著元類
Class本身就是一個指標,佔用8位元組
2.Class superclass
顧名思義就是類的父類
(一般為NSObject)superclass是Class型別,所以佔用8位元組
3.cache_t cache
雖然對這個屬性比較陌生(詳見iOS探索 cache_t分析),但是cache
在英文中的意思是快取
cache_t
是一個結構體,記憶體長度有所有元素決定:_buckets
是一個指標,佔用8位元組;mask_t
是個int型別,_mask
佔用4位元組;_occupied
佔用4位元組
=>cache_t
佔用16位元組
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
...
};
複製程式碼
4.class_data_bits_t bits
又是一個陌生的屬性,但是蘋果工程師還是蠻友好的,這一看就是存資料的地方
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit)
{
return bits & bit;
}
...
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...
};
複製程式碼
那麼問題來了,類的屬性方法都去哪兒了?是在
cache
還是在bits
? 其實前文中有提到一丟丟——objc_class
中有個class_rw_t *data()
方法
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
...
}
複製程式碼
三、類的屬性方法
為FxPerson新增nickname
成員屬性、nationality
屬性變數、walk
類方法、fly
例項方法
@interface FXPerson : NSObject {
NSString *nickname;
}
@property (nonatomic, copy) NSString *nationality;
+ (void)walk;
- (void)fly;
@end
@implementation FXPerson
+ (void)walk {}
- (void)fly {}
@end
複製程式碼
1.類的屬性
x/4gx cls
列印當前類結構
bits
剛好是類的記憶體首地址
+isa、superclass、cache
的記憶體長度
=> 0x100001560+32位元組 = 0x100001580
po列印不出來,那就型別強轉列印輸出bits
的記憶體地址
根據class_rw_t *data() { return bits.data(); }
列印bits.data()
再往裡面探索進去(這比西天取經還難)
天吶!怎麼只有一個nationality
了?難道我FXPerson類
只配擁有“國籍”不配擁有“姓名”?讓我靜靜... 既然此路不通,那麼換條路走走(內心很不情願)
在一頓猛如虎的操作(開天眼)之後,發現了class_rw_t
有個屬性class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
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;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
複製程式碼
清空控制檯輸出ro
,跟class_ro_t
的結構型別一摸一樣
列印ro
的baseProperties
天吶?這個成員變數nickname
是迷路了嗎?等等!可能是自己迷路了!
嘗試了一下列印ivars
找到了nickname
,但是為什麼count = 2
呢?
ivar_list_t
繼承entsize_list_tt
,而後者又有個get(uint32_t i)
方法
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
bool containsIvar(Ivar ivar) const {
return (ivar >= (Ivar)&*begin() && ivar < (Ivar)&*end());
}
};
struct entsize_list_tt {
...
Element& get(uint32_t i) const {
assert(i < count);
return getOrEnd(i);
}
...
}
複製程式碼
如願拿到了nickname
和nationality
,但是這個nationality
長得有點不一樣
仔細想想這不就是編譯器會在底層自動將屬性變數生成一個成員變數_nationality
(_字首+屬性變數)
2.類的方法
列印ro
的baseMethodList
系統在底層新增了一個c++的.cxx_destruct
方法,同時編譯器還在底層幫屬性變數生成了一個setter方法
和getter方法
但是FXPerson的walk類方法
又被吃了...
無奈之下只能開大招(開天眼)在元類的data
的ro
的baseMethodList
找到了
其實也很好理解:類方法
可以理解成元類物件
的例項方法
,因此存在元類
中
3.結論
- 成員變數存放在
ivar
- 屬性存放在
property
,同時也會存一份在ivar
,並生成setter
、getter
方法 - 物件方法存放在
類
裡面 - 類方法存放在
元類
裡面
4.API驗證
利用底層開放的API可以驗證以上結論
void testObjc_copyIvar_copyProperies(Class cls) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
//獲取例項變數名
const char*cName = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:cName];
NSLog(@"class_copyIvarList:%@",ivarName);
}
free(ivars);
unsigned int pCount = 0;
objc_property_t *properties = class_copyPropertyList(cls, &pCount);
for (unsigned int i=0; i < pCount; i++) {
objc_property_t const property = properties[i];
//獲取屬性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
//獲取屬性值
NSLog(@"class_copyProperiesList:%@",propertyName);
}
free(properties);
}
void testObjc_copyMethodList(Class cls) {
unsigned int count = 0;
Method *methods = class_copyMethodList(cls, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//獲取方法名
NSString *key = NSStringFromSelector(method_getName(method));
NSLog(@"Method, name: %@", key);
}
free(methods);
}
void testInstanceMethod_classToMetaclass(Class cls) {
const char *className = class_getName(cls);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(cls, @selector(walk));
Method method2 = class_getInstanceMethod(metaClass, @selector(fly));
Method method3 = class_getInstanceMethod(cls, @selector(walk));
Method method4 = class_getInstanceMethod(metaClass, @selector(fly));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
NSLog(@"%s",__func__);
}
void testClassMethod_classToMetaclass(Class cls) {
const char *className = class_getName(cls);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(cls, @selector(walk));
Method method2 = class_getClassMethod(metaClass, @selector(fly));
Method method3 = class_getClassMethod(cls, @selector(walk));
Method method4 = class_getClassMethod(metaClass, @selector(fly));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
NSLog(@"%s",__func__);
}
void testIMP_classToMetaclass(Class cls) {
const char *className = class_getName(cls);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(cls, @selector(walk));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(fly));
IMP imp3 = class_getMethodImplementation(cls, @selector(walk));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(fly));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
NSLog(@"%s",__func__);
}
複製程式碼
5.clang編譯驗證
其實直接用clang編譯
也能看出蹊蹺
關於v@:
這個玩意,在蘋果開發者文件上也有介紹
四、寫在後面
雖然一直碰壁,但是本文最後還是找到了想要的答案,有的時候你離成功只差一步之遙,也有的時候是你選錯了研究方向。學習是如此,人生亦是如此,道阻且長,且行且珍惜。幸運的是有前人替我們踩坑,我們可以吸取前人的教訓少走彎路