iOS 底層探索 - 類

iOS 底層探索系列編程

咱們在前面探索了 iOS 中的對象原理,面向對象編程中有一句名言:緩存

萬物皆對象bash

那麼對象又是從哪來的呢?有過面向對象編程基礎的同窗確定都知道是類派生出對象的,那麼今天咱們就一塊兒來探索一下類的底層原理吧。數據結構

1、iOS 中的類究竟是什麼?

咱們在平常開發中大多數狀況都是從 NSObject 這個基類來派生出咱們須要的類。那麼在 OC 底層,咱們的類 Class 到底被編譯成什麼樣子了呢?app

咱們新建一個 macOS 控制檯項目,而後新建一個 Animal 類出來。post

// Animal.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Animal : NSObject

@end

NS_ASSUME_NONNULL_END

// Animal.m
@implementation Animal

@end

// main.m
#import <Foundation/Foundation.h>
#import "Animal.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Animal alloc] init];
        NSLog(@"%p", animal);
    }
    return 0;
}
複製代碼

咱們在終端執行 clang 命令:測試

clang -rewrite-objc main.m -o main.cpp
複製代碼

這個命令是將咱們的 main.m 重寫成 main.cpp,咱們打開這個文件搜索 Animal:ui

咱們發現有多個地方都出現了 Animal:this

// 1
typedef struct objc_object Animal;

// 2
struct Animal_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

// 3
objc_getClass("Animal")
複製代碼

咱們先全局搜索第一個 typedef struct objc_object,發現有 843 個結果atom

咱們經過 Command + G 快捷鍵快速翻閱一下,最終在 7626 行找到了 Class 的定義:

typedef struct objc_class *Class;
複製代碼

由這行代碼咱們能夠得出一個結論,Class 類型在底層是一個結構體類型的指針,這個結構體類型爲 objc_class。 再搜索 typedef struct objc_class 發現搜不出來了,這個時候咱們須要在 objc4-756 源碼中進行探索了。

咱們在 objc4-756 源碼中直接搜索 struct objc_class ,而後定位到 objc-runtime-new.h 文件

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_object 再次出現了,而且此次是做爲 objc_class 的父類。這裏再次引用那句經典名言 萬物皆對象,也就是說類其實也是一種對象

由此,咱們能夠簡單總結一下類和對象在 COC 中分別的定義

C OC
objc_object NSObject
objc_class NSObject(Class)

2、類的結構是什麼樣的呢?

經過上面的探索,咱們已經知道了類本質上也是對象,而平常開發中常見的成員變量、屬性、方法、協議等都是在類裏面存在的,那麼咱們是否是能夠猜測在 iOS 底層,類其實就存儲了這些內容呢?

咱們能夠經過分析源碼來驗證咱們的猜測。

從上一節中 objc_class 的定義處,咱們能夠梳理出 Class 中的 4 個屬性

  • isa 指針
  • superclass 指針
  • cache
  • bits

須要值得注意的是,這裏的 isa 指針在這裏是隱藏屬性.

2.1 isa 指針

首先是 isa 指針,咱們以前已經探索過了,在對象初始化的時候,經過 isa 可讓對象和類關聯,這一點很好理解,但是爲何在類結構裏面還會有 isa 呢?看過上一篇文章的同窗確定知道這個問題的答案了。沒錯,就是元類。咱們的對象和類關聯起來須要 isa,一樣的,類和元類之間關聯也須要 isa

2.2 superclass 指針

顧名思義,superclass 指針代表當前類指向的是哪一個父類。通常來講,類的根父類基本上都是 NSObject 類。根元類的父類也是 NSObject 類。

2.3 cache 緩存

cache 的數據結構爲 cache_t,其定義以下:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    ...省略代碼...
}
複製代碼

類的緩存裏面存放的是什麼呢?是屬性?是實例變量?仍是方法?咱們能夠經過閱讀 objc-cache.mm 源文件來解答這個問題。

  • objc-cache.m
  • Method cache management
  • Cache flushing
  • Cache garbage collection
  • Cache instrumentation
  • Dedicated allocator for large caches

上面是 objc-cache.mm 源文件的註釋信息,咱們能夠看到 Method cache management 的出現,翻譯過來就是方法緩存管理。那麼是否是就是說 cache 屬性就是緩存的方法呢?而 OC 中的方法咱們如今尚未進行探索,先假設咱們已經掌握了相關的底層原理,這裏先簡單提一下。

咱們在類裏面編寫的方法,在底層實際上是以 SEL + IMP 的形式存在。SEL 就是方法的選擇器,而 IMP 則是具體的方法實現。這裏能夠以書籍的目錄以及內容來類比,咱們查找一篇文章的時候,須要先知道其標題(SEL),而後在目錄中看有沒有對應的標題,若是有那麼就翻到對應的頁,最後咱們就找到了咱們想要的內容。固然,iOS 中方法要比書籍的例子複雜一些,不過暫時能夠這麼簡單的理解,後面咱們會深刻方法的底層進行探索。

2.4 bits 屬性

bits 的數據結構類型是 class_data_bits_t,同時也是一個結構體類型。而咱們閱讀 objc_class 源碼的時候,會發現不少地方都有 bits 的身影,好比:

class_rw_t *data() { 
    return bits.data();
}

bool hasCustomRR() {
    return ! bits.hasDefaultRR();
}    

bool canAllocFast() {
    assert(!isFuture());
    return bits.canAllocFast();
}
複製代碼

這裏值得咱們注意的是,objc_classdata() 方法實際上是返回的 bitsdata() 方法,而經過這個 data() 方法,咱們發現諸如類的字節對齊、ARC、元類等特性都有 data() 的出現,這間接說明 bits 屬性實際上是個大容器,有關於內存管理、C++ 析構等內容在其中有定義。

這裏咱們會遇到一個十分重要的知識點: class_rw_tdata() 方法的返回值就是 class_rw_t 類型的指針對象。咱們在本文後面會重點介紹。

3、類的屬性存在哪?

上一節咱們對 OC 中類結構有了基本的瞭解,可是咱們平時最常打交道的內容-屬性,咱們還不知道它到底是存在哪一個地方。接下來咱們要作一件事情,就是在 objc4-756 的源碼中新建一個 Target,爲何不直接用上面的 macOS 命令行項目呢?由於咱們要開始結合 LLDB 打印一些類的內部信息,因此只能是新建一個依靠於 objc4-756 源碼 projecttarget 出來。一樣的,咱們仍是選擇 macOS 的命令行做爲咱們的 target

接着咱們新建一個類 Person,而後添加一些實例變量和屬性出來。

// Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end

NS_ASSUME_NONNULL_END

// main.m
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        NSLog(@"%s", p);
    }
    return 0;
}
複製代碼

咱們打一個斷點到 main.m 文件中的 NSLog 語句處,而後運行剛纔新建的 target

target 跑起來以後,咱們在控制檯先打印輸出一下 pClass 的內容:

3.1 類的內存結構

咱們這個時候須要藉助指針平移來探索,而對於類的內存結構咱們先看下面這張表格:

類的內存結構 大小(字節)
isa 8
superclass 8
cache 16

前兩個大小很好理解,由於 isasuperclass 都是結構體指針,而在 arm64 環境下,一個結構體指針的內存佔用大小爲 8 字節。而第三個屬性 cache 則須要咱們進行抽絲剝繭了。

cache_t cache;

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4
}

typedef uint32_t mask_t; 
複製代碼

從上面的代碼咱們能夠看出,cache 屬性實際上是 cache_t 類型的結構體,其內部有一個 8 字節的結構體指針,有 2 個各爲 4 字節的 mask_t。因此加起來就是 16 個字節。也就是說前三個屬性總共的內存偏移量爲 8 + 8 + 16 = 32 個字節,32 是 10 進制的表示,在 16 進制下就是 20。

3.2 探索 bits 屬性

咱們剛纔在控制檯打印輸出了 pClass 類對象的內容,咱們簡單畫個圖以下所示:

那麼,類的 bits 屬性的內存地址瓜熟蒂落的就是在 isa 的初始偏移量地址處進行 16 進制下的 20 遞增。也就是

0x1000021c8 + 0x20 = 0x1000021e8
複製代碼

咱們嘗試打印這個地址,注意這裏須要強轉一下:

這裏報錯了,問題實際上是出在咱們的 target 沒有關聯上 libobjc.A.dylib 這個動態庫,咱們關聯上從新運行項目

咱們重複一遍上面的流程:

這一次成功了。在 objc_class 源碼中有:

class_rw_t *data() { 
    return bits.data();
}
複製代碼

咱們不妨打印一下里面的內容:

返回了一個 class_rw_t 指針對象。咱們在 objc4-756 源碼中搜索 class_rw_t:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;
    
    ...省略代碼...    
}
複製代碼

顯然的,class_rw_t 也是一個結構體類型,其內部有 methodspropertiesprotocols 等咱們十分熟悉的內容。咱們先猜測一下,咱們的屬性應該存放在 class_rw_tproperties 裏面。爲了驗證咱們的猜測,咱們接着進行 LLDB 打印:

咱們再接着打印 properties:

properties 竟然是空的,難道是 bug?其實否則,這裏咱們還漏掉了一個很是重要的屬性 ro。咱們來到它的定義:

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;

    ...隱藏代碼...    
}
複製代碼

ro 的類型是 class_ro_t 結構體,它包含了 baseMethodListbaseProtocolsivarsbaseProperties 等屬性。咱們剛纔在 class_rw_t 中沒有找到咱們聲明在 Person 類中的實例變量 hobby 和屬性 nickName,那麼但願就在 class_ro_t 身上了,咱們打印看看它的內容:

根據名稱咱們猜想屬性應該在 baseProperties 裏面,咱們打印看看:

Bingo! 咱們的屬性 nickName 被找到了,那麼咱們的實例變量 hobby 呢?咱們從 $8 的 count 爲 1 能夠得知確定不在 baseProperites 裏面。根據名稱咱們猜想應該是在 ivars 裏面。

哈哈,hobby 實例變量也被咱們找到了,不過這裏的 count 爲何是 2 呢?咱們打印第二個元素看看:

結果爲 _nickName。這一結果證明了編譯器會幫助咱們給屬性 nickName 生成一個帶下劃線前綴的實例變量 _nickName

至此,咱們能夠得出如下結論:

class_ro_t 是在編譯時就已經肯定了的,存儲的是類的成員變量、屬性、方法和協議等內容。 class_rw_t 是能夠在運行時來拓展類的一些屬性、方法和協議等內容。

4、類的方法存在哪?

研究完了類的屬性是怎麼存儲的,咱們再來看看類的方法。

咱們先給咱們的 Person 類增長一個 sayHello 的實例方法和一個 sayHappy 的類方法。

// Person.h
- (void)sayHello;
+ (void)sayHappy;

// Person.m
- (void)sayHello
{
    NSLog(@"%s", __func__);
}

+ (void)sayHappy
{
    NSLog(@"%s", __func__);
}
複製代碼

按照上面的思路,咱們直接讀取 class_ro_t 中的 baseMethodList 的內容:

sayHello 被打印出來了,說明 baseMethodList 就是存儲實例方法的地方。咱們接着打印剩下的內容:

能夠看到 baseMethodList 中除了咱們的實例方法 sayHello 外,還有屬性 nickNamegettersetter 方法以及一個 C++ 析構方法。可是咱們的類方法 sayHappy 並無被打印出來。

5、類的類方法存在哪?

咱們上面已經獲得了屬性,實例方法的是怎麼樣存儲,還留下了一個疑問點,就是類方法是怎麼存儲的,接下來咱們用 Runtime 的 API 來實際測試一下。

// main.m
void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        
        testInstanceMethod_classToMetaclass(pClass);
        NSLog(@"%p", p);
    }
    return 0;
}
複製代碼

運行後打印結果以下:

首先 testInstanceMethod_classToMetaclass 方法測試的是分別從類和元類去獲取實例方法、類方法的結果。由打印結果咱們能夠知道:

  • 對於類對象來講,sayHello 是實例方法,存儲於類對象的內存中,不存在於元類對象中。而 sayHappy 是類方法,存儲於元類對象的內存中,不存在於類對象中。
  • 對於元類對象來講,sayHello 是類對象的實例方法,跟元類不要緊;sayHappy 是元類對象的實例方法,因此存在元類中。

咱們再接着測試:

// main.m
void testClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        Class pClass = object_getClass(p);
        
        testClassMethod_classToMetaclass(pClass);
        NSLog(@"%p", p);
    }
    return 0;
}
複製代碼

運行後打印結果以下:

從結果咱們能夠看出,對於類對象來講,經過 class_getClassMethod 獲取 sayHappy 是有值的,而獲取 sayHello 是沒有值的;對於元類對象來講,經過 class_getClassMethod 獲取 sayHappy 也是有值的,而獲取 sayHello 是沒有值的。這裏第一點很好理解,可是第二點會有點讓人糊塗,不是說類方法在元類中是體現爲對象方法的嗎?怎麼經過 class_getClassMethod 從元類中也能拿到 sayHappy,咱們進入到 class_getClassMethod 方法內部能夠解開這個疑惑:

Method class_getClassMethod(Class cls, SEL sel) {
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}    
複製代碼

能夠很清楚的看到,class_getClassMethod 方法底層其實調用的是 class_getInstanceMethod,而 cls->getMeta() 方法底層的判斷邏輯是若是已是元類就返回,若是不是就返回類的 isa。這也就解釋了上面的 sayHappy 爲何會出如今最後的打印中了。

除了上面的 LLDB 打印,咱們還能夠經過 isa 的方式來驗證類方法存放在元類中。

  • 經過 isa 在類對象中找到元類
  • 打印元類的 baseMethodsList

具體的過程筆者再也不贅述。

6、類和元類的建立時機

咱們在探索類和元類的時候,對於其建立時機還不是很清楚,這裏咱們先拋出結論:

  • 類和元類是在編譯期建立的,即在進行 alloc 操做以前,類和元類就已經被編譯器建立出來了。

那麼如何來證實呢,咱們有兩種方式能夠來證實:

  • LLDB 打印類和元類的指針

  • 編譯項目後,使用 MachoView 打開程序二進制可執行文件查看:

6、總結

  • 類和元類建立於編譯時,能夠經過 LLDB 來打印類和元類的指針,或者 MachOView 查看二進制可執行文件
  • 萬物皆對象:類的本質就是對象
  • 類在 class_ro_t 結構中存儲了編譯時肯定的屬性、成員變量、方法和協議等內容。
  • 實例方法存放在類中
  • 類方法存放在元類中

咱們完成了對 iOS 中類的底層探索,下一章咱們將對類的緩存進行深一步探索,敬請期待~

相關文章
相關標籤/搜索