iOS開(kāi)發(fā)經(jīng)驗(yàn)(14)-runtime

目錄

  1. Objective-C Runtime到底是什么
  2. Objective-C的元素認(rèn)知
  3. Runtime詳解
  4. 應(yīng)用場(chǎng)景
  5. Runtime缺點(diǎn)及Runtime常用函數(shù)

引用:
Objective-C Runtime 1小時(shí)入門教程

一纵东、Objective-C Runtime到底是什么

我們將C++和Objective進(jìn)行對(duì)比溃肪,雖然C++和Objective-C都是在C的基礎(chǔ)上加入面向?qū)ο蟮奶匦詳U(kuò)充而成的程序設(shè)計(jì)語(yǔ)言,但二者實(shí)現(xiàn)的機(jī)制差異很大曹货。C++是基于靜態(tài)類型料滥,而Objective-C是基于動(dòng)態(tài)運(yùn)行時(shí)類型然眼。也就是說(shuō)用C++編寫的程序通過(guò)編譯器直接把函數(shù)地址硬編碼進(jìn)入可執(zhí)行文件;而Objective-C無(wú)法通過(guò)編譯器直接把函數(shù)地址硬編碼進(jìn)入可執(zhí)行文件葵腹,而是在程序運(yùn)行的時(shí)候高每,利用Runtime根據(jù)條件判斷作出決定。函數(shù)標(biāo)識(shí)與函數(shù)過(guò)程的真正內(nèi)容之間的關(guān)聯(lián)可以動(dòng)態(tài)修改践宴。Runtime是Objective不可缺少的重要一部分鲸匿。

程序執(zhí)行過(guò)程:預(yù)處理->編譯->鏈接->運(yùn)行。

  • Objective-C 是面相運(yùn)行時(shí)的語(yǔ)言阻肩,就是說(shuō)它會(huì)盡可能的把編譯和鏈接時(shí)要執(zhí)行的邏輯延遲到運(yùn)行時(shí)带欢。
  • OC之所以從C變成了面向?qū)ο蟮腃,擁有動(dòng)態(tài)特性,都是由于運(yùn)行時(shí)系統(tǒng)的存在。
  • Objective-C動(dòng)態(tài)運(yùn)行庫(kù)會(huì)自動(dòng)注冊(cè)我們代碼中定義的所有的類烤惊。我們也可以在運(yùn)行時(shí)創(chuàng)建類定義并使用objc_addClass函數(shù)來(lái)注冊(cè)它們乔煞。

二、Objective-C的元素認(rèn)知

2.1 對(duì)象(id)

typedef struct objc_class *Class;

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

Objective-C 中的對(duì)象的定義 struct objc_object撕氧,Objective-C 中的對(duì)象本質(zhì)上是結(jié)構(gòu)體瘤缩,它是struct objc_object 類型的指針,objc_object被源碼typedef成了id類型伦泥,這也是為什么 id 類型可以指向任意對(duì)象的原因剥啤,其中 isa 是它唯一的私有成員變量。這個(gè)對(duì)象的 isa指針指向它所屬的類不脯。

2.2 類(Class)

Objective-C 中的類的定義 struct objc_class 府怯。同樣的,Objective-C 中類也是一個(gè)結(jié)構(gòu)體防楷。所以牺丙,Objective-C 中的類本質(zhì)上也是對(duì)象,我們稱之為類對(duì)象。
objc_class源碼如下:

typedef struct objc_class *Class;
struct objc_class { 
 Class isa                                 OBJC_ISA_AVAILABILITY; // metaclass
#if !__OBJC2__
 Class super_class                         OBJC2_UNAVAILABLE; // 父類
 const char *name                          OBJC2_UNAVAILABLE; // 類名
 long version                              OBJC2_UNAVAILABLE; // 類的版本信息冲簿,默認(rèn)為0粟判,可以通過(guò)runtime函數(shù)class_setVersion或者class_getVersion進(jìn)行修改、讀取
 long info                                 OBJC2_UNAVAILABLE; // 類信息峦剔,供運(yùn)行時(shí)期使用的一些位標(biāo)識(shí)档礁,如CLS_CLASS (0x1L) 表示該類為普通 class憨攒,其中包含實(shí)例方法和變量;CLS_META (0x2L) 表示該類為 metaclass矫俺,其中包含類方法;
 long instance_size                        OBJC2_UNAVAILABLE; // 該類的實(shí)例變量大小(包括從父類繼承下來(lái)的實(shí)例變量)
 struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE; // 該類的成員變量地址列表
 struct objc_method_list **methodLists     OBJC2_UNAVAILABLE; // 方法地址列表涂臣,與 info 的一些標(biāo)志位有關(guān)惨险,如CLS_CLASS (0x1L)羹幸,則存儲(chǔ)實(shí)例方法,如CLS_META (0x2L)辫愉,則存儲(chǔ)類方法;
 struct objc_cache *cache                  OBJC2_UNAVAILABLE; // 緩存最近使用的方法地址栅受,用于提升效率;
 struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE; // 存儲(chǔ)該類聲明遵守的協(xié)議的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */

由以上代碼可見(jiàn):

Class是一個(gè)指向objc_class結(jié)構(gòu)體的指針一屋,而id是一個(gè)指向objc_object結(jié)構(gòu)體的指針窘疮,其中的isa是一個(gè)指向objc_class結(jié)構(gòu)體的指針。其中的id就是我們所說(shuō)的對(duì)象冀墨,Class就是我們所說(shuō)的類闸衫。

類與對(duì)象的區(qū)別就是類比對(duì)象多了很多特征成員,類也可以當(dāng)做一個(gè)objc_object來(lái)對(duì)待诽嘉,也就是說(shuō)類和對(duì)象都是對(duì)象蔚出,分別稱作類對(duì)象(class object)和實(shí)例對(duì)象(instance object),這樣我們就可以區(qū)別對(duì)象和類了虫腋。

isa:

在OC中骄酗,除了NSProxy類以外,所有的類都是NSObject的子類悦冀。在Foundation框架下趋翻,NSObject和NSProxy是兩個(gè)基類。id是一個(gè)指向 objc_object 結(jié)構(gòu)體的指針盒蟆,該結(jié)構(gòu)體只有一個(gè)成員isa踏烙,所以任何繼承自 NSObject 的類對(duì)象都可以用 id 來(lái)指代。

從上述兩個(gè)代碼塊內(nèi)历等,object和class里面分別都包含一個(gè)isa:

  1. objc_object(實(shí)例對(duì)象)中isa指針指向的類結(jié)構(gòu)稱為class(也就是該對(duì)象所屬的類)讨惩,其中存放著普通成員變量與動(dòng)態(tài)方法(“-”開(kāi)頭的方法)。對(duì)象的實(shí)例方法調(diào)用時(shí)寒屯,通過(guò)對(duì)象的 isa 在類中獲取方法的實(shí)現(xiàn)荐捻;
  2. objc_class中isa指針指向的類結(jié)構(gòu)稱為metaclass,其中存放著static類型的成員變量與static類型的方法(“+”開(kāi)頭的方法)。類對(duì)象的類方法調(diào)用時(shí)处面,通過(guò)類的 isa 在元類中獲取方法的實(shí)現(xiàn)厂置;

super_class:

指向該類的父類的指針,如果該類是根類(如NSObject或NSProxy)魂角,那么super_class就為nil农渊。

元類(metaClass):

所有的metaclass中isa指針都是指向根metaclass,而根metaclass則指向自身或颊。根metaclass是通過(guò)繼承根類產(chǎn)生的,與根class結(jié)構(gòu)體成員一致传于,不同的是根metaclass的isa指針指向自身囱挑。

  • 類也是對(duì)象,也稱類對(duì)象沼溜,類是元類的實(shí)例平挑,因此,我們也可以通過(guò)調(diào)用類方法系草,比如 [NSObject new]通熄,給類對(duì)象發(fā)送消息。同樣的找都,類對(duì)象能否響應(yīng)這個(gè)消息也要通過(guò) isa 找到類對(duì)象所屬的類(元類)才能知道唇辨。也就是說(shuō),實(shí)例方法是保存在類中的能耻,而類方法是保存在元類中的赏枚。
  • 元類也是對(duì)象(元類對(duì)象),元類也是某個(gè)類的實(shí)例晓猛,這個(gè)類我們稱之為根元類(root metaclass)饿幅。元類的isa指向NSObject(NSObject:根元類,其isa指向自己戒职,其super_class也指向自己)栗恩;
  • 存放的是靜態(tài)成員變量和類方法;沒(méi)有實(shí)例方法洪燥。

2.3 方法(Method)

2.3.1 SEL

SEL是selector在Objective-C中的表示類型磕秤, 表示方法的名字/簽名,selector可以理解為區(qū)別方法的ID蚓曼。

typedef struct objc_selector *SEL;

objc_selector的定義如下:

struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;// 名稱
    char *types;                      OBJC2_UNAVAILABLE;// 存儲(chǔ)著方法的參數(shù)類型和返回值類型亲澡。
};

name和types都是char類型。

typedef struct objc_selector *SEL; 

2.3.2 IMP

終于到IMP了纫版,它在objc.h中得定義如下:

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

IMP是“implementation”的縮寫床绪,它是由編譯器生成的一個(gè)函數(shù)指針。當(dāng)你發(fā)起一個(gè)消息后(下文介紹),這個(gè)函數(shù)指針決定了最終執(zhí)行哪段代碼癞己。

2.3.3 Method

方法鏈表里面存儲(chǔ)的是Method 類型膀斋,Method代表類中的某個(gè)方法的類型:Method = IMP + SEL + types,Method也是一個(gè)結(jié)構(gòu)體對(duì)象痹雅。
Method 在頭文件 objc_class.h中定義如下:

typedef struct objc_method *Method;

objc_method的定義如下:

struct objc_method {
    SEL method_name                   OBJC2_UNAVAILABLE; // 方法名(SEL仰担、_cmd)
    char *method_types                OBJC2_UNAVAILABLE; // 方法返回值和參數(shù)的類型
    IMP method_imp                    OBJC2_UNAVAILABLE; // 方法實(shí)現(xiàn),指向該方法的具體實(shí)現(xiàn)的函數(shù)指針
}

Method由三個(gè)部分組成:

  1. 方法名method_name類型為SEL绩社,Selector相當(dāng)于一個(gè)方法的id摔蓝;
  2. 方法類型method_types是一個(gè)char指針,存儲(chǔ)著方法的參數(shù)類型和返回值類型愉耙;
  3. 方法實(shí)現(xiàn)method_imp的類型為IMP贮尉,IMP是方法的實(shí)現(xiàn);

這樣分開(kāi)的一個(gè)便利之處是selector和IMP之間的對(duì)應(yīng)關(guān)系可以被改變朴沿。比如一個(gè) IMP 可以有多個(gè) selectors 指向它猜谚。

注:

  1. 實(shí)例方法在對(duì)象的class中找,而類方法在對(duì)象所屬的類的的metaClass中找赌渣。
  2. OC中的方法實(shí)質(zhì)上是一個(gè)有id self和 SEL _cmd兩個(gè)參數(shù)的C方法魏铅。

2.4 Ivar

Ivar代表類中實(shí)例變量的類型

typedef struct objc_ivar *Ivar;

objc_ivar的定義如下:

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

2.5 objc_property_t

objc_property_t是屬性,它的定義如下:

typedef struct objc_property *objc_property_t;

objc_property是內(nèi)置的類型坚芜,與之關(guān)聯(lián)的還有一個(gè)objc_property_attribute_t览芳,它是屬性的attribute,也就是其實(shí)是對(duì)屬性的詳細(xì)描述鸿竖,包括屬性名稱路操、屬性編碼類型、原子類型/非原子類型等千贯。它的定義如下:

typedef struct {
    const char *name; // 名稱
    const char *value;  // 值(通常是空的)
} objc_property_attribute_t;

2.6 Cache

Catch的定義如下:

typedef struct objc_cache *Cache

objc_cache的定義如下:

struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};

mask: 指定分配cache buckets的總數(shù)屯仗。在方法查找中,Runtime使用這個(gè)字段確定數(shù)組的索引位置搔谴。
occupied: 實(shí)際占用cache buckets的總數(shù)魁袜。
buckets: 指定Method數(shù)據(jù)結(jié)構(gòu)指針的數(shù)組。這個(gè)數(shù)組可能包含不超過(guò)mask+1個(gè)元素敦第。需要注意的是峰弹,指針可能是NULL,表示這個(gè)緩存bucket沒(méi)有被占用芜果,另外被占用的bucket可能是不連續(xù)的鞠呈。這個(gè)數(shù)組可能會(huì)隨著時(shí)間而增長(zhǎng)。
objc_msgSend(下文講解)每調(diào)用一次方法后右钾,就會(huì)把該方法緩存到cache列表中蚁吝,下次的時(shí)候旱爆,就直接優(yōu)先從cache列表中尋找,如果cache沒(méi)有窘茁,才從methodLists中查找方法怀伦。

2.7 Catagory

這個(gè)就是我們平時(shí)所說(shuō)的類別了,很熟悉吧山林。它可以動(dòng)態(tài)的為已存在的類添加新的方法房待。
它的定義如下:

typedef struct objc_category *Category;
objc_category的定義如下:

struct objc_category {
    char *category_name                           OBJC2_UNAVAILABLE; // 類別名稱
    char *class_name                              OBJC2_UNAVAILABLE; // 類名
    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 實(shí)例方法列表
    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 類方法列表
    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 協(xié)議列表
}
2. OC的動(dòng)態(tài)特性**

Objective-C具有相當(dāng)多的動(dòng)態(tài)特性,這個(gè)動(dòng)態(tài)體現(xiàn)在三個(gè)方面:動(dòng)態(tài)類型(Dynamic typing)驼抹,動(dòng)態(tài)綁定(Dynamic binding)和動(dòng)態(tài)加載(Dynamic loading)桑孩。

1. 動(dòng)態(tài)類型
即運(yùn)行時(shí)再?zèng)Q定對(duì)象的類型。這類動(dòng)態(tài)特性在日常應(yīng)用中非常常見(jiàn)框冀,簡(jiǎn)單說(shuō)就是id類型洼怔。id類型即通用的對(duì)象類,任何對(duì)象都可以被id指針?biāo)缸蠹荨F漕愋托枰鹊竭\(yùn)行時(shí)才能決定,在編譯時(shí)id就是一個(gè)通用類型极谊。

2. 動(dòng)態(tài)綁定

動(dòng)態(tài)綁定概念:基于動(dòng)態(tài)類型诡右,在某個(gè)實(shí)例對(duì)象被確定后,其類型便被確定了轻猖。該對(duì)象對(duì)應(yīng)的屬性和響應(yīng)的消息也被完全確定帆吻,這就是動(dòng)態(tài)綁定。在繼續(xù)之前咙边,需要明確Objective-C中消息的概念猜煮。由于OC的動(dòng)態(tài)特性,在OC中其實(shí)很少提及“函數(shù)”的概念败许,傳統(tǒng)的函數(shù)一般在編譯時(shí)就已經(jīng)把參數(shù)信息和函數(shù)實(shí)現(xiàn)打包到編譯后的源碼中了王带,****而在OC中最常使用的是消息機(jī)制。調(diào)用一個(gè)實(shí)例的方法市殷,所做的是向該實(shí)例的指針發(fā)送消息愕撰,實(shí)例在收到消息后,從自身的實(shí)現(xiàn)中尋找響應(yīng)這條消息的方法醋寝。

動(dòng)態(tài)綁定作用:即是在實(shí)例所屬類確定后搞挣,將某些屬性和相應(yīng)的方法綁定到實(shí)例上。這里所指的屬性和方法當(dāng)然包括了原來(lái)沒(méi)有在類中實(shí)現(xiàn)的音羞,而是在運(yùn)行時(shí)才需要的新加入的實(shí)現(xiàn)囱桨。
在Cocoa層,我們一般向一個(gè)NSObject對(duì)象發(fā)送-respondsToSelector:或者-instancesRespondToSelector:等來(lái)確定對(duì)象是否可以對(duì)某個(gè)SEL做出響應(yīng)嗅绰,而在OC消息轉(zhuǎn)發(fā)機(jī)制被觸發(fā)之前舍肠,對(duì)應(yīng)的類的+resolveClassMethod:+resolveInstanceMethod:將會(huì)被調(diào)用搀继,在此時(shí)有機(jī)會(huì)動(dòng)態(tài)地向類或者實(shí)例添加新的方法,也即類的實(shí)現(xiàn)是可以動(dòng)態(tài)綁定的貌夕。

//該方法在OC消息轉(zhuǎn)發(fā)生效前被調(diào)用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{ 
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        //向[self class]中新加入返回為void的實(shí)現(xiàn)律歼,SEL名字為aSEL,實(shí)現(xiàn)的具體內(nèi)容為dynamicMethodIMP class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);
        return YES;
    }
    return [super resolveInstanceMethod:aSel];
}  

3. 動(dòng)態(tài)加載
根據(jù)需求加載所需要的資源啡专,這點(diǎn)很容易理解险毁,對(duì)于iOS開(kāi)發(fā)來(lái)說(shuō),基本就是根據(jù)不同的機(jī)型做適配们童。最經(jīng)典的例子就是在Retina設(shè)備上加載@2x的圖片畔况,而在老一些的普通屏設(shè)備上加載原圖。隨著Retina iPad的推出慧库,和之后可能的Retina Mac的出現(xiàn)跷跪,這個(gè)特性相信會(huì)被越來(lái)越多地使用。

基本的動(dòng)態(tài)特性在常規(guī)的Cocoa開(kāi)發(fā)中非常常用齐板,特別是動(dòng)態(tài)類型和動(dòng)態(tài)綁定吵瞻。以下主要結(jié)合Runtime原理深入運(yùn)行時(shí)特性。

3. RunTime基本概念

RunTime簡(jiǎn)稱運(yùn)行時(shí)甘磨,是一套底層的 C 語(yǔ)言 API橡羞,OC就是運(yùn)行時(shí)機(jī)制,也就是在運(yùn)行時(shí)候的一些機(jī)制济舆,其中最主要的是消息機(jī)制卿泽,主要特征就是動(dòng)態(tài)綁定,消息轉(zhuǎn)發(fā)滋觉。

C與OC區(qū)別:

  • 在編譯階段签夭,C語(yǔ)言調(diào)用未實(shí)現(xiàn)的函數(shù)就會(huì)報(bào)錯(cuò),函數(shù)的調(diào)用在編譯的時(shí)候會(huì)決定調(diào)用哪個(gè)函數(shù)椎侠;
  • 在編譯階段第租,OC可以調(diào)用任何函數(shù),即使這個(gè)函數(shù)并未實(shí)現(xiàn)我纪,只要聲明過(guò)就不會(huì)報(bào)錯(cuò)煌妈。對(duì)于OC的函數(shù),屬于動(dòng)態(tài)調(diào)用過(guò)程宣羊,在編譯的時(shí)候并不能決定真正調(diào)用哪個(gè)函數(shù)璧诵,只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找到對(duì)應(yīng)的函數(shù)來(lái)調(diào)用。

Objective-C 是一個(gè)動(dòng)態(tài)語(yǔ)言仇冯,這意味著它不僅需要一個(gè)編譯器之宿,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來(lái)動(dòng)態(tài)得創(chuàng)建類和對(duì)象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā)苛坚。

開(kāi)發(fā)者在編碼過(guò)程中比被,可以給任意一個(gè)對(duì)象發(fā)送消息色难,在編譯階段只是確定了要向接收者發(fā)送這條消息,而接受者將要如何響應(yīng)和處理這條消息等缀,那就要看運(yùn)行時(shí)來(lái)決定了枷莉。你向一個(gè)對(duì)象發(fā)送消息并不意味著它會(huì)執(zhí)行它。Object(對(duì)象)會(huì)檢查消息的發(fā)送者尺迂,基于這點(diǎn)再?zèng)Q定是執(zhí)行一個(gè)不同的方法還是轉(zhuǎn)發(fā)消息到另一個(gè)目標(biāo)對(duì)象上笤妙。

一般情況開(kāi)發(fā)者只需要編寫 OC 代碼即可,Runtime 系統(tǒng)自動(dòng)在幕后把我們寫的源代碼在編譯階段轉(zhuǎn)換成運(yùn)行時(shí)代碼噪裕,在運(yùn)行時(shí)確定對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)和調(diào)用具體哪個(gè)方法蹲盘。消息直到運(yùn)行時(shí)才綁定到方法實(shí)現(xiàn)上。

Runtime中方法的動(dòng)態(tài)綁定讓我們寫代碼時(shí)更具靈活性膳音,如我們可以把消息轉(zhuǎn)發(fā)給我們想要的對(duì)象召衔,或者隨意交換一個(gè)方法的實(shí)現(xiàn)等。

1. objc_msgSend函數(shù)簡(jiǎn)介
當(dāng)一個(gè)對(duì)象能接收一個(gè)消息時(shí)醇蝴,就會(huì)走正常的方法調(diào)用流程嵌纲。但如果一個(gè)對(duì)象無(wú)法接收指定消息時(shí),又會(huì)發(fā)生什么事呢窿锉?默認(rèn)情況下技潘,如果是以 [object message]的方式調(diào)用方法砸彬,如果object無(wú)法響應(yīng)message消息時(shí)滋迈,編譯器會(huì)報(bào)錯(cuò)庇忌。但如果是以perform…的形式來(lái)調(diào)用,則需要等到運(yùn) 行時(shí)才能確定object是否能接收message消息。如果不能倔幼,則程序崩潰。

通常翩腐,當(dāng)我們不能確定一個(gè)對(duì)象是否能接收某個(gè)消息時(shí)们豌,會(huì)先調(diào)用respondsToSelector:來(lái)判斷一下浅妆。不過(guò)辩尊,我們這邊想討論下不使用respondsToSelector:判斷的情況。

當(dāng)一個(gè)對(duì)象無(wú)法接收某一消息時(shí)康辑,就會(huì)啟動(dòng)所謂”消息轉(zhuǎn)發(fā)(message forwarding)“機(jī)制摄欲,通過(guò)這一機(jī)制轿亮,我們可以告訴對(duì)象如何處理未知的消息。默認(rèn)情況下胸墙,對(duì)象接收到未知的消息我注,會(huì)導(dǎo)致程序崩潰
最初接觸到OC Runtime,一定是從[receiver message]這里開(kāi)始的迟隅。[receiver message]會(huì)被編譯器轉(zhuǎn)化為:

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

這是一個(gè)可變參數(shù)函數(shù)但骨。第二個(gè)參數(shù)類型是SEL。SEL在OC中是selector方法選擇器智袭。

typedef struct objc_selector *SEL;

objc_selector是一個(gè)映射到方法的C字符串奔缠。需要注意的是@selector()選擇只與函數(shù)名有關(guān)。

在receiver拿到對(duì)應(yīng)的selector之后吼野,如果自己無(wú)法執(zhí)行這個(gè)方法校哎,那么該條消息要被轉(zhuǎn)發(fā)◇锎福或者臨時(shí)動(dòng)態(tài)的添加方法實(shí)現(xiàn)贬蛙。如果轉(zhuǎn)發(fā)到最后依舊沒(méi)法處理,程序就會(huì)崩潰谚攒。

所以編譯期僅僅是確定了要發(fā)送消息阳准,而消息如何處理是要運(yùn)行期需要解決的事情。

objc_msgSend函數(shù)究竟會(huì)干什么事情呢馏臭?
2. 消息發(fā)送Messaging階段
當(dāng)我們創(chuàng)建一個(gè)新對(duì)象時(shí)野蝇,先為其分配內(nèi)存,并初始化其成員變量括儒。其中isa指針也會(huì)被初始化绕沈,讓對(duì)象可以訪問(wèn)類及類的繼承體系。
總結(jié)一下objc_msgSend會(huì)做一下幾件事情:

  1. 檢查target是不是為nil帮寻。
  • 如果這里有相應(yīng)的nil的處理函數(shù)乍狐,就跳轉(zhuǎn)到相應(yīng)的函數(shù)中。
  • 如果沒(méi)有處理nil的函數(shù)固逗,就自動(dòng)清理現(xiàn)場(chǎng)并返回浅蚪。這一點(diǎn)就是為何在OC中給nil發(fā)送消息不會(huì)崩潰的原因。
  1. 檢測(cè)這個(gè) selector是不是要忽略的烫罩。
  2. 確定不是給nil發(fā)消息之后惜傲,objc_msgSend通過(guò)對(duì)象的isa指針獲取到類的結(jié)構(gòu)體,在該class的緩存中查找方法對(duì)應(yīng)的IMP實(shí)現(xiàn)贝攒。如果找到盗誊,就跳轉(zhuǎn)進(jìn)去執(zhí)行。如果沒(méi)有找到,就在方法分發(fā)表里面繼續(xù)查找哈踱。如果以上嘗試都失敗了荒适,接下來(lái)就會(huì)循環(huán)嘗試父類的緩存和方法列表,一直找到NSObject為止(因?yàn)镹SObject的superclass為nil(還是它自己开镣?)吻贿,才跳出循環(huán))。一旦定位到selector哑子,函數(shù)會(huì)就獲取到了實(shí)現(xiàn)的入口點(diǎn)舅列,并傳入相應(yīng)的參數(shù)來(lái)執(zhí)行方法的具體實(shí)現(xiàn);如果最后沒(méi)有定位到selector卧蜓,則會(huì)走消息轉(zhuǎn)發(fā)流程

注:為了加速消息的處理帐要,運(yùn)行時(shí)系統(tǒng)緩存使用過(guò)的selector及對(duì)應(yīng)的方法的地址。

至此弥奸,發(fā)送消息Messaging階段完成榨惠。這一階段主要完成的是通過(guò)select()快速查找IMP的過(guò)程。

默認(rèn)情況下盛霎,如果是以 [object message]的方式調(diào)用方法赠橙,如果object無(wú)法響應(yīng)message消息時(shí),編譯器會(huì)報(bào)錯(cuò)愤炸。但如果是以perform…的形式來(lái)調(diào)用期揪,則需要等到運(yùn) 行時(shí)才能確定object是否能接收message消息。如果不能规个,則程序崩潰凤薛。

通常,當(dāng)我們不能確定一個(gè)對(duì)象是否能接收某個(gè)消息時(shí)诞仓,會(huì)先調(diào)用respondsToSelector:來(lái)判斷一下缤苫。如下代碼所示:

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

不過(guò),我們這邊想討論下不使用respondsToSelector:判斷的情況墅拭。

上面提到的活玲,拋出判斷和一般發(fā)送消息的方法:當(dāng)消息傳遞過(guò)程中找不到對(duì)應(yīng)的方法時(shí),會(huì)拋出unrecognzed selector send to instace ...的錯(cuò)誤谍婉,即找不到指定的方法舒憾,在此之前可以在三個(gè)方法中實(shí)現(xiàn)補(bǔ)救。

3. 動(dòng)態(tài)綁定階段
動(dòng)態(tài)綁定屡萤,從名稱來(lái)看就大致懂了珍剑。如果調(diào)用一個(gè)類的方法掸宛,而這個(gè)類及其父類均沒(méi)有實(shí)現(xiàn)這個(gè)方法死陆。那么我們就在運(yùn)行時(shí)綁定此方法到該類。注意我們可以在這里動(dòng)態(tài)增加方法實(shí)現(xiàn),不過(guò)這種方案更多的是為了實(shí)現(xiàn)@dynamic屬性措译。

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

  • 將未能識(shí)別的消息動(dòng)態(tài)添加到接收者的類中,resolveInstanceMethod方法返回的是一個(gè)BOOL類型的值别凤,用于判斷是否接收這消息;
  • 這個(gè)函數(shù)首先判斷是否是meta-class類,如果不是元類领虹,就執(zhí)行_class_resolveInstanceMethod规哪,如果是元類,執(zhí)行_class_resolveClassMethod塌衰。

對(duì)象在接收到未知的消息時(shí)诉稍,首先會(huì)調(diào)用所屬類的類方法-resolveInstanceMethod:(實(shí)例方法)或 者+resolveClassMethod:(類方法)。
在這個(gè)方法中最疆,我們有機(jī)會(huì)為該未知消息新增一個(gè)”處理方法”“杯巨。不過(guò)使用該方法的前提是我們已經(jīng) 實(shí)現(xiàn)了該”處理方法”,只需要在運(yùn)行時(shí)通過(guò)class_addMethod函數(shù)動(dòng)態(tài)添加到類里面就可以了努酸。如下代碼所示:

#import <Foundation/Foundation.h>
@interface Father : NSObject
@end

#import "Father.h"
#import "Son.h"
@implementation Father

- (void)son {
    Son *s = [[Son alloc] init];
    // 默認(rèn)Son服爷,沒(méi)有實(shí)現(xiàn)run方法,可以通過(guò)performSelector調(diào)用获诈,但是會(huì)報(bào)錯(cuò)仍源。
    // 動(dòng)態(tài)添加方法就不會(huì)報(bào)錯(cuò)
    [s performSelector:@selector(run)];
}

@end

son:

#import <Foundation/Foundation.h>
@interface Son : NSObject
@end


#import "Son.h"
#import <objc/runtime.h>
@implementation Son
// void(*)()
// 默認(rèn)方法都有兩個(gè)隱式參數(shù),
void testRun(id self,SEL sel) {
    [Son.new  eat];
    NSLog(@"son is runing");
}

- (void)eat {
    NSLog(@"son is eating");
}
// 當(dāng)一個(gè)對(duì)象調(diào)用未實(shí)現(xiàn)的方法舔涎,會(huì)調(diào)用這個(gè)方法處理,并且會(huì)把對(duì)應(yīng)的方法列表傳過(guò)來(lái).
// 剛好可以用來(lái)判斷笼踩,未實(shí)現(xiàn)的方法是不是我們想要?jiǎng)討B(tài)添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //判斷方法是否是run
    if ([NSStringFromSelector(sel) isEqualToString:@"run"]) {
        // 動(dòng)態(tài)添加run方法

        // 第一個(gè)參數(shù):給哪個(gè)類添加方法
        // 第二個(gè)參數(shù):添加方法的方法編號(hào)
        // 第三個(gè)參數(shù):添加方法的函數(shù)實(shí)現(xiàn)(函數(shù)地址)
        // 第四個(gè)參數(shù):函數(shù)的類型,(返回值+參數(shù)類型) v:void @:對(duì)象->self :表示SEL->_cmd
        class_addMethod([self class], sel, (IMP)testRun, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

打印輸出:
son is eating
son is runing

@selector(run)被動(dòng)態(tài)添加到了Son的類方法列表中亡嫌。

如果也沒(méi)有找到IMP的實(shí)現(xiàn),resloveInstanceMethod:返回NO之后戳表,就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)階段。

4. 消息轉(zhuǎn)發(fā)Message Forwarding階段
如果在上一步無(wú)法處理消息昼伴,則Runtime會(huì)繼續(xù)調(diào)以下方法:
-forwardindTargetWithSelctor:方法匾旭。在這個(gè)方法中,返回的對(duì)象就是message的接收者圃郊,然后會(huì)回到resloveInstanceMethod方法价涝,從新開(kāi)始消息轉(zhuǎn)發(fā)過(guò)程,如果返回nil則會(huì)進(jìn)入下一個(gè)方法中(-forwardInvocation)去判斷是否響應(yīng)這個(gè)消息持舆。

消息轉(zhuǎn)發(fā)機(jī)制基本上分為兩個(gè)步驟:
1. 備用接收者 forwardingTargetForSelector

  • 如果一個(gè)對(duì)象實(shí)現(xiàn)了這個(gè)方法色瘩,并返回一個(gè)非nil的結(jié)果,則這個(gè)對(duì)象會(huì)作為消息的新接收者逸寓,且消息會(huì)被分發(fā)到這個(gè)對(duì)象居兆。當(dāng)然這個(gè)對(duì)象不能是self自身,否則就是出現(xiàn)無(wú)限循環(huán)竹伸。
  • 可借這個(gè)對(duì)象來(lái)處理消息并返回泥栖,這樣在對(duì)象外部看來(lái)簇宽,還是由該對(duì)象親自處理了這一消息。
  • 這一步合適于我們只想將消息轉(zhuǎn)發(fā)到另一個(gè)能處理該消息的對(duì)象上吧享。但這一步無(wú)法對(duì)消息進(jìn)行處理魏割,如操作消息的參數(shù)和返回值。

當(dāng)前的SEL無(wú)法找到相應(yīng)的IMP的時(shí)候钢颂,開(kāi)發(fā)者可以通過(guò)重寫
- (id)forwardingTargetForSelector:(SEL)aSelector方法來(lái)“偷梁換柱”挡爵,把消息的接受者換成一個(gè)可以處理該消息的對(duì)象捡絮。

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

當(dāng)然也可以替換類方法,那就要重寫 + (id)forwardingTargetForSelector:(SEL)aSelector方法,返回值是一個(gè)類對(duì)象兑障。

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

我們將上面的代碼修改了下變成了這樣:
father:

#import <Foundation/Foundation.h>
@interface Father : NSObject
@end


#import "Father.h"
#import "Son.h"
@implementation Father

- (void)son {
    Son *s = [[Son alloc] init];
    [s performSelector:@selector(run)];
}

static void fatherRun() {
    NSLog(@"father is runing");
}

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

@end

son:

#import <Foundation/Foundation.h>
@interface Son : NSObject
@end


#import "Son.h"
#import <objc/runtime.h>
@implementation Son

- (void)eat {
    NSLog(@"son is eating");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    [self eat];
    return [Father new];
}
@end

打游醋础:
son is eating
father is runing

這就是runtime的神奇之處棉钧,消息的接收者由“本應(yīng)該是”的Son轉(zhuǎn)變?yōu)榱薋ather吉执。

這一步是替消息找備援接收者,如果這一步返回的是nil牲尺,那么補(bǔ)救措施就完全的失效了卵酪,Runtime系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對(duì)象谤碳。為接下來(lái)的完整的消息轉(zhuǎn)發(fā)生成一個(gè) NSMethodSignature對(duì)象溃卡。NSMethodSignature 對(duì)象會(huì)被包裝成 NSInvocation 對(duì)象,forwardInvocation:方法里就可以對(duì) NSInvocation 進(jìn)行處理了蜒简。

消息轉(zhuǎn)發(fā)第二步:如果在上一步還不能處理未知消息瘸羡,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。此時(shí)會(huì)進(jìn)入完整轉(zhuǎn)發(fā)階段

2. 完整轉(zhuǎn)發(fā) forwardInvocation
我們只需要重寫下面這個(gè)方法搓茬,就可以自定義我們自己的轉(zhuǎn)發(fā)邏輯了犹赖。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

補(bǔ)充:NSMethodSignature 和 NSInvocation
在 iOS中可以直接調(diào)用某個(gè)對(duì)象的消息方式有兩種:

  • 一種是performSelector:withObject;
  • 再一種就是NSInvocation卷仑。

第一種方式比較簡(jiǎn)單峻村,能完成簡(jiǎn)單的調(diào)用。但是對(duì)于>2個(gè)的參數(shù)或者有返回值的處理锡凝,那performSelector:withObject就顯得有點(diǎn)有心無(wú)力了粘昨,那么在這種情況下,我們就可以使用NSInvocation來(lái)進(jìn)行這些相對(duì)復(fù)雜的操作窜锯。
NSInvocation介紹:

  • NSInvocation中保存了方法所屬的對(duì)象/方法名稱/參數(shù)/返回值
  • NSInvocation就是將一個(gè)方法變成一個(gè)對(duì)象

NSMethodSignature:方法簽名類

  • 方法簽名類中保存了方法的名稱/參數(shù)/返回值张肾,協(xié)同NSInvocation來(lái)進(jìn)行消息的轉(zhuǎn)發(fā)
  • 方法簽名類一般是用來(lái)設(shè)置參數(shù)和獲取返回值的, 和方法的調(diào)用沒(méi)有太大的關(guān)系

NSInvocation的基本使用
1.根據(jù)方法來(lái)初始化NSMethodSignature.方法簽名類

NSMethodSignature  *signature = [ViewController instanceMethodSignatureForSelector:@selector(run:)];

2.根據(jù)方法簽名類來(lái)創(chuàng)建NSInvocation對(duì)象

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
//設(shè)置方法調(diào)用者
invocation.target = self;
//注意:這里的方法名一定要與方法簽名類中的方法一致
invocation.selector = @selector(run:);
NSString *way = @"byCar";
//這里的Index要從2開(kāi)始,以為0跟1已經(jīng)被占據(jù)了锚扎,分別是self(target),selector(_cmd)
[invocation setArgument:&way atIndex:2];
//3吞瞪、調(diào)用invoke方法
[invocation invoke];
//實(shí)現(xiàn)run:方法
- (void)run:(NSString *)method{

}

** NSMethodSignature 和 NSInvocation的基本使用補(bǔ)充結(jié)束!**

運(yùn)行時(shí)系統(tǒng)會(huì)在這一步給消息接收者最后一次機(jī)會(huì)將消息轉(zhuǎn)發(fā)給其它對(duì)象驾孔。對(duì)象會(huì)創(chuàng)建一個(gè)表示消息的NSInvocation對(duì)象芍秆,把與尚未處理的消息 有關(guān)的全部細(xì)節(jié)都封裝在anInvocation中惯疙,包括selector,目標(biāo)(target)和參數(shù)浪听。我們可以在forwardInvocation 方法中選擇將消息轉(zhuǎn)發(fā)給其它對(duì)象。

forwardInvocation:方法的實(shí)現(xiàn)有兩個(gè)任務(wù):

  • 定位可以響應(yīng)封裝在anInvocation中的消息的對(duì)象眉菱。這個(gè)對(duì)象不需要能處理所有未知消息迹栓。
  • 使用anInvocation作為參數(shù),將消息發(fā)送到選中的對(duì)象俭缓。anInvocation將會(huì)保留調(diào)用結(jié)果克伊,運(yùn)行時(shí)系統(tǒng)會(huì)提取這一結(jié)果并將其發(fā)送到消息的原始發(fā)送者。

不過(guò)华坦,在這個(gè)方法中我們可以實(shí)現(xiàn)一些更復(fù)雜的功能愿吹,我們可以對(duì)消息的內(nèi)容進(jìn)行修改,比如追回一個(gè)參數(shù)等惜姐,然后再去觸發(fā)消息犁跪。另外,若發(fā)現(xiàn)某個(gè)消息不應(yīng)由本類處理歹袁,則應(yīng)調(diào)用父類的同名方法坷衍,以便繼承體系中的每個(gè)類都有機(jī)會(huì)處理此調(diào)用請(qǐng)求。

還有一個(gè)很重要的問(wèn)題条舔,我們必須重寫以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息轉(zhuǎn)發(fā)機(jī)制使用從這個(gè)方法中獲取的信息來(lái)創(chuàng)建NSInvocation對(duì)象枫耳。因此我們必須重寫這個(gè)方法,為給定的selector提供一個(gè)合適的方法簽名孟抗。

完整的示例如下所示:
修改上面的代碼為:
father:

#import <Foundation/Foundation.h>
@interface Father : NSObject
@end


#import "Father.h"
#import "Son.h"
@implementation Father

- (void)son {
    Son *s = [[Son alloc] init];
    [s performSelector:@selector(run)];
}

- (void)run {
    NSLog(@"father is running");
}

@end

son:

#import <Foundation/Foundation.h>
@interface Son : NSObject
@end


#import "Son.h"
#import <objc/runtime.h>
@implementation Son

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {

    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

    Father *f = [[Father alloc] init];
    //改變selector
    [anInvocation setSelector:@selector(run)];
    //在這里指定消息接收者迁杨,如果不指定的話還是會(huì)拋出找不到方法的異常
    [anInvocation invokeWithTarget:f];
}

@end

打印:father is running

我們已經(jīng)成功的將消息轉(zhuǎn)發(fā)給了Father實(shí)例凄硼。以上就是消息轉(zhuǎn)發(fā)的流程和具體實(shí)踐铅协。

NSObject的forwardInvocation:方法實(shí)現(xiàn)只是簡(jiǎn)單調(diào)用了doesNotRecognizeSelector:方法,它不會(huì)轉(zhuǎn)發(fā)任何消息摊沉。這樣警医,如果不在以上所述的三個(gè)步驟中處理未知消息,則會(huì)引發(fā)一個(gè)異常坯钦。

從某種意義上來(lái)講预皇,forwardInvocation:就像一個(gè)未知消息的分發(fā)中心,將這些未知的消息轉(zhuǎn)發(fā)給其它對(duì)象婉刀∫魑拢或者也可以像一個(gè)運(yùn)輸站一樣將所有未知消息都發(fā)送給同一個(gè)接收對(duì)象。這取決于具體的實(shí)現(xiàn)突颊。

實(shí)現(xiàn)此方法之后鲁豪,若發(fā)現(xiàn)某調(diào)用不應(yīng)由本類處理潘悼,則會(huì)調(diào)用超類的同名方法。如此爬橡,繼承體系中的每個(gè)類都有機(jī)會(huì)處理該方法調(diào)用的請(qǐng)求治唤,一直到NSObject根類。如果到NSObject也不能處理該條消息糙申,那么就是再無(wú)挽救措施了宾添,只能拋出“does Not Recognize Selector”異常了。

消息傳遞流程圖.jpg

至此柜裸,消息發(fā)送和轉(zhuǎn)發(fā)的過(guò)程結(jié)束缕陕。

4. 應(yīng)用場(chǎng)景

** 1.實(shí)現(xiàn)多繼承**
** 2.Method Swizzling(方法攪拌)**
** 3.Associated Object(關(guān)聯(lián)對(duì)象)**
** 4.動(dòng)態(tài)的增加方法**
** 5.NSCoding的自動(dòng)歸檔和自動(dòng)解檔**
** 6.字典和模型互相轉(zhuǎn)換**

1. 實(shí)現(xiàn)多繼承
根據(jù)上述任意消息轉(zhuǎn)發(fā)樣例,可知實(shí)現(xiàn)了在A中調(diào)用B中的方法b疙挺,大概可以猜到扛邑,這是不是類似繼承機(jī)制?答案是肯定的铐然,因?yàn)镺C不支持多繼承蔬崩,此處就給了一個(gè)實(shí)現(xiàn)多繼承的方式,因?yàn)槲覀兛梢詫?shí)現(xiàn)任意個(gè)類消息的轉(zhuǎn)發(fā)
通過(guò)forwardingTargetForSelector:方法搀暑,一個(gè)類可以做到繼承多個(gè)類的效果舱殿,只需要在這一步將消息轉(zhuǎn)發(fā)給正確的類對(duì)象就可以模擬多繼承的效果。
在OC程序中可以借用消息轉(zhuǎn)發(fā)機(jī)制來(lái)實(shí)現(xiàn)多繼承的功能险掀。 一個(gè)對(duì)象對(duì)一個(gè)消息做出回應(yīng)沪袭,類似于另一個(gè)對(duì)象中的方法借過(guò)來(lái)或是“繼承”過(guò)來(lái)一樣。A實(shí)例轉(zhuǎn)發(fā)了一個(gè)SEL消息到B實(shí)例中樟氢,執(zhí)行B中的SEL方法冈绊,結(jié)果看起來(lái)像是A實(shí)例執(zhí)行了一個(gè)和B實(shí)例一樣的SEL方法,其實(shí)執(zhí)行者還是B實(shí)例埠啃。

消息轉(zhuǎn)發(fā)提供了許多類似于多繼承的特性死宣,但是他們之間有一個(gè)很大的不同:

  • 多繼承:合并了不同的行為特征在一個(gè)單獨(dú)的對(duì)象中,會(huì)得到一個(gè)重量級(jí)多層面的對(duì)象碴开。
  • 消息轉(zhuǎn)發(fā):將各個(gè)功能分散到不同的對(duì)象中毅该,得到的一些輕量級(jí)的對(duì)象,這些對(duì)象通過(guò)消息通過(guò)消息轉(zhuǎn)發(fā)聯(lián)合起來(lái)潦牛。

這里值得說(shuō)明的一點(diǎn)是眶掌,即使我們利用轉(zhuǎn)發(fā)消息來(lái)實(shí)現(xiàn)了“假”繼承,但是NSObject類還是會(huì)將兩者區(qū)分開(kāi)巴碗。像respondsToSelector:和 isKindOfClass:這類方法只會(huì)考慮繼承體系朴爬,不會(huì)考慮轉(zhuǎn)發(fā)鏈。

如果非要制造假象橡淆,反應(yīng)出這種“假”的繼承關(guān)系召噩,那么需要重新實(shí)現(xiàn) respondsToSelector:和 isKindOfClass:來(lái)加入你的轉(zhuǎn)發(fā)算法

如果一個(gè)對(duì)象轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息母赵,它得給出一個(gè)methodSignatureForSelector:來(lái)返回準(zhǔn)確的方法描述,這個(gè)方法會(huì)最終響應(yīng)被轉(zhuǎn)發(fā)的消息具滴。比如一個(gè)對(duì)象能給它的替代者對(duì)象轉(zhuǎn)發(fā)消息凹嘲,它需要像下面這樣實(shí)現(xiàn)methodSignatureForSelector:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

需要引起注意的一點(diǎn),實(shí)現(xiàn)methodSignatureForSelector方法是一種先進(jìn)的技術(shù)构韵,只適用于沒(méi)有其他解決方案的情況下周蹭。它不會(huì)作為繼承的替代。如果您必須使用這種技術(shù)贞绳,請(qǐng)確保您完全理解類做的轉(zhuǎn)發(fā)和您轉(zhuǎn)發(fā)的類的行為谷醉。請(qǐng)勿濫用致稀!

2.Method Swizzling(方法攪拌)
它可以通過(guò)Runtime的API實(shí)現(xiàn)更改任意的方法冈闭,理論上可以在運(yùn)行時(shí)通過(guò)類名/方法名hook到任何 OC 方法,替換任何類的實(shí)現(xiàn)以及新增任意類抖单。
核心函數(shù)就是method_exchangeImplementations
Method Swizzling原理:
Method Swizzling本質(zhì)上就是對(duì)IMP和SEL進(jìn)行交換萎攒。Method Swizzing是發(fā)生在運(yùn)行時(shí)的,主要用于在運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換矛绘,我們可以將Method Swizzling代碼寫到任何地方耍休,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用。而且Method Swizzling也是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式货矮,我們可以利用蘋果這一特性來(lái)實(shí)現(xiàn)AOP編程羊精。

優(yōu)勢(shì):可以重寫某個(gè)方法而不用繼承,同時(shí)還可以調(diào)用原先的實(shí)現(xiàn)囚玫。通常的做法是在category中添加一個(gè)方法(當(dāng)然也可以是一個(gè)全新的class)喧锦。

具體場(chǎng)景應(yīng)用:替換兩個(gè)方法的實(shí)現(xiàn),可以做一些埋點(diǎn)抓督、容錯(cuò)等工作燃少;AFN也用到了,每一次resume铃在,都會(huì)用到method swizzling阵具。
一般我們使用都是新建一個(gè)分類,在分類中進(jìn)行Method Swizzling方法的交換:
具體場(chǎng)景應(yīng)用1:

#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(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

Method Swizzling可以在運(yùn)行時(shí)通過(guò)修改類的方法列表中selector對(duì)應(yīng)的函數(shù)或者設(shè)置交換方法實(shí)現(xiàn)定铜,來(lái)動(dòng)態(tài)修改方法阳液。可以重寫某個(gè)方法而不用繼承,同時(shí)還可以調(diào)用原先的實(shí)現(xiàn)揣炕。所以通常應(yīng)用于在category中添加一個(gè)方法趁舀。

補(bǔ)充:在我們替換的方法- (void)xxx_viewWillAppear:(BOOL)animated中,調(diào)用了[self xxx_viewWillAppear:animated];這不是死循環(huán)了么祝沸?
其實(shí)這里并不會(huì)死循環(huán)矮烹。由于我們進(jìn)行了Swizzling越庇,所以其實(shí)在原來(lái)的- (void)viewWillAppear:(BOOL)animated方法中,調(diào)用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的實(shí)現(xiàn)奉狈。所以不會(huì)造成死循環(huán)卤唉。相反的,如果這里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就會(huì)造成死循環(huán)仁期。因?yàn)橥饷嬲{(diào)用[self viewWillAppear:animated];的時(shí)候桑驱,會(huì)交換方法走到[self xxx_viewWillAppear:animated];這個(gè)方法實(shí)現(xiàn)中來(lái),然后這里又去調(diào)用[self viewWillAppear:animated]跛蛋,就會(huì)造成死循環(huán)了熬的。

具體場(chǎng)景應(yīng)用2:NSArray數(shù)組越界容錯(cuò)處理
常見(jiàn)做法是給NSArray,NSMutableArray增加分類赊级,增加這些異常保護(hù)的方法押框,不過(guò)如果原有工程里面已經(jīng)寫了大量的AtIndex系列的方法,去替換成新的分類的方法理逊,效率會(huì)比較低橡伞。這里可以考慮用Swizzling做。

#import "NSArray+ Swizzling.h"
#import "objc/runtime.h"
@implementation NSArray (Swizzling)
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzling_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)swizzling_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 異常處理
        @try {
            return [self swizzling_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 打印崩潰信息
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } else {
        return [self swizzling_objectAtIndex:index];
    }
}
@end

注意:調(diào)用這個(gè)objc_getClass方法的時(shí)候晋被,要先知道類對(duì)應(yīng)的真實(shí)的類名才行兑徘,NSArray其實(shí)在Runtime中對(duì)應(yīng)著__NSArrayI,NSMutableArray對(duì)應(yīng)著__NSArrayM羡洛,NSDictionary對(duì)應(yīng)著__NSDictionaryI挂脑,NSMutableDictionary對(duì)應(yīng)著__NSDictionaryM

Method Swizzling注意點(diǎn):

  • ** Swizzling應(yīng)該總在+load中執(zhí)行欲侮;**
  • ** Swizzling應(yīng)該總是在dispatch_once中執(zhí)行:**
    Swizzling會(huì)改變?nèi)譅顟B(tài)崭闲,所以在運(yùn)行時(shí)采取一些預(yù)防措施,使用dispatch_once就能夠確保代碼不管有多少線程都只被執(zhí)行一次锈麸。
    這里有一個(gè)很容易犯的錯(cuò)誤镀脂,那就是繼承中用了Swizzling。如果不寫dispatch_once就會(huì)導(dǎo)致Swizzling失效忘伞!
    舉個(gè)例子薄翅,比如同時(shí)對(duì)NSArray和NSMutableArray中的objectAtIndex:方法都進(jìn)行了Swizzling,這樣可能會(huì)導(dǎo)致NSArray中的Swizzling失效的氓奈。
    原因是:我們沒(méi)有用dispatch_once控制Swizzling只執(zhí)行一次翘魄。如果這段Swizzling被執(zhí)行多次,經(jīng)過(guò)多次的交換IMP和SEL之后舀奶,結(jié)果可能就是未交換之前的狀態(tài)暑竟。
    3. Swizzling在+load中執(zhí)行時(shí),不要調(diào)用[super load]
    原因同注意點(diǎn)二,如果是多繼承但荤,并且對(duì)同一個(gè)方法都進(jìn)行了Swizzling罗岖,那么調(diào)用[super load]以后,父類的Swizzling就失效了腹躁。

兩個(gè)類方法區(qū)別
我們知道了 Objective-C 中絕大部分的類都繼承自 NSObject 類桑包。而在 NSObject 類中有兩個(gè)非常特殊的類方法 +load 和 +initialize ,用于類的初始化纺非。
+load

  • +load 方法是當(dāng)類或分類被添加到 Objective-C runtime 時(shí)被調(diào)用的哑了,+load會(huì)在類初始加載時(shí)調(diào)用。
  • 子類的 +load 方法會(huì)在它的所有父類的 +load 方法之后執(zhí)行烧颖,而分類的 +load 方法會(huì)在它的主類的 +load 方法之后執(zhí)行弱左。但是不同的類之間的 +load 方法的調(diào)用順序是不確定的。
  • 只調(diào)用一次
  • 不能使用Super且不沿用父類實(shí)現(xiàn)

+initialize

  • +initialize 方法是在類或它的子類收到第一條消息之前被調(diào)用的炕淮,這里所指的消息包括實(shí)例方法和類方法的調(diào)用拆火。也就是說(shuō) +initialize 方法是以懶加載的方式被調(diào)用的,如果程序一直沒(méi)有給某個(gè)類或它的子類發(fā)送消息鳖悠,那么這個(gè)類的 +initialize 方法是永遠(yuǎn)不會(huì)被調(diào)用的榜掌。
  • 調(diào)用次數(shù)多次

總結(jié)
+load 和 +initialize 調(diào)用機(jī)制和各自的特點(diǎn)

| | +load | +initialize
|-----------------
| 調(diào)用時(shí)機(jī) | 被添加到 runtime 時(shí) | 收到第一條消息前优妙,可能永遠(yuǎn)不調(diào)用
| 調(diào)用順序 | 父類->子類->分類 | 父類->子類
| 調(diào)用次數(shù) | 1次 | 多次
| 是否需要顯式調(diào)用父類實(shí)現(xiàn) | 否 | 否
| 是否沿用父類的實(shí)現(xiàn) | 否 | 是
| 分類中的實(shí)現(xiàn) | 類和分類都執(zhí)行 | 覆蓋類中的方法乘综,只執(zhí)行分類的實(shí)現(xiàn)

3. Associated Object(關(guān)聯(lián)對(duì)象)
Category
Category是表示一個(gè)指向分類的結(jié)構(gòu)體的指針.
需要注意的有兩點(diǎn):

  • category的方法沒(méi)有“完全替換掉”原來(lái)類已經(jīng)有的方法,也就是說(shuō)如果category和原來(lái)類都有methodA套硼,那么category附加完成之后卡辰,類的方法列表里會(huì)有兩個(gè)methodA
  • category的方法被放到了新方法列表的前面,而原來(lái)類的方法被放到了新方法列表的后面邪意,這也就是我們平常所說(shuō)的category的方法會(huì)“覆蓋”掉原來(lái)類的同名方法九妈,這是因?yàn)檫\(yùn)行時(shí)在查找方法的時(shí)候是順著方法列表的順序查找的,它只要一找到對(duì)應(yīng)名字的方法雾鬼,就會(huì)罷休萌朱,殊不知后面可能還有一樣名字的方法。

在 Category 中策菜,我們無(wú)法添加@property晶疼,因?yàn)樘砑恿薂property之后并不會(huì)自動(dòng)幫我們生成實(shí)例變量以及存取方法。

關(guān)聯(lián)對(duì)象API在runtime里又憨,所有的關(guān)聯(lián)對(duì)象都由AssociationsManager管理
使用objc_setAssociate()能夠?qū)⒁粋€(gè)變量通過(guò)指定的key值講實(shí)例與實(shí)例變量綁定在一起翠霍,在讀取的時(shí)候值調(diào)用objc_getAssociate(),在指定的實(shí)例中通過(guò)key將變量取出蠢莺,可以簡(jiǎn)單理解成字典一樣存取
這里涉及到了3個(gè)函數(shù):

//setter寒匙,就像字典中的 setValue:ForKey:
void objc_setAssociatedOject(id object, void *key, id value, objc_AssociationPolicy policy)

//getter,就像字典中的 objectForKey
id objc_getAssociatedObject(id object, void *key)

//remove躏将,就像字典中的 removeAllObject
void objc_removeAssocaitedObjected(id object)

那么锄弱,我們現(xiàn)在就可以通過(guò)關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)在 Category 中添加屬性的功能了考蕾。

// NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
// NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
    objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
    return objc_getAssociatedObject(self, @selector(associatedObject));
}

原理:給一個(gè)類聲明屬性,其實(shí)本質(zhì)就是給這個(gè)類添加關(guān)聯(lián)会宪,并不是直接把這個(gè)值的內(nèi)存空間添加到類存空間辕翰。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 給系統(tǒng)NSObject類動(dòng)態(tài)添加屬性name

    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"aaaaa";
    NSLog(@"%@",objc.name);

}


@end


// 定義關(guān)聯(lián)的key
static const char *key = "name";

@implementation NSObject (Property)

- (NSString *)name
{
    // 根據(jù)關(guān)聯(lián)的key,獲取關(guān)聯(lián)的值狈谊。
    return objc_getAssociatedObject(self, key);
}

- (void)setName:(NSString *)name
{
    // 第一個(gè)參數(shù):給哪個(gè)對(duì)象添加關(guān)聯(lián)
    // 第二個(gè)參數(shù):關(guān)聯(lián)的key喜命,通過(guò)這個(gè)key獲取
    // 第三個(gè)參數(shù):關(guān)聯(lián)的value
    // 第四個(gè)參數(shù):關(guān)聯(lián)的策略
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

關(guān)聯(lián)對(duì)象3種使用場(chǎng)景

  1. 為現(xiàn)有的類添加私有變量
  2. 為現(xiàn)有的類添加公有屬性
  3. 為KVO創(chuàng)建一個(gè)關(guān)聯(lián)的觀察者。

4.動(dòng)態(tài)的增加方法
在消息發(fā)送階段河劝,如果在父類中也沒(méi)有找到相應(yīng)的IMP壁榕,就會(huì)執(zhí)行resolveInstanceMethod方法。在這個(gè)方法里面赎瞎,我們可以動(dòng)態(tài)的給類對(duì)象或者實(shí)例對(duì)象動(dòng)態(tài)的增加方法牌里。

當(dāng)你發(fā)送了一個(gè)object無(wú)法處理的消息時(shí)會(huì)發(fā)生什么呢?很明顯务甥,"it breaks"牡辽。大多數(shù)情況下確實(shí)如此,但Cocoa和runtime也提供了一些應(yīng)對(duì)方法敞临。
首先是動(dòng)態(tài)方法處理态辛。通常來(lái)說(shuō),處理一個(gè)方法挺尿,運(yùn)行時(shí)尋找匹配的selector然后執(zhí)行之奏黑。有時(shí),你只想在運(yùn)行時(shí)才創(chuàng)建某個(gè)方法编矾,比如有些信息只有在運(yùn)行時(shí)才能得到熟史。要實(shí)現(xiàn)這個(gè)效果,你需要重寫+resolveInstanceMethod: 和/或 +resolveClassMethod:窄俏。如果確實(shí)增加了一個(gè)方法蹂匹,記得返回YES。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"method1"]) {
        class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
    }
    return [super resolveInstanceMethod:sel];
}

5.NSCoding的自動(dòng)歸檔和自動(dòng)解檔
現(xiàn)在雖然手寫歸檔和解檔的時(shí)候不多了凹蜈,但是自動(dòng)操作還是用Runtime來(lái)實(shí)現(xiàn)的限寞。直接取出全部的實(shí)例變量列表+for循環(huán)。

手動(dòng)實(shí)現(xiàn):手動(dòng)的有一個(gè)缺陷踪区,如果屬性多起來(lái)昆烁,要寫好多行相似的代碼,雖然功能是可以完美實(shí)現(xiàn)缎岗,但是看上去不是很優(yōu)雅静尼。

- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
}

- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

用runtime實(shí)現(xiàn)的思路就比較簡(jiǎn)單,我們循環(huán)依次找到每個(gè)成員變量的名稱,然后利用KVC讀取和賦值就可以完成encodeWithCoder和initWithCoder了鼠渺。

#import "Student.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Student

- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar var = vars[i];
        const char *name = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:name];

        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}
@end
  • class_copyIvarList方法用來(lái)獲取當(dāng)前 Model 的所有成員變量
  • ivar_getName方法用來(lái)獲取每個(gè)成員變量的名稱鸭巴。

6.字典和模型互相轉(zhuǎn)換

先來(lái)了解下KVC的底層原理:取出字典中的鍵值,去模型中找與之對(duì)應(yīng)的屬性

  1. 去模型中查找有沒(méi)有setValue:拦盹,直接調(diào)用這個(gè)對(duì)象setValue:賦值
  2. 如果沒(méi)有setValue:鹃祖,就在模型中查找_value屬性
  3. 如果沒(méi)有_value屬性,就查找value屬性
  4. 如果還沒(méi)有就報(bào)錯(cuò)

利用runtime轉(zhuǎn)換原理:與KVC相反普舆,先在模型中找到對(duì)應(yīng)的成員變量恬口,然后去字典中找到對(duì)應(yīng)的數(shù)據(jù)進(jìn)行賦值。

  1. 獲取成員變量列表 :class_copyIvarList沼侣,進(jìn)而獲取模型中的所有實(shí)例變量
  • 將他們加入到一個(gè)數(shù)組當(dāng)中祖能,然后遍歷數(shù)組,在遍歷過(guò)程中獲取字典中對(duì)應(yīng)的value給屬性對(duì)象賦值蛾洛。

字典轉(zhuǎn)模型

  1. 調(diào)用 class_getProperty 方法獲取當(dāng)前 Model 的所有屬性养铸。
  2. 調(diào)用 property_copyAttributeList 獲取屬性列表。
  3. 根據(jù)屬性名稱生成 setter 方法轧膘。
  4. 使用 objc_msgSend 調(diào)用 setter 方法為 Model 的屬性賦值(或者 KVC)
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 解析Plist文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];

    NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];

    // 獲取字典數(shù)組
    NSArray *dictArr = statusDict[@"statuses"];

    // 自動(dòng)生成模型的屬性字符串
//    [NSObject resolveDict:dictArr[0][@"user"]];


    _statuses = [NSMutableArray array];

    // 遍歷字典數(shù)組
    for (NSDictionary *dict in dictArr) {

        Status *status = [Status modelWithDict:dict];

        [_statuses addObject:status];

    }

    // 測(cè)試數(shù)據(jù)
    NSLog(@"%@ %@",_statuses,[_statuses[0] user]);


}

@end
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 思路:遍歷模型中所有屬性-》使用運(yùn)行時(shí)

    // 0.創(chuàng)建對(duì)應(yīng)的對(duì)象
    id objc = [[self alloc] init];

    // 1.利用runtime給對(duì)象中的成員屬性賦值

    // class_copyIvarList:獲取類中的所有成員屬性
    // Ivar:成員屬性的意思
    // 第一個(gè)參數(shù):表示獲取哪個(gè)類中的成員屬性
    // 第二個(gè)參數(shù):表示這個(gè)類有多少成員屬性钞螟,傳入一個(gè)Int變量地址,會(huì)自動(dòng)給這個(gè)變量賦值
    // 返回值Ivar *:指的是一個(gè)ivar數(shù)組谎碍,會(huì)把所有成員屬性放在一個(gè)數(shù)組中鳞滨,通過(guò)返回的數(shù)組就能全部獲取到。
    /* 類似下面這種寫法

     Ivar ivar;
     Ivar ivar1;
     Ivar ivar2;
     // 定義一個(gè)ivar的數(shù)組a
     Ivar a[] = {ivar,ivar1,ivar2};

     // 用一個(gè)Ivar *指針指向數(shù)組第一個(gè)元素
     Ivar *ivarList = a;

     // 根據(jù)指針訪問(wèn)數(shù)組第一個(gè)元素
     ivarList[0];

     */
    unsigned int count;

    // 獲取類中的所有成員屬性
    Ivar *ivarList = class_copyIvarList(self, &count);

    for (int i = 0; i < count; i++) {
        // 根據(jù)角標(biāo)椿浓,從數(shù)組取出對(duì)應(yīng)的成員屬性
        Ivar ivar = ivarList[i];

        // 獲取成員屬性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員屬性名->字典中的key
        // 從第一個(gè)角標(biāo)開(kāi)始截取
        NSString *key = [name substringFromIndex:1];

        // 根據(jù)成員屬性名去字典中查找對(duì)應(yīng)的value
        id value = dict[key];

        // 二級(jí)轉(zhuǎn)換:如果字典中還有字典太援,也需要把對(duì)應(yīng)的字典轉(zhuǎn)換成模型
        // 判斷下value是否是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            // 字典轉(zhuǎn)模型
            // 獲取模型的類對(duì)象闽晦,調(diào)用modelWithDict
            // 模型的類名已知扳碍,就是成員屬性的類型

            // 獲取成員屬性類型
           NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
          // 生成的是這種@"@\"User\"" 類型 -》 @"User"  在OC字符串中 \" -> ",\是轉(zhuǎn)義的意思仙蛉,不占用字符
            // 裁剪類型字符串
            NSRange range = [type rangeOfString:@"\""];

           type = [type substringFromIndex:range.location + range.length];

            range = [type rangeOfString:@"\""];

            // 裁剪到哪個(gè)角標(biāo)笋敞,不包括當(dāng)前角標(biāo)
          type = [type substringToIndex:range.location];


            // 根據(jù)字符串類名生成類對(duì)象
            Class modelClass = NSClassFromString(type);


            if (modelClass) { // 有對(duì)應(yīng)的模型才需要轉(zhuǎn)

                // 把字典轉(zhuǎn)模型
                value  =  [modelClass modelWithDict:value];
            }


        }

        // 三級(jí)轉(zhuǎn)換:NSArray中也是字典,把數(shù)組中的字典轉(zhuǎn)換成模型.
        // 判斷值是否是數(shù)組
        if ([value isKindOfClass:[NSArray class]]) {
            // 判斷對(duì)應(yīng)類有沒(méi)有實(shí)現(xiàn)字典數(shù)組轉(zhuǎn)模型數(shù)組的協(xié)議
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 轉(zhuǎn)換成id類型荠瘪,就能調(diào)用任何對(duì)象的方法
                id idSelf = self;

                // 獲取數(shù)組中字典對(duì)應(yīng)的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
               Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數(shù)組夯巷,生成模型數(shù)組
                for (NSDictionary *dict in value) {
                    // 字典轉(zhuǎn)模型
                  id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }

                // 把模型數(shù)組賦值給value
                value = arrM;

            }
        }


        if (value) { // 有值,才需要給模型的屬性賦值
            // 利用KVC給模型中的屬性賦值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

@end

注意:這段代碼里面有一處判斷typeString的哀墓,這里判斷是防止model嵌套趁餐,比如說(shuō)Student里面還有一層Student,那么這里就需要再次轉(zhuǎn)換一次篮绰,當(dāng)然這里有幾層就需要轉(zhuǎn)換幾次后雷。

幾個(gè)出名的開(kāi)源庫(kù)JSONModel、MJExtension等都是通過(guò)這種方式實(shí)現(xiàn)的(利用runtime的class_copyIvarList獲取屬性數(shù)組,遍歷模型對(duì)象的所有成員屬性臀突,根據(jù)屬性名找到字典中key值進(jìn)行賦值勉抓,當(dāng)然這種方法只能解決NSString、NSNumber等候学,如果含有NSArray或NSDictionary藕筋,還要進(jìn)行第二步轉(zhuǎn)換,如果是字典數(shù)組梳码,需要遍歷數(shù)組中的字典隐圾,利用objectWithDict方法將字典轉(zhuǎn)化為模型,在將模型放到數(shù)組中掰茶,最后把這個(gè)模型數(shù)組賦值給之前的字典數(shù)組)

模型轉(zhuǎn)字典
這里是上一部分字典轉(zhuǎn)模型的逆步驟:

  1. 調(diào)用 class_copyPropertyList 方法獲取當(dāng)前 Model 的所有屬性翎承。
  2. 調(diào)用 property_getName 獲取屬性名稱。
  3. 根據(jù)屬性名稱生成 getter 方法符匾。
  4. 使用 objc_msgSend 調(diào)用 getter 方法獲取屬性值(或者 KVC)
//模型轉(zhuǎn)字典
-(NSDictionary *)keyValuesWithObject{
    unsigned int outCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (int i = 0; i < outCount; i ++) {
        objc_property_t property = propertyList[i];

        //生成getter方法叨咖,并用objc_msgSend調(diào)用
        const char *propertyName = property_getName(property);
        SEL getter = sel_registerName(propertyName);
        if ([self respondsToSelector:getter]) {
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);

            /*判斷當(dāng)前屬性是不是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                value = [value keyValuesWithObject];
            }

            if (value) {
                NSString *key = [NSString stringWithUTF8String:propertyName];
                [dict setObject:value forKey:key];
            }
        }

    }
    free(propertyList);
    return dict;
}

注:中間注釋那里的判斷也是防止model嵌套,如果model里面還有一層model啊胶,那么model轉(zhuǎn)字典的時(shí)候還需要再次轉(zhuǎn)換甸各,同樣,有幾層就需要轉(zhuǎn)換幾次焰坪。
不過(guò)上述的做法是假設(shè)字典里面不再包含二級(jí)字典趣倾,如果還包含數(shù)組,數(shù)組里面再包含字典某饰,那還需要多級(jí)轉(zhuǎn)換儒恋。

7.動(dòng)態(tài)獲取 class 和 slector
比較基礎(chǔ)的一個(gè)動(dòng)態(tài)特性是通過(guò)String來(lái)生成Classes和Selectors。Cocoa提供了NSClassFromString和NSSelectorFromString方法黔漂,使用起來(lái)很簡(jiǎn)單:

Class stringclass = NSClassFromString(@"NSString");
NSString *myString = [stringclass stringWithString:@"Hello World"];
NSClassFromString(@"MyClass");
NSSelectorFromString(@"showShareActionSheet");

在NSObject協(xié)議中诫尽,有以下5個(gè)方法,我們可以通過(guò)NSObject的一些方法獲取運(yùn)行時(shí)信息或動(dòng)態(tài)執(zhí)行一些消息:

// 獲取對(duì)象對(duì)應(yīng)的class
- (Class)class;
// 判斷一個(gè)對(duì)象或者類是不是某個(gè)class或者這個(gè)class的派生類
- (BOOL)isKindOfClass:(Class)aClass;
// 判斷是否是該類的實(shí)例,不包括子類或者父類炬守;
- (BOOL)isMemberOfClass:(Class)aClass;
// 判斷一個(gè)對(duì)象或者類對(duì)應(yīng)的objc_class里面是否實(shí)現(xiàn)了某個(gè)協(xié)議
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 判斷一個(gè)對(duì)象或者類對(duì)應(yīng)的objc_class里面有沒(méi)有某個(gè)方法
- (BOOL)respondsToSelector:(SEL)aSelector;

自動(dòng)打印屬性字符串

    @implementation NSObject (Log)


// 自動(dòng)打印屬性字符串
+ (void)resolveDict:(NSDictionary *)dict{

    // 拼接屬性字符串代碼
    NSMutableString *strM = [NSMutableString string];

    // 1.遍歷字典牧嫉,把字典中的所有key取出來(lái),生成對(duì)應(yīng)的屬性代碼
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

        // 類型經(jīng)常變减途,抽出來(lái)
         NSString *type;

        if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
            type = @"NSString";
        }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
            type = @"NSArray";
        }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
            type = @"int";
        }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
            type = @"NSDictionary";
        }


        // 屬性字符串
        NSString *str;
        if ([type containsString:@"NS"]) {
            str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
        }else{
            str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
        }

        // 每生成屬性字符串酣藻,就自動(dòng)換行。
        [strM appendFormat:@"\n%@\n",str];

    }];

    // 把拼接好的字符串打印出來(lái)鳍置,就好了辽剧。
    NSLog(@"%@",strM);
}
@end
5.Runtime缺點(diǎn)及Runtime常用函數(shù)**

1.危險(xiǎn)性主要體現(xiàn)以下幾個(gè)方面:

  • Method swizzling不是原子性操作。如果在+load方法里面寫税产,是沒(méi)有問(wèn)題的怕轿,但是如果寫在+initialize方法中就會(huì)出問(wèn)題坊夫。

  • 調(diào)用super方法會(huì)出問(wèn)題
    如果你在一個(gè)類中重寫一個(gè)方法,并且不調(diào)用super方法撤卢,你可能會(huì)導(dǎo)致一些問(wèn)題出現(xiàn)环凿。在大多數(shù)情況下,super方法是期望被調(diào)用的(除非有特殊說(shuō)明)放吩。如果你使用同樣的思想來(lái)進(jìn)行Swizzling智听,可能就會(huì)引起很多問(wèn)題。如果你不調(diào)用原始的方法實(shí)現(xiàn)渡紫,那么你Swizzling改變的太多了到推,而導(dǎo)致整個(gè)程序變得不安全。

2. 日程枧欤可能用的比較多的Runtime函數(shù)可能就是下面這些

//獲取cls類對(duì)象所有成員ivar結(jié)構(gòu)體
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//獲取cls類對(duì)象name對(duì)應(yīng)的實(shí)例方法結(jié)構(gòu)體
Method class_getInstanceMethod(Class cls, SEL name)
//獲取cls類對(duì)象name對(duì)應(yīng)類方法結(jié)構(gòu)體
Method class_getClassMethod(Class cls, SEL name)
//獲取cls類對(duì)象name對(duì)應(yīng)方法imp實(shí)現(xiàn)
IMP class_getMethodImplementation(Class cls, SEL name)
//測(cè)試cls對(duì)應(yīng)的實(shí)例是否響應(yīng)sel對(duì)應(yīng)的方法
BOOL class_respondsToSelector(Class cls, SEL sel)
//獲取cls對(duì)應(yīng)方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//測(cè)試cls是否遵守protocol協(xié)議
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//為cls類對(duì)象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替換cls類對(duì)象中name對(duì)應(yīng)方法的實(shí)現(xiàn)
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//為cls添加新成員
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
//為cls添加新屬性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
//獲取m對(duì)應(yīng)的選擇器
SEL method_getName(Method m)
//獲取m對(duì)應(yīng)的方法實(shí)現(xiàn)的imp指針
IMP method_getImplementation(Method m)
//獲取m方法的對(duì)應(yīng)編碼
const char *method_getTypeEncoding(Method m)
//獲取m方法參數(shù)的個(gè)數(shù)
unsigned int method_getNumberOfArguments(Method m)
//copy方法返回值類型
char *method_copyReturnType(Method m)
//獲取m方法index索引參數(shù)的類型
char *method_copyArgumentType(Method m, unsigned int index)
//獲取m方法返回值類型
void method_getReturnType(Method m, char *dst, size_t dst_len)
//獲取方法的參數(shù)類型
void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)
//設(shè)置m方法的具體實(shí)現(xiàn)指針
IMP method_setImplementation(Method m, IMP imp)
//交換m1莉测,m2方法對(duì)應(yīng)具體實(shí)現(xiàn)的函數(shù)指針
void method_exchangeImplementations(Method m1, Method m2)
//獲取v的名稱
const char *ivar_getName(Ivar v)
//獲取v的類型編碼
const char *ivar_getTypeEncoding(Ivar v)
//設(shè)置object對(duì)象關(guān)聯(lián)的對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取object關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key)
//移除object關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object)
這些API看上去不好記,其實(shí)使用的時(shí)候不難唧喉,關(guān)于方法操作的捣卤,一般都是method開(kāi)頭,關(guān)于類的八孝,一般都是class開(kāi)頭的董朝,其他的基本都是objc開(kāi)頭的,剩下的就看代碼補(bǔ)全的提示干跛,看方法名基本就能找到想要的方法了子姜。當(dāng)然很熟悉的話,可以直接打出指定方法楼入,也不會(huì)依賴代碼補(bǔ)全哥捕。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市嘉熊,隨后出現(xiàn)的幾起案子遥赚,更是在濱河造成了極大的恐慌,老刑警劉巖记舆,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸽捻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡泽腮,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門衣赶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)诊赊,“玉大人,你說(shuō)我怎么就攤上這事府瞄”贪酰” “怎么了碘箍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鲸郊。 經(jīng)常有香客問(wèn)我丰榴,道長(zhǎng),這世上最難降的妖魔是什么秆撮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任四濒,我火速辦了婚禮,結(jié)果婚禮上职辨,老公的妹妹穿的比我還像新娘盗蟆。我一直安慰自己,他們只是感情好舒裤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布喳资。 她就那樣靜靜地躺著,像睡著了一般腾供。 火紅的嫁衣襯著肌膚如雪仆邓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天伴鳖,我揣著相機(jī)與錄音宏赘,去河邊找鬼。 笑死黎侈,一個(gè)胖子當(dāng)著我的面吹牛察署,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播峻汉,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拆宛,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了薪铜?” 一聲冷哼從身側(cè)響起蛤肌,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瘤礁,沒(méi)想到半個(gè)月后阳懂,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柜思,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年岩调,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赡盘。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡号枕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出陨享,到底是詐尸還是另有隱情葱淳,我是刑警寧澤钝腺,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站赞厕,受9級(jí)特大地震影響艳狐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜皿桑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一毫目、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唁毒,春花似錦蒜茴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至近零,卻和暖如春诺核,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背久信。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工窖杀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裙士。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓入客,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親腿椎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桌硫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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