讀 objc4 源碼,深入理解 Objective-C Runtime

注:本文始發于個人 GitHub 項目 ShannonChenCHN/iOSDevLevelingUp。

關于 objc4 源碼的一些說明:

  • objc4 的源碼不能直接編譯,需要配置相關環境才能運行??梢栽?a target="_blank">這里下載可調式的源碼。
  • objc 運行時源碼的入口在 void _objc_init(void) 函數。

目錄

  • 1.Objective-C 對象是什么?Class 是什么?id 又是什么?
  • 2.isa 是什么?為什么要有 isa?
  • 3.為什么在 Objective-C 中,所以的對象都用一個指針來追蹤?
  • 4.Objective-C 對象是如何被創建(alloc)和初始化(init)的?
  • 5.Objective-C 對象的實例變量是什么?為什么不能給 Objective-C 對象動態添加實例變量?
  • 6.Objective-C 對象的屬性是什么?屬性跟實例變量的區別?
  • 7.Objective-C 對象的方法是什么?Objective-C 對象的方法在內存中的存儲結構是什么樣的?
  • 8.什么是 IMP?什么是選擇器 selector ?
  • 9.消息發送和消息轉發
  • 10.Method Swizzling
  • 11.Category
  • 12.Associated Objects 的原理是什么?到底能不能在 Category 中給 Objective-C 類添加屬性和實例變量?
  • 13.Objective-C 中的 Protocol 是什么?
  • 14.selfsuper 的本質
  • 15.load 方法和 initialize 方法

1. Objective-C 對象是什么?Class 是什么?id 又是什么?

所有的類都繼承 NSObject 或者 NSProxy,先來看看這兩個類在各自的公開頭文件中的定義:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
@interface NSProxy <NSObject> {
    Class   isa;
}

在 objc.h 文件中,對于 Class,id 以及 objc_object 的定義:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

runtime.h 文件中對 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;

在 Objective-C 中,每一個對象是一個結構體,每個對象都有一個 isa 指針,類對象 Class 也是一個對象。所以,我們說,凡是包含 isa 指針的,都可以被認為是 Objective-C 中的對象。運行時可以通過 isa 指針,查找到該對象是屬于什么類(Class)。

2. isa 是什么?為什么要有 isa?

在 Runtime 源碼中,對于 objc_object 和 objc_class 的定義分別如下:

struct objc_object {
private:
    isa_t isa;  // isa 是一個 union 聯合體,其包含這個對象所屬類的信息

public:
    Class ISA();     // ISA() assumes this is NOT a tagged pointer object
    Class getIsa();  // getIsa() allows this to be a tagged pointer object
    
    ...
};
struct objc_class : objc_object {
    // 這里沒寫 isa,其實繼承了 objc_object 的 isa , 在這里 isa 是一個指向元類的指針
    // Class ISA;
    Class superclass;           // 指向當前類的父類
    cache_t cache;              // formerly cache pointer and vtable
                                // 用于緩存指針和 vtable,加速方法的調用
    class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
                                // 相當于 class_rw_t 指針加上 rr/alloc 的標志
                                // bits 用于存儲類的方法、屬性、遵循的協議等信息的地方

    // 針對 class_data_bits_t 的 data() 函數的封裝,最終返回一個 class_rw_t 類型的結構體變量
    // Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中
    class_rw_t *data() { 
        return bits.data();
    }
    
    ...
};

objc_class 繼承于 objc_object,所以 objc_class 也是一個 objc_object,objc_object 和 objc_class 都有一個成員變量 isa。isa 變量的類型是 isa_t,這個 isa_t 其實是一個聯合體(union),其中包括成員量 cls。也就是說,每個 objc_object 通過自己持有的 isa,都可以查找到自己所屬的類,對于 objc_class 來說,就是通過 isa 找到自己所屬的元類(meta class)。

#define ISA_MASK        0x00007ffffffffff8ULL
#define ISA_MAGIC_MASK  0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE   (1ULL<<56)
#define RC_HALF  (1ULL<<7)

// isa 的類型是一個 isa_t 聯合體
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;          // 所屬的類
    uintptr_t bits;

    struct {
        uintptr_t nonpointer        : 1;  // 表示 isa_t 的類型,0 表示 raw isa,也就是沒有結構體的部分,訪問對象的 isa 會直接返回一個指向 cls 的指針,也就是在 iPhone 遷移到 64 位系統之前時 isa 的類型。1 表示當前 isa 不是指針,但是其中也有 cls 的信息,只是其中關于類的指針都是保存在 shiftcls 中。
        uintptr_t has_assoc         : 1;  // 對象含有或者曾經含有關聯引用,沒有關聯引用的可以更快地釋放內存
        uintptr_t has_cxx_dtor      : 1;  // 表示當前對象有 C++ 或者 ObjC 的析構器(destructor),如果沒有析構器就會快速釋放內存。
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;  // 用于調試器判斷當前對象是真的對象還是沒有初始化的空間
        uintptr_t weakly_referenced : 1;  // 對象被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放
        uintptr_t deallocating      : 1;  // 對象正在釋放內存
        uintptr_t has_sidetable_rc  : 1;  // 對象的引用計數太大了,存不下
        uintptr_t extra_rc          : 8;  // 對象的引用計數超過 1,會存在這個這個里面,如果引用計數為 10,extra_rc 的值就為 9
    };
};

而在 Objective-C 中,對象的方法都是存儲在類中,而不是對象中(如果每一個對象都保存了自己能執行的方法,那么對內存的占用有極大的影響)。

// Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 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;
    
    ...
    
};

當一個對象的實例方法被調用時,它要通過自己持有的 isa 來查找對應的類,然后在這里的 class_data_bits_t 結構體中查找對應方法的實現(每個對象可以通過 cls->data()-> methods 來訪問所屬類的方法)。同時,每一個 objc_class 也有一個指向自己的父類的指針 super_class 用來查找繼承的方法。

因為在 Objective-C 中,類其實也是一個對象,每個類也有一個 isa 指向自己所屬的元類。所以無論是類還是對象都能通過相同的機制查找方法的實現。

isa 在方法調用時扮演的角色:

  • 調用一個對象的實例方法時,通過對象的 isa 在類中獲取方法的實現
  • 調用一個類的類方法時,通過類的 isa 在元類中獲取方法的實現
  • 如果在當前類/元類中沒找到,就會通過類/元類的 superclass 在繼承鏈中一級一級往上查找
ios-runtime-class.png

<div align='center'>圖 1. 對象,類與元類之間的關系</div>

isa_t 中包含什么:

isa 的類型 isa_t 是一個 union 類型的結構體,也就是說其中的 isa_t、cls、 bits 還有結構體共用同一塊地址空間,而 isa 總共會占據 64 位的內存空間(決定于其中的結構體)。其中包含的信息見上面的代碼注釋。

現在直接訪問對象(objc_object)的 isa 已經不會返回類指針了,取而代之的是使用 ISA() 方法來獲取類指針。其中 ISA_MASK 是宏定義,這里通過掩碼的方式獲取類指針。

結論

(1)isa 的作用:用于查找對象(或類對象)所屬類(或元類)的信息,比如方法列表。

(2)isa 是什么:isa 的數據結構是一個 isa_t 聯合體,其中包含其所屬的 Class 的地址,通過訪問對象的 isa,就可以獲取到指向其所屬 Class 的指針(針對 tagged pointer 的情況,也就是 non-pointer isa,有點不一樣的是,除了指向 class 的指針,isa 中還會包含對象本身的一些信息,比如對象是否被弱引用)。

3. 為什么在 Objective-C 中,所以的對象都用一個指針來追蹤?

內存中的數據類型分為兩種:值類型和引用類型。指針就是引用類型,struct 類型就是值類型。

值類型在傳值時需要拷貝內容本身,而引用類型在傳遞時,拷貝的是對象的地址。所以,一方面,值類型的傳遞占用更多的內存空間,使用引用類型更節省內存開銷;另一方面,也是最主要的原因,很多時候,我們需要把一個對象交給另一個函數或者方法去修改其中的內容(比如說一個 Person 對象的 age 屬性),顯然如果我們想讓修改方獲取到這個對象,我們需要的傳遞的是地址,而不是復制一份。

對于像 int 這樣的基本數據類型,拷貝起來更快,而且數據簡單,方便修改,所以就不用指針了。

另一方面,對象的內存是分配在堆上的,而值類型是分配到棧上的。所以一般對象的生命周期會比普通的值類型要長,而且創建和銷毀對象以及內存管理是要消耗性能的,所以通過指針來引用一個對象,比直接復制和創建對象要更有效率、更節省性能。

參考:

4. Objective-C 對象是如何被創建(alloc)和初始化(init)的?

整個對象的創建過程其實就做了兩件事情:為對象分配內存空間,以及初始化 isa(一個聯合體)。

(1)創建 NSObject 對象的過程

+ (id)alloc {
    return _objc_rootAlloc(self);
}


id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}


static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    id obj = class_createInstance(cls, 0);
    if (slowpath(!obj)) return callBadAllocHandler(cls);
    return obj;
}


id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

最核心的邏輯就在 _class_createInstanceFromZone 函數中:

static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) {

    // 實例變量內存大小,實例大小 instanceSize 會存儲在類的 isa_t 結構體中,經過對齊最后返回
    size_t size = cls->instanceSize(extraBytes);  

    // 給對象申請內存空間
    id obj = (id)calloc(1, size);
    if (!obj) return nil;
    
    // 初始化 isa
    obj->initInstanceIsa(cls, hasCxxDtor);

    return obj;
}

獲取對象內存空間大?。?/p>

size_t instanceSize(size_t extraBytes) {
    // Core Foundation 需要所有的對象的大小至少有 16 字節。
    size_t size = alignedInstanceSize() + extraBytes;
    if (size < 16) size = 16;
    return size;
}

uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
}

uint32_t unalignedInstanceSize() {
    assert(isRealized());
    return data()->ro->instanceSize;
}

初始化 isa,isa 是一個 isa_t 聯合體:

inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) {
    if (!indexed) {
        isa.cls = cls;
    } else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}

(2)NSObject 對象初始化的過程

NSObject 對象的初始化實際上就是返回 +alloc 執行后得到的對象本身:

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj) {
    return obj;
}

5. Objective-C 對象的實例變量是什么?為什么不能給 Objective-C 對象動態添加實例變量?

(1)兩個注意點:

  • Objective-C 的 -> 操作符不是C語言指針操作
  • Objective-C 對象不能簡單對應于一個 C struct,訪問成員變量不等于訪問 C struct 成員

(2)Non Fragile ivars

在 Runtime 的現行版本中,最大的特點就是健壯的實例變量。

當一個類被編譯時,實例變量的布局也就形成了,它表明訪問類的實例變量的位置。用舊版OSX SDK 編譯的 MyObject 類成員變量布局是這樣的,MyObject的成員變量依次排列在基類NSObject 的成員后面。

當蘋果發布新版本OSX SDK后,NSObject增加了兩個成員變量。如果沒有Non Fragile ivars特性,我們的代碼將無法正常運行,因為MyObject類成員變量布局在編譯時已經確定,有兩個成員變量和基類的內存區域重疊了。此時,我們只能重新編譯MyObject代碼,程序才能在新版本系統上運行。

現在有了 Non Fragile ivars 之后,問題就解決了。在程序啟動后,runtime加載MyObject類的時候,通過計算基類的大小,runtime 動態調整了 MyObject 類成員變量布局,把MyObject成員變量的位置向后移動8個字節。于是我們的程序無需編譯,就能在新版本系統上運行。

(3)Non Fragile ivars 是如何實現的呢?

當成員變量布局調整后,靜態編譯的native程序怎么能找到變量的新偏移位置呢?

根據 Runtime 源碼可知,一個變量實際上就是一個 ivar_t 結構體。而每個 Objective-C 對象對應于 struct objc_object,后者的 isa 指向類定義,即 struct objc_class:

typedef struct ivar_t *Ivar;

struct objc_object {
private:
    isa_t isa;
    //...
};

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 的 data()->ro->ivars 找下去,struct ivar_list_t是類所有成員變量的定義列表。通過 first 變量,可以取得類里任意一個類成員變量的定義。

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;               // 一個指向常量的指針,其中存儲了當前類在編譯期就已經確定的屬性、方法以及遵循的協議
    //...
    
};

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;  // (編譯時確定的)屬性列表

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

struct ivar_list_t {
    uint32_t entsize;
    uint32_t count;
    ivar_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    //...
};

這里的 offset,應該就是用來記錄這個成員變量在對象中的偏移位置。也就是說,runtime在發現基類大小變化時,通過修改offset,來更新子類成員變量的偏移值。

假如我們現在有一個繼承于 NSError 的類 MyClass,在編譯時,LLVM計算出基類 NSError 對象的大小為40字節,然后記錄在MyClass的類定義中,如下是對應的C代碼。在編譯后的可執行程序中,寫死了“40”這個魔術數字,記錄了在此次編譯時MyClass基類的大小。

class_ro_t class_ro_MyClass = {
    .instanceStart = 40,
    .instanceSize = 48,
    //...
}

現在假如蘋果發布了OSX 11 SDK,NSError類大小增加到48字節。當我們的程序啟動后,runtime加載MyClass類定義的時候,發現基類的真實大小和MyClass的instanceStart不相符,得知基類的大小發生了改變。于是runtime遍歷MyClass的所有成員變量定義,將offset指向的值增加8。具體的實現代碼在runtime/objc-runtime-new.mm的 moveIvars()函數中。

并且,MyClass類定義的instanceSize也要增加8。這樣runtime在創建MyClass對象的時候,能分配出正確大小的內存塊。

static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
    uint32_t diff;

    diff = superSize - ro->instanceStart;
    if (ro->ivars) {
        // Find maximum alignment in this class's ivars
        uint32_t maxAlignment = 1;
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t alignment = ivar.alignment();
            if (alignment > maxAlignment) maxAlignment = alignment;
        }

        // Compute a slide value that preserves that alignment
        uint32_t alignMask = maxAlignment - 1;
        diff = (diff + alignMask) & ~alignMask;

        // Slide all of this class's ivars en masse
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t oldOffset = (uint32_t)*ivar.offset;
            uint32_t newOffset = oldOffset + diff;
            *ivar.offset = newOffset;

            if (PrintIvars) {
                _objc_inform("IVARS:    offset %u -> %u for %s "
                             "(size %u, align %u)", 
                             oldOffset, newOffset, ivar.name, 
                             ivar.size, ivar.alignment());
            }
        }
    }

    *(uint32_t *)&ro->instanceStart += diff;
    *(uint32_t *)&ro->instanceSize += diff;
}


(4)為什么Objective-C類不能動態添加成員變量

runtime 提供了一個 class_addIvar() 函數用于給類添加成員變量,但是根據文檔中的注釋,這個函數只能在“構建一個類的過程中”調用。一旦完成類定義,就不能再添加成員變量了。經過編譯的類在程序啟動后就被runtime加載,沒有機會調用addIvar。程序在運行時動態構建的類需要在調用 objc_allocateClassPair 之后,調用 objc_registerClassPair 之前才可以添加成員變量。

這樣做會帶來嚴重問題,為基類動態增加成員變量會導致所有已創建出的子類實例都無法使用。那為什么runtime允許動態添加方法和屬性,而不會引發問題呢?

因為方法和屬性并不“屬于”實例,而成員變量“屬于”實例。我們所說的“類實例”概念,指的是一塊內存區域,包含了isa指針和所有的成員變量。所以假如允許動態修改類成員變量布局,已經創建出的實例就不符合類定義了,變成了無效對象。但方法定義是在objc_class中管理的,不管如何增刪類方法,都不影響實例的內存布局,已經創建出的實例仍然可正常使用。

美團的技術博客中給出的解釋比較簡單,其中“破壞破壞類的內部布局”這句話本身也是有些問題的:

extension在編譯期決議,它就是類的一部分,在編譯期和頭文件里的@interface以及實現文件里的@implement一起形成一個完整的類,它伴隨類的產生而產生,亦隨之一起消亡。extension一般用來隱藏類的私有信息,你必須有一個類的源碼才能為一個類添加extension,所以你無法為系統的類比如NSString添加extension。

但是category則完全不一樣,它是在運行期決議的。
就category和extension的區別來看,我們可以推導出一個明顯的事實,extension可以添加實例變量,而category是無法添加實例變量的(因為在運行期,對象的內存布局已經確定,如果添加實例變量就會破壞類的內部布局,這對編譯型語言來說是災難性的)。

參考:

6. Objective-C 對象的屬性是什么?屬性跟實例變量的區別?

屬性是一個結構體,其中包含屬性名和屬性本身的屬性(attributes)。

我們一般是通過使用 @property 進行屬性定義,編譯時編譯器會自動生成對應的實例變量(默認情況下生成的實例變量名是在對應的屬性名前加了下劃線“_”),同時還會自動合成對應的 setter 和 getter 方法用于存取屬性值。

我們可以驗證一下,先定義一個帶有屬性的類 NyanCat,如下:


@interface NyanCat : NSObject {
    int age;
    NSString *name;
}

@property (nonatomic, copy) NSString *cost;

@end

@implementation NyanCat

@end

然后再通過 clang -rewrite-objc NyanCat.m 將該類重寫為 cpp 代碼后,得到了下面這些內容:


#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat; // NyanCat 類實際上就是一個 objc_object 結構體
typedef struct {} _objc_exc_NyanCat;
#endif

extern "C" unsigned long OBJC_IVAR_$_NyanCat$_cost;
struct NyanCat_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int age;
    NSString *name;
    NSString *_cost;
};


//...

// 屬性 cost 的 setter 和 getter 對應的函數
static NSString * _I_NyanCat_cost(NyanCat * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_NyanCat$_cost)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct NyanCat, _cost), (id)cost, 0, 1); }

// 屬性的數據結構
struct _prop_t {
    const char *name;
    const char *attributes;
};

extern "C" unsigned long int OBJC_IVAR_$_NyanCat$age __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, age);
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, name);
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$_cost __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, _cost);


// 實例變量列表
static struct /*_ivar_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count;
    struct _ivar_t ivar_list[3];
} _OBJC_$_INSTANCE_VARIABLES_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_ivar_t),
    3,
    {{(unsigned long int *)&OBJC_IVAR_$_NyanCat$age, "age", "i", 2, 4},
     {(unsigned long int *)&OBJC_IVAR_$_NyanCat$name, "name", "@\"NSString\"", 3, 8},
     {(unsigned long int *)&OBJC_IVAR_$_NyanCat$_cost, "_cost", "@\"NSString\"", 3, 8}}
};


// 實例方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {(struct objc_selector *)"cost", "@16@0:8", (void *)_I_NyanCat_cost},
    {(struct objc_selector *)"setCost:", "v24@0:8@16", (void *)_I_NyanCat_setCost_}}
};

// 屬性列表
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"cost","T@\"NSString\",C,N,V_cost"}}
};


從上面 clang 重寫的代碼中可以看到:

  • 屬性列表的數據結構 _prop_list_t 中有屬性cost對應的屬性名和屬性的 attributes(attributes 字符串所代表的含義可以在官方文檔上查閱到)。
  • 實例變量列表 _method_list_t 中也有屬性 cost 對應的變量信息,變量名為 _cost,類型為 @"NSString"。
  • 實例方法列表 _method_list_t 中有屬性 cost 對應的 setter 和 getter 方法,這兩個方法的實現分別對應的是兩個函數—— _I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost)_I_NyanCat_cost(NyanCat * self, SEL _cmd)。

以上三條結果正好驗證了我們一開始提出的結論。

實際上,在 runtime 源碼 objc-runtime-new.h 的實現中,屬性就是一個 property_t 類型的結構體,其中包含屬性名以及屬性自己的屬性(attributes)。

typedef struct property_t *objc_property_t;

// 屬性的數據結構
struct property_t {
    const char *name;         // property 的名字
    const char *attributes;   // property 的屬性
};

在實際使用 runtime 時,通過下面兩個函數分別可以獲取 property 名字和 attributes 字符串。

// Returns the name of a property.
const char * _Nonnull property_getName(objc_property_t _Nonnull property);

// Returns the attribute string of a property.
const char * _Nullable property_getAttributes(objc_property_t _Nonnull property);

小結:

  • 一個對象的屬性實際上包括實例變量以及存取屬性值(實際上就是實例變量值)的 setter/getter 方法兩部分(這里只討論類本身定義的屬性,category 中的 @property 并沒有為我們生成實例變量以及存取方法,而需要我們手動實現)。
  • 屬性的實際結構是一個結構體,其中包含屬性名和 attributes 兩部分。

7. Objective-C 對象的方法是什么?Objective-C 對象的方法在內存中的存儲結構是什么樣的?

objc_class 有一個 class_data_bits_t 類型的變量 bits,Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中,通過調用 objc_class 的 class_rw_t *data() 方法,可以獲取這個 class_rw_t 類型的變量。

// Objective-C 類是一個結構體,繼承于 objc_object
struct objc_class : objc_object {
    // 這里沒寫 isa,其實繼承了 objc_object 的 isa , 在這里 isa 是一個指向元類的指針
    // Class ISA;
    Class superclass;           // 指向當前類的父類
    cache_t cache;              // formerly cache pointer and vtable
                                // 用于緩存指針和 vtable,加速方法的調用
    class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
                                // 相當于 class_rw_t 指針加上 rr/alloc 的標志
                                // bits 用于存儲類的方法、屬性、遵循的協議等信息的地方
                                

    // 針對 class_data_bits_t 的 data() 函數的封裝,最終返回一個 class_rw_t 類型的結構體變量
    // Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中
    class_rw_t *data() { 
        return bits.data();
    }

     ...
}

class_rw_t 中還有一個指向常量的指針 ro,其中存儲了當前類在編譯期就已經確定的屬性、方法以及遵循的協議。

/ Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 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;         // 所遵循的協議的列表

    ...

}
// 用于存儲一個 Objective-C 類在編譯期就已經確定的屬性、方法以及遵循的協議
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;  // (編譯時確定的)屬性列表

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

加載 ObjC 運行時的過程中在 realizeClass() 方法中:

  1. 從 class_data_bits_t 調用 data 方法,將結果強制轉換為 class_ro_t 指針;
  2. 初始化一個 class_rw_t 結構體;
  3. 設置結構體 ro 的值以及 flag。
  4. 最后重新將這個 class_rw_t 設置給 class_data_bits_t 的 data。
...
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
...

在上面這段代碼運行之后 class_rw_t 中的方法,屬性以及協議列表均為空。這時需要 realizeClass 調用 methodizeClass 方法來將類自己實現的方法(包括分類)、屬性和遵循的協議加載到 methods、 properties 和 protocols 列表中。

方法的結構,與類和對象一樣,方法在內存中也是一個結構體 method_t,其中包括成員變量 name(SEL 類型,實際上就是方法名)、types(一個C字符串方法類型,詳見 Type Encodings)、imp(IMP 類型方法實現)。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

結論:

(1) 在 runtime 初始化之后,realizeClass 之前,從 class_data_bits_t 結構體中獲取的 class_rw_t 一直都不是 class_rw_t 結構體,而是class_ro_t。因為類的一些方法、屬性和協議都是在編譯期決定的(baseMethods 等成員以及類在內存中的位置都是編譯期決定的)。

(2) 類在內存中的位置是在編譯期間決定的,在之后修改代碼,也不會改變內存中的位置。
類的方法、屬性以及協議在編譯期間存放到了“錯誤”的位置,直到 realizeClass 執行之后,才放到了 class_rw_t 指向的只讀區域 class_ro_t,這樣我們即可以在運行時為 class_rw_t 添加方法,也不會影響類的只讀結構。

(3) 在 class_ro_t 中的屬性在運行期間就不能改變了,再添加方法時,會修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods。

(4)一個類(Class)持有一個分發表,在運行期分發消息,表中的每一個實體代表一個方法(Method),它的名字叫做選擇子(SEL),對應著一種方法實現(IMP)。

參考:

8. 什么是 IMP?什么是選擇器 selector ?

8.1 IMP

IMP 在 runtime 源碼 objc.h 中的定義是:

/// A pointer to the function of a method implementation. 
typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法的實現,以達到更好的性能(在 MantleMTLModelAdapter.m 中可以看到這方面的應用)。

你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每個方法名都對應一個 SEL 類型的方法選擇器,而每個實例對象中的 SEL 對應的方法實現肯定是唯一的,通過一組 id 和 SEL 參數就能確定唯一的方法實現地址;反之亦然。

8.2 選擇器 selector

選擇器代表方法在 Runtime 期間的標識符,為 SEL 類型,SEL 與普通字符串的區別在于 SEL 對于選擇器來說總是能保證其唯一性。在類加載的時候,編譯器會生成與方法相對應的選擇子,并注冊到 Objective-C 的 Runtime 運行系統。

SEL 在 objc.h 中的定義是:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL 看上去是一個指向結構體的指針,但是實際上是什么類型呢?objc.h 中提供了運行時向系統注冊選擇器的函數 sel_registerName()。而在開源的 objc-sel.mm 中提供了sel_registerName() 函數的實現,其中能找到一些蛛絲馬跡:

SEL sel_registerName(const char *name) {
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}

static SEL __sel_registerName(const char *name, int lock, int copy) 
{
    SEL result = 0;

    if (lock) selLock.assertUnlocked();
    else selLock.assertWriting();

    // name  為空直接返回 0
    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    if (lock) selLock.read();
    if (namedSelectors) {
        // 到全局的表中去找
        result = (SEL)NXMapGet(namedSelectors, name);
    }
    if (lock) selLock.unlockRead();
    if (result) return result;

    // No match. Insert.

    if (lock) selLock.write();

    if (!namedSelectors) {
        namedSelectors = NXCreateMapTable(NXStrValueMapPrototype, 
                                          (unsigned)SelrefCount);
    }
    if (lock) {
        // Rescan in case it was added while we dropped the lock
        result = (SEL)NXMapGet(namedSelectors, name);
    }
    if (!result) {
        // 創建一個 SEL
        result = sel_alloc(name, copy);
        // fixme choose a better container (hash not map for starters)
        NXMapInsert(namedSelectors, sel_getName(result), result);
    }

    if (lock) selLock.unlockWrite();
    return result;
}

static SEL sel_alloc(const char *name, bool copy)
{
    selLock.assertWriting();
    return (SEL)(copy ? strdupIfMutable(name) : name);   
}

從創建 SEL 的實現來看, SEL 實際上是一個 char * 類型,也就是一個字符串。

(1) 使用 @selector() 生成的選擇子不會因為類的不同而改變(即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇子),其內存地址在編譯期間就已經確定了。也就是說向不同的類發送相同的消息時,其生成的選擇子是完全相同的。

(2) 通過 @selector(方法名) 就可以返回一個選擇子,通過 (void *)@selector(方法名), 就可以讀取選擇器的地址。

(3) 推斷出的 selector 的特性:

  • Objective-C 為我們維護了一個巨大的選擇子表
  • 在使用 @selector() 時會從這個選擇子表中根據選擇子的名字查找對應的 SEL。如果沒有找到,則會生成一個 SEL 并添加到表中。
  • 在編譯期間會掃描全部的頭文件和實現文件將其中的方法以及使用 @selector() 生成的選擇子加入到選擇子表中。

參考:

9. 消息發送和消息轉發

具體過程查看源碼中 lookUpImpOrForward() 函數部分的注釋

  1. 發送 hello 消息后,編譯器會將上面這行 [obj hello]; 代碼轉成 objc_msgSend()(注:objc_msgSend 是一個私有方法,而且是用匯編實現的,我們沒有辦法進入它的實現,但是我們可以通過 lookUpImpOrForward 函數斷點攔截)
  2. 到當前類的緩存中去查找方法實現,如果找到了直接 done
  3. 如果沒找到,就到當前類的方法列表中去查找,如果找到了直接 done
  4. 如果還沒找到,就到父類的緩存中去查找方法實現,如果找到了直接 done
  5. 如果沒找到,就到父類的方法列表中去查找,如果找到了直接 done
  6. 如果還沒找到,就進行方法決議
  7. 最后還沒找到的話,就走消息轉發

參考:

10. Method Swizzling

  • 什么是 Method Swizzling ?
  • Method Swizzling 有什么注意點?
  • Method Swizzling 的原理是什么?
  • Method Swizzling 為什么要在 +load 方法中進行?

11. Category

  • Category 是什么?
  • Category 中的方法和屬性以及協議是怎么存儲和加載的?
  • Category 和 Class 的關系

12. Associated Objects 的原理是什么?到底能不能在 Category 中給 Objective-C 類添加屬性和實例變量?

  • Associated Objects 的原理是什么?
  • Associated Objects 的內存管理機制?
  • 到底能不能在 Category 中給 Objective-C 類添加屬性和實例變量?

13. Objective-C 中的 Protocol 是什么?

14. selfsuper 的本質

self 和 super 兩個是不同的,self 是方法的一個隱藏參數(每個方法都有兩個隱藏的參數,self_cmd),每個方法的實現的第一個參數即為 self。而 super 不是一個隱藏參數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當調用 [super xxx]方法時,去調用父類的方法,而不是本類中的方法。

我們可以看看 message.h 中提供的發消息給父類的函數:

OBJC_EXPORT void 
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ );

當我們發送消息給 super 時,runtime 時就不是使用 objc_msgSend 方法了,而是 objc_msgSendSuper。函數的第一個參數也不再是 self 了,編譯器會生成一個 objc_super 結構體。下面是 message.h 中 objc_super 結構體的定義:


/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;
    
    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

objc_super 包含了兩個變量,receiver 是消息的實際接收者,super_class 是指向當前類的父類。

通過 clang -rewrite-objc NyanCat.m 命令將下面定義的 NyanCat 類轉成 cpp 代碼。

#import <Foundation/Foundation.h>

@interface NyanCat : NSObject
@end

@implementation NyanCat

- (instancetype)init {
    self = [super init];
    
    return self;
}

@end
struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};


#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat;
typedef struct {} _objc_exc_NyanCat;
#endif

struct NyanCat_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

/* @end */


// @implementation NyanCat


static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
    self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));

    return self;
}

// ...

由此可見,[super xxxx] 在運行時確實被轉成了 objc_msgSendSuper 函數。

那么 objc_msgSendSuper 這個函數的內部實現是怎么樣的呢?文檔 objc_msgSendSuper 函數的注釋中對 super 參數的注釋是這樣寫的:

super - A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.

我們可以推斷出,objc_msgSendSuper 函數實現實際上就是:objc_super 結構體指向的 objc_super->superClass 的方法列表開始查找調用方法的 selector 對應的實現,找到后以 objc_super->receiver 去調用這個 selector,最后就變成了調用 objc_msgSend 函數給 self 發消息的形式了。

objc_msgSend(objc_super->receiver, @selector(xxx));

這里的 objc_super->receiver 就相當于 self,上面的操作其實就是:

objc_msgSend(self, @selector(xxx));

[self init][super init] 的相同點在于消息接收者實際上都是 self(方法調用源頭),區別就在于查找方法的實現時,前者是從 currentClass(self 所屬的類)的方法列表中開始往上找,而后者是從 objc_super->spuerClass(也就是調用了 super 的地方的父類,這是在編譯時就確定了的)的方法列表中開始往上查找。

需要強調的地方是,[self xxx] 要調用的實現是在運行時動態決定的,而 [super xxx] 要調用的實現是編譯時就確定了的(這里有個例子可以測試一下)。從上面轉換出來的 cpp 代碼中也可以看出來,這其實是因為 objc_msgSendSuper 函數的第一個參數 objc_super 結構體中的 receiver 是通過接收方法中的 self 參數得來的,所以動態決定的,而 objc_super->superClass 是通過 class_getSuperclass(objc_getClass("NyanCat")) 得到的,所以是靜態的,在編譯時就確定了的。

static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
    self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));

    return self;
}

參考:

15. load 方法和 initialize 方法

  • +load 方法和 +initialize 方法分別在什么時候被調用?
  • 這兩個方法是用來干嘛的?
  • ProtocolKit 的實現中為什么要在 main 函數執行前進行 Protocol 方法默認實現的注冊?

延伸

  1. clang 命令的使用(比如 clang -rewrite-objc test.m),clang -rewrite-objc 的作用是什么?clang rewrite 出來的文件跟 objc runtime 源碼的實現有什么區別嗎?

參考:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容