類的結構分析

前言:

在了解了類與isa的關聯後,了解了isa是如何關聯到類以及isa內部存在的信息分佈,接下來我們來探討類的isa走位以及類的結構底層存在了什麼呢?

isa與類的關係

類的分析:

  • 延續著isa的關聯,分析isa走位,以及isa之間的繼承關係。

準備工作

  • 透過以下例子,探索類的繼承關係
  • 定義兩個類 Person類繼承自NSObject Teacher類繼承自Person類
//Person 類的聲明
@interface Person : NSObject
{
    int _age;
}
@property (nonatomic, copy) NSString *name;

- (void)sayHi;
+ (void)eat;

@end
//Person類的實現
@implementation Person
- (void)sayHi
{
    NSLog(@"大家好");
}
+ (void)eat
{
    NSLog(@"吃吃吃");
}
@end
//----------------------------------------------------------------------------
//Teacher類的聲明
@interface Teacher : NSObject

@end
//Teacher類的實現
@implementation Teacher

@end
//----------------------------------------------------------------------------
//main.m文件

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p1 = [[Person alloc]init];
        Teacher *t1 = [[Teacher alloc]init];
        NSLog(@"Hello, World! %@ - %@",p1,t1);
    }
    return 0;
}

打印調試

  • 先下一個斷點在Teacher,即已執行Person *p1 = [[Person alloc]init];

問題

  • 為什麼p/x 0x001d80010000227d & 0x00007ffffffffff8ULL 打印出的信息與p/x 0x0000000100002250 & 0x00007ffffffffff8ULL 都是LGPerson
    • 0x001d80010000227d 是p1對象的isa指針地址,其&後得到的結果是創建person的类LGPerson
    • 0x0000000100002250是isa中獲取的類信息所指的類的isa的指針地址,即LGPerson類的類 的isa指针地址,在Apple中,我们簡稱LGPerson類的類為元類

什麼是元類

  • 我們知道對象isa是指向的其實也是一個對象,可以稱為類對象,其isa的位域指向蘋果定義的元類
  • 元類系统給的,其定義和創建都是由編譯器完成,在這個過程中,類對象指向元類。
  • 元類類對象,每個都有一個獨一無二的元類用來存儲類方法的相關信息
  • 元類本身是沒有名稱的,由於與相關聯,所以使用了一樣的名稱

NSObject有幾個?

  • 從圖中可以看出,最後的根元类是NSObject,這個NSObject與我們日開開發中所知道的NSObject是同一個嗎?

證明NSObject只有一份

lldb命令驗證

  • 我們也通過lldb調試,來驗證這兩個NSObject是否是同一個,如下圖所示
  • 從圖中可以看出,最後NSObject類的元類也是NSObject,與上面的LGPerson中的根元類(NSObject)的元類,是同一個,所以可以得出一個結論:内存中只存在存在一份根元類NSObject,根元類的元類是指向它自己

程式碼驗證

  • 通過三種不同的方式獲取類,看他們打印的地址是否相同

void testClassNum(){
    Class class1 = [LGPerson class];
    Class class2 = [LGPerson alloc].class;
    Class class3 = object_getClass([LGPerson alloc]);
    NSLog(@"\\n%p-\\n%p-\\n%p-\\n", class1, class2, class3);
}

  • 打印結果,三個地址都相同,證明NSObject只有一份,即NSObject根元類在內存中只存一份

類存在幾份?

由於類的信息在內存中永遠只存在一份,所以類對象只有一份

isa指向&繼承關係(經典圖)

  • 以上驗證了,對象,類,元類,根元類的關係圖如下。
006.png

isa指向

isa的走向有以下幾點說明:

  • 實例對象(Instance of Subclass)isa指向類對象(class)
  • 類對象(class) isa 指向 元類(Meta class)
  • 元類(Meta class)isa指向根元類(Root metal class)
  • 根元類(Root metal class)isa指向它自己本身,就是NSObject

superclass指向

superclass(即繼承關係)的走向也有以下幾點說明:

  • 類之間的繼承關係
    • 類(subClass) 繼承自 父類(superClass)
    • 父類(superClass)繼承自根類(RootClass),此時的根類是指NSObject
    • 根類繼承自nil,所以根類NSObject可以理解為萬物起源,即無中生有
  • 元類也存在繼承,元類之間的繼承關係如下:
    • 子類的元類(metal SubClass) 繼承自 父類的元類(metal SuperClass)
    • 父類的元類(metal SuperClass) 繼承自 根元類(Root metal Class)
    • 根元類(Root metal Class)繼承於根類(Root class),此時的根類是指NSObject

【注意】實例對象之間沒有繼承關係

isa 指向鏈

  • teacher的isa走位鏈:t1(子類对象) --> Teacher(子類)--> Teacher(子元類) --> NSObject(根元類) --> NSObject(根根元類,即自己)
  • person的isa走位圖:p1(父類对象) --> Person (父類)--> Person(父元類) --> NSObject(根元類) --> NSObject(根根元类,即自己)

superclass繼承鏈

  • 類的繼承關係鏈:Teacher(子類) --> Person(父類) --> NSObject(根類)--> nil
  • 元類的繼承關係鏈:Teacher(子元類) --> Person(父元類) --> NSObject(根元類)--> NSObject(根類)--> nil

為什麼對象和類都有isa屬性?

  • 了解了isa指向,以及類的繼承,但是為什麼對象與類都有isa屬性呢,請接下看下去。

探索objc_class & objc_object

  • 我們透過isa與類的關係這篇文章中,利用了clang編譯main.m文件,從從編譯後C++文件中,可以看到如下C++源碼。

isa與類的關係

  • NSObject的底層編譯是NSObject_IMPL 結構體

    • 其中Classisa指針類型,是由objc_class 定義類型
    • objc_class 是一個結構體,iOS中,所有Class都是以objc_class 為模板創建的
    struct NSObject_IMPL {
        Class isa;
    };
    
    typedef struct objc_class *Class;
    
    

objc_class

  • 在objc源碼中搜索objc_class 的定義,可以看到舊版及新版

  • 舊版-runtime.h文件中struct objc_class (棄用)

  • 新版- objc-runtime-new.hstruct objc_class (objc4-781最新優化)

  • 新版struct objc_class 繼承自objc_object

objc_object

在objc源碼中搜索objc_object 的定義,可以看到舊版及新版

  • 在源碼中可以看到objc.h 文件中的objc_object
  • objc-privat.h

  • 編譯後的main.cpp中的objc_object的定義

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

objc_class & objc_object之間的關係

  • 結構體類型objc_class繼承自objc_object ,objc_object 這個結構體內部有一個isa屬性,所以objc_class 也擁有isa 屬性
  • main.cpp底層編譯文件中,NSObject中的isa在底層是由Class定義,其中class的底層編碼來自objc_class 類型,所以NSObject 也擁有了isa屬性
struct NSObject_IMPL {
    Class isa;
};

typedef struct objc_class *Class;

  • NSObject 是一個類,任何類都繼承於他,使用NSObject 來實例化一個對象,而這個對象擁有了objc_object的特性,等同於擁有isa這個屬性,而這個isaobjc_class繼承過來,又objc_class繼承自objc_objectobjc_objectisa屬性,所以對象都有一個isa,來自於當前的objc_object。

  • 實例化對象底層編譯為objc_object ,即擁有isa屬性,另外所有的類繼承於NSObject 類,經由編譯後,變成了objc_class 結構體,繼承於objc_object 自然也擁有isa屬性。

    objc_object 與對象的關係

    • 所有的對象 經由編譯會變成結構體objc_object
    • 所有的對象是來自NSObject (OC),但是真正到底層的是一個objc_object(C/C++)的結構體類型

    objc_object對象關係繼承關係

總結

  • 所有的對象+ + 元類都有isa屬性
  • 所有的對象都是由objc_object繼承來的
  • 萬物皆來源於objc_object,有以下兩點結論:
    • 所有以objc_object為模板創建的對象,都有isa屬性
    • 所有以objc_class為模板,創建的,都有isa屬性
  • 在結構層面可以理解為上層OC底層C/C++對接
    • 下層是通過結構體定義的模板,例如objc_class、objc_object
    • 上層是通過繼承NSObject類,而NSObject底層編譯後,繼承得是擁有isa屬性的objc_object

類的結構分析

  • 我們知道了萬物的起源NSObject,其實是來自於底層編譯後的objc_class ,那麼這個結構體objc_class 又有了哪些屬性呢,接著就來分析這個結構體
  struct objc_class : objc_object {
      // Class ISA; //8字節
      Class superclass; //Class 类型 8字節
      cache_t cache;             // formerly cache pointer and vtable
      class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
  }
  • isa屬性:繼承自objc_objectisa,佔8字節。
  • superclass屬性:Class類型,Class是由objc_object定義的,是一個指针,佔8字節
  • cache屬性:簡單從類型cache_t目前無法得知,而cache_t是一個結構體類型,結構體的内存大小需要根據内部的屬性來確定,而結構體指針才是8字節。
  • bits屬性:只有首地址經過上面3個屬性的內存大小總和的平移,才能獲取到bits。

計算cache_t 類的內存大小

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
        explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
        mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节

    #if __LP64__
        uint16_t _flags;  //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    #endif
        uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节

【情況一】if流程

  • buckets類型是struct bucket_t *,是結構體指针類型,佔8字節

  • maskmask_t類型,而mask_tunsigned int的別名,佔4字節
    【情況二】elseif流程

  • _maskAndBucketsuintptr_t類型,它是一個指针,佔8字節

  • _mask_unusedmask_t類型,而mask_tuint32_t類型定義的別名,佔4字節

計算前兩個屬性的内存大小,有以下兩種情況,內存大小總和都是12字節

  • _flagsuint16_t類型,uint16_t是unsigned short的別名,佔2個字節
  • _occupieduint16_t類型,uint16_t是unsigned short的別名,佔2個字節

總結:所以最後計算出cache類的內存大小= 12 + 2 + 2 = 16字節

獲取bits

所以有上述計算可知,想要獲取bit的內容,只需要通過類的首地址平移32個字節即可。

以下通過lldb命令調適的過程。

  • 其中的data()就是objc_class的提供的方法,透過訪問data()獲取bits數據


  • 從對象$2可以看到data()的數據,是一個結構體,但內部沒有屬性列表方法列表 ,那我們查看他的類型(class_rw_t)

探索屬性列表(property list)

  • 透過查看類型class_rw_t ,可以發現這個結構體class_rw_t 有提供methods(),properties(),protocols() 方法獲取屬性列表,方法列表。
  • 了解結構體class_rw_t 內部提供的方法,我們繼續探索屬性列表,流程如下。
  • p $3.properties()命令中的properties方法是由class_rw_t提供的,方法中返回的實際類型為property_array_t
  • 由於list的類型是property_list_t,是一個指針,所以通過p *$4獲取內存中的信息,同時也證明bits中存儲了property_list,即屬性列表。
  • 但是當我們想取得成員變量,卻發現數組越界,那麼也代表property_list 中只有一個屬性name。

獲取成員變量存儲位置

  • 由此可得出property_list 中只有屬性,沒有成員變量。

  • 屬性與成員變量兩者差異,就是屬性@property編譯後有生成getset方法

  • 接下來我們通過查看objc_classbits屬性中存儲數據的類class_rw_t的定義發現,除了methods、properties、protocols方法,還有一個ro方法,其返回類型是class_ro_t,通過查看其定義,發現其中有一個ivars屬性,我們可以做如下猜測:是否成員變量就存儲在這個ivar_list_t類型的ivars屬性中呢?

  • 探索成員變量(lldb調適流程)

class_ro_t結構體中的屬性如下所示,想要獲取ivars,需要ro的首地址平移48字節

 struct class_ro_t {
     uint32_t flags;     //4
     uint32_t instanceStart;//4
     uint32_t instanceSize;//4
 #ifdef __LP64__
     uint32_t reserved;  //4
 #endif

     const uint8_t * ivarLayout; //8

     const char * name; //8
     method_list_t * baseMethodList; // 8
     protocol_list_t * baseProtocols; // 8
     const ivar_list_t * ivars;

     const uint8_t * weakIvarLayout;
     property_list_t *baseProperties;

 }

總結

  • 通過@interface{ }定義的成員變量,會儲存在類的bits屬性中,通過bits→data()→ro()→ivars獲取成員變量列表,有成員變量(hobby)以及屬性定義的成員變量(name)
  • 通過@property定義的屬性,也會存儲在bits屬性中,通過bits → data() → properties() → list獲取属性列表,其中只包含屬性

探索方法列表,即methods_list

  • 在LGPerson類裡面添加兩個方法
 @interface LGPerson : NSObject
 {
 NSString *hobby;
 }

 @property (nonatomic, copy) NSString *name;

 - (void)sayHello;
 + (void)sayHappy;

 @implementation LGPerson
 - (void)sayHello
 {
 }
 + (void)sayHappy
 {   
 }

 @end

  • 通過lldb調適來獲取方法列表,步驟如圖所示

  • 通過p $3.methods()獲得具體的方法列表list結構,其中methods也是class_rw_t提供的方法

  • 通過打印的count = 4可知,存儲了4個方法,可以通過p $6.get(i)內存偏移的方式獲取單個方法,i的範圍是0-3

  • 如果在打印p $6.get(4),獲取第五個方法,也會報錯,提示數組越界

  • 如此以來可以找到對象方法,但是類方法呢?接著我們探索類方法的儲存位置

    類方法儲存位置

    • 透過上述查找methods→list 內部只有實例方法,沒有發現類方法。
    • 類對象的isa指向元類,元類用來儲存類的相關信息,於是猜測類方法是否儲存在元類的bits中,如下圖透過lldb調適流程。
  • 通過元類方法列表打印結果,確實類方法儲存在元類裡

總結

  • 類的實例方法儲存在類的bits屬性中,通過bits --> methods() --> list 獲取方法實例方法實例列表,列表中除了擁有實例方法,還有set方法即get方法。
  • 類的類方法 儲存在元類的bits屬性中,通過元类bits --> methods() --> list獲取類方法列表。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。