Objective - C 對象的本質(四)isa與superclass

(一)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指針的作用就在于此!

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指針的作用就在于此!

class對象的superclass指針
meta-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進行總結:


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)疑問?
  1. 基類的class對象的superclass指針為nil,會造成什么結果?
    答:如果instance對象調用intance方法,首先通過isa指針找到自己的class對象,如果沒有找到對應的方法,則會通過superclass指針一層一層往上,最終找到基類的class對象依然找不到的話,會報常見的unrecognized selector sent to class錯誤,crash。

  2. 為什么基類的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

我們通過位運算后的值是否和我們所想的一樣呢?


isa與ISA_MASK進行一個位運算即可得到對應的地址值
圖解

(五)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類型的結構體指針
    }
}

我們進行整理一下:


objc_class內存結構
代碼證明底層內存結構

我們從內存結構可以側面證實,我們現在通過代碼證明(自己仿寫底層結構進行強制類型轉換):

 //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函數中調用:

通過斷點調試可以直接看到對應的成員變量、對象方法、類方法、屬性、協議均可以在結構中可以找到。如下圖:


image.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容