(一)isa指針
我們在前面幾章一直提到isa指針,isa指針是三種對象中都有的成員變量,那么三種對象的isa指針有沒有什么區別?指向哪里呢?
我們首先看下面的代碼:
MJPerson *personInstance = [[MJPerson alloc]init];
personInstance.no = 432423;
[personInstance personInstanceMethod];//instance對象 調用實例方法
[MJPerson personClassMethod];//class對象 調用類方法
對消息發送機制有一定了解的同學,一定知道對象調用方法的時候,實際上是給對象發送消息,或者通過轉換為C++代碼后可以看到:
MJPerson *personInstance = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)personInstance, sel_registerName("setNo:"), (NSInteger)432423);
((void (*)(id, SEL))(void *)objc_msgSend)((id)personInstance, sel_registerName("personInstanceMethod"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("personClassMethod"));
但是問題來了,實例對象中并沒有存放對象方法,類對象中沒有存放類方法,它們是如何調用成功的呢?
isa指針的作用就在于此!
(1)instance的isa
- instance的isa指向class
- 當調用對象方法時,通過instance的isa找到class對象,最后找到對象方法的實現進行調用
(2)class的isa
- class的isa指向meta-class
- 當調用類方法時,通過class的isa找到meta-class對象,最后找到類方法的實現進行調用
(二)superclass指針
class對象與meta-class都有super-class指針。那superclass指針又有什么作用呢?我們看看下面的代碼:
@interface MJPerson : NSObject<NSCopying>
@property(nonatomic,assign) NSInteger no;
-(void)personInstanceMethod;
+(void)personClassMethod;
@end
@implementation MJPerson
-(void)personInstanceMethod{
}
+(void)personClassMethod{
}
-(id)copyWithZone:(NSZone *)zone{
return nil;
}
@end
@interface MJStudent : MJPerson<NSCoding>
@property(nonatomic,assign) int height;
-(void)studentInstanceMethod;
+(void)studentClassMethod;
@end
@implementation MJStudent
-(void)studentInstanceMethod{
}
+(void)studentClassMethod{
}
-(instancetype)initWithCoder:(NSCoder *)coder{
return nil;
}
-(void)encodeWithCoder:(NSCoder *)coder{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJStudent *student = [[MJStudent alloc]init];
[student studentInstanceMethod];//調用instance方法
[MJStudent studentClassMethod];//調用class方法
[student personInstanceMethod];//調用MJPerson的instance方法
[MJStudent personClassMethod];//調用MJPerson的class方法
[student init];//調用NSObject的instance方法
[MJStudent initialize];//調用NSObject的class方法
}
return 0;
}
同理main函數中的代碼轉為C++代碼后:
//MJStudent *student = [[MJStudent alloc]init];
MJStudent *student = ((MJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)((MJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJStudent"), sel_registerName("alloc")), sel_registerName("init"));
//[student studentInstanceMethod];
((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("studentInstanceMethod"));
//[MJStudent studentClassMethod];
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJStudent"), sel_registerName("studentClassMethod"));
//[student personInstanceMethod];
((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("personInstanceMethod"));
//[MJStudent personClassMethod];
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJStudent"), sel_registerName("personClassMethod"));
//[student init];
((MJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("init"));
//[MJStudent initialize];
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJStudent"), sel_registerName("initialize"));
可以看出并沒有什么區別,同樣是給instance對象和class對象發送信息,那么是如何找到父類的對象方法以及類方法并調用成功的呢?
superclass指針的作用就在于此!
(1)instance對象的superclass
- 當Student的instance對象要調用Person的對象方法時,會先通過isa找到Student的class對象,然后通過superclass找到Person的class對象,最后找到對象方法的實現進行調用
(2)class對象的superclass
- 當Student的class對象要調用Person的類方法時,會先通過isa找到Student的meta-class對象,然后通過superclass找到Person的meta-class對象,最后找到類方法的實現進行調用
(三)isa、superclass總結
下面對isa、superclass進行總結:
- instance對象的isa指向class對象
- class對象的isa指向meta-class對象
- 注意:meta-class對象的isa指向基類的meta-class對象,基類的meta-class對象的isa指向自身
- class對象的superclass指向父類的class對象;注意:如果沒有父類,superclass指針為nil
- meta-class對象的superclass指向父類的meta-class對象;注意:基類的meta-class的superclass指向基類的class對象
- instance調用對象方法的軌跡,isa找到class;方法不存在,就通過superclass找父類
- class調用類方法的軌跡,isa找meta-class;方法不存在,就通過superclass找父類
(1)疑問?
基類的class對象的superclass指針為nil,會造成什么結果?
答:如果instance對象調用intance方法,首先通過isa指針找到自己的class對象,如果沒有找到對應的方法,則會通過superclass指針一層一層往上,最終找到基類的class對象依然找不到的話,會報常見的unrecognized selector sent to class
錯誤,crash。為什么基類的meta-class的superclass指向基類的class對象?這樣不就造成 +(類方法)和 - (對象方法)方法混了嗎?
答:①我們通過下面的轉換C++后的代碼可以看出,不管是instance對象還是class對象調用對應的方法,都是像自身發送消息,通過isa到對應的位置尋找方法(沒有區分+ - )
②如果基類的class對象調用類方法,對應的meta-class對象沒有對應的方法,則會通過superclass指針找到自身(class對象),調用自身的方法(instance方法)
從面向對象的角度來說,是不合理的,但是從本質上來說
//[student personInstanceMethod];
((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("personInstanceMethod"));
//[MJStudent personClassMethod];
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJStudent"), sel_registerName("personClassMethod"));
①我們通過下面的代碼可以證明,確實會這樣:(將不同文件的代碼放在一塊)
//NSObject+Test.h
@interface NSObject (Test)
+(void)test;
@end
//NSObject+Test.m
@implementation NSObject (Test)
+(void)test{
NSLog(@"NSObject + Test %p", self);
}
@end
@interface MJPerson : NSObject
+(void)test;
@end
@implementation MJPerson
+(void)test{
NSLog(@"MJPerson + Test %p", self);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"MJPerson class %p",[MJPerson class]);
NSLog(@"NSObject class %p",[MJPerson class]);
[MJPerson test];
[NSObject test];
}
return 0;
}
打印結果:
MJPerson class 0x1000024c0
NSObject class 0x7fff91c77140
MJPerson + Test 0x1000024c0
NSObject + Test 0x7fff91c77140
這個結果應該沒有什么疑問
②接下來我們將MJPerson的test方法去除,再看結果:
MJPerson class 0x100002480
NSObject class 0x7fff91c77140
NSObject + Test 0x100002480
NSObject + Test 0x7fff91c77140
即可證明,當自身的meta-class對象沒有對應的方法,通過superclass指針尋找到NSObject的meta-class對象并調用
③我們繼續將NSObject分類.m文件中的test方法注釋掉,再看結果:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[MJPerson test]: unrecognized selector sent to class 0x100001440'
常見的報錯提示,找不到對應的test方法
④我們繼續將test方法改為實例方法,又會是什么結果呢?
MJPerson class 0x100002480
NSObject class 0x7fff91c77140
NSObject - Test 0x100002480
NSObject - Test 0x7fff91c77140
是不是很震驚!!結果就是class對象調用了實例方法,即可證明上面的結論
(2)其他疑問?
問: 如果給NSObject的class對象發送test消息,可能實例方法與類方法有存在且同名,那會如何調用呢?
答:給NSObject的class對象發送一條test消息,首先通過isa找到meta-class對象,有則直接調用,此時類方法調用;如果找不到在通過superclass回到自身尋找是否有同名的實例方法,有則實例方法調用,沒有則報錯
(四)isa細節
我們上面已經知道instance對象通過isa可以找到class對象,class對象可以通過isa找到meta-class對象,那么isa存放的真的是對應的地址嗎?
我們可以打印一下,發現isa并不是直接存放的對應的地址,那么isa又是存放的是什么呢?又是如何找到對應的對象呢?
其實,在以前的系統中,isa確實存放的是地址值。但是,從64位之后,isa需要進行一個位運算,才能計算出真實地址
#######ISA_MASK
在runtime源碼中有它的定義(其余不相關定義已省略)
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# else
# error unknown architecture for packed isa
# endif
我們通過位運算后的值是否和我們所想的一樣呢?
(五)class和meta-class的結構
我們上面講到class存放isa、superclass、其他成員變量,屬性,對象方法等;meta-class存放isa、superclass、類方法等;class與meta-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
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
雖然我們看到確實好像是這樣的,有isa、成員變量、屬性、實例方法列表、協議等等。但是很遺憾這個在_OBJC2__
時已經不可用了,我們無法通過一個過時的定義來證明,我們只能看runtime的源碼來證明。
我們在objc-runtime-new.h文件中,找到下面的定義:
① 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_data_bits_t這個類型我們繼續跟進
class_rw_t *data() const {//這個方法很重要,我們進去繼續查看
return bits.data();
}
...//還有其他我們前面已經看到過很熟悉的方法比如instanceSize等,不過并不在本次討論點。略
}
我們發現,
objc_class
是一個結構體,繼承自objc_object
,還可以定義方法,這是C++的語法,不要驚訝??
重要參數:
- Class ISA 繼承自
objc_object
- superclass
- cache
- bits class_data_bits_t類型
- data方法 返回
class_rw_t
類型的結構體指針
②class_rw_t
結構
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t version;
uint16_t witness;
const class_ro_t *ro;//這里我們再進去看
method_array_t methods;//方法列表
property_array_t properties;//屬性列表
protocol_array_t protocols;//協議列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
...//省略
}
③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;
...//省略
}
④class_data_bits_t
結構:
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
...
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);//bits通過與FAST_DATA_MASK位運算后得到class_rw_t類型的結構體指針
}
}
我們進行整理一下:
代碼證明底層內存結構
我們從內存結構可以側面證實,我們現在通過代碼證明(自己仿寫底層結構進行強制類型轉換):
//MJClassInfo.h文件
#ifndef MJClassInfo_h
#define MJClassInfo_h
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
};
struct method_t {
SEL name;
const char *types;
IMP imp;
};
struct method_list_t : entsize_list_tt {
method_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
};
struct ivar_list_t : entsize_list_tt {
ivar_t first;
};
struct property_t {
const char *name;
const char *attributes;
};
struct property_list_t : entsize_list_tt {
property_t first;
};
struct chained_property_list {
chained_property_list *next;
uint32_t count;
property_t list[0];
};
typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
uintptr_t count;
protocol_ref_t list[0];
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // instance對象占用的內存空間
#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;
};
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t * methods; // 方法列表
property_list_t *properties; // 屬性列表
const protocol_list_t * protocols; // 協議列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
#define FAST_DATA_MASK 0x00007ffffffffff8UL
struct class_data_bits_t {
uintptr_t bits;
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
/* OC對象 */
struct mj_objc_object {
void *isa;
};
/* 類對象 */
struct mj_objc_class : mj_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
mj_objc_class* metaClass() {
return (mj_objc_class *)((long long)isa & ISA_MASK);
}
};
#endif /* MJClassInfo_h */
main.mm的main函數中調用:
通過斷點調試可以直接看到對應的成員變量、對象方法、類方法、屬性、協議均可以在結構中可以找到。如下圖: