目錄
- Objective-C Runtime到底是什么
- Objective-C的元素認(rèn)知
- Runtime詳解
- 應(yīng)用場(chǎng)景
- 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:
- 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)荐捻;
- 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è)部分組成:
- 方法名method_name類型為SEL绩社,Selector相當(dāng)于一個(gè)方法的id摔蓝;
- 方法類型method_types是一個(gè)char指針,存儲(chǔ)著方法的參數(shù)類型和返回值類型愉耙;
- 方法實(shí)現(xiàn)method_imp的類型為IMP贮尉,IMP是方法的實(shí)現(xiàn);
這樣分開(kāi)的一個(gè)便利之處是selector和IMP之間的對(duì)應(yīng)關(guān)系可以被改變朴沿。比如一個(gè) IMP 可以有多個(gè) selectors 指向它猜谚。
注:
- 實(shí)例方法在對(duì)象的class中找,而類方法在對(duì)象所屬的類的的metaClass中找赌渣。
- 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ì)做一下幾件事情:
- 檢查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ì)崩潰的原因。
- 檢測(cè)這個(gè) selector是不是要忽略的烫罩。
- 確定不是給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”異常了。
至此柜裸,消息發(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)景
- 為現(xiàn)有的類添加私有變量
- 為現(xiàn)有的類添加公有屬性
- 為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)的屬性
- 去模型中查找有沒(méi)有setValue:拦盹,直接調(diào)用這個(gè)對(duì)象setValue:賦值
- 如果沒(méi)有setValue:鹃祖,就在模型中查找_value屬性
- 如果沒(méi)有_value屬性,就查找value屬性
- 如果還沒(méi)有就報(bào)錯(cuò)
利用runtime轉(zhuǎn)換原理:與KVC相反普舆,先在模型中找到對(duì)應(yīng)的成員變量恬口,然后去字典中找到對(duì)應(yīng)的數(shù)據(jù)進(jìn)行賦值。
- 獲取成員變量列表 :class_copyIvarList沼侣,進(jìn)而獲取模型中的所有實(shí)例變量
- 將他們加入到一個(gè)數(shù)組當(dāng)中祖能,然后遍歷數(shù)組,在遍歷過(guò)程中獲取字典中對(duì)應(yīng)的value給屬性對(duì)象賦值蛾洛。
字典轉(zhuǎn)模型
- 調(diào)用 class_getProperty 方法獲取當(dāng)前 Model 的所有屬性养铸。
- 調(diào)用 property_copyAttributeList 獲取屬性列表。
- 根據(jù)屬性名稱生成 setter 方法轧膘。
- 使用 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)模型的逆步驟:
- 調(diào)用 class_copyPropertyList 方法獲取當(dāng)前 Model 的所有屬性翎承。
- 調(diào)用 property_getName 獲取屬性名稱。
- 根據(jù)屬性名稱生成 getter 方法符匾。
- 使用 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ǔ)全哥捕。