Runtime (整理筆記)

強(qiáng)烈推薦,關(guān)于runtime只需要看下一縷殤流化隱半邊冰霜的這幾篇文章就夠凸主。

作者:一縷殤流化隱半邊冰霜
神經(jīng)病院Objective-C Runtime住院第二天——消息發(fā)送與轉(zhuǎn)發(fā)
神經(jīng)病院Objective-C Runtime入院第一天——isa和Class
神經(jīng)病院Objective-C Runtime出院第三天——如何正確使用Runtime

**說明:此文是自己的總結(jié)筆記橘券,主要參考這幾篇文章:
iOS開發(fā)-Runtime詳解
NSHipster里面的這兩篇文章 :
Associated Objects
Method Swizzling
**

樹1.jpg

一.Runtime簡介

  • Runtime又叫運(yùn)行時,是一套底層的C語言API,其為iOS內(nèi)部的核心之一卿吐,我們平時編寫的OC代碼旁舰,底層都是基于它來實(shí)現(xiàn)的。
    比如:

      [receiver doSomething];
      底層運(yùn)行會被編譯器轉(zhuǎn)化為:
      objc_msgSend(receiver, @selector(doSomething))
      如果帶有參數(shù)比如:
      [receiver doSomething:(id)arg...];
      底層運(yùn)行時會被編譯器轉(zhuǎn)化為:
      objc_msgSend(receiver, @selector(doSomething), arg1, arg2, ...)
    

可能通過以上你看不出它的價值嗡官,但是我們需要了解OC是一門動態(tài)語言箭窜,它會將一些工作放在代碼運(yùn)行時才處理而非在編譯的時候,也就是說衍腥,有很多類和成員變量在我們編譯的時候是不知道的绽快,而在運(yùn)行時芥丧,我們所編寫的代碼會轉(zhuǎn)換成完整的代碼運(yùn)行。
因此坊罢,編譯器是不夠,我們還需要一個運(yùn)行時的系統(tǒng)來處理編譯后的代碼

  • Runtime 基本是C和匯編寫的擅耽,可以充分保證動態(tài)系統(tǒng)的高效性

二. Runtime的作用:

  • 獲取某個類的所有成員變量
  • 獲取某個類的所有屬性
  • 獲取某個類的所有方法
  • 交換方法實(shí)現(xiàn)
  • 動態(tài)添加一個成員變量
  • 動態(tài)添加一個方法

三.Runtime的術(shù)語的數(shù)據(jù)結(jié)構(gòu)

1.SEL

  • 它是selector在objc中的表示(Swift 中是 Selector類)活孩。selector 是方法選擇器,其實(shí)作用是對方法名進(jìn)行包裝乖仇,以便找到對應(yīng)的方法實(shí)現(xiàn)(注意:Objc 在相同的類中不會有命名相同的兩個方法)憾儒。

  • 對應(yīng)的數(shù)據(jù)結(jié)構(gòu):
    typedef struct objc_selector *SEL;
    我們可以看出它是個映射到方法的C字符串,你可以通過Objc編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來獲取一個SEL類型的方法選擇器乃沙。

  • 注意:不同類中相同名字的方法所對應(yīng)的selector是相同的起趾,由于變量的類型不同(即實(shí)例變量所對應(yīng)的類的類型不同如NSString、NSMutableString等類型不同)警儒,所以不會導(dǎo)致它們調(diào)用方法實(shí)現(xiàn)的混亂训裆。

2.id
id 是一個參數(shù)類型,它是指向某個類的實(shí)例的指針蜀铲。定義如下:

typedef struct objc_object *id;
struct objc_object { Class isa; };

通過以上定義边琉,可以看到:objc_object結(jié)構(gòu)體包含一個isa指針,根據(jù)isa指針就可以找到對象所屬的類记劝。

注意:isa指針在代碼運(yùn)行時并不總是指向?qū)嵗龑ο笏鶎俚念愋捅湟蹋圆荒芤揽克鼇泶_定類型,要想確定類型需要用對象的 - class方法厌丑。

3.Class

typedef struct objc_class *Class;

Class 其實(shí)是指向 objc_class 結(jié)構(gòu)體的指針定欧。objc_class的數(shù)據(jù)結(jié)構(gòu)如下:

 struct objc_class {
     Class isa;//指針,顧名思義怒竿,表示是一個什么砍鸠,
      //實(shí)例的isa指向類對象,類對象的isa指向元類

   #if !__OBJC2__
      Class super_class;  //指向父類
      const char *name;  //類名
      long version; // 類的版本信息愧口,初始化默認(rèn)為0睦番,可以通過runtime函數(shù)class_setVersion和class_getVersion進(jìn)行修改、讀取
      long info; // 一些標(biāo)識信息,如CLS_CLASS (0x1L) 表示該類為普通 class 耍属,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass托嚣,其中包含類方法;
      long instance_size;  // 該類的實(shí)例變量大小(包括從父類繼承下來的實(shí)例變量);
      struct objc_ivar_list *ivars // 成員變量列表
      struct objc_method_list **methodLists; // 方法列表
      struct objc_cache *cache;// 緩存,存儲最近使用的方法指針,用于提升效率
      struct objc_protocol_list *protocols // 協(xié)議列表
      #endif
  } OBJC2_UNAVAILABLE;
  /* Use `Class` instead of `struct objc_class *` */

從結(jié)構(gòu)體可以看出厚骗,一個運(yùn)行時類中關(guān)聯(lián)了它的父類指針示启、類名、成員變量领舰、方法夫嗓、緩存以及附屬的協(xié)議迟螺。
其中objc_ivar_list和objc_method_list 分別是成員變量列表和方法列表:

// 成員變量列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

// 方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}



我們都知道,OC中一切都被設(shè)計(jì)成對象舍咖,一個類被初始化成一個實(shí)例矩父,這個實(shí)例是一個對象。實(shí)際上一個類的本質(zhì)也是一個對象排霉,在runtime中用如上結(jié)構(gòu)體表示窍株。
關(guān)于isa指針:

 比如 : NSString *tmpStr = [NSString string];

這里的tmpStr的isa指針指向類對象NSString,而NSString的isa指針指向元類NSObject.

4. Method
Method 代表類中某個方法的類型

typedef struct objc_method *Method;

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

objc_method存儲了方法名、方法類型和方法實(shí)現(xiàn):

  • 方法名類型為SEL

  • 方法類型 method_types 是個 char 指針攻柠,存儲方法的參數(shù)類型和返回值類型

  • method_imp 指向了方法的實(shí)現(xiàn)球订,本質(zhì)是一個函數(shù)指針

5.Ivar
Ivar 是成員變量的類型。

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                     // 變量名稱;
    char *ivar_type                                       // 變量類型;
    int ivar_offset                                         // 基地址偏移字節(jié);
#ifdef __LP64__
    int space                                               // 大小;
#endif
}

其中 ivar_offset 是基地址偏移字節(jié)

6.IMP
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它是一個函數(shù)指針瑰钮,這是由編譯器生成的冒滩。當(dāng)你發(fā)送一個objc消息之后,最終它會執(zhí)行那段代碼浪谴,就是由這個函數(shù)指針指定的开睡。而IMP這個函數(shù)指針就指向了這個方法的實(shí)現(xiàn)。

我們發(fā)現(xiàn)IMP指向的方法和objc_msgSend函數(shù)類型相同较店,參數(shù)都包含id和SEL類型士八。每個方法名都對應(yīng)一個SEL類型的方法選擇器,而每個實(shí)例對象中的SEL對應(yīng)的方法實(shí)現(xiàn)肯定是唯一的梁呈,通過一組id和SEL參數(shù)就能確定唯一的實(shí)現(xiàn)方法地址婚度。所以一個確定的方法也只有一組id和SEL參數(shù):
比如:

NSString *tmpStr = [NSString string];
BOOL isContain =  [tmpStr containsString:@"1"];

這里的containsString的實(shí)現(xiàn)方法就是由id(NSString)和SEL參數(shù)(containsString)確定的。

7.Cache

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache主要用來提高查找效率官卡,當(dāng)一個方法被調(diào)用蝗茁,首先在Cache列表中查找,如果找到直接返回寻咒,如果沒有找到哮翘,再到類的方法列表去查找,找到了將該方法返回同時存入緩存列表毛秘。

8.Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協(xié)議中的屬性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意返回的是屬性列表饭寺,列表中的每個元素都是一個objc_property_t指針

  • 總結(jié):

    // 描述類中的一個方法
    typedef struct objc_method *Method;
    
    // 實(shí)例變量
    typedef struct objc_ivar *Ivar;
    
    // 緩存(類方法)
    typedef struct objc_cache *Cache;
    
    // 實(shí)現(xiàn) 方法
    typedef id (*IMP)(id, SEL, ...);
    
    // 類別Category
    typedef struct objc_category *Category;
    
    // 類中聲明的屬性
    typedef struct objc_property *objc_property_t;
    


樹2.jpg

四.獲取列表

有時候會有這樣的需求叫挟,我們需要知道當(dāng)前類中每個屬性的名字(比如字典轉(zhuǎn)模型艰匙,字典的key和模型對象的屬性名字不匹配)。
我們可以通過runtime的一系列方法獲取類的一些信息(包括屬性列表抹恳、方法列表员凝、成員變量列表和遵循的協(xié)議列表)

unsigned int count;
//獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}

//獲取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
    Method method = methodList[i];
    NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}

//獲取成員變量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
    Ivar myIvar = ivarList[i];
    const char *ivarName = ivar_getName(myIvar);
    NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}

//獲取協(xié)議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
    Protocol *myProtocal = protocolList[i];
    const char *protocolName = protocol_getName(myProtocal);
    NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}

五.方法調(diào)用

方法調(diào)用在運(yùn)行時的過程:

  • 如果調(diào)用的是類方法,就會到類對象的isa指針指向的對象(也就是元類對象)中操作奋献。
    比如:NSString *tmpStr = [NSString string];這里的 [NSString string]調(diào)用的是NSString的類方法健霹,就會到NSString類對象的isa指針指向的對象NSObject元類中操作旺上。

  • 如果用實(shí)例對象調(diào)用實(shí)例方法,會到實(shí)例的isa指針指向的對象(也就是類對象)操作糖埋。
    比如: BOOL isContain = [tmpStr containsString:@"1"];這里的 [tmpStr containsString:@"1"],就是實(shí)例對象tmpStr調(diào)用containsString這個實(shí)例方法宣吱,得到實(shí)例tmpStr的isa指針指向的對象也就是類對象NSString操作。

    NSString *tmpStr = [NSString string];
    BOOL isContain =  [tmpStr containsString:@"1"];
    


1. 首先阶捆,在相應(yīng)操作對象中的緩存列表中查找調(diào)用的方法凌节,如果找到,轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)并執(zhí)行.(即先在tmpStr這個對象的緩存列表中查找是否有containsString這個方法洒试,如果有,則轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)函數(shù)朴上,并執(zhí)行)垒棋;

2.如果沒有找到,在相應(yīng)操作對象的方法中找調(diào)用的方法痪宰,如果找到叼架,轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)并執(zhí)行。(即tmpStr的緩存列表找沒有找到containsString這個方法衣撬,就到tmpStr的方法列表里面查找乖订,如果找到,則轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)函數(shù)具练,并執(zhí)行)乍构;

3.如果沒找到,去父類指針?biāo)赶虻膶ο笾袌?zhí)行1扛点,2(即如果在tmpStr的方法列表里面沒有找到containsString這個方法哥遮,則轉(zhuǎn)向tmpStr的父類,也就是NSObject類去查找該方法)

4.以此類推陵究,如果一直到根類都還沒找到眠饮,轉(zhuǎn)向攔截調(diào)用(即如果在父類NSObject里面也沒有找到containsString這個方法,就往上一層父類再去查找铜邮,知道最頂層(根層)父類仪召,因?yàn)镹SObject在OC中是根層父類,所以如果在NSObjec的方法列表找沒找到containsString松蒜,就轉(zhuǎn)向攔截調(diào)用)

5.如果沒有重寫攔截調(diào)用的方法扔茅,程序報(bào)錯。(即在tmpStr及其父類的方法列表中都沒有containsString這個方法牍鞠,就轉(zhuǎn)向攔截調(diào)用咖摹,但是卻沒有實(shí)現(xiàn)攔截調(diào)用的方法,系統(tǒng)就報(bào)錯)

所以:

  • 重寫父類的方法难述,并沒有覆蓋父類的方法萤晴,只是在當(dāng)前類對象中找到了這個方法后吐句,就不會再去父類中尋找了。

  • 如果子類重寫父類的方法店读,但也想調(diào)用父類方法的實(shí)現(xiàn)嗦枢,只需使用super這個編譯器標(biāo)識,它會在運(yùn)行時先去調(diào)用父類的方法屯断,在執(zhí)行之類的方法文虏。
    比如我們很常見的viewWillAppear函數(shù):

    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        [self.navigationController setNavigationBarHidden:YES animated:animated];
    }
    

這里的super標(biāo)識,在運(yùn)行時會先去執(zhí)行父類的viewWillAppear方法殖演,執(zhí)行完畢之后氧秘,在回來接著執(zhí)行原函數(shù)里面的隱藏導(dǎo)航欄的操作,如果沒有 [super viewWillAppear:animated];就不會去調(diào)用父類的該方法趴久,直接執(zhí)行隱藏導(dǎo)航欄的操作丸相。

六.攔截調(diào)用

攔截調(diào)用就是在找不到調(diào)用方法程序崩潰之前,你有機(jī)會通過重寫NSObject的四個方法來處理彼棍,防止崩潰發(fā)生灭忠。

消息轉(zhuǎn)發(fā)順序圖.png
  + (BOOL)resolveClassMethod:(SEL)sel;

該方法就是當(dāng)你調(diào)用一個不存在的類方法的時候,會調(diào)用該方法座硕,默認(rèn)返回NO,你可以加上自己的處理然后返回YES.

  + (BOOL)resolveInstanceMethod:(SEL)sel;

這個方法和上一個方法相似弛作,處理的是實(shí)例方法。(備注: NSString *tmpStr = [NSString string];像 [NSString string]這里的string就是類方法华匾, [tmpStr containsString:@"1"]這里的containsString就是實(shí)例方法映琳。)

  - (id)forwardingTargetForSelector:(SEL)aSelector;

該方法將你調(diào)用的不存在的方法重定向到一個聲明了這個方法的類,只需要你返回一個有這個方法的target.

  - (void)forwardInvocation:(NSInvocation *)anInvocation;

該方法將你調(diào)用的不存在的方法打包成NSInvocation傳給你瘦真。做完你自己的處理后刊头,調(diào)用inovkeWithTarget:方法讓某個target觸發(fā)這個方法。

樹3.jpg

七.動態(tài)添加方法

重寫了攔截調(diào)用的方法并且返回YES,接下來可以根據(jù)傳入的SEL類型的selector诸尽,動態(tài)添加一個方法原杂。

首先從外部隱式調(diào)用一個不存在的方法:

// 隱式調(diào)用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];

然后,在target對象內(nèi)部重寫攔截調(diào)用的方法您机,動態(tài)添加方法

void runAddMethod(id self, SEL _cmd, NSString *string){
    NSLog(@"add C IMP ", string);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    //給本類動態(tài)添加一個方法
    if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
        class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
    }
    return YES;
}

其中class_addMethod的四個參數(shù)分別是:

  • Class cls 給哪個類添加方法穿肄,本例中是self
  • SEL name 添加的方法繁成,本例中是重寫攔截調(diào)用傳進(jìn)來的selector
  • IMP imp 方法的實(shí)現(xiàn)颠锉,C方法的實(shí)現(xiàn)可以直接獲得。如果是OC方法慎陵,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實(shí)現(xiàn)仲闽。
  • "v@:*"方法的簽名脑溢,代表有一個參數(shù)的方法

八.關(guān)聯(lián)對象

比如現(xiàn)在你準(zhǔn)備用一個系統(tǒng)的類,但是系統(tǒng)的類并不能滿足你的需求,你需要額外添加一個屬性屑彻。
這種情況的一般解決辦法就是繼承验庙。
但是,只增加一個屬性社牲,就去繼承一個類粪薛,總結(jié)太麻煩,這時候runtime的關(guān)聯(lián)屬性就發(fā)揮它的作用了搏恤。

1.首先定義一個全局變量违寿,用它的地址作為關(guān)聯(lián)對象的key

static char kAssociatedObjectKey; 

2.在NSObject+AssociatedObject.h里面添加新的屬性

 NSObject+AssociatedObject.h

@interface NSObject (AssociatedObject) 
@property (nonatomic, strong) id associatedObject; 
@end 

3.在NSObject+AssociatedObject.m里面添加設(shè)置和獲取方法

 //設(shè)置關(guān)聯(lián)對象
@implementation NSObject (AssociatedObject) 
@dynamic associatedObject; 

- (void)setAssociatedObject:(id)object { 
     objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
} 

//獲取關(guān)聯(lián)對象
- (id)associatedObject { 
    return objc_getAssociatedObject(self, @selector(associatedObject)); 
} 

objc_setAssociatedObject的四個參數(shù):

  • id object 給誰設(shè)置關(guān)聯(lián)對象

  • const void *key 關(guān)聯(lián)對象唯一的key,獲取時會用到

  • id value 關(guān)聯(lián)的對象

  • objc_AssociationPolicy 關(guān)聯(lián)策略,有以下幾種策略:

    enum {
        OBJC_ASSOCIATION_ASSIGN = 0, // 給關(guān)聯(lián)對象指定弱引用
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,  // 給關(guān)聯(lián)的對象指定非原子操作哦
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 給關(guān)聯(lián)對象指定費(fèi)原子的copy特性
        OBJC_ASSOCIATION_RETAIN = 01401, // 給關(guān)聯(lián)的對象指定原子的強(qiáng)引用
        OBJC_ASSOCIATION_COPY = 01403  // 給關(guān)聯(lián)的對象指定原子的copy 特性
    };
    

objc_getAssociatedObject 的兩個參數(shù):

  • id object 獲取誰的關(guān)聯(lián)對象
  • const void *key 根據(jù)這個唯一的key獲取關(guān)聯(lián)對象。

其實(shí)熟空,你還可以把添加和獲取關(guān)聯(lián)對象的方法寫在你需要用到這個功能類的類別里面藤巢,方便調(diào)用。

//添加關(guān)聯(lián)對象
- (void)addAssociatedObject:(id)object{
    objc_setAssociatedObject(self, @selector(getAssociatedObject), object,   OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//獲取關(guān)聯(lián)對象
- (id)getAssociatedObject{
    return objc_getAssociatedObject(self, _cmd);
}
  • getAssociatedObject 方法的地址作為唯一的key息罗,
  • _cmd代表當(dāng)前調(diào)用方法的地址菌瘪,也就是getAssociatedObject方法的地址

** 注意 :**
4.移除關(guān)聯(lián)對象:
objc_removeAssociatedObjects()這個函數(shù)很容易讓對象恢復(fù)成它"原始狀態(tài)",你不應(yīng)該使用它來移除關(guān)聯(lián)的對象阱当,因?yàn)樗矔瞥ㄆ渌胤郊尤氲娜筷P(guān)聯(lián)對象。所以你一般只需要通過調(diào)用objc_setAssociatedObject并傳入nil值類清除關(guān)聯(lián)值糜工。

優(yōu)秀樣例

  • 添加私有屬性用于更好地去實(shí)現(xiàn)細(xì)節(jié)弊添。當(dāng)擴(kuò)展一個內(nèi)建類的行為時,保持附加屬性的狀態(tài)可能非常必要捌木。注意以下說的是一種非常教科書式的關(guān)聯(lián)對象的用例:AFNetworking在 UIImageView
    的category上用了關(guān)聯(lián)對象來保持一個operation對象油坝,用于從網(wǎng)絡(luò)上某URL異步地獲取一張圖片。

  • 添加public屬性來增強(qiáng)category的功能刨裆。有些情況下這種(通過關(guān)聯(lián)對象)讓category行為更靈活的做法比在用一個帶變量的方法來實(shí)現(xiàn)更有意義澈圈。在這些情況下,可以用關(guān)聯(lián)對象實(shí)現(xiàn)一個一個對外開放的屬性帆啃∷才回到上個AFNetworking的例子中的 UIImageView
    category,它的 imageResponseSerializer
    方法允許圖片通過一個濾鏡來顯示努潘、或在緩存到硬盤之前改變圖片的內(nèi)容诽偷。

  • 創(chuàng)建一個用于KVO的關(guān)聯(lián)觀察者。當(dāng)在一個category的實(shí)現(xiàn)中使用KVO時疯坤,建議用一個自定義的關(guān)聯(lián)對象而不是該對象本身作觀察者报慕。

錯誤模式

在不必要的時候使用關(guān)聯(lián)對象。使用視圖時一個常見的情況是通過數(shù)據(jù)模型或一些復(fù)合的值來創(chuàng)建一個便利的方法設(shè)置填充字段或?qū)傩匝沟 H绻@些值在后面不會再被使用到眠冈,最好就不要使用關(guān)聯(lián)對象了。(比如你將自定義的UITableViewCell跟模型關(guān)聯(lián)起來菌瘫,但這個cell值用在一個ViewController里面蜗顽,也就是說這個關(guān)聯(lián)對象只用到一處布卡,之后就不再使用,這種情況下就沒必要使用關(guān)聯(lián)對象)诫舅。

使用關(guān)聯(lián)對象來保存一個可以被推算出來的值羽利。例如,有人可能想通過關(guān)聯(lián)對象存儲UITableViewCell上一個自定義accessoryView的引用刊懈,使用tableView:accessoryButtonTappedForRowWithIndexPath: 和 cellForRowAtIndexPath:即可以達(dá)到要求这弧。

使用關(guān)聯(lián)對象來代替X。其中X代表下面的一些項(xiàng):
子類化虚汛,當(dāng)使用繼承比使用組合更合適的時候匾浪。

Target-Action給響應(yīng)者添加交互事件。

手勢識別卷哩,當(dāng)target-action模式不夠用的時候蛋辈。

代理,當(dāng)事件可以委托給其他對象将谊。

消息 & 消息中心使用低耦合的方式來廣播消息冷溶。

樹4.jpg

九.方法交換

顧名思義:就是將兩個方法的實(shí)現(xiàn)交換,比如尊浓,將A方法和B方法交換逞频,調(diào)用A方法的時候,就回去執(zhí)行B方法中的代碼栋齿,反之亦然苗胀。
參考Mattt Thompson的[Method Swizzling]文章:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

在自己定義的viewController中重寫viewWillAppear

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear");
}

就會調(diào)用xxx_viewWillAppear,輸出log, method swizzling在視圖控制器的生命周期、響應(yīng)事件瓦堵、繪制視圖或者Foundation框架的網(wǎng)絡(luò)棧等方法中需要插入代碼的時候基协,都是很好的解決方法。

+load vs +initialize:

swizzling應(yīng)該只在+load中完成菇用。 在 Objective-C 的運(yùn)行時中澜驮,每個類有兩個方法都會自動調(diào)用。

  • +load 是在一個類被初始裝載時調(diào)用(iOS應(yīng)用啟動的時候刨疼,就會加載所有的類泉唁,就會調(diào)用這個方法);并且因?yàn)榧虞d進(jìn)內(nèi)存只會加載一次揩慕,所以也一般也只會調(diào)用一次

  • +initialize 是在應(yīng)用第一次調(diào)用該類的類方法或?qū)嵗椒ㄇ罢{(diào)用的亭畜,調(diào)用次數(shù)根據(jù)子類和具體調(diào)用情況確定。

dispatch_once:

swizzling 應(yīng)該只在 dispatch_once 中完成迎卤。

由于 swizzling 改變了全局的狀態(tài)拴鸵,所以我們需要確保每個預(yù)防措施在運(yùn)行時都是可用的。原子操作就是這樣一個用于確保代碼只會被執(zhí)行一次的預(yù)防措施,就算是在不同的線程中也能確保代碼只執(zhí)行一次劲藐。Grand Central Dispatch 的 dispatch_once 滿足了所需要的需求八堡,并且應(yīng)該被當(dāng)做使用 swizzling 的初始化單例方法的標(biāo)準(zhǔn)。

Selectors, Methods, & Implementations
蘋果定義:

Selector(typedef struct objc_selector *SEL):在運(yùn)行時 Selectors 用來代表一個方法的名字聘芜。Selector 是一個在運(yùn)行時被注冊(或映射)的C類型字符串兄渺。Selector由編譯器產(chǎn)生并且在當(dāng)類被加載進(jìn)內(nèi)存時由運(yùn)行時自動進(jìn)行名字和實(shí)現(xiàn)的映射。

Method(typedef struct objc_method *Method):方法是一個不透明的用來代表一個方法的定義的類型汰现。

Implementation(typedef id (*IMP)(id, SEL,...)):這個數(shù)據(jù)類型指向一個方法的實(shí)現(xiàn)的最開始的地方挂谍。該方法為當(dāng)前CPU架構(gòu)使用標(biāo)準(zhǔn)的C方法調(diào)用來實(shí)現(xiàn)。該方法的第一個參數(shù)指向調(diào)用方法的自身(即內(nèi)存中類的實(shí)例對象瞎饲,若是調(diào)用類方法口叙,該指針則是指向元類對象metaclass)。第二個參數(shù)是這個方法的名字selector嗅战,該方法的真正參數(shù)緊隨其后妄田。

三者之間的關(guān)系:

在運(yùn)行時,類(Class)維護(hù)了一個消息分發(fā)列表來解決消息的正確發(fā)送驮捍。每一個消息列表的入口是一個方法(Method)疟呐,這個方法映射了一對鍵值對,其中鍵值是這個方法的名字 selector(SEL)东且,值是指向這個方法實(shí)現(xiàn)的函數(shù)指針 implementation(IMP)萨醒。 Method swizzling 修改了類的消息分發(fā)列表使得已經(jīng)存在的 selector 映射了另一個實(shí)現(xiàn) implementation,同時重命名了原生方法的實(shí)現(xiàn)為一個新的 selector苇倡。

也就是說swizzling只是交換兩個方法在函數(shù)表中的指向地址而已。

調(diào)用 _cmd

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

初看這段代碼囤踩,我們都會覺得會出現(xiàn)遞歸死循環(huán)旨椒。但事實(shí)不是這樣的:

method swizzling 在交換方法的實(shí)現(xiàn)后,xxx_viewWillAppear:方法的實(shí)現(xiàn)已經(jīng)被替換為UIViewController 的-viewWillAppear:這個原生方法堵漱。

所以當(dāng)我們在UIViewController調(diào)用這個- (void)viewWillAppear:(BOOL)animated 方法的時候综慎,實(shí)際上調(diào)用的是xxx_viewWillAppear這個方法,而 [self xxx_viewWillAppear:animated];這個方法實(shí)際上調(diào)用的是系統(tǒng)的viewWillAppear勤庐。

這就證實(shí)了swizzling只是交換兩個方法在函數(shù)表中的指向地址而已示惊。

常見坑

  1. Method swizzling 是非原子性的,在多線程環(huán)境下可能被多次修改愉镰,但同樣 Method swizzling 又是全局性的米罚,就會造成不可預(yù)知的錯誤。

  2. 可能出現(xiàn)命名沖突的問題丈探,這樣就不會調(diào)用到系統(tǒng)原方法录择,可能導(dǎo)致未知問題。

  3. Method swizzling 看起來像遞歸,對新人來說不容易理解隘竭。

  4. 出現(xiàn)問題 Method swizzling 不容易進(jìn)行debug塘秦,來發(fā)現(xiàn)問題

  5. 隨著項(xiàng)目迭代和人員更換,使用Method swizzling 的項(xiàng)目不容易維護(hù)动看,因?yàn)殚_發(fā)人員有時根本不知道在Method swizzling 里面修改了東西尊剔。

預(yù)防措施

  • 在交換方法實(shí)現(xiàn)后記得要調(diào)用原生方法的實(shí)現(xiàn)(除非你非常確定可以不用調(diào)用原生方法的實(shí)現(xiàn)):APIs 提供了輸入輸出的規(guī)則,而在輸入輸出中間的方法實(shí)現(xiàn)就是一個看不見的黑盒菱皆。交換了方法實(shí)現(xiàn)并且一些回調(diào)方法不會調(diào)用原生方法的實(shí)現(xiàn)這可能會造成底層實(shí)現(xiàn)的崩潰须误。
  • 避免沖突:為分類的方法加前綴,一定要確保調(diào)用了原生方法的所有地方不會因?yàn)槟憬粨Q了方法的實(shí)現(xiàn)而出現(xiàn)意想不到的結(jié)果搔预。
  • 理解實(shí)現(xiàn)原理: 只是簡單的拷貝粘貼交換方法實(shí)現(xiàn)的代碼而不去理解實(shí)現(xiàn)原理不僅會讓 App 很脆弱霹期,并且浪費(fèi)了學(xué)習(xí) Objective-C 運(yùn)行時的機(jī)會。閱讀 Objective-C Runtime Reference 并且瀏覽 能夠讓你更好理解實(shí)現(xiàn)原理拯田。
  • 持續(xù)的預(yù)防: 不管你對你理解 swlzzling 框架历造,UIKit 或者其他內(nèi)嵌框架有多自信,一定要記住所有東西在下一個發(fā)行版本都可能變得不再好使船庇。做好準(zhǔn)備吭产,在使用這個黑魔法中走得更遠(yuǎn),不要讓程序反而出現(xiàn)不可思議的行為鸭轮。

十.感想

runtime是把雙刃劍臣淤,因?yàn)樗械拇a都運(yùn)行在它之上,改變它窃爷,可能會改變代碼的正常運(yùn)行邏輯和所有與之交互的東西邑蒋,因此會產(chǎn)生可怕的副作用。但同時它強(qiáng)大的功能也可以給應(yīng)用的框架或者代碼的編寫帶來非常大的便利按厘。

因此医吊,對于runtime唯一的建議就是,需謹(jǐn)慎使用逮京,一旦使用卿堂,必須先了解runtime的相關(guān)原理,做好預(yù)防措施懒棉,在添加完自己的代碼之后草描,一定要調(diào)用系統(tǒng)原來的方法。

十一.最后:

送上一張喜歡的圖片:

樹5.jpg

提醒:不應(yīng)該把runtime的使用看成是高大上的東西策严,并以使用這個為榮穗慕,實(shí)際開發(fā)中runtime能少用應(yīng)該少用,正常的系統(tǒng)方法才是正道妻导!

這是一篇總結(jié)筆記揍诽,大家有興趣可以蠻看一下诀蓉,如果覺得不錯,麻煩給個喜歡或star,若發(fā)現(xiàn)有錯誤的地方請及時反饋暑脆,謝謝渠啤!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市添吗,隨后出現(xiàn)的幾起案子沥曹,更是在濱河造成了極大的恐慌,老刑警劉巖碟联,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妓美,死亡現(xiàn)場離奇詭異,居然都是意外死亡鲤孵,警方通過查閱死者的電腦和手機(jī)壶栋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來普监,“玉大人贵试,你說我怎么就攤上這事】” “怎么了毙玻?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長廊散。 經(jīng)常有香客問我桑滩,道長,這世上最難降的妖魔是什么允睹? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任运准,我火速辦了婚禮,結(jié)果婚禮上缭受,老公的妹妹穿的比我還像新娘戳吝。我一直安慰自己,他們只是感情好贯涎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著慢洋,像睡著了一般塘雳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上普筹,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天败明,我揣著相機(jī)與錄音,去河邊找鬼太防。 笑死妻顶,一個胖子當(dāng)著我的面吹牛酸员,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播讳嘱,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼幔嗦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沥潭?” 一聲冷哼從身側(cè)響起邀泉,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钝鸽,沒想到半個月后汇恤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拔恰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年因谎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颜懊。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡财岔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饭冬,到底是詐尸還是另有隱情使鹅,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布昌抠,位于F島的核電站患朱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏炊苫。R本人自食惡果不足惜裁厅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侨艾。 院中可真熱鬧执虹,春花似錦、人聲如沸唠梨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽当叭。三九已至茬故,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蚁鳖,已是汗流浹背磺芭。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留醉箕,地道東北人钾腺。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓徙垫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親放棒。 傳聞我的和親對象是個殘疾皇子姻报,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,721評論 0 9
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,560評論 33 466
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 735評論 0 2
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識哨查,它使得 Objective-C 如虎添翼逗抑,具備了靈活的...
    lylaut閱讀 802評論 0 4
  • 前言 runtime其實(shí)在我們?nèi)粘i_發(fā)過程中很少使用到,尤其是像我現(xiàn)在比較初級的程序猿就更用不到了寒亥。但是去面試很多...
    WolfTin閱讀 625評論 0 2