首先用網(wǎng)上的經(jīng)典圖片鎮(zhèn)樓,并搭配簡單粗暴的名字(我是不是很有才= =)。說起對象,在我們平時編程中可謂是息息相關(guān)。對于OC,我們都知道它是擴充C的面向?qū)ο缶幊陶Z言,然而C是一門面向過程的開發(fā)語言,并沒有類和對象這樣的概念,所以要充分的理解鎮(zhèn)樓圖,我們必須從對象打開局面。
OC中的對象
OC中的對象可分為三大類:實例對象(instance)、類對象(class)、元類對象(meta)
-
實例對象
NSObject *obj = [[NSObject alloc] init];
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
obj就是一個實例對象,實例對象就是通過類alloc出來的對象。直接Command點擊NSObject進去可發(fā)現(xiàn)NSObject其實就是一個結(jié)構(gòu)體,并且只有一個Class類型的isa指針(直接Command點擊class可看到Class是一個指針類型)。因為NSObject是一個結(jié)構(gòu)體,而isa又是它的第一個屬性,C功底較好的同學(xué)應(yīng)該不難看出isa的地址應(yīng)該就是obj對象的起始地址(lldb指令不了解的同學(xué)可另行百度),如圖所示。所以繼承自NSObject的實例對象在內(nèi)存中的結(jié)構(gòu)包括isa指針與其他成員變量的值(ps:這里說的是成員變量的值)。
-
類對象
Class cls = [NSObject class];
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
cls就是一個類對象。直接點擊Command點擊Class可以看到它是objc_class類型的指針,接著Command點擊objc_class可以發(fā)現(xiàn)它也有一個isa指針。一個類對象在整個程序運行過程中只會存在一個(如圖所示),不管你調(diào)用多少次class方法,同一個類返回的類對象地址始終是一樣的。
類對象存放著isa指針、superclass指針(一個指向其父類類對象的指針)、所有的屬性信息(ps:這里說的是屬性的信息)和該類的所有對象方法。另外,類對象是通過initialize方法創(chuàng)建出來的,initialize方法在第一個創(chuàng)建該類的實例對象的時候會被調(diào)用且只被調(diào)用一次(可繼承NSObject實現(xiàn)initialize方法自行證明)。
-
元類對象
Class meta = object_getClass([NSObject class]);
meta就是一個元類對象。元類對象需要通過object_getClass這個runtime函數(shù)獲取,在整個程序運行過程中也只存在一個該類的元類對象(如圖所示)。和類對象一樣,元類對象也是Class類型,包括isa指針、superclass指針,不過元類對象存放的是該類所有的類方法。
isa指針
沒錯,只要是繼承自NSObject的類創(chuàng)造出來的實例對象(instance)、類對象(class)和元類對象(meta)都會有一個isa指針。回到鎮(zhèn)樓圖,isa指針就是圖中虛線的箭頭,而NSObject則是圖中的Root class。在圖中實例對象的isa指向其類對象,類對象的isa指向其元類對象,元類對象的isa則指向自己。
這里也可以這樣說,實例對象的isa存放的是類對象的地址,以此類推類對象的isa和元類對象的isa。我們可以來簡單證明一下。我們創(chuàng)建一個最簡單的NSObject的對象,
NSObject *obj = [[NSObject alloc] init];
在調(diào)試欄中(注意讓斷點過掉這行代碼)打印obj的isa的值和obj的類對象的地址結(jié)果如圖:
結(jié)果發(fā)現(xiàn)并不一樣,這是因為從arm64開始,蘋果對isa進行了優(yōu)化,之前的isa只是一個指針,從arm64開始,isa中包含了很多其他信息,從官網(wǎng)上下載最新的OC源碼可以看到
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
arm64是iPhone處理器的架構(gòu)(忽略x86架構(gòu)的,那是mac上的,這里需要連接你的真機設(shè)備),源碼中ISA_MASK就是isa指向的地址的掩碼,
我們清空調(diào)試欄,接著在調(diào)試欄輸入lldb指令(如下圖),我們首先取到obj的isa(0x000001a1b2c8bea1),然后將其與ISA_MASK按位與得到地址0x00000001b2c8bea0,最后我們直接打印obj的類對象的地址(0x00000001b2c8bea0),結(jié)果一致。
Ps:掩碼的作用就是用來取到或者修改對應(yīng)位置的數(shù)據(jù),如掩碼為0x01,則表示想要取到或者修改第0位的數(shù)據(jù),與它作按位與操作表示想要取到這一位數(shù)據(jù),這里isa指向的地址的掩碼是0x00007ffffffffff8,與其按位與即可得到其指向的地址。
superclass指針
superclass指針相信不用我解釋大家也都能知道它是一個指向父類的指針,在類對象和元類對象中存在。我們創(chuàng)建一個繼承自NSObject的類來看看它的值,因為Class無法直接獲取superclass,但是我們知道它是一個結(jié)構(gòu)體,所以我們可以創(chuàng)建一個與它內(nèi)存結(jié)構(gòu)一樣的結(jié)構(gòu)體,然后強轉(zhuǎn)一下就可以獲得(這里可以看到還是有必要學(xué)習(xí)一下c的,還好我的c功底還不錯,哈哈)。
首先我們創(chuàng)建了一個繼承自NSObject的Person類,并獲取它的類對象,然后將其強轉(zhuǎn)成dv_objc_class并將其地址保存在dvPCls指針中,通過lldb指令我們可以看見Person類的類對象的superclass指針指向的確實是NSObjec的類對象地址。
回到主題
現(xiàn)在我們回過頭來看看鎮(zhèn)樓圖,是不是感覺到一目了然?
一個NSObject可產(chǎn)生的對象有三類:instance、class、meta。
- instance中存放著isa和所有對象的值,instance的isa指向的是class。
- class存放著isa、superclass以及NSObject的屬性信息和對象方法。superclass指向其父類,NSObject(Root class)的類對象的superclass指向nil。isa指向的是meta。通過對象調(diào)用對象方法的時候,首先會通過instance的isa找到其class,然后在class中找到對象方法并調(diào)用,沒有找到則會通過superclass一層一層往上找,最終還是找不到的時候就會報錯。
- meta和class類似,只不過它存儲的不是對象方法,而是類方法。并且meta的isa全部指向NSObject的meta(就是圖中Root class的meta)。superclass指向其父類,NSObject(Root class)的元類對象的superclass指向的是其類對象。通過類對象調(diào)用類方法的時候,首先會通過class的isa找到其meta,然后在meta中找到類方法并調(diào)用,沒有找到則會通過superclass一層一層往上找,最終還是找不到的時候就會報錯。
@interface Person : NSObject
+ (void)eat;
- (void)walk;
@end
@implementation Person
+ (void)eat {
NSLog(@"吃飯");
}
- (void)walk {
NSLog(@"走路");
}
@end
@interface Student : Person
- (void)examination;
@end
@implementation Student
- (void)examination {
NSLog(@"考試");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Student eat];
Student *s = [[Student alloc] init];
[s examination];
[s walk];
}
return 0;
}
- 首先Student調(diào)用eat類方法會通過Student的類對象的isa指針找到其元類對象,在元類對象里找eat方法,沒有找到,就會通過superclass指針找到其父類Person的元類指針,接著尋找eat方法并執(zhí)行。
- 然后Student的實例對象s調(diào)用對象方法examination會先通過實例對象s的isa指針找到其類對象,并在類對象中找到examination方法并執(zhí)行。
- 最后Student的實例對象s調(diào)用對象方法walk會先通過實例對象s的isa指針找到其類對象,然后在類對象中尋找walk方法,沒有找到會通過superclass指針找到其父類Person的類對象,找到walk方法,并調(diào)用。
相信這樣一講之后,大家對實例對象、類對象、元類對象以及isa和superclass的了解能有更進一步的了解啦(有錯誤或者更合適的希望各位大神能夠指出)。
小彩蛋
首先大家看一下以下一段代碼
@interface NSObject (DVObject)
@end
@implementation NSObject (DVObject)
- (void)eat {
NSLog(@"吃飯");
}
@end
@interface Person : NSObject
+ (void)eat;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Person eat];
}
return 0;
}
我們給NSObject加一個分類,并且增加一個eat對象方法,然后定義一個Person對象繼承自NSObject,然后定義一個eat類方法,并沒有去實現(xiàn)這個方法。按照常理來講這段代碼運行是會崩潰的,但是我們看看執(zhí)行結(jié)果。居然調(diào)用了NSObject的對象方法,這是為什么呢,讓我們回到鎮(zhèn)樓圖仔細(xì)查看。
這里Person調(diào)用eat類方法,首先會通過isa找到自己的元類對象,發(fā)現(xiàn)沒有eat方法,然后通過superclass找到NSObject的元類對象繼續(xù)查找eat方法,依然沒有,再通過superclass,注意,此時的superclass指向的是NSObject的類對象,而通過代碼可以發(fā)現(xiàn)NSObject的類對象中是存在eat方法的,并且調(diào)用成功。
所以在我看來,其實對象方法和類方法都是一樣的,只是存儲的位置不同,對象方法存儲在類對象中,類方法存儲在元類對象中。然而我們都知道oc是一門運行時語言,調(diào)用方法的本質(zhì)其實是向?qū)ο蟀l(fā)送消息,這里的[Person eat]其實就是向Person類對象發(fā)送了一條eat消息,并通過isa和superclass一層一層往上找,找到就執(zhí)行,找不到就報錯。