OC對象的本質(下)—— 詳解isa&superclass指針

OC對象的本質(上):OC對象的底層實現原理
OC對象的本質(中):OC對象的種類
OC對象的本質(下):詳解isa&superclass指針

isa指針

先總結一下我們在對象的分類一文里面分析過的問題,OC對象氛圍三類

instance對象,內部包含

  • 成員變量
  • 特殊的成員變量isa指針

class對象,用來描述instance對象,內部包含

  • isa指針
  • superclass指針
  • 屬性信息
  • 對象方法信息(-方法)
  • 協議信息
  • instance對象的成員變量的描述信息

meta-class對象,用來存放類方法,內部包含

  • isa指針
  • superclass指針
  • 類方法信息(+方法)
oc對象的分類以及內部結構

對一個類來說,它的instanceclassmete-class對象之間,一定是有某種聯系的。假設這種聯系不存在,我們看看會碰到什么問題。比如我調用一個instance對象的方法

[InstanceObj InstanceObjMethod];

它的底層是

objc_msgSend(instanceObj, @sel_registerName("instanceObjMethod"));

也就是給instanceObj對象發消息。

instance對象不跟外界關聯的情況下,它內部只有一些成員變量信息,是不可能完成方法調用的,因為對象方法是存放在class對象里面的,對class對象調用+方法的時候也是一樣,必須有辦法跟meta-class對象關聯起來,才能完成對+方法的調用。所以isa指針,就只它們之間的關聯。可以通過下圖來理解isa的作用。

isa指針的作用

大致可以歸納為

  • instance對象isa指針指向class對象。當調用對象方法(-方法)時,通過instanceisa找到class,然后在class的方法列表里面找到對應的實現進行調用。
  • class對象的isa指針指向meta-class對象。當調用類方法(+)方法時,通過classisa找到meta-class,最后在meta-class的方法列表找到對應的實現進行調用。

那么通過isa的橋接作用,我夢應該能更近一步地理解OC消息發送以及方法調用的過程了。

superclass指針

顯而易見,從字面意思,我們就能知道,superclass就是父類的意思。
假定我們有以下幾個類

@interface Person : NSObject
@end

@interface Student : Person
@end

我們知道superclass指針存在于class對象meta-class對象里面。我們根據接下來的圖示來闡述一下:

class的superclass指針

一個類的class對象里面的superclass指針指向該類的父類的class對象
Studentinstance對象要調用Person的對象方法時,會先通過isa找到Studentclass對象,然后通過這個class對象superclass找到Person(Student的父類)class對象,最后找到相應的對象方法(-方法)的實現進行調用

meta-class的superclass指針

一個類的meta-class里面的superclass指針指向該類的父類的meta-class對象
Studentclass對象要調用Person的類方法時,會先通過isa找到Studentmeta-class對象,然后通過這個meta-class對象superclass找到Person(Student的父類)meta-class對象,最后找到相應的類方法(+方法)的實現進行調用

isa、superclass總結

isa、superclass指針作用圖例

上圖來自蘋果官方,完整描述了isa、superclass指針的作用,為了更加便于理解,我們在后面的圖例中用Student代替subclass,Person代替superclass,NSObject代替rootclass。

  • instanceisa指向class
  • classisa指向meta-class
  • meta-classisa指向基類的meta-class
  • classsuperclass指向父類的class如果沒有父類,superclass指針為nil
  • meta-classsuperclass指向父類的meta-class,基類的meta-classsuperclass指向基類的class

instance調用對象方法的軌跡
我們以[student abc];為例,studentStudent類的實例對象,調用軌跡如下圖


對于student來說,并不知道abc方法在哪里,唯一知道的就是可以去它的class對象里面找,

  • 于是先通過isa指針進入Student類的class對象,如果在其中找到了abc就直接進行調用,調用過程結束,
  • 沒找到的話,就通過class對象superclass指針進入Student類的父類,也就是Person類的class對象,重復上一步的查找邏輯
  • 以此類推,一層一層往上尋找,如果最終到了基類,也就是NSObject類的class對象里面,還沒找到的話,由于它的superclassnil,最終就會碰到一個經典的報錯[ERROR: unrecognized selector sent to instance],調用軌跡結束

class調用類方法的軌跡
我們以[Student abc];為例調用軌跡圖如下


對與Student類來說,abc在哪也是不知道的,我們知道類方法被規定放在meta-class對象里面,所以

  • 首先,通過Studentclass對象isa指針找到其meta-class對象,然后在方法列表里面尋找是否有abc,有的話就調用,調用邏輯結束。
  • 沒有的話,就通過meta-class對象superclass指針找到Student的父類Personmeta-class對象,然后查找abc方法,找到就調用,結束調用軌跡
  • 沒有的話,就通過Personmeta-class對象superclass指針,重復上一步的流程
  • 一次類推,通過meta-class對象superclass指針,一層層往上查找
  • 如果到了基類(NSObject)的meta-class還沒能夠找到abc,此時比較特殊,接下來的superclass指針會找到NSObject的class對象,你可能會奇怪,我們調用一個類方法,怎么跑到class對象里面來了,先保留你的疑問,只需記住,蘋果確實是這么設計的,此時會繼續在NSObject的class對象里面,尋找abc,如果真的找到了abc,就會調用
  • 如果還沒有找到,由于此時的superclassnil,最終系統將給出報錯

面試題 isa指針指向哪里?

根據我們上面的梳理和總結,我們可以得出結論

isa(of instance) --> isa(of class) --> isa(of meta-class)
下面我們通過代碼來驗證一下

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface CLPerson : NSObject <NSCopying>

@end

@implementation CLPerson

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        Class personClass = [CLPerson class];
        Class personMetaClass = object_getClass(personClass);
        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
    return 0;
}

我們在代碼中加入斷點,通過控制臺查看一下personisa信息。但是貌似系統只給出了有限信息


還有個辦法,可以右擊紅框中的isa,下拉菜單第一個有個打印功能Print Description of "xxx",可以得到更為詳細的輸出

看起來結果仍然被系統包裹了一層
如果你習慣直接在代碼上快捷操作,也可以這么做試試

但我還是喜歡用LLDB來查看,便于比較,和復制數據。

通過p/x命令來打印指針,/后面是打印參數,x參數表示用16進制數輸出。因為我們知道person這個instance的結構體的包含一個isa成員變量,person本身就是指針,所以可以通過person->isa訪問isa的值。
代碼里面,personClassPerson類class對象,輸出結果顯示,
person的isa = 0x001d8001000014d1
personClass = 0x00000001000014d0
它倆。。。并不相等!!!
這是什么情況?不是說好了instance對象isa指向class對象嘛?

其實在64位機器出現之前,instance對象isa確實是直接指向class對象的,
也就是
person->isa == personClass
從64bit開始,isa需要進行一次為運算,才能計算出真實的class對象地址,系統給我們提供了一個ISA_MASK,這個可以在objc4源碼里面找到。我先直接貼出來

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      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
#   define ISA_BITFIELD                                                        \
      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)

# else
#   error unknown architecture for packed isa
# endif

大家請看清這里是分了arm64和x86_64的,分別對應的是移動設備開發和mac開發。我的代碼是一個mac命令行工程,所以我們用x86的這個值來試一下


可以看到,結果就顯而易見了。通過和ISA_MASK進行一次&運算,我們得到了personClass的地址。同樣,我們來試一下personClass的isa指針。

結果我試圖通過personClass->isa先打印出其isa指針的時候,得到了錯誤提示,告訴我們說personClass的類型Class不是一個結構體,看不太明白,那就先查看一下Class的定義,typedef struct objc_class *Class;,然后在往下看一下objc_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;

雖然這個結構體里面有isa指針,但是尾部的OBJC2_UNAVAILABLE;提示我們,這已經是過時的API了。
不過我們在第一篇文章中,已經得出結論,知道class對象里面第一個成員變量確實是一個isa指針,我們可以通過一個小技巧來處理這個問題

struct cl_objc_class {
    Class isa;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CLPerson *person = [[CLPerson alloc] init];
        Class personClass = [CLPerson class];
        struct cl_objc_class *personClass2 = (__bridge struct cl_objc_class *)(personClass);
        Class personMetaClass = object_getClass(personClass);
        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
    return 0;
}

我們自定義一個struct,包含一個isa指針,然后再借助這個結構體類型來讀取personClass里面的內容,如上代碼,我們用personClass2在來嘗試一次


ok,結果顯示,class對象isa指針經過ISA_MASK轉換之后,得到了正確的mete-class對象的地址。到此,上面的面試題相信大家已經可以完整回答了。在用一個圖來總結一下就是

你會許還會問,那么superclass指針呢,是不是也需要一個什么mask轉換?答案是不需要的,可以用上面相同的方法進行驗證,這里不作贅述。總之isa指針稍微特殊一點點,特別記住一下關于ISA_MASK的細節就行。

深度窺探class/meta-class的內部結構----struct objc_class

在OC對象的本質(一)中,我們得知了一個事實,在class對象中,存放了一個類的方法列表、屬性信息、協議信息、成員變量信息;在meta-class對象中,存放了類的類信息。但是還沒有對其仔細驗證過。下面我們就來研究一下這個問題。
因為classmeta-class的類型都是struct objc_class*,所以我們問題的答案,就都在這個objc_class里面。上面的段落我們已經看了它的結構了,很可惜是一個已經廢棄的API,所以我們必須去最新的源碼里面,去看一下它的實現。在objc4源碼里面,我們找到如下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是一個C++的結構體,如果對C++不太熟的話,先不用過分糾結,可以借用OC的類來理解就行了,它們的相似度很高,可以有成員變臉,也可以有方法,區別主要是一些成員變量的默認作用域不一樣。可以看到objc_class繼承自objc_object,我們可以在源碼objc-private.h里看一下objc_object的實現

struct objc_object {
private:
    isa_t isa;
//剩下的都是方法
...
...
...
}

看得出來,其實就是一個isa指針。于是和objc_class的內容融合一下,我們可以理解成下面的這個結構

struct objc_class {
    isa_t 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的內部,頭兩個成員分別是isa和superclass,跑不了。但是下面的好像不是我們期待的內容,沒看到方法列表、屬性協議信息啥的呀。但是我門可以看到這里的第一個方法,返回一個class_rw_t *,看字面意思,class代表類,rw通常代表讀寫(readwrite),t通常指的是 表信息(table),也就是類的可讀寫信息。那么我們有理由懷疑這里面肯定有寶貝。進去看一看


果然,原來方法、屬性、協議信息都放在了這里。同時,我們還發現了一個class_ro_t *ro,字面就是只讀表,類對象里面有什么信息是只讀的呢?沒錯,成員變量信息,于是我們在進去驗證一下

看上去推斷是對的,確實找到成員變量信息。注意一下,這里的ivars是成員變量的描述信息,如名稱,類型等,只需要一份的,所以存在class對象里面,成員變量的具體值是存在具體的instance對象里面的,不要理解混了。

對于meta-class來說,結構上和class是一樣的,只不過有些內容可能用不到,例如屬性,協議列表。meta-class的類方法信息其實就放在我們剛才看到的那個方法列表里面,沒錯。class對象的對象方法信息也正是放在這個方法列表里的


還有一點就是,怎么說呢,還是看圖明白


途中我們看出來,objc_class有個成員變量bits,正是通過 bits & FAST_DATA_MASK,將objc_class和它的可讀寫表關聯起來了。下面我引用大神的一張ppt總結一下struct objc_class的結構

面試題解答

  • 對象的isa指針指向哪里?
  1. instance對象的isa指針指向class對象
  2. class對象的isa指針指向meta-class對象
  3. meta-class對象的isa指針指向基類(也就是NSObject)的meta-class對象
  • OC的類信息存放在哪里?
  1. 對象方法,屬性信息,成員變量信息,協議信息,存放在class對象中
  2. 類方法,存放在meta-class對象中
  3. 成員變量的具體值,存放在instance對象中



OC對象的本質(上):OC對象的底層實現
OC對象的本質(中):OC對象的分類
OC對象的本質(下):詳解isa&superclass指針

特別備注

本系列文章總結自MJ老師在騰訊課堂開設的OC底層原理課程,相關圖片素材均取自課程中的課件。

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