Runtime相關(guān)問(wèn)題

1. 介紹下runtime的內(nèi)存模型(isa、對(duì)象、類、metaclass、結(jié)構(gòu)體的存儲(chǔ)信息等)

objc4-779源碼

首先,關(guān)于NSObject,objc_class 和 objc_object 三者之間的關(guān)系,我們可以用下面的圖來(lái)更清晰的了解:


image.png
一 NSObject

NSObject是OC 中的基類,除了NSProxy其他都繼承自NSObject

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
二 對(duì)象結(jié)構(gòu)體 objc_object

在運(yùn)行時(shí),類的對(duì)象被定義為objc_object類型,就是對(duì)象結(jié)構(gòu)體,在OC 中每一個(gè)對(duì)象都是一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體都包含了一個(gè)isa成員變量。根據(jù)isa的定義可以知道,類型為isa_t類型的

struct objc_object {
private:
    isa_t isa;

public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    // 省略其余方法
    ...
}

isa_t 的定義是什么?
是一個(gè)union的結(jié)構(gòu)對(duì)象,類似于C++結(jié)構(gòu)體,其內(nèi)部可以定義為成員變量和函數(shù),是一個(gè)聯(lián)合類型,其中的 isa_t、cls、 bits 還有結(jié)構(gòu)體共用同一塊地址空間。

三 類結(jié)構(gòu)體 objc_class

類也是一個(gè)對(duì)象,類的結(jié)構(gòu)體objc_class 是繼承自objc_object的,具備對(duì)象的所有特征

iOS中不管類對(duì)象還是元類對(duì)象類型都是Class,而Class的結(jié)構(gòu)則是objc_class結(jié)構(gòu)體

typedef struct objc_class *Class;
typedef struct objc_object *id;

//注意,有些人看到的objc_class結(jié)構(gòu)體定義不一樣,有OBJC2_UNAVAILABLE 的注釋,在OC 2.0中,
//這種關(guān)于之前objc_class的定義已經(jīng)廢棄掉了,可以在 objc-runtime-new.h 看OC 2.0之后的,如下:

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() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...//此處省略
}

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
    private:
    ...
 
    public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        assert(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }
    ...

cache_t cache

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
image.png

根據(jù)源碼,我們可以知道 cache_t 中存儲(chǔ)了一個(gè) bucket_t 的結(jié)構(gòu)體,和兩個(gè)unsigned int的變量。
mask:分配用來(lái)緩存bucket的總數(shù)。
occupied:表明目前實(shí)際占用的緩存bucket的個(gè)數(shù)。

bucket_t 的結(jié)構(gòu)體中存儲(chǔ)了一個(gè)unsigned long和一個(gè)IMP。IMP是一個(gè)函數(shù)指針,指向了一個(gè)方法的具體實(shí)現(xiàn)。

cache_t 中的bucket_t *_buckets其實(shí)就是一個(gè)散列表,用來(lái)存儲(chǔ)Method的鏈表。

Cache 的作用主要是為了優(yōu)化方法調(diào)用的性能。當(dāng)對(duì)象receiver調(diào)用方法message時(shí),首先根據(jù)對(duì)象receiver的isa指針查找到它對(duì)應(yīng)的類,然后在類的methodLists中搜索方法,如果沒(méi)有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調(diào)用方法。如果沒(méi)有找到,有可能消息轉(zhuǎn)發(fā),也可能忽略它。但這樣查找方式效率太低,因?yàn)橥粋€(gè)類大概只有20%的方法經(jīng)常被調(diào)用,占總調(diào)用次數(shù)的80%。所以使用Cache來(lái)緩存經(jīng)常調(diào)用的方法,當(dāng)調(diào)用方法時(shí),優(yōu)先在Cache查找,如果沒(méi)有找到,再到methodLists查找。

class_data_bits_t bits

存儲(chǔ)類的方法、屬性、遵循的協(xié)議等信息的地方

class_data_bits_t bits有一個(gè)成員uintptr_t bits, 可以理解為一個(gè)‘復(fù)合指針’。什么意思呢,就是bits不僅包含了指針,同時(shí)包含了Class的各種異或flag,來(lái)說(shuō)明Class的屬性。把這些信息復(fù)合在一起,僅用一個(gè)uint指針bits來(lái)表示。當(dāng)需要取出這些信息時(shí),需要用對(duì)應(yīng)的以FAST_前綴開(kāi)頭的flag掩碼對(duì)bits按位與操作。

例如,我們需要取出Classs的核心信息class_rw_t, 則需要調(diào)用方法:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
image.png
image.png
2. 為什么要設(shè)計(jì)metaclass

類對(duì)象(class object)中包含了類的實(shí)例變量,實(shí)例方法的定義,
元類對(duì)象(metaclass object)中包括了類對(duì)象的方法,也就是類方法(也就是C++中的靜態(tài)方法)的定義。

那么可不可以把元類干掉,在類中把實(shí)例方法和類方法存在兩個(gè)不同的數(shù)組中?

__class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
`lookUpImpOrForward`用于查找方法
1、首先會(huì)再一次的從類中尋找需要調(diào)用方法的緩存,如果能命中緩存直接返回該方法的實(shí)現(xiàn),如果不能命中則繼續(xù)往下走。
2、從類的方法列表中尋找該方法,如果能從列表中找到方法則對(duì)方法進(jìn)行緩存并返回該方法的實(shí)現(xiàn),如果找不到該方法則繼續(xù)往下走。
3、從父類的緩存尋找該方法,如果父類緩存能命中則將方法緩存至當(dāng)前調(diào)用方法的類中(注意這里不是存進(jìn)父類),如果緩存未命中則遍歷父類的方法列表,之后操作如同第2步,未能命中則繼續(xù)走第3步直到尋找到基類。
4、如果到基類依然沒(méi)有找到該方法則觸發(fā)動(dòng)態(tài)方法解析流程。=
5、還是找不到就觸發(fā)消息轉(zhuǎn)發(fā)流程

答:行是肯定可行的,但是在lookUpImpOrForward執(zhí)行的時(shí)候就得標(biāo)注上傳入的cls到底是實(shí)例對(duì)象還是類對(duì)象,這也就意味著在查找方法的緩存時(shí)同樣也需要判斷cls到底是個(gè)啥。

從OC的消息機(jī)制分析了元類存在的意義,元類的存在巧妙的簡(jiǎn)化了實(shí)例方法和類方法的調(diào)用流程,大大提升了消息發(fā)送的效率。

metaclass代表的是類對(duì)象的對(duì)象,它存儲(chǔ)了類的類方法,它的目的是將實(shí)例和類的相關(guān)方法列表以及構(gòu)建信息區(qū)分開(kāi)來(lái),方便各司其職,符合單一職責(zé)設(shè)計(jì)原則。

具體可以參考這篇文章

3. class_copyIvarList & class_copyPropertyList區(qū)別

class_copyIvarList

獲取 類對(duì)象 中的所有實(shí)例變量信息,從 class_ro_t 中獲取

class_copyPropertyList

獲取 類對(duì)象 中的屬性信息, class_rw_tproperties,先后輸出了 category / extension/ baseClass 的屬性,而且僅輸出 當(dāng)前的類 的屬性信息,而不會(huì)向上去找 superClass 中定義的屬性。

Q1: class_ro_t 中的 baseProperties 呢?
Q2: class_rw_t 中的 properties 包含了所有屬性,那何時(shí)注入進(jìn)去的呢?
An: methodizeClass 方法,會(huì)把類里面的屬性,協(xié)議,方法都加載進(jìn)來(lái)

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
4. class_rw_t 和 class_ro_t 的區(qū)別
image.png
class_rw_t 表示read write,class_ro_t 表示 read only

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;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
}

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

每個(gè)類都對(duì)應(yīng)有一個(gè)class_ro_t結(jié)構(gòu)體和一個(gè)class_rw_t結(jié)構(gòu)體。在編譯期間,class_ro_t結(jié)構(gòu)體就已經(jīng)確定,objc_class中的bits的data部分存放著該結(jié)構(gòu)體的地址。在runtime運(yùn)行之后,具體說(shuō)來(lái)是在運(yùn)行runtime的realizeClass 方法時(shí),會(huì)生成class_rw_t結(jié)構(gòu)體,該結(jié)構(gòu)體包含了class_ro_t,并且更新data部分,換成class_rw_t結(jié)構(gòu)體的地址

區(qū)別在于:class_ro_t存放的是編譯期間就確定的屬性、方法和遵守協(xié)議;而class_rw_t是在runtime時(shí)才確定,它會(huì)先將class_ro_t的內(nèi)容拷貝過(guò)去,然后再將當(dāng)前類的分類的這些屬性、方法等拷貝到其中。所以可以說(shuō)class_rw_t是class_ro_t的超集,當(dāng)然實(shí)際訪問(wèn)類的方法、屬性等也都是訪問(wèn)的class_rw_t中的內(nèi)容

OC對(duì)象中存儲(chǔ)的屬性、方法、遵循的協(xié)議數(shù)據(jù)其實(shí)被存儲(chǔ)在這兩塊兒內(nèi)存區(qū)域的,而我們通過(guò)runtime動(dòng)態(tài)修改類的方法時(shí),是修改在class_rw_t區(qū)域中存儲(chǔ)的方法列表。

參考這篇文章
更加詳細(xì)的分析,請(qǐng)看@Draveness 的這篇文章深入解析 ObjC 中方法的結(jié)構(gòu)

5. category如何被加載的,兩個(gè)category的load方法的加載順序,兩個(gè)category的同名方法的加載順序

objc_init ->... -> realizeClass -> methodizeClass(用于Attach categories)-> attachCategories -> attachLists

在運(yùn)行時(shí)調(diào)用 realizeClass方法,會(huì)做以下3件事情:

  1. class_data_bits_t調(diào)用 data方法,將結(jié)果從 class_rw_t強(qiáng)制轉(zhuǎn)換為 class_ro_t指針
  2. 初始化一個(gè) class_rw_t結(jié)構(gòu)體
  3. 設(shè)置結(jié)構(gòu)體 ro的值以及 flag

最后調(diào)用methodizeClass方法,把分類里面的屬性,協(xié)議,方法都加載進(jìn)來(lái)。
關(guān)鍵就是在methodizeClass 方法實(shí)現(xiàn)中

category的加載是在運(yùn)行時(shí)發(fā)生的,加載過(guò)程是,把category的實(shí)例方法、屬性、協(xié)議添加到類對(duì)象上。把category的類方法、屬性、協(xié)議添加到metaclass上。

category的load方法執(zhí)行順序是根據(jù)類的編譯順序決定的,即:xcode中的Build Phases中的Compile Sources中的文件從上到下的順序加載的。

category并不會(huì)替換掉同名的方法的,也就是說(shuō)如果 category 和原來(lái)類都有 methodA,那么 category 附加完成之后,類的方法列表里會(huì)有兩個(gè) methodA,并且category添加的methodA會(huì)排在原有類的methodA的前面,因此如果存在category的同名方法,那么在調(diào)用的時(shí)候,則會(huì)先找到最后一個(gè)編譯的 category里的對(duì)應(yīng)方法

6、category & extension區(qū)別,能給NSObject添加Extension嗎,結(jié)果如何?

category:分類

  • 給類添加新的方法
  • 不能給類添加成員變量
  • 通過(guò)@property定義的變量,只能生成對(duì)應(yīng)的getter和setter的方法聲明,但是不能實(shí)現(xiàn)getter和* setter方法,同時(shí)也不能生成帶下劃線的成員屬性
  • 是運(yùn)行期決定的

注意:為什么不能添加屬性,原因就是category是運(yùn)行期決定的,在運(yùn)行期類的內(nèi)存布局已經(jīng)確定,如果添加實(shí)例變量會(huì)破壞類的內(nèi)存布局,會(huì)產(chǎn)生意想不到的錯(cuò)誤。

extension:擴(kuò)展

  • 可以給類添加成員變量,但是是私有的
  • 可以給類添加方法,但是是私有的
  • 添加的屬性和方法是類的一部分,在編譯期就決定的。在編譯器和頭文件的@interface和實(shí)現(xiàn)文件里的@implement一起形成了一個(gè)完整的類。
  • 伴隨著類的產(chǎn)生而產(chǎn)生,也隨著類的消失而消失
  • 必須有類的源碼才可以給類添加extension,所以對(duì)于系統(tǒng)一些類,如nsstring,就無(wú)法添加類擴(kuò)展

不能給NSObject添加Extension,因?yàn)樵趀xtension中添加的方法或?qū)傩员仨氃谠搭惖奈募?m文件中實(shí)現(xiàn)才可以,即:你必須有一個(gè)類的源碼才能添加一個(gè)類的extension。

8. 在方法調(diào)用的時(shí)候,方法查詢-> 動(dòng)態(tài)解析-> 消息轉(zhuǎn)發(fā) 之前做了什么

OC中的方法調(diào)用,編譯后的代碼最終都會(huì)轉(zhuǎn)成objc_msgSend(id , SEL, ...)方法進(jìn)行調(diào)用,這個(gè)方法第一個(gè)參數(shù)是一個(gè)消息接收者對(duì)象,runtime通過(guò)這個(gè)對(duì)象的isa指針找到這個(gè)對(duì)象的類對(duì)象,從類對(duì)象中的cache中查找是否存在SEL對(duì)應(yīng)的IMP,若不存在,則會(huì)在 method_list中查找,如果還是沒(méi)找到,則會(huì)到supper_class中查找,仍然沒(méi)找到的話,就會(huì)調(diào)用_objc_msgForward(id, SEL, ...)進(jìn)行消息轉(zhuǎn)發(fā)。

9. IMP、SEL、Method的區(qū)別和使用場(chǎng)景
typedef struct objc_selector *SEL

SEL: 方法選擇器,雖然 SELobjc_selector 結(jié)構(gòu)體指針,但實(shí)際上它只是一個(gè) C 字符串,方法的名稱

typedef id (*IMP)(id, SEL, …)

IMP-函數(shù)指針,指向?qū)嶋H執(zhí)行函數(shù)體

typedef struct objc_method *Method

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
};

方法名 method_name 類型為 SEL,前面提到過(guò)相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
方法類型 method_types 是個(gè) char 指針,其實(shí)存儲(chǔ)著方法的參數(shù)類型返回值類型,即是 Type Encoding 編碼。
method_imp 指向方法的實(shí)現(xiàn),本質(zhì)上是一個(gè)函數(shù)的指針,就是前面講到的 Implementation。

使用場(chǎng)景:
實(shí)現(xiàn)類的swizzle的時(shí)候會(huì)用到,通過(guò)class_getInstanceMethod(class, SEL)來(lái)獲取類的方法Method,其中用到了SEL作為方法名
調(diào)用method_exchangeImplementations(Method1, Method2)進(jìn)行方法交換

我們還可以給類動(dòng)態(tài)添加方法,此時(shí)我們需要調(diào)用class_addMethod(Class, SEL, IMP, types),該方法需要我們傳遞一個(gè)方法的實(shí)現(xiàn)函數(shù)IMP,例如:

static void funcName(id receiver, SEL cmd, 方法參數(shù)...) {
   // 方法具體的實(shí)現(xiàn)   
}

函數(shù)第一個(gè)參數(shù):方法接收者,第二個(gè)參數(shù):調(diào)用的方法名SEL,方法對(duì)應(yīng)的參數(shù),這個(gè)順序是固定的。

10、load、initialize方法的區(qū)別什么?在繼承關(guān)系中他們有什么區(qū)別

load:當(dāng)類被裝載的時(shí)候被調(diào)用,只調(diào)用一次

  • 調(diào)用方式并不是采用runtime的objc_msgSend方式調(diào)用的,而是直接采用函數(shù)的內(nèi)存地址直接調(diào)用的
  • 多個(gè)類的load調(diào)用順序,是依賴于compile sources中的文件順序決定的,根據(jù)文件從上到下的順序調(diào)用
  • 子類和父類同時(shí)實(shí)現(xiàn)load的方法時(shí),父類的方法先被調(diào)用
  • 本類與category的調(diào)用順序是,優(yōu)先調(diào)用本類的(注意:category是在最后被裝載的)
  • 多個(gè)category,每個(gè)load都會(huì)被調(diào)用(這也是load的調(diào)用方式不是采用objc_msgSend的方式調(diào)用的),同樣按照compile sources中的順序調(diào)用的
  • load是被動(dòng)調(diào)用的,在類裝載時(shí)調(diào)用的,不需要手動(dòng)觸發(fā)調(diào)用

initialize:當(dāng)類或子類第一次收到消息時(shí)被調(diào)用(即:靜態(tài)方法或?qū)嵗椒ǖ谝淮伪徽{(diào)用,也就是這個(gè)類第一次被用到的時(shí)候),只調(diào)用一次

  • 調(diào)用方式是通過(guò)runtime的objc_msgSend的方式調(diào)用的,此時(shí)所有的類都已經(jīng)裝載完畢
  • 子類和父類同時(shí)實(shí)現(xiàn)initialize,父類的先被調(diào)用,然后調(diào)用子類
  • 本類與category同時(shí)實(shí)現(xiàn)initialize,category會(huì)覆蓋本類的方法,只調(diào)用category的initialize一次(這也說(shuō)明initialize的調(diào)用方式采用objc_msgSend的方式調(diào)用的)
  • initialize是主動(dòng)調(diào)用的,只有當(dāng)類第一次被用到的時(shí)候才會(huì)觸發(fā)

參考這篇文章

11. _objc_msgForward函數(shù)是做什么的,直接調(diào)用它將會(huì)發(fā)生什么?

_objc_msgForward是一個(gè)函數(shù)指針(和 IMP 的類型一樣),是用于消息轉(zhuǎn)發(fā)的:
當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒(méi)有實(shí)現(xiàn)的時(shí)候,_objc_msgForward會(huì)嘗試做消息轉(zhuǎn)發(fā)。

也就是說(shuō)_objc_msgForward在進(jìn)行消息轉(zhuǎn)發(fā)的過(guò)程中會(huì)涉及以下這幾個(gè)方法:

resolveInstanceMethod:方法 (或 resolveClassMethod:)。
forwardingTargetForSelector:方法
methodSignatureForSelector:方法
forwardInvocation:方法
doesNotRecognizeSelector: 方法

12. 通過(guò)runtime動(dòng)態(tài)創(chuàng)建一個(gè)類

要?jiǎng)?chuàng)建一個(gè)新類,

  1. 首先調(diào)用objc_allocateClassPair。
  2. 然后使用class_addMethodclass_addIvar等函數(shù)設(shè)置類的屬性。
  3. 完成構(gòu)建類后調(diào)用objc_registerClassPair。
/** 
 * 創(chuàng)建一個(gè)新類和元類.
 * 
 * @param superclass 這個(gè)類是新創(chuàng)建的類的父類,可以傳入Nil去創(chuàng)建一個(gè)新根類.
 * @param name 這個(gè)字符串是類的名字(例:"NSObject")
 * @param extraBytes 一般傳入0 
 * @return 新的類,如果返回的是Nil,那么就是這個(gè)類創(chuàng)建失敗了(例:創(chuàng)建的是"NSObject"類,然而這個(gè)類已經(jīng)存在了)
 */
objc_allocateClassPair(
Class _Nullable superclass, 
const char * _Nonnull name,  
size_t extraBytes
) 

objc_allocateClassPair只返回一個(gè)值:Class。那么pair的另一半在哪里呢?
是的,估計(jì)你已經(jīng)猜到了這個(gè)另一半就是meta-class

/** 
 * 注冊(cè)使用`objc_allocateClassPair`方法創(chuàng)建的類
 * 
 * @param cls 需要注冊(cè)的類(不能為Nil)
 */ 
objc_registerClassPair(Class _Nonnull cls) 

代碼樣例

/// 創(chuàng)建一個(gè)元類
Class class = objc_allocateClassPair([NSObject class], "Person", 0);
/// 添加方法
//class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
/// 添加屬性
//class_addIvar(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
/// 注冊(cè)元類
objc_registerClassPair(class);
13. object_getClass 和 object_setClass

1. Class object_getClass(id obj)

    //返回對(duì)象的isa指針
     eg:參數(shù)為實(shí)例對(duì)象返回類對(duì)象
     eg:參數(shù)為類對(duì)象返回元類對(duì)象
     eg:參數(shù)為元類對(duì)象返回根元類對(duì)象
     eg:參數(shù)為根元類對(duì)象返回自身

    Class object_getClass(id obj)

   // 根據(jù)字符串返回類對(duì)象
    Class _Nullable objc_getClass(const char * _Nonnull name)
    // 創(chuàng)建一個(gè)NSObject對(duì)象obj,然后獲取obj的類
    NSObject *obj = [[NSObject alloc] init]; // 1
    Class objClass = object_getClass(obj); // 2
    NSLog(@"%@", NSStringFromClass(objClass)); // 3
    
    Class nsobjectClass = object_getClass([NSObject class]); // 4
    NSLog(@"%@", NSStringFromClass(nsobjectClass)); // 5
    
    BOOL isMeta1 = class_isMetaClass(objClass); // 6
    BOOL isMeta2 = class_isMetaClass(nsobjectClass); // 7
    NSLog(@"isMeta1:%i, isMeta2:%i", isMeta1, isMeta2); // 8

//打印結(jié)果
2020-04-03 18:08:48.875921+0800 ImplementKVO[81240:4259330] NSObject
2020-04-03 18:08:48.876057+0800 ImplementKVO[81240:4259330] NSObject
2020-04-03 18:08:48.876155+0800 ImplementKVO[81240:4259330] isMeta1:0, isMeta2:1

說(shuō)明:class_isMetaClass函數(shù)的作用是判斷傳入的類是不是元類,經(jīng)過(guò)判斷是不是元類可看出區(qū)別,因此可得出結(jié)論:
object_getClass函數(shù)的參數(shù)傳一個(gè)類的實(shí)例時(shí),返回的是該實(shí)例的類對(duì)象;
參數(shù)傳時(shí),返回的是該類的元類。

    Class class1 = [obj class];
    Class class2 = [[NSObject class] class];

    NSLog(@"%@", NSStringFromClass(class1));
    NSLog(@"%@", NSStringFromClass(class2));
    NSLog(@"%i, %i", class_isMetaClass(class1), class_isMetaClass(class2));

2020-04-03 18:17:05.175123+0800 ImplementKVO[81544:4270627] NSObject
2020-04-03 18:17:05.175261+0800 ImplementKVO[81544:4270627] NSObject
2020-04-03 18:17:05.175363+0800 ImplementKVO[81544:4270627] 0, 0

通過(guò)打印結(jié)果可以看出,class方法的調(diào)用者是一個(gè)實(shí)例時(shí),獲取到的是該實(shí)例的類,此時(shí)和object_getClass函數(shù)作用相同;而調(diào)用者是一個(gè)(比如[NSObject class])時(shí),獲取到的并不是該類的元類,此時(shí)和object_getClass函數(shù)的作用不同.

//[xxx class]方法內(nèi)部實(shí)現(xiàn)

+ (Class)class {
return self;
}

- (Class)class {
return object_getClass(self);
}

2. Class object_setClass(id obj, Class cls)

/**
將一個(gè)對(duì)象設(shè)置為別的類類型,返回原來(lái)的Class ,將一個(gè)對(duì)象的isa指針指向設(shè)置的Class
 * Sets the class of an object.
 * 
 * @param obj The object to modify.
 * @param cls A class object.
 * 
 * @return The previous value of \e object's class, or \c Nil if \e object is \c nil.
 */

Class _Nullable  object_setClass(id _Nullable obj, Class _Nonnull cls) 


    // 分別創(chuàng)建一個(gè)可變數(shù)組對(duì)象mutArray和不可變數(shù)組對(duì)象array
    NSMutableArray *mutArray = [NSMutableArray arrayWithObjects:@"a", @"b", nil]; // 1
    NSArray *array = @[@"c", @"d"]; // 2

    // 獲取這兩個(gè)對(duì)象mutArray和array的類并打印
    Class mutArrayClassBefore = object_getClass(mutArray); // 3
    Class arrayClassBefore = object_getClass(array); // 4
    NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassBefore), NSStringFromClass(arrayClassBefore)); // 5

    Class setclass = object_setClass(mutArray, arrayClassBefore); // 6
    NSLog(@"%@", NSStringFromClass(setclass)); // 7

    Class mutArrayClassNow = object_getClass(mutArray); // 8
    Class arrayClassNow = object_getClass(array); // 9

    NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassNow), NSStringFromClass(arrayClassNow)); // 10

2020-04-03 18:27:43.569594+0800 ImplementKVO[81775:4277728] __NSArrayM -- __NSArrayI
2020-04-03 18:27:43.569724+0800 ImplementKVO[81775:4277728] __NSArrayM
2020-04-03 18:27:43.569822+0800 ImplementKVO[81775:4277728] __NSArrayI -- __NSArrayI

從打印結(jié)果可以看出用object_setClass函數(shù)將可變數(shù)組對(duì)象mutArray的類設(shè)置為它的父類是可以的,此時(shí)再用mutArray調(diào)用NSMutableArray的方法會(huì)導(dǎo)致程序crash,如

[mutArray addObject:@"e"]; // 11

crash信息為經(jīng)典的:

2020-04-03 18:31:04.101045+0800 ImplementKVO[81860:4280357] -[__NSArrayI addObject:]: unrecognized selector sent to instance 0x6000000fec40

總結(jié)

  • 使用object_setClass后會(huì)使對(duì)象的isa的值指向新的Class。
  • 使用object_setClass對(duì)象的內(nèi)存布局不會(huì)發(fā)生變化。
  • 使用object_setClass不能訪問(wèn)超過(guò)原對(duì)象申請(qǐng)的內(nèi)存區(qū)域,否則程序會(huì)crash。
14. Method Swizzle注意事項(xiàng)
  1. 對(duì)于一般的Class.推薦在load方法中交換, 系統(tǒng)的類,可以通過(guò)Category添加方法交換
  2. 避免交換父類方法(先class_addMethod,判斷是否成功)
  3. 交換方法應(yīng)在+load方法
  4. 交換方法應(yīng)該放到dispatch_once中執(zhí)行
  5. 交換的分類方法應(yīng)該添加自定義前綴,避免沖突
  6. 交換的分類方法應(yīng)調(diào)用原實(shí)現(xiàn)

注意:如果是交換不同類的方法,并且在方法中訪問(wèn)了類的屬性,會(huì)造成Crash,更安全的做法,Runtime 還提供了另一種 Swizzle 函數(shù) method_setImplementation。
method_setImplementation 可以讓我們提供一個(gè)新的函數(shù)來(lái)代替我們要替換的方法。 而不是將兩個(gè)方法的實(shí)現(xiàn)做交換。 這樣就不會(huì)造成 method_exchangeImplementations 的潛在對(duì)已有實(shí)現(xiàn)的副作用了。
method_setImplementation 接受兩個(gè)參數(shù),第一個(gè)還是我們要替換的方法, 而第二個(gè)參數(shù)是一個(gè) IMP 類型的。

Objc 黑科技 - Method Swizzle 的一些注意事項(xiàng)

例如:統(tǒng)計(jì)VC加載次數(shù)并打印

  • UIViewController+Logging.m
#import "UIViewController+Logging.h"
#import <objc/runtime.h>

@implementation UIViewController (Logging)

+ (void)load
{
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}

- (void)swizzled_viewDidAppear:(BOOL)animated
{
  //交換前需要先調(diào)用系統(tǒng)的方法初始化調(diào)用[self xxx]的系統(tǒng)方法的時(shí)候.需要把xxx改成xx_xxx(你交換的方法)
  //這里是因?yàn)槟愕姆椒ㄒ呀?jīng)和系統(tǒng)的方法交換了,調(diào)用你的方法其實(shí)是調(diào)用的系統(tǒng)方法,調(diào)用系統(tǒng)方法的話就調(diào)用的是你的方法,
  //然后就會(huì)產(chǎn)生循環(huán)調(diào)用.
    // call original implementation
    [self swizzled_viewDidAppear:animated];
    
    // Logging
    NSLog(@"%@", NSStringFromClass([self class]));
}

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
    // the method might not exist in the class, but in its superclass
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // class_addMethod will fail if original method already exists
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    // the method doesn’t exist and we just added one
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
}

參考文章

15. iOS 中內(nèi)省的幾個(gè)方法有哪些??jī)?nèi)部實(shí)現(xiàn)原理是什么

反射機(jī)制

16. 怎么防止UI控件短時(shí)間多次激活事件?

需求

當(dāng)前項(xiàng)目寫(xiě)好的按鈕,還沒(méi)有全局地控制他們短時(shí)間內(nèi)不可連續(xù)點(diǎn)擊(也許有過(guò)零星地在某些網(wǎng)絡(luò)請(qǐng)求接口之前做過(guò)一些控制)?,F(xiàn)在來(lái)了新需求:本APP所有的按鈕1秒內(nèi)不可連續(xù)點(diǎn)擊。你怎么做?一個(gè)個(gè)改?這種低效率低維護(hù)度肯定是不妥的。

方案

給按鈕添加分類,并添加一個(gè)點(diǎn)擊事件間隔的屬性,執(zhí)行點(diǎn)擊事件的時(shí)候判斷一下是否時(shí)間到了,如果時(shí)間不到,那么攔截點(diǎn)擊事件。
怎么攔截點(diǎn)擊事件呢?其實(shí)點(diǎn)擊事件在runtime里面是發(fā)送消息,我們可以把要發(fā)送的消息的SEL 和自己寫(xiě)的SEL交換一下,然后在自己寫(xiě)的SEL里面判斷是否執(zhí)行點(diǎn)擊事件。

實(shí)踐

UIButton是UIControl的子類,因而根據(jù)UIControl新建一個(gè)分類即可

  • UIControl+Limit.m
#import "UIControl+Limit.h"
#import <objc/runtime.h>

static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";

@implementation UIControl (Limit)

#pragma mark - acceptEventInterval
- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
{
    objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSTimeInterval)acceptEventInterval {
    return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
}

#pragma mark - ignoreEvent
-(void)setIgnoreEvent:(BOOL)ignoreEvent{
    objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}

-(BOOL)ignoreEvent{
    return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
}

#pragma mark - Swizzling
+(void)load {
    Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
    Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));
    method_exchangeImplementations(a, b);//交換方法
}

- (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
{
    if(self.ignoreEvent){
        NSLog(@"btnAction is intercepted");
        return;
    }
    if(self.acceptEventInterval>0){
        self.ignoreEvent=YES;
        //延遲執(zhí)行
        [self performSelector:@selector(setIgnoreEventWithNo)  withObject:nil afterDelay:self.acceptEventInterval];
    }
    //調(diào)用原方法實(shí)現(xiàn)
    [self swizzled_sendAction:action to:target forEvent:event];
}

-(void)setIgnoreEventWithNo{
    self.ignoreEvent=NO;
}

@end

  • ViewController.m
-(void)setupSubViews{
    
    UIButton *btn = [UIButton new];
    btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
    [btn setTitle:@"btnTest"forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
    btn.acceptEventInterval = 3;
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
}

- (void)btnAction{
    NSLog(@"btnAction is executed");
}

//日志打印
2020-04-06 21:07:44.244251+0800 Runtime[5230:326947] btnAction is executed
2020-04-06 21:07:45.077352+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:45.871633+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:46.844650+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:47.719857+0800 Runtime[5230:326947] btnAction is executed
17. 防奔潰處理

崩潰攔截

18. AOP(切面編程)

參考總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、什么是runtime runtime又叫運(yùn)行時(shí),將數(shù)據(jù)類型的確定由編譯時(shí)推遲到了運(yùn)行時(shí) 是一套比較底層的純C語(yǔ)...
    蘑菇三十九閱讀 464評(píng)論 0 0
  • objc_getAssociatedObject返回與給定鍵的特定對(duì)象關(guān)聯(lián)的值。ID objc_getAssoci...
    有一種再見(jiàn)叫青春閱讀 1,763評(píng)論 0 7
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,351評(píng)論 0 7
  • runtime 和 runloop 作為一個(gè)程序員進(jìn)階是必須的,也是非常重要的, 在面試過(guò)程中是經(jīng)常會(huì)被問(wèn)到的, ...
    made_China閱讀 1,276評(píng)論 0 7
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí),它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 869評(píng)論 0 4

友情鏈接更多精彩內(nèi)容