深度剖析 Runtime

杭城小劉發表於2023-05-18

做很多需求或者是技術細節驗證的時候會用到 Runtime 技術,用了挺久的了,本文就寫一些場景和原始碼分析相關的文章。
先問幾個小問題:

  1. class_rw_t的結構是陣列,陣列裡面的元素是陣列,那它是二維陣列嗎?
  2. 為什麼16位元組對齊的?
  3. 有類物件、為什麼設計元類物件?
  4. Super 原理的什麼?

閱讀完本文,你會掌握 Runtime 的原理和細節

動態語言

Runtime 是實現 OC 語言動態的 API。

靜態語言:在編譯階段確定了變數資料型別、函式地址等,無法動態修改。

動態語言:只有在執行的時候才可以決定變數屬於什麼型別、方法真正的地址,

物件 objc_object 存了:isa、成員變數的值

類 objc_class: superclass、成員變數、例項變數

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

malloc_size((__bridge const void *)(p))    // 24 isa佔8位元組 + _name 指標佔8位元組 + hobby 指標佔8位元組 = 24 
class_getInstanceSize(p.class)             // 32 ,系統記憶體對齊 

為什麼系統是由16位元組對齊的?

成員變數佔用8位元組對齊,每個物件的第一個都是 isa 指標,必須要佔用8位元組。舉例一個極端 case,假設 n 個物件,其中 m 個物件沒有成員變數,只有 isa 指標佔用8位元組,其中的 n-m個物件既有 isa 指標,又有成員變數。每個類交錯排列,那麼 CPU 在訪問物件的時候會耗費大量時間去識別具體的物件。很多時候會取捨,這個 case 就是時間換空間。以16位元組對齊,會加快訪問速度(參考連結串列和陣列的設計)

class_rw_t、class_ro_t、class_rw_ext_t 區別?

class_ro_t 在編譯時期生成的,class_rw_t 是在執行時期生成的。

那麼什麼是 class_rw_ext_t?首先明確2個概念

  • clean memory:載入後不會被修改。當系統記憶體緊張時,可以從記憶體中移除,需要時可以再次載入
  • dirty memory:載入後會被修改,一直處於記憶體中

Runtime 初始化的時候,遇到一個類,則會利用類的 class_ro_t 中的基礎資訊(methods、properties、protocols)來建立 class_rw_t 物件。class_rw_t 設計的目的就是為了 Runtime 所需(Category 增加屬性、協議、動態增加方法等),但是實際上那麼多類大多數情況只有少部分類才需要 Runtime 能力。所以 Apple 為了記憶體最佳化,在 iOS 14 對 class_rw_t 拆分出 class_rw_ext_t,用來儲存 Methods、Protocols、Properties 資訊,會在使用的時候才建立,節省更多記憶體。

比如訪問 method 的過程

// 新版
const method_array_t methods() const {
    auto v = get_ro_or_rwe();
    if (v.is<class_rw_ext_t *>()) {
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
    } else {
        return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
    }
}

有類物件、為什麼設計元類物件

複用訊息機制。比如 [Person new]

元類物件: isa、元類方法、

objc_msgSend 設計初衷就是為了訊息傳送很快。假如沒有元類,則類方法也儲存在類物件的方法資訊中,則可能需要加額外的欄位來標記某個方法是類方法還是物件方法。遍歷或者尋找會比較慢。所以引入元類(單一職責),設計元類的目的就是為了提高 objc_msgSend 的效率。

isa 本質

在 arm64 架構之前,isa 就是一個普通的指標,儲存著 Class或Meta-Class 物件的記憶體地址。

在 arm64 之後,對 isa 進行了最佳化,變成了一個共用體(union)結構,還使用位域來儲存更多的資訊。

union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        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)
    };
};

struct 內部的成員變數可以指定佔用記憶體位數, uintptr_t nonpointer : 1 代表佔用1個位元組

其中,結構體裡面的屬於”位域“

  • nonpointer:0,代表普通的指標,儲存著Class、Meta-Class物件的記憶體地址;1,代表最佳化過,使用位域儲存更多的資訊
  • has_assoc:是否有設定過關聯物件,如果沒有,釋放時會更快
  • has_cxx_dtor:是否有C++的解構函式(.cxx_destruct),如果沒有,釋放時會更快
  • shiftcls:儲存著Class、Meta-Class物件的記憶體地址資訊
  • magic:用於在除錯時分辨物件是否未完成初始化
  • weakly_referenced:是否有被弱引用指向過,如果沒有,釋放時會更快
  • deallocating:物件是否正在釋放
  • extra_rc:裡面儲存的值是引用計數器減1(剛建立出的物件,檢視這個資訊位0,因為儲存著-1之後的引用計數)
  • has_sidetable_rc:引用計數器是否過大無法儲存在isa中;如果為1,那麼引用計數會儲存在一個叫SideTable的類的屬性中

上面說的更快,是如何得出結論的?

檢視 objc4 原始碼看到物件執行銷燬函式的時候會判斷物件是否有關聯物件、解構函式,有的話分別呼叫解構函式、移除關聯物件等邏輯。

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

isa 在 arm64 之後必須透過 ISA_MASK 去查詢 class(類物件、元類物件) 真正的地址

0x0000000ffffffff8ULL 用程式設計師模式開啟計算器

其中,結構體中的資料存放大體是下面的結構:

extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer

知道結構體可以指定儲存大小這個功能後,可以看到 isa_t 聯合體與 ISA_MASK 按位與之後的地址,其實就是類真實的地址資訊(可能是類物件、也有可能是元類物件)

如果要找出下面中間的 1010 如何實現?按位與即可,且要找的位置補充位1,其他位置為0

0b0010 1000

0b0011 1100
-----------
0b0010 1000

結論:根據按位與的效果。ISA_MASK 的後3位都是0,所以我們找到的類地址二進位制表示時後3位一定為0

我們可以驗證下

Person *p = [[Person alloc] init];
NSLog(@"%p", [p class]);    // 0x1000081d8
NSLog(@"%p", object_getClass([Person class])); // 0x100008200
NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0
NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28

為什麼有的結尾是8?

16進位制的8轉為二進位制,0x1000

關於這部分的除錯,需要在真機上執行,真機上 arm64,複製物件地址到系統自帶的運算器(程式設計師模式),檢視64位地址。按照下面的順序一一檢視

extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer

所以可以根據 isa 資訊檢視物件是否建立過關聯物件、有沒有設定弱引用、

模仿系統位運算設計 API

系統很多 API 都有位或運算。比如 KVO 中的 options,可以傳遞 NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld ,那麼系統是如何知道我到底傳遞了哪幾個值?

按位或運算

0b0000 0001 // 1
0b0000 0010 // 2 
0b0000 0100 // 4
------------
0b0000 0111 // 7

可以看到上面3個數,按位或之後的結果為 0b0000 0111

按位與運算。

0b0000 0111
0b0000 0001
-----------
0b0000 0001

0b0000 0111
0b0000 0010
-----------
0b0000 0010

0b0000 0111
0b0000 0100
-----------
0b0000 0100

0b0000 0111
0b0000 1000
-----------
0b0000 0000

我們發現上面3個數按位或之後的數字,分別與每個數按位與,得到的結果就是資料本身。

與一個不是3個數之一的數按位與,得到的結果為0b0000 0000。利用這個特性我們可以判斷傳遞來的引數是不是包含了某個值

typedef enum {
    OptionsEast = 1<<0,    // 0b0001
    OptionsSouth = 1<<1,   // 0b0010
    OptionsWest = 1<<2,    // 0b0100
    OptionsNorth = 1<<3    // 0b1000
} Options;

- (void)setOptions:(Options)options
{
    if (options & OptionsEast) {
        NSLog(@"我自東邊來");
    }

    if (options & OptionsSouth) {
        NSLog(@"我自南邊來");
    }

    if (options & OptionsWest) {
        NSLog(@"我自西邊來");
    }

    if (options & OptionsNorth) {
        NSLog(@"我自北邊來");
    }
}
[self setOptions: OptionsWest | OptionsNorth];
// 我自西邊來
// 我自北邊來

類物件 Class 的結構

檢視 objc4 原始碼看看

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
};

結構體繼承於 objc_object 等同於下面程式碼

struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    cache_t cache;             // 方法快取
    class_data_bits_t bits;    // 用於獲取具體的類資訊
};
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;

    char *demangledName;
};

struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
public:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}

可以看到 objc_class 獲取 bits 裡的真實資料需要經過按位與 FAST_DATA_MASK

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;

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

具體關係整理如下圖

說明:

  • class_rw_t裡面的 methods、properties、protocols 是陣列(陣列元素是也是方法組成的 Array),是可讀可寫的,包含了類的初始內容、分類的內容。

    為什麼不是二維陣列?因為Array 中的子 Array長度不一致,且不能補空

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
    
        runtimeLock.assertWriting();
    
        isMeta = cls->isMetaClass();
    
        // Re-methodizing: check for more categories
        if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
            if (PrintConnecting) {
                _objc_inform("CLASS: attaching categories to class '%s' %s", 
                             cls->nameForLogging(), isMeta ? "(meta)" : "");
            }
    
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first
        int mcount = 0;
        int propcount = 0;
        int protocount = 0;
        int i = cats->count;
        bool fromBundle = NO;
        while (i--) {
            auto& entry = cats->list[i];
    
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
    
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
    
        auto rw = cls->data();
    
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists, mcount);
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
        rw->properties.attachLists(proplists, propcount);
        free(proplists);
    
        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }

    檢視 objc4 原始碼發現針對類自身資訊、Category 資訊會進行組合。

  • class_ro_t 裡面的 baseMethodList、baseProtocols、ivars、baseProperties 是一維陣列,是隻讀的,包含了類的(原始資訊)初始內容

Method_t

method_t 是對方法\函式的封裝

struct method_t {
    SEL name; // 函式名、方法名    
    const char *types;    // 編碼(返回值型別、引數型別)
    IMP imp;    // 指向函式的指標(函式地址)
}

IMP 代表函式的具體實現

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

SEL 代表方法、函式名,一般叫做選擇器,底層結構跟 char * 類似

typedef struct objc_selector *SEL;
  • 可以透過 @selector()sel_registerName() 獲得
  • 可以透過 sel_getName()NSStringFromSelector() 轉成字串
  • 不同類中相同名字的方法,所對應的方法選擇器是相同的

types 包含了函式返回值、引數編碼的字串。返回值|引數1|引數2| ... | 引數n

Type Encoding

iOS 中提供了一個叫做 @encode 的指令,可以將具體的型別表示成字串編碼

- (int)calcuate:(int)age heigith:(float)height;

比如這個方法的 type encoding 為 i24@0:8i16f20

解讀下,上面的方法其實攜帶了2個基礎引數。

(id)self _cmd:(SEL)_cmd

i 代表方法返回值為 int

24 代表引數共佔24個位元組大小。4個引數分別為 id 型別的 selfSEL 型別的 _cmd, int 型別的 age、float 型別的 height。8+8+4+4 共24個位元組(id、SEL 都為指標,長度為8)

@ 代表第一個引數為 object 型別,從第0個位元組開始

:代表第二個引數為 SEL,從第8個位元組開始

i 代表第三個引數為 int,從第16個位元組開始

f 代表第四個引數為 float,從第20個位元組開始

方法快取

呼叫方法的本質,比如說物件方法,先根據物件的 isa 找到類物件,在類物件的 method_list_t 型別的 methods 方法陣列(Array 中的元素是方法 Array)中(類的Category1、類的 Category2... 類自身的方法)查詢方法,找不到則呼叫 superclass 查詢父類的 methods 方法陣列(Array 中的元素是方法 Array),效率較低,所以為了方便,給類設定了方法快取。比如呼叫 Student 物件的 eat 方法,eat 在 student 中不存在,透過 isa 不斷找,在 Person 類中找到了,則將 Person 類中的 eat 方法快取在 Student 的 cache_t 型別的 cache 中。

Class 內部結構中有個方法快取(cache_t),用雜湊表(雜湊表)來快取曾經呼叫過的方法,可以提高方法的查詢速度

所以完整結構為:先根據物件的 isa 找到類物件,在類物件的 cache 列表中查詢方法實現,如果找不到,則去 method_list_t 型別的 methods 方法陣列(Array 中的元素是方法 Array)中(類的Category1、類的 Category2... 類自身的方法)查詢方法,找不到則呼叫 superclass 查詢父類的 cache 中查詢,找到則呼叫方法,同時將父類 cache 快取中的方法,在子類的 cache 中快取一邊。父類 cache 沒找到,則在 methods 方法陣列(Array 中的元素是方法 Array)查詢,找到則呼叫,同時在子類 cache 中快取一份。父類 methods 方法陣列(Array 中的元素是方法 Array)沒找到則繼續呼叫 superclass,依次類推

struct cache_t {
    struct bucket_t *_buckets; // 雜湊表
    mask_t _mask;              // 雜湊表的引數 -1
    mask_t _occupied;          // 已經快取的方法數量
}
struct bucket_t {
private:
    cache_key_t _key;    // SEL 作為 key
    IMP _imp;            // 函式的記憶體地址
}

_buckets -> | bucket_t |bucket_t |bucket_t |bucket_t |...

方法快取查詢原理,雜湊表查詢

objc4 原始碼 objc-cache.mm

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

雜湊表不夠了,則會雜湊拓容,此時快取會釋放 cache_collect_free

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this
    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    setBucketsAndMask(newBuckets, newCapacity - 1);
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

雜湊查詢元素核心是一個求 key 的過程,Java 中是求餘,iOS 中是按位與 key & mask

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

空間換時間的一個實現。

查詢類的方法快取 Demo

#import <Foundation/Foundation.h>

#ifndef MockClassInfo_h
#define MockClassInfo_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 eint main () {
    GoodStudent *goodStudent = [[GoodStudent alloc] init];
    mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class];
    [goodStudent goodStudentTest];
    [goodStudent studentTest];
    [goodStudent personTest];
    return 0;
}ntsize_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 mock_objc_object {
    void *isa;
};

/* 類物件 */
struct mock_objc_class : mock_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }

    mock_objc_class* metaClass() {
        return (mock_objc_class *)((long long)isa & ISA_MASK);
    }
};

#endif /* MockClassInfo_h */

@interface Person : NSObject
- (void)personSay;
@end

@interface Student : Person
- (void)studentSay;
@end

@interface GoodStudent : Student
- (void)goodStudentSay;
@end

int main () {
    GoodStudent *goodStudent = [[GoodStudent alloc] init];
    mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class];
    // breakpoints1
    [goodStudent goodStudentSay];
    // breakpoints2
    [goodStudent studentSay];
    // breakpoints3
    [goodStudent personSay];
    // breakpoints4    
    [goodStudent goodStudentSay];
    // breakpoints5
    [goodStudent studentSay];
    // breakpoints6
    NSLog(@"well donw");
    return 0;
}

流程:

斷點1的地方可以看到 mock_objc_class 結構體 cache_occupied 為1,_mask 為3,初始化雜湊表長度為4

在斷點1的地方,_occupied 為1則代表只有 init 方法被快取,本行程式碼執行完,_occupied 為2.

在斷點2的地方,_occupied 為2則代表只有 init、goodStudentSay 方法被快取。本行程式碼執行完,_occupied 為3

在斷點3的地方,_occupied 為3則代表只有 init 、goodStudentSay 、studentSay方法被快取。本行程式碼執行完,_occupied 為1,且 _mask 為7。

奇了怪了,為什麼 _occupied為1,且_mask 為7?

因為雜湊表長度為4,快取3個方法後,到第4個方法需要快取的時候會執行雜湊表拓容,快取會失效。拓容策略為乘以2 即 uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; 所以長度為8,mask 為長度-1 ,則為7,第4個方法剛好被快取下來,_occupied 為1。

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }
    reallocate(oldCapacity, newCapacity);
}

繼續執行

在斷點4的地方,_occupied 為1則代表只有 personSay方法被快取。本行程式碼執行完,_occupied 為2,且 _mask 為7。

在斷點5的地方,_occupied 為2則代表只有 personSay、goodStudentSay 方法被快取。本行程式碼執行完,_occupied 為3,且 _mask 為7。

在斷點6的地方,_occupied 為3則代表只有 personSay、goodStudentSay、studentSay 方法被快取, _mask 為7。

如何根據方法雜湊表查詢某個方法

GoodStudent *student = [[GoodStudent alloc] init];
mock_objc_class *studentClass = (__bridge mock_objc_class *)[GoodStudent class];
[student goodStudentSay];
[student studentSay];
[student personSay];
NSLog(@"Well done");

cache_t cache = studentClass->cache;
bucket_t *buckets = cache._buckets;

bucket_t bucket = buckets[(long long)@selector(personSay) & cache._mask];
NSLog(@"%s %p", bucket._key, bucket._imp);
// personSay 0xbec8

原理就是根據類物件結構體找到 cache 結構體,cache 結構體內部的 _buckets 是一個方法雜湊表,檢視原始碼,根據雜湊表的雜湊尋找策略 (key & mask) 找到雜湊索引,然後找到方法物件 bucket,其中尋找方法索引的 key 就是 方法 selector。

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

objc_msgSend

oc 方法(物件方法、類方法)呼叫本質就是 objc_msgSend

[person eat];
objc_msgSend(person, sel_registerName("eat")); 
[Person initialize];
objc_msgSend([Person class], sel_registerName("initialize")); 

objc_msgSend 可以分為3個階段:

  • 訊息傳送
  • 動態方法解析
  • 訊息轉發

檢視原始碼 objc-msg-arm64.s

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START
    // x0 暫存器代表訊息接受者,receiver。objc_msgSend(person, sel_registerName("eat")) 的 person
    cmp    x0, #0            // nil check and tagged pointer check
    // b 代表指令跳轉。le 代表 小於等於。<=0則跳轉到 LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr    x13, [x0]        // x13 = isa // ldr 代表載入指令。這裡的意思是將 x0 暫存器資訊寫入到 x13中
    and    x16, x13, #ISA_MASK    // x16 = class    // 這裡就是將 x13 與  ISA_MASK 按位與,然後得到真實的 isa 資訊,然後寫入到 x16 中
LGetIsaDone:
    CacheLookup NORMAL        // calls imp or objc_msgSend_uncached // 這裡執行 objc_msgSend_uncached 邏輯,CacheLookup 是一個彙編宏,看下面的說明

LNilOrTagged:
    // 判斷為 nil 則跳轉到  LReturnZero
    b.eq    LReturnZero        // nil check

    // tagged
    mov    x10, #0xf000000000000000
    cmp    x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add    x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr    x16, [x10, x11, LSL #3]
    b    LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add    x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr    x16, [x10, x11, LSL #3]
    b    LGetIsaDone

LReturnZero:
    // x0 is already zero
    mov    x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    // 彙編中 ret 代表 return
    ret 

    END_ENTRY _objc_msgSend


.macro CacheLookup // 彙編宏,可以看到根據 (SEL & mask) 來尋找真正的方法地址
    // x1 = SEL, x16 = isa
    ldp    x10, x11, [x16, #CACHE]    // x10 = buckets, x11 = occupied|mask
    and    w12, w1, w11        // x12 = _cmd & mask
    add    x12, x10, x12, LSL #4    // x12 = buckets + ((_cmd & mask)<<4)

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #-16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // wrap: x12 = first bucket, w11 = mask
    add    x12, x12, w11, UXTW #4    // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp    x9, x17, [x12]        // {x9, x17} = *bucket
1:    cmp    x9, x1            // if (bucket->sel != _cmd)
    b.ne    2f            //     scan more
    CacheHit $0            // call or return imp

2:    // not hit: x12 = not-hit bucket
    // 這裡是方法查詢失敗,則走 checkMiss 邏輯,具體看下面
    CheckMiss $0            // miss if bucket->sel == 0
    cmp    x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp    x9, x17, [x12, #-16]!    // {x9, x17} = *--bucket
    b    1b            // loop

3:    // double wrap
    JumpMiss $0

.endmacro

// CheckMiss 彙編宏,上面走 Normal 邏輯,內部走 __objc_msgSend_uncached 流程
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz    x9, LGetImpMiss
.elseif $0 == NORMAL
    cbz    x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz    x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro


// __objc_msgSend_uncached 內部其實走  MethodTableLookup 邏輯
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search

MethodTableLookup
br    x17

END_ENTRY __objc_msgSend_uncached

// MethodTableLookup 是一個彙編宏,內部指令跳轉到 __class_lookupMethodAndLoadCache3。
.macro MethodTableLookup

    // push frame
    stp    fp, lr, [sp, #-16]!
    mov    fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub    sp, sp, #(10*8 + 8*16)
    stp    q0, q1, [sp, #(0*16)]
    stp    q2, q3, [sp, #(2*16)]
    stp    q4, q5, [sp, #(4*16)]
    stp    q6, q7, [sp, #(6*16)]
    stp    x0, x1, [sp, #(8*16+0*8)]
    stp    x2, x3, [sp, #(8*16+2*8)]
    stp    x4, x5, [sp, #(8*16+4*8)]
    stp    x6, x7, [sp, #(8*16+6*8)]
    str    x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov    x2, x16
    bl    __class_lookupMethodAndLoadCache3

    // imp in x0
    mov    x17, x0

    // restore registers and return
    ldp    q0, q1, [sp, #(0*16)]
    ldp    q2, q3, [sp, #(2*16)]
    ldp    q4, q5, [sp, #(4*16)]
    ldp    q6, q7, [sp, #(6*16)]
    ldp    x0, x1, [sp, #(8*16+0*8)]
    ldp    x2, x3, [sp, #(8*16+2*8)]
    ldp    x4, x5, [sp, #(8*16+4*8)]
    ldp    x6, x7, [sp, #(8*16+6*8)]
    ldr    x8,     [sp, #(8*16+8*8)]

    mov    sp, fp
    ldp    fp, lr, [sp], #16

.endmacro

Tips:c 方法在彙編中使用的時候,需要在方法名前加 _ 。所以在彙編中某個方法為 _xxx,則在其他地方查詢實現,需要去掉 _
此時 __class_lookupMethodAndLoadCache3 在彙編中沒有實現,則按照 _class_lookupMethodAndLoadCache3 查詢

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }


 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }

            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }

            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

訊息傳送階段

上面的程式碼走到 getMethodNoSuper_nolock 尋找類裡的方法

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    // 這裡根據類結構體找到 data(),然後找到 methods (Array 陣列,陣列元素是方法 Array)
    /*
    data() 其實就是 class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    */
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 排好序則呼叫 `findMethodInSortedMethodList`,其內部是二分查詢實現。
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 沒排序則線性查詢 
        // Linear search of unsorted method list
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;

    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);

        uintptr_t probeValue = (uintptr_t)probe->name;

        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }

        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }

    return nil;
}

cls->data()->methods.beginLists 這裡根據類結構體呼叫到 data() 方法,獲取到 class_rw_t

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

然後透過 class_rw_t 找到 methods (Array 陣列,陣列元素是方法 Array)。內部呼叫 search_method_list 方法。

search_method_list 方法內部判斷方法陣列是否排好序

  • 排好序則呼叫 findMethodInSortedMethodList,其內部是二分查詢實現。
  • 沒排序,則線性查詢 (Linear search of unsorted method list)

getMethodNoSuper_nolock 執行完則會將方法寫入到當前類物件的快取中。

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

摘出 lookUpImpOrForward 方法中的一段程式碼

// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
    Method meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
}
// Try superclass caches and method lists.

如果程式碼沒有找到,則不會 gotodone,開始走父類快取查詢邏輯

// Try superclass caches and method lists.
{
    unsigned attempts = unreasonableClassCount();
    // for 迴圈不斷查詢,找當前類的父類,直到當前類為 nil。
    for (Class curClass = cls->superclass;
            curClass != nil;
            curClass = curClass->superclass)
    {
        // Halt if there is a cycle in the superclass chain.
        if (--attempts == 0) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 先在父類的方法快取中查詢(根據 sel & mask)`cache_getImp` ,找到則將方法寫入到自身類的方法快取中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);`
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.
        // 如果在父類的方法快取中沒找到,則呼叫 `getMethodNoSuper_nolock` 父類的 方法陣列(Array 元素為方法陣列),按照排序好和沒排序好分別走二分查詢和線性查詢。
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
           // 如果找到則繼續填充到當前類的方法快取中去
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
} 

for 迴圈不斷查詢,找當前類的父類,直到當前類為 nil。

先在父類的方法快取中查詢(根據 sel & mask)cache_getImp ,找到則將方法寫入到自身類的方法快取中去 log_and_fill_cache(cls, imp, sel, inst, curClass);

比如 Person 類有 eat 方法,Student 類有 stduy 方法,呼叫 Student 物件的 eat 方法,則會走到這裡,從父類找到方法後寫入到 Student 類的方法快取中去。

如果在父類的方法快取中沒找到,則呼叫 getMethodNoSuper_nolock 父類的 方法陣列(Array 元素為方法陣列),按照排序好和沒排序好分別走二分查詢和線性查詢。

如果找到則繼續填充到當前類的方法快取中去 log_and_fill_cache(cls, meth->imp, sel, inst, curClass);,最後 goto done

上面的流程是整個 objc_msgSend 的訊息傳送階段的整個流程。可以用下圖表示

動態方法解析階段

接著檢視原始碼

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //...
    // No implementation found. Try method resolver once.
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
    // ...
}

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

判斷當前類沒有走過動態方法解析階段,則走動態方法解析階段,呼叫 _class_resolveMethod 方法。

內部會判斷但前類是不是元類物件、還是類物件走不同邏輯。

類物件走 _class_resolveInstanceMethod 邏輯

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

核心就呼叫 bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); 執行 resolveInstanceMethod 方法。

元類物件走 _class_resolveClassMethod 邏輯

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

其實就是呼叫 `bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);`

最後還是走到了 goto retry; 繼續走完整的訊息傳送流程(因為新增了方法,所以會按照方法查詢再去執行的邏輯)

完整流程如下

上 Demo

Person *person = [[Person alloc] init];
[person eat];

呼叫不存在方法則報錯 ***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person eat]: unrecognized selector sent to instance 0x101b2d900'**

因為呼叫物件不存在的方法,所以會 Crash

知道 objc_msgSend 的流程,我們嘗試給它修正下

- (void)customEat {
    NSLog(@"我的假的 eat 方法,為了解決奔潰問題");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(eat)) {
        // 物件方法,存在於物件上。
        Method method = class_getInstanceMethod(self, @selector(customEat));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

也可以新增 c 語音方法

void customEat (id self, SEL _cmd) {
    NSLog(@"%@-%s-%s", self, sel_getName(_cmd), __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        // 物件方法,存在於物件上。
        class_addMethod(self, sel, (IMP)customEat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

因為 c 語言方法名就是函式地址,所以不需要直接傳遞即可,需要做下型別轉換 (IMP)customEat

也可以給類方法做動態方法解析。需要注意的是類方法。

  • 呼叫 -(BOOL)resolveClassMethod:(SEL)sel
  • class_addMethod 方法中的第一個引數,需要加到類的元類物件中,所以是 object_getClass
Person *person = [[Person alloc] init];
[Person drink];
void customDrink (id self, SEL _cmd) {
    NSLog(@"假喝水");
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(drink)) {
        // 類方法,存在於元類物件上。
        class_addMethod(object_getClass(self), sel, (IMP)customDrink, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

訊息轉發階段

能走到訊息轉發,說明

  1. 類自身沒有該方法(objc_msgSend 的訊息傳送)
  2. objc_msgSend 動態方法解析失敗或者沒有做

說明類自身和父類沒有可以處理該訊息的能力,此時應該將該訊息轉發給其他物件。

檢視 objc4 的原始碼

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //...
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
    // ...
}

繼續查詢 _objc_msgForward_impcache

STATIC_ENTRY __objc_msgForward_impcache

MESSENGER_START
nop
MESSENGER_END_SLOW

// No stret specialization.
b    __objc_msgForward
END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr    x17, [x17, __objc_forward_handler@PAGEOFF]
br    x1

END_ENTRY __objc_msgForward

查詢 __objc_forward_handler 沒有找到,可以猜想是一個 c 方法,去掉最前面的 _,按照 _objc_forward_handler 查詢得到

__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

訊息轉發的程式碼是不開源的,查詢資料找到一份靠譜的 __forwarding 方法實現

為什麼是 __forwarding__ 方法。我們可以根據 Xcode 崩潰窺探一二

int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 呼叫 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 呼叫 methodSignatureForSelector 獲取方法簽名後再呼叫 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
            NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}

具體地址可以參考 __frowarding

完整流程如下

上 Demo

Person 類不存在 drink 方法,Bird 類存在

@implementation Bird
- (void)drink
{
    NSLog(@"一隻鳥兒在喝水");
}
@end

Person *person = [[Person alloc] init];
[person drink];

方法1

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return [[Bird alloc] init];
    } 
    return [super forwardingTargetForSelector:aSelector];
}
@end

方法2

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return nil;
    } 
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation invokeWithTarget:[[Bird alloc] init]];
}

注意:methodSignatureForSelector 如果返回 nil,則 forwardInvocation 不會執行

給 Person 類方法進行訊息轉發處理

方法1

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return [Bird class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

方法2

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return [[Bird class] methodSignatureForSelector:@selector(drink)];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation invokeWithTarget:[Bird class]];
}

方法簽名的獲取

方法1: 自己根據方法的返回值型別,方法2個基礎引數引數:id selfSEL _cdm,其他引數型別按照 Encoding 自己拼。 類似 v16@0:8

方法2 :根據某個類的物件,去呼叫 methodSignatureForSelector 方法獲取。

[[[Bird alloc] init] methodSignatureForSelector:**@selector**(drink)];

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink)) {
        return [[[Bird alloc] init] methodSignatureForSelector:@selector(drink)];
    }
    return [super methodSignatureForSelector:aSelector];
}

Super 原理

@implementation Person
@end

@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"%@", [self class]);        // Student
        NSLog(@"%@", [self superclass]);   // Person 
        NSLog(@"%@", [super class]);       // Student
        NSLog(@"%@", [super superclass]);  // Person 
    }
    return self;
}
@end

後面2個的列印似乎不符合預期?轉成 c++ 程式碼看看

static instancetype _I_Student_init(Student * self, SEL _cmd) {
    if (self = ((Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_3, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("superclass")));
    }
    return self;
}

[super class] 這句程式碼底層實現為 objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));

__rw_objc_super 是什麼?

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

objc_msgSendSuper 如下

/** 
 * Sends a message with a simple return value to the superclass of an instance of a class.
 * 
 * @param super A pointer to an \c 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.
 * @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
 * @param ...
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method identified by \e op.
 * 
 * @see objc_msgSend
 */
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

所以 objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class")); 等同於下面程式碼

struct objc_super arg = {self, class_getSuperclass(self)};
objc_msgSendSuper(arg, sel_registerName("class"))

[super class] super 呼叫的 receiver 還是 self

結構體的目的是為了在類物件查詢的過程中,直接從當前類的父類中查詢,而不是本類(比如 Student 類的 [super init] 會直接從 Person 的類物件中查詢 init,找不到則透過 superclass 向上查詢)

大致推測系統的 class、superclass 方法實現如下

@implementation Person
- (Class)class{
    return object_getClass(self);   
}
- (Class)superclass {
    return class_getSuperclass(object_getClass(self));
}
@end

class 方法是在 NSObject 類物件的方法列表中的。所以

[self class] 等價於 objc_msgSend(self, sel_registerName("class"))

[super class] 等價於 objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("class"))

其實2個方法本質上訊息 receiver 都是 self,也就是當前的 Student,所以列印都是 Student

結論:[super message] 有2個特徵

  • super 訊息的呼叫者還是 self
  • 方法查詢是根據當前 self 的父類開始查詢

透過將程式碼轉為 c++ 發現,super 呼叫本質就是 objc_msgSendSuper,實際不然

我們對 iOS 專案[super viewDidLoad] 下符號斷點,發現objc_msgSendSuper2

檢視 objc4 原始碼發現是一段彙編實現。

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START

ldp    x0, x16, [x0]        // x0 = real receiver, x16 = class
ldr    x16, [x16, #SUPERCLASS]    // x16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2

所以 super viewDidLoad本質上就是

struct objc_super arg = {
    self, 
    [UIViewController class]    
};
objc_msgSendSuper2(arg, sel_registerName("viewDidLoad"));

objc_msgSendSuper2 和 objc_msgSendSuper 區別在於第二個引數

objc_msgSendSuper2 底層原始碼(彙編程式碼 objc-msg-arm64.s 422 行)會將第二個引數找到父類,然後進行方法快取查詢

objc_msgSendSuper 直接從第二個引數查詢方法。

總結:clang 轉 c++ 可以窺探系統實現,可以作為研究參考。super 本質上就是 objc_msgSendSuper2,傳遞2個引數,第一個引數為結構體,第二個引數是sel。

為什麼轉為 c++ 和真正實現不一樣?思考下

原始碼變為機器碼之前,會經過 LLVM 編譯器轉換為中間程式碼(Intermediate Representation),最後轉為彙編、機器碼

我們來驗證下 super 在中間碼上是什麼

clang -emit-llvm -S Student.m

llvm 中間碼如下,可以看到確實內部是 objc_msgSendSuper2

; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[Student sayHi]"(%0* %0, i8* %1) #1 {
  %3 = alloca %0*, align 8
  %4 = alloca i8*, align 8
  %5 = alloca %struct._objc_super, align 8
  store %0* %0, %0** %3, align 8
  store i8* %1, i8** %4, align 8
  %6 = load %0*, %0** %3, align 8
  %7 = bitcast %0* %6 to i8*
  %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0
  store i8* %7, i8** %8, align 8
  %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
  %10 = bitcast %struct._class_t* %9 to i8*
  %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1
  store i8* %10, i8** %11, align 8
  %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.6, align 8, !invariant.load !12
  call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12)
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.8 to i8*))
  ret void
}

指令介紹

@ - 全域性變數
% - 區域性變數
alloca - 在當前執行的函式的堆疊幀中分配記憶體,當該函式返回到其呼叫者時,將自動釋放記憶體
i32 - 32位4位元組的整數
align - 對齊
load - 讀出,store 寫入
icmp - 兩個整數值比較,返回布林值
br - 選擇分支,根據條件來轉向label,不根據條件跳轉的話類似 goto
label - 程式碼標籤
call - 呼叫函式

isKindOfClass、isMemberOfClass

Demo

Student *student = [[Student alloc] init];
NSLog(@"%hhd", [student isMemberOfClass:[Student class]]); // 1
NSLog(@"%hhd", [student isKindOfClass:[Person class]]);    // 1
NSLog(@"%hhd", [Student isMemberOfClass:[Student class]]); // 0
NSLog(@"%hhd", [Student isKindOfClass:[Student class]]);    // 0

有些人答對了,有些人錯了。

上面2個判斷都是呼叫物件方法的 isMemberOfClassisKindOfClass

由於 objc4 是開源的,檢視 object.mm

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

isMemberOfClass 判斷當前物件是不是傳遞進來的物件

isKindOfClass 內部是一個 for 迴圈,第一次迴圈先拿當前類的類物件,判斷是不是和傳遞進來的物件一樣,一樣則 return YES,否則先給 tlcs 賦值當前類的父類,然後走第二次判斷,直到 cls 不存在位置(NSObject 的父類為 nil)。所以 isKindOfClass 其實判斷的是當前類是傳遞進來的類,或者傳遞進來類的子類

下面面2個判斷都是呼叫類方法的 isMemberOfClassisKindOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

可以看到 +(BOOL)isMemberOfClass:(Class)cls 方法內部就是對當前類獲取類物件,然後與傳遞進來的 cls 判斷是否相等。由於是 [Student isMemberOfClass:[Student class]]) Student 類呼叫類方法 +isMemberOfClass 所以類物件的類物件也就是元類物件,cls 引數也就是 [Student class] 是一個類物件,元類物件等於類物件嗎?顯然不是

想讓判斷成立,可以改為 [Student isMemberOfClass:object_getClass([Student class])] 或者 [[Student **class**] isMemberOfClass:object_getClass([Student class])]

+(BOOL)isKindOfClass:(Class)cls 同理分析。作用是當前類的元類,是否是右邊傳入物件的元類或者元類的子類。

來個特殊 case

NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); // NO

輸出 1。為什麼?

看坐右邊的部分,呼叫 isKindOfClass 方法,本質上就是 Student 類的類物件,也就是 Student 元類,和傳入的右邊 [NSObject class]判斷是否想透過

第一次 for 迴圈當然不同,所以不能 return,會將 tcls 走步長改變邏輯 tcls = tcls->superclass,也就是找到當前 Student 元類物件的父類。

第二次 for 迴圈也一樣不相等,Person 元類不等於 [NSObject class] 繼續向上,直到 tcls = NSObject。此時還是不等,這時候 tcls  走步長改變邏輯,tcls = tcls->superclass NSObject 元類的 superclass 還是 NSObject。所以 for 迴圈內部的判斷編委 [NSObject class] == [NSObject class],return YES。

tips:基類的元類物件指向基類的類物件。

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

Quiz

NSLog(@"%hhd", [NSObject isKindOfClass:[NSObject class]]);  // 1
NSLog(@"%hhd", [NSObject isMemberOfClass:[NSObject class]]);    //0
NSLog(@"%hhd", [Person isKindOfClass:[Person class]]);  // 0
NSLog(@"%hhd", [Person isMemberOfClass:[Person class]]);    //0

Runtime 刁鑽題

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHi;
@end
@implementation Person
- (void)sayHi{
    NSLog(@"hi,my name is %@", self->_name); // hi,my name is 杭城小劉
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *temp = @"杭城小劉";
        id obj = [Person class];
        void *p = &obj;
        [(__bridge id)p sayHi];

        test();
    }
    return 0;
}

程式執行什麼結果?

hi,my name is 杭城小劉

為什麼會方法呼叫成功?為什麼 name 列印出為 @"杭城小劉"

我們來分析下:

1.方法呼叫本質就是尋找 isa 進行訊息傳送

Person *person = [[Person alloc] init];
[person sayHi];

[[Person alloc] init]在記憶體中分配一塊記憶體,然後 isa 指向這塊記憶體,然後 person 指標,指向結構體,結構體的第一個成員。

2.棧空間資料記憶體向下生長。第一個變數地址高,其次降低。且每個變數的記憶體地址是連續的。

這個流程其實和上面的程式碼一樣的。所以可以正常呼叫

void test () {
    long long a = 4;        // 0x7ff7bfeff2d8
    long long b = 5;        // 0x7ff7bfeff2d0
    long long c = 6;        // 0x7ff7bfeff2c8
    NSLog(@"%p %p %p", &a, &b, &c);
}

方法內的變數儲存在棧上,堆向上增長,棧向下增長。

3.例項物件的本質就是一個結構體,儲存所有成員變數(isa 是一個特殊成員變數,其他的成員變數,這裡就是 _name),sayHi 方法內部的 self 就是 obj,找成員變數的本質就是找記憶體地址的過程(此時就是偏移8個位元組)

上面程式碼可以類比類呼叫方法的流程。 obj 指標指向 Person 這塊記憶體,給類物件傳送 sayHi 訊息也就是透過 obj 指標找到 isa,恰好 obj 指標指向的地址就是類物件的類結構體的地址,結構體成員變數第一個就是 isa 指標,結構體的其他成員變數就是類的其他屬性,這裡也就是 _name,所以我們給自定義的指標 void *p 呼叫 sayHi 方法,系統 runtime 在列印 name 的時候,會在 p 附近(下8個位元組,因為 isa 是指標,長度為8)找 _name 屬性,此時也就找到了 temp 字串。

struct Person_IMPL {
    Class isa; // 8位元組
    NSString *_name;    // 8位元組
}

再看一個變體1

NSObject *temp = [[NSObject alloc] init];
id obj = [Person class];
void *p = &obj;
[(__bridge id)p sayHi];
// hi,my name is <NSObject: 0x101129d60>

再看一個變體2(將程式碼放在 ViewController中)

- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [Person class];
    void *p = &class;
    NSObject *temp = [[NSObject alloc] init];
    [(__bridge id)p sayHi];
}
// hi,my name is <ViewController: 0x7fe246204fd0>

搞懂的小夥伴不迷惑了。沒搞懂其實就是沒搞懂棧地址由高到低,向下生長super 呼叫的本質。

再強調一句,根據指標尋找成員變數 _name 的過程其實就是根據記憶體偏移找物件的過程。在變體2中,isa 地址就是 class 的地址,所以按照地址 +8 的策略,其實前一個區域性變數。

[super viewDidLoad]; 本質就是 objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("viewDidLoad"))

struct objc_super arg = {self, class_getSuperclass(self)};
objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));

所以此時的“前一個區域性變數” 也就是結構體 objc_super 型別的 arg。arg 是一個結構體,結構體第一個成員變數就是 self,所以“前一個區域性變數” 也就是 self(ViewController)

應用場景

1.統計 App 中未響應的方法。給 NSObject 新增分類

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 本來能呼叫的方法
    if ([self respondsToSelector:aSelector]) {
        return [super methodSignatureForSelector:aSelector];
    }
    // 找不到的方法
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 找不到的方法,都會來到這裡
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
}
@end

2.修改類的 isa

object_setClass 實現

Person *p = [Person new];
object_setClass(p, [Student class]);

3.動態建立類

objc_allocateClassPairobjc_registerClassPair 成對存在

動態建立類、新增屬性、方法

void study (id self, SEL _cmd) {
    NSLog(@"在學習了");
}

void createClass (void) {
    Class newClass = objc_allocateClassPair([NSObject class], "GoodStudent", 0);
    class_addIvar(newClass, "_score", 4, 1, "i");
    class_addIvar(newClass, "_height", 4, 1, "i");
    class_addMethod(newClass, @selector(study), (IMP)study, "v16@0:8");
    objc_registerClassPair(newClass);
    id student = [[newClass alloc] init];
    [student setValue:@100 forKey:@"_score"];
    [student setValue:@177 forKey:@"_height"];
    [student performSelector:@selector(study)];
    NSLog(@"%@ %@", [student valueForKey:@"_score"], [student valueForKey:@"_height"]);
}

runtime 中 copy、create 等出來的記憶體,不使用的時候需要手動釋放objc_disposeClassPair(newClass>)

4.訪問成員變數資訊

void ivarInfo (void) {
    Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
    NSLog(@"%s %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar)); //_name @"NSString"
    // 設定、獲取成員變數
    Person *p = [[Person alloc] init];
    Ivar ageIvar = class_getInstanceVariable([Person class], "_age");
    object_setIvar(p, ageIvar, (__bridge id)(void *)27);
    NSLog(@"%d", p.age);
}

runtime 設定值 api object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 第三個引數要求為 id 型別,但是我們給 int 型別的屬性設定值,怎麼辦?可以將27這個數字的地址傳進去,同時需要型別轉換為 id (__bridge id)(void *)27)

KVC 可以根據具體的值,去取出 NSNumber ,然後呼叫 intValue

[p setValue:@27 forKey:@"_age"];

5.訪問物件的所有成員變數資訊

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@end

unsigned int count;
// 陣列指標
Ivar *properties = class_copyIvarList([Person class], &count);
for (int i =0 ; i<count; i++) {
    Ivar property = properties[i];
    NSLog(@"屬性名稱:%s, 屬性型別:%s", ivar_getName(property), ivar_getTypeEncoding(property));
}
free(properties);
//屬性名稱:_age, 屬性型別:i
// 屬性名稱:_name, 屬性型別:@"NSString"

根據這個可以做很多事情,比如設定解模型、給 UITextField 設 placeholder 的顏色

先根據 class_copyIvarList 訪問到 UITextFiled 有很多屬性,然後找到可疑累_placeholderLabel,透過列印 class、superclass 得到型別為 UILabel。所以用 UILabel 物件設定 color 即可,要麼透過 KVC 直接設定

[self.textFiled setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];

或者設定字典轉模型(不夠健壯,隨便寫的。具體可以參考 YYModel)

+ (instancetype)lbp_modelWithDic:(NSDictionary *)dict
{
    id obj = [[self alloc] init];
    unsigned int count;
    Ivar *properties = class_copyIvarList([self class], &count);
    for (int i =0 ; i<count; i++) {
        Ivar property = properties[i];
        NSString *keyName = [[NSString stringWithUTF8String:ivar_getName(property)] stringByReplacingOccurrencesOfString:@"_" withString:@""];
        id value = [dict objectForKey:keyName];
        [self setValue:value forKey:keyName];
    }
    free(properties);
    return obj;
}

6.替換方法實現

注意

  • 類似 NSMutableArray 的時候,+load 方法進行方法替換的時候需要注意類簇的存在,比如 __NSArrayM
  • 方法交換一般寫在類的 +load 方法中,且為了防止出問題,比如別人手動呼叫 load,程式碼需要加 dispatch_once
void studentSayHi (void) {
    NSLog(@"Student say hi");
}
void changeMethodImpl (void){
    class_replaceMethod([Person class], @selector(sayHi), (IMP)studentSayHi, "v16@0:8");
    Person *p = [[Person alloc] init];
    [p sayHi];
}
// Student say hi

上述程式碼可以換一種寫法

class_replaceMethod([Person class], @selector(sayHi), imp_implementationWithBlock(^{
    NSLog(@"Student say hi");
}), "v16@0:8");
Person *p = [[Person alloc] init];
[p sayHi];

imp_implementationWithBlock(id _Nonnull block) 該方法將方法實現替換為包裝好的 block

Person *p = [[Person alloc] init];
Method sleep = class_getInstanceMethod([Person class], @selector(sleep));
Method sayHi = class_getInstanceMethod([Person class], @selector(sayHi));
method_exchangeImplementations(sleep, sayHi);
[p sayHi];    // 人生無常,抓緊睡覺
[p sleep];    // Person sayHi

7.無痕埋點

對 App 內所有的按鈕點選事件進行監聽並上報。發現 UIButton 繼承自 UIControl,所以新增分類,在 load 方法內,替換方法實現。UIControl 存在方法 sendAction:to:forEvent:

@implementation UIControl (Monitor)
+ (void)load {
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(lbp_sendAction:to:forEvent:));
    method_exchangeImplementations(method1, method2);
}
- (void)lbp_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    // 呼叫系統原來的實現
    [self mj_sendAction:action to:target forEvent:event];
//    [target performSelector:action];
}
@end

為了對業務程式碼無影響,在 hook 程式碼內部又要呼叫回去,所以需要呼叫原來的方法,此時因為交換方法實現,所以原來的方法應該是 lbp_sendAction:to:forEvent:

method_exchangeImplementations 方法實現交換了,系統會清空快取,呼叫 flushCaches 方法,內部呼叫 cache_erase_nolock 來清空方法快取。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;
    rwlock_writer_t lock(runtimeLock);
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?
    flushCaches(nil);
    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

static void flushCaches(Class cls)
{
    runtimeLock.assertWriting();
    mutex_locker_t lock(cacheUpdateLock);
    if (cls) {
        foreach_realized_class_and_subclass(cls, ^(Class c){
            cache_erase_nolock(c);
        });
    }
    else {
        foreach_realized_class_and_metaclass(^(Class c){
            cache_erase_nolock(c);
        });
    }
}

總結:

OC 是一門動態性很強的程式語言,允許很多操作推遲到程式執行時決定。OC 動態性其實就是由 Runtime 來實現的,Runtime 是一套 c 語言 api,封裝了很多動態性相關函式。平時寫的 oc 程式碼,底層大多都是轉換為 Runtime api 進行呼叫的。

  • 關聯物件
  • 遍歷類的所有成員變數(可以訪問私有變數,比如修改 UITextFiled 的 placeholder 顏色、字典轉模型、自動歸檔接檔)
  • 交換方法實現
  • 擴大點選區域
  • 利用訊息轉發機制,解決訊息找不到的問題
  • 無痕埋點
  • 熱修復(熱修復方案有幾大類:內建虛擬機器、下發指令碼關聯到 runtime 修改原始行為、AST 解析處理)
  • 安全氣墊(使用場景褒貶不一:比如責任邊界問題、雖然兜住了 crash,但是問題沒有充分暴露。一個優雅的策略是線上兜住 crash,但是同時收集案發資料,走業務異常報警,開發立馬去根據資料分析這個問題是業務異常還是什麼情況,要不要釋出熱修,還是後端資料/邏輯錯誤)

相關文章