iOS runtime 機(jī)制解讀(結(jié)合 objc4 源碼)

歡迎訪問我的博客原文

Runtime 是指將數(shù)據(jù)類型的確定由編譯時(shí)推遲到了運(yùn)行時(shí)。它是一套底層的純 C 語(yǔ)言 API找御,我們平時(shí)編寫的 Objective-C 代碼冗尤,最終都會(huì)轉(zhuǎn)換成 runtime 的 C 語(yǔ)言代碼琢唾。

不過楞件,runtime API 的實(shí)現(xiàn)是用 C++ 開發(fā)的(源碼中的實(shí)現(xiàn)文件都是 .mm 文件)。

為了更全面地理解 runtime 機(jī)制视粮,我們結(jié)合最新的objc4 源碼來進(jìn)行解讀。

消息傳遞

我們知道 Objective-C 是面向?qū)ο箝_發(fā)的橙凳,而 C 語(yǔ)言則是面向過程開發(fā)蕾殴,這就需要將面向?qū)ο蟮念愞D(zhuǎn)變成面向過程的結(jié)構(gòu)體

在 Objective-C 中岛啸,所有的消息傳遞中的“消息”都會(huì)被編譯器轉(zhuǎn)化為:

id objc_msgSend ( id self, SEL op, ... );

比如執(zhí)行一個(gè)對(duì)象的方法:[obj foo];钓觉,底層運(yùn)行時(shí)會(huì)被編譯器轉(zhuǎn)化為:objc_msgSend(obj, @selector(foo));

那么方法內(nèi)部的執(zhí)行流程究竟是怎么樣的呢坚踩?我先來了解一些概念荡灾。

概念

objc_object

Objective-C 對(duì)象是由 id 類型表示的,它本質(zhì)上是一個(gè)指向 objc_object 結(jié)構(gòu)體的指針瞬铸。

typedef struct objc_object *id;

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

struct objc_object {
private:
    isa_t isa;
// public & private method...
}

我們看到 objc_object 的結(jié)構(gòu)體中只有一個(gè)對(duì)象批幌,就是指向其類的 isa 指針。

當(dāng)向一個(gè)對(duì)象發(fā)送消息時(shí)嗓节,runtime 會(huì)根據(jù)實(shí)例對(duì)象的 isa 指針找到其所屬的類荧缘。

objc_class

Objective-C 的類是由 Class 類型來表示的,它實(shí)際上是一個(gè)指向 objc_class 結(jié)構(gòu)體的指針拦宣。

typedef struct objc_class *Class;

objc_class 結(jié)構(gòu)體中定義了很多變量:

struct objc_class : objc_object {
    // 指向類的指針(位于 objc_object)
    // Class ISA;
    // 指向父類的指針
    Class superclass;
    // 用于緩存指針和 vtable截粗,加速方法的調(diào)用
    cache_t cache;             // formerly cache pointer and vtable
    // 存儲(chǔ)類的方法、屬性鸵隧、遵循的協(xié)議等信息的地方
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // class_data_bits_t 結(jié)構(gòu)體的方法绸罗,用于返回class_rw_t 指針()
    class_rw_t *data() { 
        return bits.data();
    }
    // other methods...
}

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
    // other methods
}

objc_class 繼承自 objc_object,因此它也擁有了 isa 指針豆瘫。除此之外珊蟀,它的結(jié)構(gòu)體中還保存了指向父類的指針、緩存靡羡、實(shí)例變量列表系洛、方法列表、遵守的協(xié)議等略步。

元類

元類(metaclass)是類對(duì)象的類描扯,它的結(jié)構(gòu)體和 objc_class 是一樣的。

由于所有的類自身也是一個(gè)對(duì)象趟薄,我們可以向這個(gè)對(duì)象發(fā)送消息绽诚,比如調(diào)用類方法。那么為了調(diào)用類方法,這個(gè)類的 isa 指針必須指向一個(gè)包含類方法的一個(gè) objc_class 結(jié)構(gòu)體恩够。而類對(duì)象中只存儲(chǔ)了實(shí)例方法卒落,卻沒有類方法,這就引出了元類的概念蜂桶,元類中保存了創(chuàng)建類對(duì)象以及類方法所需的所有信息儡毕。

image

為了更方便理解,舉個(gè)例子:

- (void)eat;    // 一個(gè)實(shí)例方法
+ (void)sleep;  // 一個(gè)類方法

// 那么實(shí)例方法需要由類對(duì)象來調(diào)用:
[person eat];
// 而類方法需要由元類來調(diào)用:
[Person sleep];

假如 person 對(duì)象也能調(diào)用 sleep 方法扑媚,那我們就無法區(qū)分它調(diào)用的就究竟是 + (void)sleep; 還是 - (void)sleep;腰湾。

類對(duì)象是元類的實(shí)例,類對(duì)象的 isa 指針指向了元類疆股。

這個(gè)說法可能有點(diǎn)繞费坊,借助這張經(jīng)典的圖來理解:

image

當(dāng)向?qū)ο蟀l(fā)消息,runtime 會(huì)在這個(gè)對(duì)象所屬類方法列表中查找發(fā)送消息對(duì)應(yīng)的方法旬痹,但當(dāng)向類發(fā)送消息時(shí)附井,runtime 就會(huì)在這個(gè)類的 meta class 方法列表里查找。所有的 meta class两残,包括 Root class永毅,Superclass,Subclass 的 isa 都指向 Root class 的 meta class磕昼,這樣能夠形成一個(gè)閉環(huán)卷雕。

Method(method_t)

Method 是一個(gè)指向 method_t 結(jié)構(gòu)體的指針,我們?cè)?objc-private.hobjc-runtime-new.h 中找到關(guān)于它的定義:

typedef struct method_t *Method;
struct method_t {
    // 方法選擇器
    SEL name;
    // 類型編碼
    const char *types;
    // 方法實(shí)現(xiàn)的指針
    MethodListIMP imp;
}

所以 Method 和 SEL票从、IMP 的關(guān)系就是 Method = SEL + IMP + types漫雕。

關(guān)于 types 的寫法,參考 Type Encodings峰鄙。

SEL(objc_selector)

SEL 又稱方法選擇器浸间,是一個(gè)指向 objc_selector 結(jié)構(gòu)體的指針,也是 objc_msgSend 函數(shù)的第二個(gè)參數(shù)類型吟榴。

typedef struct objc_selector *SEL;

方法的 selector 用于表示運(yùn)行時(shí)方法的名稱魁蒜。代碼編譯時(shí),會(huì)根據(jù)方法的名字(不包括參數(shù))生成一個(gè)唯一的整型標(biāo)識(shí)( Int 類型的地址)吩翻,即 SEL兜看。

一個(gè)類的方法列表中不能存在兩個(gè)相同的 SEL,這也是 Objective-C 不支持重載的原因狭瞎。

不同類之間可以存在相同的 SEL细移,因?yàn)椴煌惖膶?shí)例對(duì)象執(zhí)行相同的 selector 時(shí),會(huì)在各自的方法列表中去尋找自己對(duì)應(yīng)的 IMP熊锭。

獲取 SEL 的方式有三種:

  • sel_registerName 函數(shù)
  • Objective-C 編譯器提供的 @selector() 方法
  • NSSeletorFromString() 方法

IMP

IMP 本質(zhì)上就是一個(gè)函數(shù)指針弧轧,指向方法實(shí)現(xiàn)的地址雪侥。

typedef void (*IMP)(void /* id, SEL, ... */ ); 

參數(shù)說明:

  • id:指向 self 的指針(如果是實(shí)例方法,則是類實(shí)例的內(nèi)存地址精绎;如果是類方法速缨,則是指向元類的指針)
  • SEL:方法選擇器
  • ...:方法的參數(shù)列表

SEL 與 IMP 的關(guān)系類似于哈希表中 key 與 value 的關(guān)系。采用這種哈希映射的方式可以加快方法的查找速度代乃。

cache_t

cache_t 表示類緩存旬牲,是 object_class 的結(jié)構(gòu)體變量之一。

struct cache_t {
    // 存放方法的數(shù)組
    struct bucket_t *_buckets;
    // 能存儲(chǔ)的最多數(shù)量
    mask_t _mask;
    // 當(dāng)前已存儲(chǔ)的方法數(shù)量
    mask_t _occupied;
    // ...
}

為了加速消息分發(fā)搁吓,系統(tǒng)會(huì)對(duì)方法和對(duì)應(yīng)的地址進(jìn)行緩存引谜,就放在 cache_t 中。

實(shí)際運(yùn)行中擎浴,大部分常用的方法都是會(huì)被緩存起來的,runtime 系統(tǒng)實(shí)際上非扯窘В快贮预,接近直接執(zhí)行內(nèi)存地址的程序速度。

category_t

category_t 表示一個(gè)指向分類的結(jié)構(gòu)體的指針契讲。

struct category_t {
    // 是指類名仿吞,而不是分類名
    const char *name;
    // 要擴(kuò)展的類對(duì)象,編譯期間是不會(huì)定義的捡偏,而是在運(yùn)行時(shí)階段通過name對(duì)應(yīng)到相應(yīng)的類對(duì)象
    classref_t cls;
    // 實(shí)例方法列表
    struct method_list_t *instanceMethods;
    // 類方法列表
    struct method_list_t *classMethods;
    // 協(xié)議列表
    struct protocol_list_t *protocols;
    // 實(shí)例屬性
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    // 類(元類)屬性列表
    struct property_list_t *_classProperties;
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

這里涉及到一個(gè)經(jīng)典問題:

分類中可以添加實(shí)例變量/成員變量/屬性嗎唤冈?

首先,分類中無法直接添加實(shí)例變量和成員變量银伟。

實(shí)踐一下你虹,我們就會(huì)發(fā)現(xiàn),在分類中添加實(shí)例變量/成員變量彤避,在編譯階段傅物,就會(huì)報(bào)錯(cuò),但添加屬性是允許的琉预。

image

這是因?yàn)?strong>在分類的結(jié)構(gòu)體當(dāng)中董饰,沒有“實(shí)例變量/成員變量”的結(jié)構(gòu),但是有“屬性”的結(jié)構(gòu)圆米。

那么分類中就可以直接添加屬性嗎卒暂?

其實(shí)也不然,雖然分類的 .h 中沒有報(bào)錯(cuò)信息娄帖,.m 中卻報(bào)出了如下的警告也祠,且運(yùn)行時(shí)會(huì)報(bào)錯(cuò)。

image

警告提示上表明有兩種解決方法:

第一種:用 @dynamic修飾块茁。但實(shí)際上齿坷,@dynamic 修飾只是告訴編譯器桂肌,屬性的 setter 和 getter 方法會(huì)由用戶自行實(shí)現(xiàn)。但這樣做只能消除警告永淌,無法解決問題崎场,運(yùn)行時(shí)依然會(huì)崩潰。

第二種:給分類手動(dòng)添加 setter 和 getter 方法遂蛀,這是一種有效的方案谭跨。

我們知道 @property = ivar + setter + getter

可以通過 objc_setAssociatedObjectobjc_getAssociatedObject 向分類中動(dòng)態(tài)添加屬性李滴,具體實(shí)現(xiàn)見下文中的“關(guān)聯(lián)對(duì)象給分類增加屬性”螃宙。

流程

消息傳遞的完整過程為:

消息傳遞流程

也就是查找 IMP 的過程:

  • 先從當(dāng)前 class 的 cache 方法列表里去查找。
  • 如果找到了所坯,如果找到了就返回對(duì)應(yīng)的 IMP 實(shí)現(xiàn)谆扎,并把當(dāng)前的 class 中的 selector 緩存到 cache 里面。
  • 如果類的方法列表中找不到芹助,就到父類的方法列表中查找堂湖,一直找到 NSObject 類為止。
  • 最后再找不到状土,就會(huì)進(jìn)入動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)的機(jī)制无蜂。

消息轉(zhuǎn)發(fā)

如果消息傳遞后仍無法找到 IMP,就進(jìn)入了消息轉(zhuǎn)發(fā)流程蒙谓。

  1. 通過運(yùn)行期的動(dòng)態(tài)方法解析功能斥季,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中。
  2. 對(duì)象可以把其無法解讀的某些選擇子轉(zhuǎn)交給備用接受者來處理累驮。
  3. 經(jīng)過上述兩步之后酣倾,如果還是沒有辦法處理選擇子,那就啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制慰照。

動(dòng)態(tài)方法解析

動(dòng)態(tài)方法解析的兩個(gè)方法:

// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// 添加實(shí)例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

我們?cè)倏纯催@兩個(gè)方法在源碼中的調(diào)用:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    // 判斷是不是元類
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        // 調(diào)用類的 resolveInstanceMethod 方法灶挟,動(dòng)態(tài)添加實(shí)例方法
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        // 調(diào)用元類的 resolveClassMethod 方法,動(dòng)態(tài)添加類方法
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

下面看一個(gè)動(dòng)態(tài)方法解析的例子毒租。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");
}

可以看到雖然沒有實(shí)現(xiàn) foo 這個(gè)函數(shù)稚铣,但是我們通過 class_addMethod 動(dòng)態(tài)添加 fooMethod 函數(shù),并執(zhí)行 fooMethod 這個(gè)函數(shù)的IMP墅垮。

如果 resolveInstanceMethod: 方法返回 NO 惕医,運(yùn)行時(shí)就會(huì)移到下一步:forwardingTargetForSelector:

備用接收者

如果目標(biāo)對(duì)象實(shí)現(xiàn)了 forwardingTargetForSelector: 方法算色,runtime 就會(huì)調(diào)用這個(gè)方法抬伺,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他接受者的機(jī)會(huì)。

實(shí)現(xiàn)一個(gè)備用接收者的例子如下:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函數(shù)
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 返回 NO灾梦,進(jìn)入下一步轉(zhuǎn)發(fā)峡钓。
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        //返回 Person對(duì)象妓笙,讓 Person 對(duì)象接收這個(gè)消息
        return [Person new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

上面的實(shí)現(xiàn)就是利用 forwardingTargetForSelector 把當(dāng)前 ViewController 類的方法 foo 轉(zhuǎn)發(fā)給了備用接受者 Person 類去執(zhí)行了。

完整的消息轉(zhuǎn)發(fā)

如果在上一步還無法處理未知消息能岩,唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制寞宫。

主要涉及到兩個(gè)方法:

  • 發(fā)送 methodSignatureForSelector進(jìn)行方法簽名,這可以將函數(shù)的參數(shù)類型和返回值封裝拉鹃。如果返回 nil辈赋,runtime 會(huì)發(fā)出 doesNotRecognizeSelector 消息,程序同時(shí)崩潰膏燕。
  • 如果返回了一個(gè)函數(shù)簽名钥屈,runtime 就會(huì)創(chuàng)建一個(gè) NSInvocation 對(duì)象并發(fā)送 forwardInvocation 消息給目標(biāo)對(duì)象。

實(shí)現(xiàn)一個(gè)完整轉(zhuǎn)發(fā)的例子如下:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");
}

@end


@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 返回 NO坝辫,進(jìn)入下一步轉(zhuǎn)發(fā)篷就。
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 返回 nil,進(jìn)入下一步轉(zhuǎn)發(fā)近忙。
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 簽名腻脏,進(jìn)入 forwardInvocation
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

@end

通過簽名,runtime 生成了一個(gè)對(duì)象 anInvocation银锻,發(fā)送給方法 forwardInvocation,我們?cè)诜椒ㄖ凶?Person 對(duì)象執(zhí)行 foo 函數(shù)做鹰。

消息轉(zhuǎn)發(fā)流程

以上就是 runtime 的三次轉(zhuǎn)發(fā)流程击纬,下面列舉一下 runtime 的實(shí)際應(yīng)用。

應(yīng)用

<span id="add-prop-to-category-with-associated-objects">關(guān)聯(lián)對(duì)象給分類增加屬性</span>

關(guān)聯(lián)對(duì)象(Associated Objects) 是 Objective-C 運(yùn)行時(shí)的特性钾麸,允許開發(fā)者向已經(jīng)存在的類在擴(kuò)展中添加自定義屬性更振。

關(guān)聯(lián)對(duì)象 runtime 提供了3個(gè) API 接口:

// 獲取關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key);
// 設(shè)置關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 移除關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object);

參數(shù)說明:

  • object:被關(guān)聯(lián)的對(duì)象
  • key:關(guān)聯(lián)對(duì)象的唯一標(biāo)識(shí)
  • value: 關(guān)聯(lián)的對(duì)象
  • policy:內(nèi)存管理的策略

關(guān)于內(nèi)存管理的策略,源碼中 runtime.h 這樣描述:

/* Associative References */

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

我們看看內(nèi)存策略對(duì)應(yīng)的屬性修飾饭尝。

內(nèi)存策略 屬性修飾 描述
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一個(gè)關(guān)聯(lián)對(duì)象的弱引用肯腕。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 指定一個(gè)關(guān)聯(lián)對(duì)象的強(qiáng)引用,不能被原子化使用钥平。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一個(gè)關(guān)聯(lián)對(duì)象的 copy 引用实撒,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一個(gè)關(guān)聯(lián)對(duì)象的強(qiáng)引用涉瘾,能被原子化使用知态。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一個(gè)關(guān)聯(lián)對(duì)象的 copy 引用,能被原子化使用立叛。

下面利用關(guān)聯(lián)對(duì)象實(shí)現(xiàn)一個(gè)“在分類中增加一個(gè)用 copy 修飾的非原子性屬性 prop的功能负敏。

上文中,我們已經(jīng)知道分類中不能直接添加屬性秘蛇,需要手動(dòng)添加存取方法:

// NSObject+AssociatedObject.h

#import <Foundation/Foundation.h>

@interface NSObject (AssociatedObject)

@property (nonatomic, copy) NSString *prop;

@end

// NSObject+AssociatedObject.m

#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>

// key 有三種常見寫法:
//
// 1. static void *propKey = &propKey;
// 2. static NSString *propKey = @"propKey";
// 3. static char propKey;

static NSString *propKey = @"propKey";

@implementation NSObject (AssociatedObject)

- (void)setProp:(NSString *)prop {
    objc_setAssociatedObject(self, &propKey, prop, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)prop {
    return objc_getAssociatedObject(self, &propKey);
}

@end

黑魔法添加和替換方法

黑魔法是方法交換(method swizzling)其做,也就是交換方法的 IMP 實(shí)現(xiàn)顶考。

一般是在 + (void)load; 中執(zhí)行方法交換。因?yàn)樗募虞d時(shí)機(jī)較早妖泄,基本能確保方法已交換驹沿。

方法添加

在動(dòng)態(tài)方法解析中已經(jīng)提到了“方法添加”。

//class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");

參數(shù)說明:

  • cls:被添加方法的類
  • name:添加的方法的名稱的 SEL
  • imp:方法的實(shí)現(xiàn)浮庐。該函數(shù)必須至少要有兩個(gè)參數(shù)甚负,self,_cmd
  • types:類型編碼

方法替換

方法替換就是改變類的選擇子映射表。

image

如果要互換兩個(gè)已經(jīng)寫好的方法實(shí)現(xiàn)审残,可以用下面的函數(shù)

void method_exchangeImplementations(Method m1, Method m2);

方法實(shí)現(xiàn)可以通過下面的函數(shù)獲得:

void class_getInstanceMethod(Class aClass, SEL aSelector);

下面實(shí)現(xiàn)一個(gè)替換 ViewControllerviewDidLoad 方法的例子梭域。

@implementation ViewController
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(msviewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
        
        // 判斷 original 的方法是否已經(jīng)實(shí)現(xiàn),如果未實(shí)現(xiàn)搅轿,將 swizzledMethod 的實(shí)現(xiàn)和類型添加進(jìn) originalSelector 中
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            // 將 originalMethod 的實(shí)現(xiàn)和類型替換到 swizzledSelector 中
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
            // 交換 originalMethod 和 swizzledMethod
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)msviewDidLoad {
    NSLog(@"msviewDidLoad");
    [self msviewDidLoad];
}

- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
    [super viewDidLoad];
}
@end

KVO 實(shí)現(xiàn)

KVO 全稱是 Key-value observing病涨,也就是鍵值觀察者模式,它提供了一種當(dāng)其它對(duì)象屬性被修改的時(shí)候能通知到當(dāng)前對(duì)象的機(jī)制璧坟。

KVO 的實(shí)現(xiàn)也是依賴于 runtime 中的 isa-swizzling既穆。

當(dāng)觀察某對(duì)象 A 時(shí),KVO 機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)新的名為:NSKVONotifying_A 的新類雀鹃,該類繼承自對(duì)象 A 的本類幻工,且 KVO 為 NSKVONotifying_A 重寫觀察屬性的 setter 方法,setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后黎茎,通知所有觀察對(duì)象屬性值的更改情況囊颅。

舉個(gè)例子:

#import "ViewController.h"
#import <objc/runtime.h>
#import "A.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    A *a = [A new];
    NSLog(@"Before KVO: [a class] = %@, a -> isa = %@", [a class], object_getClass(a));
    [a addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"After KVO: [a class] = %@, a -> isa = %@", [a class], object_getClass(a));
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
}

@end

程序運(yùn)行的結(jié)果為:

Before KVO: [a class] = A, a -> isa = A
After KVO: [a class] = A, a -> isa = NSKVONotifying_A

可以看到當(dāng)對(duì) a 進(jìn)行觀察后,雖然對(duì)象 aclass 還是 A傅瞻,isa 實(shí)際指向了它的子類 NSKVONotifying_A踢代,來實(shí)現(xiàn)當(dāng)前類屬性值改變的監(jiān)聽;

所以當(dāng)我們從應(yīng)用層面上看來嗅骄,完全沒有意識(shí)到有新的類出現(xiàn)胳挎,這是系統(tǒng)“隱瞞”了對(duì) KVO 的底層實(shí)現(xiàn)過程,讓我們誤以為還是原來的類溺森。但是此時(shí)如果我們創(chuàng)建一個(gè)新的名為 NSKVONotifying_A 的類慕爬,就會(huì)發(fā)現(xiàn)系統(tǒng)運(yùn)行到注冊(cè) KVO 的那段代碼時(shí)程序就崩潰,因?yàn)橄到y(tǒng)在注冊(cè)監(jiān)聽的時(shí)候動(dòng)態(tài)創(chuàng)建了名為 NSKVONotifying_A 的中間類屏积,并指向這個(gè)中間類了澡罚。

那么子類 NSKVONotifying_A 的 setter 方法里具體實(shí)現(xiàn)了什么?

KVO 的鍵值觀察通知依賴于 NSObject 的兩個(gè)方法:

  • -willChangeValueForKey::被觀察屬性發(fā)生改變之肾请,該方法被調(diào)用留搔,通知系統(tǒng)該 keyPath 的屬性值即將變更

  • -didChangeValueForKey::被觀察屬性發(fā)生改變之铛铁,該方法被調(diào)用隔显,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更却妨。方法 observeValueForKey:ofObject:change:context:也會(huì)被調(diào)用。且重寫觀察屬性的 setter 方法這種繼承方式的注入是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的括眠。

因此彪标,KVO 為子類的觀察者屬性重寫調(diào)用存取方法的工作原理在代碼中相當(dāng)于:

- (void)setName:(NSString *)name {
    // KVO 在調(diào)用存取方法之前總調(diào)用 
    [self willChangeValueForKey:@"name"];
    // 調(diào)用父類的存取方法 
    [super setValue:newName forKey:@"name"];
    // KVO 在調(diào)用存取方法之后總調(diào)用
    [self didChangeValueForKey:@"name"];
}

實(shí)現(xiàn)字典和模型之間的轉(zhuǎn)換(MJExtension)

原理

通過在 NSObject 的分類中添加方法 -initWithDict:

具體實(shí)現(xiàn)為:用 runtime 提供的函數(shù) class_copyPropertyList 獲取屬性列表掷豺,再遍歷 Model 自身所有屬性(通過 property_getName 函數(shù)獲得屬性的名字捞烟,通過 property_getAttributes 函數(shù)獲得屬性的類型)。如果屬性在 json 中有對(duì)應(yīng)的值当船,則將其賦值题画。

源碼

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        // 1、獲取類的屬性及屬性對(duì)應(yīng)的類型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */
        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[I];
            // 通過 property_getName 函數(shù)獲得屬性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通過 property_getAttributes 函數(shù)獲得屬性類型
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        // 立即釋放properties指向的內(nèi)存
        free(properties);

        // 2德频、根據(jù)類型給屬性賦值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;
}

實(shí)現(xiàn) NSCoding 的自動(dòng)歸檔和解檔

原理

Model 的基類中重寫方法:-initWithCoder:-encodeWithCoder:苍息。

具體實(shí)現(xiàn)為:用 runtime 提供的函數(shù) class_copyIvarList 獲取實(shí)例變量列表,再遍歷 Model 自身所有屬性壹置,并對(duì)屬性進(jìn)行 encodedecode 操作竞思。

源碼

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[I];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[I];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}

JSPatch

JSPatch 是一款 iOS 動(dòng)態(tài)更新框架,只需要在項(xiàng)目中引入引擎钞护,就可以使用 JavaScript 調(diào)用所有 Objective-C 原生接口盖喷,從而實(shí)現(xiàn)熱更新。

它通過完整的消息轉(zhuǎn)發(fā)實(shí)現(xiàn)了獲取參數(shù)的問題难咕。

原理

當(dāng)調(diào)用一個(gè) NSObject 對(duì)象不存在的方法時(shí)传蹈,并不會(huì)馬上拋出異常,而是會(huì)經(jīng)過多層轉(zhuǎn)發(fā)步藕,層層調(diào)用對(duì)象的 -resolveInstanceMethod:-forwardingTargetForSelector:挑格、-methodSignatureForSelector:咙冗、-forwardInvocation: 等方法,其中 -forwardInvocation: 里的 NSInvocation 對(duì)象會(huì)保存了這個(gè)方法調(diào)用的所有信息漂彤,包括方法名雾消、參數(shù)和返回值類型等。所以只需要讓被 JS 替換的方法最后都調(diào)用到 -forwardInvocation:挫望,就可以解決無法拿到參數(shù)值的問題了立润。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市媳板,隨后出現(xiàn)的幾起案子桑腮,更是在濱河造成了極大的恐慌,老刑警劉巖蛉幸,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件破讨,死亡現(xiàn)場(chǎng)離奇詭異丛晦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)提陶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門烫沙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人隙笆,你說我怎么就攤上這事锌蓄。” “怎么了撑柔?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵瘸爽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我乏冀,道長(zhǎng)蝶糯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任辆沦,我火速辦了婚禮昼捍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肢扯。我一直安慰自己妒茬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布蔚晨。 她就那樣靜靜地躺著乍钻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪铭腕。 梳的紋絲不亂的頭發(fā)上银择,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音累舷,去河邊找鬼浩考。 笑死,一個(gè)胖子當(dāng)著我的面吹牛被盈,可吹牛的內(nèi)容都是我干的析孽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼只怎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼袜瞬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起身堡,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤邓尤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裁赠,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡殿漠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了佩捞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绞幌。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖一忱,靈堂內(nèi)的尸體忽然破棺而出莲蜘,到底是詐尸還是另有隱情,我是刑警寧澤帘营,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布票渠,位于F島的核電站,受9級(jí)特大地震影響芬迄,放射性物質(zhì)發(fā)生泄漏问顷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一禀梳、第九天 我趴在偏房一處隱蔽的房頂上張望杜窄。 院中可真熱鬧,春花似錦算途、人聲如沸塞耕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扫外。三九已至,卻和暖如春廓脆,著一層夾襖步出監(jiān)牢的瞬間筛谚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工停忿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驾讲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓瞎嬉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親厚柳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氧枣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容