iOS的消息機(jī)制

一合搅、OC語言的特性

首先,想要了解iOS的消息發(fā)送機(jī)制赌髓,我們需要先理解OC這門語言。
相較于靜態(tài)語言而言悯搔,動態(tài)語言是指程序在運行時可以改變其結(jié)構(gòu):新的函數(shù)可以被引進(jìn),已有的函數(shù)可以被刪除等在結(jié)構(gòu)上的變化。比如眾所周知的ECMAScript(JavaScript)便是一個動態(tài)語言。除此之外如Ruby增炭、Python等也都屬于動態(tài)語言厂捞,而C、C++等語言則不屬于動態(tài)語言赔嚎。

OC作為一門動態(tài)語言结缚,其很多行為是在程序運行的時候確定的喘落,其具有很多的動態(tài)的特性,基本的有以下三個方面:

  • 動態(tài)類型(Dynamic typing)
  • 動態(tài)綁定(Dynamic binding)
  • 動態(tài)加載(Dynamic loading)
1憾股、動態(tài)類型服球。實際上靜態(tài)類型因為其固定性和可預(yù)知性而使用得更加廣泛茴恰。靜態(tài)類型是強(qiáng)類型,而動態(tài)類型屬于弱類型斩熊。程序執(zhí)行時才決定接收者或真實類型往枣,才能確定其真實所屬的類。

例如:id 數(shù)據(jù)類型
id 通用的對象類型分冈,可以存儲任意類型的對象倔叼,id后面沒有號绝葡,它本身就是個指針類似于void 幽七,但只可以指向?qū)ο箢愋?/p>

靜態(tài)類型與動態(tài)類型:

  • 編譯期檢查與運行時檢查
  • 靜態(tài)類型在編譯期就能檢查出錯誤
  • 靜態(tài)類型聲明代碼可讀性好
  • 動態(tài)類型只有在運行時才能發(fā)現(xiàn)錯誤

補(bǔ)充:
語言的強(qiáng)办绝、弱類型:語言有無類型骤肛、強(qiáng)類型和弱類型三種絮蒿。無類型的不做任何檢查,甚至不區(qū)分指令和數(shù)據(jù)博杖;弱類型的檢查很弱娱两,僅能區(qū)分指令和數(shù)據(jù)宵呛;強(qiáng)類型的嚴(yán)格在編譯期進(jìn)行檢查礁竞。強(qiáng)類型語言在沒有強(qiáng)制類型轉(zhuǎn)化前,不允許兩種不同類型的變量相互操作捐腿。

2扑馁、動態(tài)綁定剑刑。讓代碼在運行時判斷需要調(diào)用什么方法峦筒,而不是在編譯時购啄。與其他面向?qū)ο笳Z言一樣穷遂,方法調(diào)用和代碼并沒有在編譯時連接在一起,而是在消息發(fā)送時才進(jìn)行連接明吩,也就是執(zhí)行時決定調(diào)用哪個方法。

動態(tài)綁定所做的焚鲜,即是在實例所屬類確定后,將某些屬性和相應(yīng)的方法綁定到實例上。
例如:我們在.h文件中定義的方法,我們在.m文件中并未實現(xiàn),但在編譯階段,并不會報錯,只有在執(zhí)行的時候,才會去驗證其是否實現(xiàn),若實現(xiàn)了坤邪,在通過@selector()方法獲取其入口地址,也就是進(jìn)行綁定或粮。

3导饲、動態(tài)加載:根據(jù)需求加載所需要的資源。讓程序在運行時添加代碼模塊以及其他資源,用戶可以根據(jù)需要加載一些可執(zhí)行代碼和資源渣锦,而不是在啟動時就加載所有組件硝岗。可執(zhí)行代碼中可以含有和程序運行時整合的新類袋毙。

例如型檀,iOS不同機(jī)型的圖片適配。這點很容易理解听盖,對于iOS開發(fā)來說胀溺,基本就是根據(jù)不同的機(jī)型做適配。最經(jīng)典的例子就是在Retina設(shè)備上加載@2x的圖片皆看,而在老一些的普通屏設(shè)備上加載原圖仓坞。

BOOL類型
bool是C語言的布爾類型,有true和false腰吟,BOOL是Objective C 語言的布爾類型无埃,有YES和NO,因為OC可以跟C混編,所以bool和BOOL可以同時出現(xiàn)在代碼中

BOOL深入解析:
typedef signed char BOOL;
BOOL類型有兩個值YES毛雇,NO嫉称。YES=1,NO=0灵疮。
說明:objective-c 中的BOOL實際上是一種對帶符號的字符類型(signed char)的類型定義(typedef)织阅,它使用8位的存儲空間。通過#define指令把YES定義為1始藕,NO定義為0蒲稳。

 Class類:表示一個類名,class被創(chuàng)建后伍派,我們可以把class來當(dāng)成對象的類江耀。
 Class cla1 = [類名 class]
 Class cla2 = [對象 class]
 Class cla3 = NSClassFromString(@"類名");
SEL 類成員方法的指針:
可以理解 @selector()就是取類方法的編號,他的行為基本可以等同C語言中的函數(shù)指針,只不過C語言中诉植,可以把函數(shù)名直接賦給一個函數(shù)指針祥国,這樣只能做一個@selector語法來取。它的結(jié)果是一個SEL類型晾腔。這個類型本質(zhì)是類方法的編號(函數(shù)地址)
 
 1>類里面的方法都是被轉(zhuǎn)換成SEL變量進(jìn)行存儲的舌稀。
 2>放類聲明一個對象,對象調(diào)用方法的時候灼擂,系統(tǒng)會被這個方法轉(zhuǎn)換成SEL壁查,然后拿這個SEL到類方法中去匹配。
 3>我們可以自己手動把方法轉(zhuǎn)換成SEL剔应,然后用這個SEL去查找方法(performSelector)睡腿。
    -isMemberOfClass:
    判斷是否是這個類的實例
    -isKindOfClass:
    判斷是否是這個類或者這個類的子類的實例
    -respondsToSelector:
    判讀實例是否有這樣方法
    +instancesRespondToSelector:
    判斷類是否有這個方法语御。此方法是類方法。

二席怪、Runtime(運行時)

通過上面的敘述应闯,我們知道,Object C把編譯時的行為延后到執(zhí)行階段挂捻,如上面說的碉纺,在執(zhí)行階段,確定動態(tài)類型的真實類屬性刻撒,確定一個實例的所屬的屬性和方法等等骨田,這些僅僅依靠編譯器是不夠的,我們還需要一個運行時的系統(tǒng)(Runtime system)來處理編譯后的代碼疫赎。

1盛撑、什么是Runtime

RunTime簡稱運行時。就是系統(tǒng)在運行時候的一些機(jī)制捧搞,其中最主要的是消息機(jī)制抵卫。對于C語言,函數(shù)的調(diào)用在編譯的時候會決定的胎撇。編譯完成之后直接順序執(zhí)行介粘,無任何二義性。OC的函數(shù)調(diào)用機(jī)制為消息發(fā)送晚树,屬于動態(tài)調(diào)用過程姻采。在編譯的時候并不能決定真正調(diào)用哪個函數(shù)(事實證明,在編譯階段爵憎,OC可以調(diào)用任何函數(shù)慨亲,即使這個函數(shù)并未實現(xiàn),只要申明過就不會報錯宝鼓。而C語言在編譯階段就會報錯)刑棵。只有在真正運行的時候才會根據(jù)函數(shù)的名稱找到對應(yīng)的函數(shù)來調(diào)用。

2愚铡、Runtime的版本

Runtime其實有兩個版本:“modern”和 “l(fā)egacy”蛉签。我們現(xiàn)在用的 Objective-C 2.0 采用的是現(xiàn)行(Modern)版的Runtime系統(tǒng),只能運行在 iOS 和 OS X 10.5 之后的64位程序中沥寥。而OS X較老的32位程序仍采用 Objective-C 中的(早期)Legacy 版本的 Runtime 系統(tǒng)碍舍。這兩個版本最大的區(qū)別在于當(dāng)你更改一個類的實例變量的布局時,在早期版本中你需要重新編譯它的子類邑雅,而現(xiàn)行版就不需要片橡。

3、Runtime的作用

Runtime (運行時)淮野,是一套底層的C 語言API锻全,其為iOS 內(nèi)部的核心之一狂塘,我們平時編寫的OC 代碼录煤,底層都是基于它來實現(xiàn)的鳄厌。

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

以上你可能看不出它的價值,但是我們已經(jīng)從上面了解了Objective-C 是一門動態(tài)語言妈踊,它會將一些工作放在代碼運行時才處理而并非編譯時了嚎。也就是說,有很多類和成員變量在我們編譯的時是不知道的廊营,而在運行時歪泳,我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運行。 因此露筒,在執(zhí)行階段呐伞,需要運行時系統(tǒng)(Runtime system)來處理編譯后的代碼。

Runtime 基本是用C 和匯編寫的慎式,由此可見蘋果為了動態(tài)系統(tǒng)的高效而做出的努力伶氢。蘋果和GNU 各自維護(hù)一個開源的Runtime 版本,這兩個版本之間都在努力保持一致瘪吏。

Objc 在三種層面上與Runtime 系統(tǒng)進(jìn)行交互:

  • 通過Objective-C 源代碼
  • 通過Foundation 框架的NSObject 類定義的方法
  • 通過對Runtime 庫函數(shù)的直接調(diào)用

Objective-C 源代碼:

多數(shù)情況我們只需要編寫OC 代碼即可癣防,Runtime 系統(tǒng)自動在幕后搞定一切,如果我們調(diào)用方法掌眠,編譯器會將OC 代碼轉(zhuǎn)換成運行時代碼蕾盯,在運行時確定數(shù)據(jù)結(jié)構(gòu)和函數(shù)。

通過Foundation 框架的NSObject 類定義的方法:

Cocoa 程序中絕大部分類都是NSObject 類的子類蓝丙,所以都繼承了NSObject 的行為级遭。(NSProxy 類時個例外,它是個抽象超類)

一些情況下渺尘,NSObject 類僅僅定義了完成某件事情的模板挫鸽,并沒有提供所需要的代碼。例如-description 方法沧烈,該方法返回類內(nèi)容的字符串表示掠兄,該方法主要用來調(diào)試程序。NSObject 類并不知道子類的內(nèi)容锌雀,所以它只是返回類的名字和對象的地址蚂夕,NSObject 的子類可以重新實現(xiàn)。

還有一些NSObject 的方法可以從Runtime 系統(tǒng)中獲取信息腋逆,允許對象進(jìn)行自我檢查婿牍。
例如:

-class方法返回對象的類;

-isKindOfClass: 和-isMemberOfClass: 方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當(dāng)前類的成員變量)惩歉;

-respondsToSelector: 檢查對象能否響應(yīng)指定的消息等脂;

-conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法俏蛮;

-methodForSelector: 返回指定方法實現(xiàn)的地址。

通過對Runtime 庫函數(shù)的直接調(diào)用:

Runtime系統(tǒng)是具有公共接口的動態(tài)共享庫上遥。頭文件存放于/user/include/objc目錄下搏屑,這意味著我們使用時只需要引入objc/Runtime.h頭文件即可。

例如:

object_getClass(id _Nullable obj);//獲取實例對象所屬的類
object_setClass(id _Nullable obj, Class _Nonnull cls)粉楚;//給一個實例對象更新所屬的類
object_getIvar(id _Nullable obj, Ivar _Nonnull ivar)辣恋;//獲取實例對象的成員變量

許多函數(shù)可以讓你使用純C 代碼來實現(xiàn)Objc 中同樣的功能。除非是寫一些Objc 與其他語言的橋接或是底層的debug 工作模软,你在寫Objc 代碼時一般不會用到這些C 語言函數(shù)伟骨。

三、Runtime術(shù)語的數(shù)據(jù)結(jié)構(gòu)(底層支撐)

由上面所說燃异,我們知道我們所寫的Object C代碼携狭,最終都會由Runtime系統(tǒng)轉(zhuǎn)化為運行時代碼,為了進(jìn)一步了解Runtime的運行時機(jī)制或消息機(jī)制回俐,我們有必要先了解一下Runtime的一些術(shù)語和對應(yīng)的數(shù)據(jù)結(jié)構(gòu)是如何定義的逛腿。

1.objc(實例對象)和id(范型實例)

id 是一個參數(shù)類型,它是指向某個類的實例的指針鲫剿。定義如下:

typedef struct objc_object *id;

objc的定義如下:

struct objc_object { Class isa; };

以上定義鳄逾,看到objc_object 結(jié)構(gòu)體包含一個isa 指針,根據(jù)isa 指針就可以找到對象所屬的類灵莲。

注意:isa 指針在代碼運行時并不總指向?qū)嵗龑ο笏鶎俚念愋偷癜迹圆荒芤揽克鼇泶_定類型,要想確定類型還是需要用對象的-class 方法政冻。

例如:在一個對象實例通過KVO添加觀察者監(jiān)聽事枚抵,該對象實例的isa會被指向一個對象實例類的派生類(通過object_setClass(id _Nullable obj, Class _Nonnull cls),實現(xiàn))。
點擊查看研究KVO的小示例
https://github.com/lwc1990/KVOStudyDemo

2明场、Class(類)
typedef struct objc_class *Class;

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

struct objc_class {

       Class isa; // 指向metaclass

       Class super_class ; // 指向其父類

       const char *name ; // 類名

       long version ; // 類的版本信息,初始化默認(rèn)為0苦锨,可以通過runtime函數(shù)class_setVersion和class_getVersion進(jìn)行修改逼泣、讀取

       long info; // 一些標(biāo)識信息,如CLS_CLASS (0x1L) 表示該類為普通class ,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為metaclass舟舒,其中包含類方法;

       long instance_size ; // 該類的實例變量大小(包括從父類繼承下來的實例變量);

       struct objc_ivar_list *ivars; // 用于存儲每個成員變量的地址

       struct objc_method_list **methodLists ; // 與info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲對象方法拉庶,如CLS_META (0x2L),則存儲類方法;

       struct objc_cache *cache; // 指向最近使用的方法的指針秃励,用于提升效率氏仗;

       struct objc_protocol_list *protocols; // 存儲該類遵守的協(xié)議

 }

從objc_class 可以看到,一個運行時類中關(guān)聯(lián)了它的父類指針夺鲜、類名皆尔、成員變量呐舔、方法、緩存以及附屬的協(xié)議慷蠕。

其中objc_ivar_list 和objc_method_list 分別是成員變量列表和方法列表:

 // 成員變量列表

 struct objc_ivar_list {

    int ivar_count     OBJC2_UNAVAILABLE;

     #ifdef __LP64__

    int   space OBJC2_UNAVAILABLE;

    endif

     /* variable length structure */

    struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;

}   OBJC2_UNAVAILABLE;

 // 方法列表

struct objc_method_list {

    struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

    int method_count       OBJC2_UNAVAILABLE;

    #ifdef __LP64__

    int space          OBJC2_UNAVAILABLE;

   #endif

    /* variable length structure */

    struct objc_method method_list[1] OBJC2_UNAVAILABLE;

}

由此可見珊拼,我們可以動態(tài)修改*methodList 的值來添加成員方法,這也是Category 實現(xiàn)的原理砌们,同樣解釋了Category 不能添加屬性的原因杆麸。

objc_ivar_list 結(jié)構(gòu)體用來存儲成員變量的列表,而objc_ivar 則是存儲了單個成員變量的信息浪感;同理,objc_method_list 結(jié)構(gòu)體存儲著方法數(shù)組的列表饼问,而單個方法的信息則由objc_method 結(jié)構(gòu)體存儲影兽。

3、類和對象的繼承層次關(guān)系

isa:objec_object(對象)中isa指針指向的類結(jié)構(gòu)稱為class(也就是該對象所屬的類)莱革,其中存放著普通成員變量與對象方法 (“-”開頭的方法)峻堰;然而此處isa指針指向的類結(jié)構(gòu)稱為metaclass,其中存放著static類型的成員變量與static類型的方法 (“+”開頭的方法)盅视。

super_class: 指向該類的父類的指針捐名,如果該類是根類(如NSObject或NSProxy),那么super_class就為NULL闹击。

所有的metaclass中isa指針都是指向根metaclass镶蹋,而根metaclass則指向自身。根metaclass是通過繼承根類產(chǎn)生的赏半,與根class結(jié)構(gòu)體成員一致贺归,不同的是根metaclass的isa指針指向自身。

在調(diào)用類或?qū)嵗姆椒〞r断箫,是如何找到方法的實現(xiàn)的拂酣?

1)、當(dāng)我們調(diào)用某個對象的對象方法時仲义,它會首先在自身isa指針指向的類(class)methodLists中查找該方法婶熬,如果找不到則會通過class的super_class指針找到其父類,然后從其methodLists中查找該方法埃撵,如果仍然找不到赵颅,則繼續(xù)通過 super_class向上一級父類結(jié)構(gòu)體中查找,直至根class盯另;

2)性含、當(dāng)我們調(diào)用某個類方法時,它會首先通過自己的isa指針找到metaclass鸳惯,并從其methodLists中查找該類方法商蕴,如果找不到則會通過metaclass的super_class指針找到父類的metaclass結(jié)構(gòu)體叠萍,然后從methodLists中查找該方法,如果仍然找不到绪商,則繼續(xù)通過super_class向上一級父類結(jié)構(gòu)體中查 找苛谷,直至根metaclass;

繼承層次關(guān)系圖如下:


類與對象的繼承層次圖

值得注意的是格郁,objc_class 中也有一個isa 指針腹殿,這說明Objc 類本身也是一個對象。為了處理類和對象的關(guān)系例书,Runtime 庫創(chuàng)建了一種叫做Meta Class(元類) 的東西锣尉,類對象所屬的類就叫做元類。Meta Class 表述了類對象本身所具備的元數(shù)據(jù)决采。

我們所熟悉的類方法自沧,就源自于Meta Class。我們可以理解為類方法就是類對象的實例方法树瞭。每個類僅有一個類對象拇厢,而每個類對象僅有一個與之相關(guān)的元類。

當(dāng)你發(fā)出一個類似[NSObject alloc](類方法) 的消息時晒喷,實際上孝偎,這個消息被發(fā)送給了一個類對象(Class Object),這個類對象必須是一個元類的實例凉敲,而這個元類同時也是一個根元類(Root Meta Class)的實例衣盾。所有元類的isa 指針最終都指向根元類。

所以當(dāng)[NSObject alloc] 這條消息發(fā)送給類對象的時候荡陷,運行時代碼objc_msgSend() 會去它元類中查找能夠響應(yīng)消息的方法實現(xiàn)雨效,如果找到了,就會對這個類對象執(zhí)行方法調(diào)用废赞。

4徽龟、category中為什么不能添加實例變量

在Objective-C提供的runtime函數(shù)中,確實有一個class_addIvar()函數(shù)用于給類添加成員變量唉地,但是閱讀過蘋果的官方文檔的人應(yīng)該會看到:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.

大概的意思說据悔,這個函數(shù)只能在“構(gòu)建一個類的過程中”調(diào)用。當(dāng)編譯你的類的時候耘沼,編譯器生成了一個實例變量內(nèi)存布局(ivar layout)极颓,來告訴運行時去那里訪問你的類的實例變量們,一旦完成類定義群嗤,就不能再添加成員變量了菠隆。經(jīng)過編譯的類在程序啟動后就被runtime加載,沒有機(jī)會調(diào)用addIvar。程序在運行時動態(tài)構(gòu)建的類需要在調(diào)用objc_registerClassPair之后才可以被使用骇径,同樣沒有機(jī)會再添加成員變量躯肌。
測試代碼:

#import "ViewController.h"
#import "MyClass.h"
#import <objc/runtime.h>
@interface ViewController ()
@end

@implementation ViewController
void otherMethod(id self,SEL _cmd){
    NSLog(@"新添加的方法");
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self dynamicAddIvarPropertyMethod];
    [self test];
}
-(void)dynamicAddIvarPropertyMethod{
    //添加Ivar
    class_addIvar([MyClass class],"newIvar",sizeof(id),log2(sizeof(id)), "@");
    //添加屬性
    objc_property_attribute_t type = {"T","@\"NSString\""};
    objc_property_attribute_t ownerShip = {"c",""};
    objc_property_attribute_t attrs[] = {type,ownerShip};
    class_addProperty([MyClass class], "age",attrs, 2);
    //添加方法
    class_addMethod([MyClass class],@selector(otherMethod),(IMP)otherMethod,"v@");
}
//測試相應(yīng)的添加是否成功
-(void)test{
    //首先調(diào)用動態(tài)添加的方法
    [[MyClass new] performSelector:NSSelectorFromString(@"otherMethod") withObject:nil];
    unsigned int nums;//用于記錄成員變量、屬性或方法的個數(shù)
    //取成員變量
    Ivar *vars = class_copyIvarList([MyClass class],&nums);
    NSString *ivarkey = @"";
    for (int i = 0;i < nums;i++) {
        Ivar ivar = vars[I];
        ivarkey = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSLog(@"variable name:%@",ivarkey);
    }
    printf("-----------成員變量打印結(jié)束---------");
    //取成員屬性
    NSString *propertyKey = @"";
    objc_property_t *properties = class_copyPropertyList([MyClass class],&nums);
    for (int i = 0;i < nums;i++) {
        objc_property_t property = properties[I];
        propertyKey = [NSString stringWithUTF8String:property_getName(property)];
        NSLog(@"property name:%@",propertyKey);
    }
    printf("-----------成員屬性打印結(jié)束--------");
    //取方法
    Method *methods = class_copyMethodList([MyClass class],&nums);
    for (int i = 0;i < nums;i++) {
        Method method = methods[I];
        SEL selector = method_getName(method);
        NSString *methodName = NSStringFromSelector(selector);
        NSLog(@"method name:%@",methodName);
    }
}

運行結(jié)果如下:

運行結(jié)果

點擊下載完整代碼:
從運行結(jié)果中看出破衔,你不能為一個類動態(tài)的添加成員變量清女,可以給類動態(tài)增加方法和屬性。

因為方法和屬性并不“屬于”類實例晰筛,而成員變量“屬于”類實例嫡丙。我們所說的“類實例”概念,指的是一塊內(nèi)存區(qū)域读第,包含了isa指針和所有的成員變量曙博。所以假如允許動態(tài)修改類成員變量布局,已經(jīng)創(chuàng)建出的類實例就不符合類定義了卦方,變成了無效對象羊瘩。但方法定義是在objc_class中管理的,不管如何增刪類方法盼砍,都不影響類實例的內(nèi)存布局,已經(jīng)創(chuàng)建出的類實例仍然可正常使用逝她。

5浇坐、Method

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

typedef struct objc_method *Method;
struct objc_method {
//方法名
SEL method_name                                         OBJC2_UNAVAILABLE;
//方法類型
char *method_types                                      OBJC2_UNAVAILABLE;
//指向方法實現(xiàn)的指針(方法的入口地址)
IMP method_imp             OBJC2_UNAVAILABLE;
}

objc_method 存儲了方法名,方法類型和方法實現(xiàn):
方法名類型為SEL
方法類型method_types 是個char 指針黔宛,存儲方法的參數(shù)類型和返回值類型
method_imp 指向了方法的實現(xiàn)近刘,本質(zhì)是一個函數(shù)指針

6、SEL

它是selector在Objc 中的表示臀晃。selector 是方法選擇器觉渴,其實作用就和名字一樣腻豌,日常生活中泛烙,我們通過人名辨別誰是誰,注意Objc 在相同的類中不會有命名相同的兩個方法介陶。selector 對方法名進(jìn)行包裝险绘,以便找到對應(yīng)的方法實現(xiàn)踢京。它的數(shù)據(jù)結(jié)構(gòu)是:

typedef struct objc_selector *SEL;

我們可以看出它是個映射到方法的C 字符串,你可以通過Objc 編譯器器命令@selector() 或者Runtime 系統(tǒng)的sel_registerName 函數(shù)來獲取一個SEL 類型的方法選擇器宦棺。通過方法器來獲取方法的實現(xiàn)地址的過程運用了hashMap的算法瓣距。

注意:不同類中相同名字的方法所對應(yīng)的selector 是相同的,由于變量的類型不同代咸,所以不會導(dǎo)致它們調(diào)用方法實現(xiàn)混亂蹈丸。

7、Ivar

Ivar 是表示成員變量的類型。

typedef struct objc_ivar *Ivar;
struct objc_ivar {
    //成員變量名  
    char *ivar_name                                         OBJC2_UNAVAILABLE;
   //成員變量類型
    char *ivar_type                                         OBJC2_UNAVAILABLE;
    int ivar_offset OBJC2_UNAVAILABLE;
     #ifdef __LP64__
    int space                                               OBJC2_UNAVAILABLE;
  #endif
 }

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

8逻杖、IMP

IMP在objc.h中的定義是:

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

它就是一個函數(shù)指針奋岁,這是由編譯器生成的。當(dāng)你發(fā)起一個ObjC 消息之后弧腥,最終它會執(zhí)行的那段代碼厦取,就是由這個函數(shù)指針指定的。而IMP 這個函數(shù)指針就指向了這個方法的實現(xiàn)管搪。

如果得到了執(zhí)行某個實例某個方法的入口虾攻,我們就可以繞開消息傳遞階段,直接執(zhí)行方法更鲁,這在后面Cache 中會提到霎箍。

你會發(fā)現(xiàn)IMP 指向的方法與objc_msgSend 函數(shù)的參數(shù)類型相同,參數(shù)都包含id 和SEL 類型澡为。每個方法名都對應(yīng)一個SEL 類型的方法選擇器漂坏,而每個實例對象中的SEL 對應(yīng)的方法實現(xiàn)肯定是唯一的,通過一組id和SEL 參數(shù)就能確定唯一的方法實現(xiàn)地址媒至。而一個確定的方法也只有唯一的一組id 和SEL 參數(shù)顶别。

9、Cache

Cache 定義如下:

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

Cache 為方法調(diào)用的性能進(jìn)行優(yōu)化拒啰,每當(dāng)實例對象接收到一個消息時驯绎,它不會直接在isa 指針指向的類的方法列表中遍歷查找能夠響應(yīng)的方法,因為每次都要查找效率太低了谋旦,而是優(yōu)先在Cache 中查找剩失。

Runtime 系統(tǒng)會把被調(diào)用的方法存到Cache 中,如果一個方法被調(diào)用册着,那么它有可能今后還會被調(diào)用拴孤,下次查找的時候就會效率更高。就像計算機(jī)組成原理中CPU 繞過主存先訪問Cache 一樣甲捏。

10演熟、Property
typedef struct objc_property *Property;

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

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

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意: 返回的是屬性列表,列表中每個元素都是一個objc_property_t 指針
示例如:

 @interface Person : NSObject
 /** 姓名*/
@property (strong, nonatomic) NSString *name;
 /** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
@end

以上是一個Person 類摊鸡,有3個屬性绽媒。讓我們用上述方法獲取類的運行時屬性。

unsigned int outCount = 0;

objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

 NSLog(@"%d", outCount);

 for (NSInteger i = 0; i < outCount; i++) {

  NSString *name = @(property_getName(properties[i]));

  NSString *attributes = @(property_getAttributes(properties[i]));

  NSLog(@"%@--------%@", name, attributes);

}

property_getName 用來查找屬性的名稱免猾,返回c 字符串是辕。property_getAttributes 函數(shù)挖掘?qū)傩缘恼鎸嵜Q和@encode 類型,返回c 字符串猎提。

objc_property_t class_getProperty(Class cls, const char *name)

objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

class_getProperty 和protocol_getProperty 通過給出屬性名在類和協(xié)議中獲得屬性的引用获三。

四旁蔼、消息和消息發(fā)送的步驟

一些 Runtime 術(shù)語講完了,接下來就要說到消息了疙教。由蘋果官方文檔中的 messages aren’t bound to method implementations until Runtime可知棺聊,消息直到運行時才會與方法實現(xiàn)進(jìn)行綁定。

通過終端命令clang -rewrite-objc xxx.m可以看到xxx.m編譯后的xxx.cpp(C++文件)贞谓,比對.m文件和.cpp文件限佩,你會發(fā)現(xiàn)方括號形式的方法調(diào)用基于返回類型的不同被編譯器轉(zhuǎn)化成objc_msgSend系列函數(shù)中的某一個)的調(diào)用。

通過clang方法也可以分析block的實現(xiàn)裸弦,傳送門:談Objective-C block的實現(xiàn)祟同、iOS中block實現(xiàn)的探究

這里要清楚一點,objc_msgSend 方法看清來好像返回了數(shù)據(jù)理疙,其實objc_msgSend 從不返回數(shù)據(jù)晕城,而是你的方法在運行時實現(xiàn)被調(diào)用后才會返回數(shù)據(jù)。

// OC形式:
[receiver messageWithArgs:arg1 and:arg2 …]; 
// C語言函數(shù)及參數(shù)說明:
// ? receiver => 消息接收者窖贤,類型為id砖顷,通過其isa指針找到指定類的結(jié)構(gòu)
// ? selector => 方法選擇器,類型為SEL赃梧,用于在類結(jié)構(gòu)的方法分發(fā)表中搜索指定名字的方法實現(xiàn)/地址
objc_msgSend(receiver, selector, arg1, arg2, ...)

有代碼可以看出滤蝠,在方法的實現(xiàn)中始終有兩個隱藏參數(shù)。
方法中的隱藏參數(shù)
疑問:
我們經(jīng)常用到關(guān)鍵字 self 授嘀,但是 self 是如何獲取當(dāng)前方法的對象呢几睛?
其實,這也是 Runtime 系統(tǒng)的作用粤攒,self 是在方法運行時被動態(tài)傳入的。
當(dāng) objc_msgSend 找到方法對應(yīng)實現(xiàn)時囱持,它將直接調(diào)用該方法實現(xiàn)夯接,并將消息中所有參數(shù)都傳遞給方法實現(xiàn),同時纷妆,它還將傳遞兩個隱藏參數(shù):

  • 接受消息的對象reciver(self 所指向的內(nèi)容盔几,當(dāng)前方法的對象指針)
  • 方法選擇器selector(_cmd 指向的內(nèi)容,當(dāng)前方法的 SEL 指針)

因為在源代碼方法的定義中掩幢,我們并沒有發(fā)現(xiàn)這兩個參數(shù)的聲明逊拍。它們是在代碼被編譯時被插入方法實現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明际邻,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?br> 這兩個參數(shù)中芯丧, self更實用。它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑世曾。

這時我們可能會想到另一個關(guān)鍵字 super 缨恒,實際上 super 關(guān)鍵字接收到消息時,編譯器會創(chuàng)建一個 objc_super 結(jié)構(gòu)體:

注意:

  • 在實例方法中,self表示對象骗露;在類方法中岭佳,self表示元類對象(即類)。
  • super關(guān)鍵字實際上會被轉(zhuǎn)化成一個objc_super類型的結(jié)構(gòu)體萧锉,其值為{self, self.superclass}珊随。
struct objc_super { id receiver; Class class; };

這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類。 receiver 仍然是 self 本身柿隙,當(dāng)我們想通過 [super class] 獲取父類時叶洞,編譯器其實是將指向 self 的 id 指針和 class 的 SEL 傳遞給了 objc_msgSendSuper 函數(shù)。只有在 NSObject 類中才能找到 class 方法优俘,然后 class 方法底層被轉(zhuǎn)換為 object_getClass()京办, 接著底層編譯器將代碼轉(zhuǎn)換為 objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數(shù)是指向 self 的 id 指針帆焕,與調(diào)用 [self class] 相同惭婿,所以我們得到的永遠(yuǎn)都是 self 的類型。因此你會發(fā)現(xiàn):

// 這句話并不能獲取父類的類型叶雹,只能獲取當(dāng)前類的類型名
NSLog(@"%@", NSStringFromClass([super class]));

獲取方法地址
NSObject 類中有一個實例方法:methodForSelector财饥,你可以用它來獲取某個方法選擇器對應(yīng)的 IMP ,舉個例子:

void (*setter)(id, SEL, BOOL);
int I;

setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

當(dāng)方法被當(dāng)做函數(shù)調(diào)用時折晦,兩個隱藏參數(shù)也必須明確給出钥星,上面的例子調(diào)用了1000次函數(shù),你也可以嘗試給 target 發(fā)送1000次 setFilled: 消息會花多久满着。

雖然可以更高效的調(diào)用方法谦炒,但是這種做法很少用,除非時需要持續(xù)大量重復(fù)調(diào)用某個方法的情況风喇,才會選擇使用以免消息發(fā)送泛濫宁改。

注意:
me t ho d F o rSelector:方法是由Runtime系統(tǒng)提供的,而不是Objc自身的特性

下面詳細(xì)敘述消息發(fā)送的步驟(如下圖):


消息發(fā)送步驟圖.png

具體步驟如下:

1).首先檢測這個selector是不是要忽略魂莫。比如 Mac OS X 開發(fā)还蹲,有了垃圾回收就不理會 retain,release 這些函數(shù)耙考。

2).檢測這個selector的 target 是不是nil谜喊,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會 Crash,因為運行時會被忽略掉倦始。

3).如果上面兩步都通過了斗遏,那么就開始查找這個類的實現(xiàn)IMP,先從 cache 里查找楣号,如果找到了就運行對應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼最易。

4).如果 cache中 找不到就通過isa指針找類的方法列表中是否有對應(yīng)的方法怒坯。

5).如果類的方法列表中找不到就通過superClass到父類的方法列表中查找,一直找到 NSObject 類為止藻懒。

6).如果還找不到剔猿,就要開始進(jìn)入動態(tài)方法解析->重定向->消息轉(zhuǎn)發(fā)了(后面會講到)。

如果第六步的攔截調(diào)用方法都未實現(xiàn)嬉荆,程序就會調(diào)用doNotRecognizeSelector方法崩潰归敬。

在消息的傳遞中,編譯器會根據(jù)情況在 objc_msgSend 鄙早, objc_msgSend_stret 汪茧, objc_msgSendSuper , objc_msgSendSuper_stret 這四個方法中選擇一個調(diào)用限番。如果消息是傳遞給父類舱污,那么會調(diào)用名字帶有 Super 的函數(shù),如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時弥虐,會調(diào)用名字帶有 stret 的函數(shù)扩灯。

五、消息轉(zhuǎn)發(fā)及步驟(動態(tài)解析 ->重定向->轉(zhuǎn)發(fā))

攔截調(diào)用如下圖所示:

消息攔截轉(zhuǎn)發(fā)圖

在一個函數(shù)找不到時霜瘪,runtime提供了三種方式去補(bǔ)救:

1)珠插、調(diào)用resolveInstanceMethod給機(jī)會讓類添加這個實現(xiàn)這個函數(shù)(動態(tài)解析)

2)、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)(fast forwarding)

3)颖对、調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行(normal forwarding)捻撑。
如果都不中,調(diào)用doesNotRecognizeSelector拋出異常缤底。

步驟一顾患、動態(tài)解析

通過實現(xiàn)resolveInstanceMethodresolveClassMethod:方法,我們有機(jī)會為該未知消息(SEL)新增一個“處理方法”(IMP)个唧。

  • 這意味著在消息轉(zhuǎn)發(fā)前描验,你有機(jī)會通過class_addMethod給類動態(tài)添加一些方法;
  • 實際上返回值YES/NO無關(guān)緊要,只要你在resovle過程中新增過方法坑鱼,就會觸發(fā)class_getMethodImplementation,其作用相當(dāng)于重新啟動一次消息發(fā)送過程絮缅。

1.你可以動態(tài)提供一個方法實現(xiàn)鲁沥。如果我們使用關(guān)鍵字 @dynamic 在類的實現(xiàn)文件中修飾一個屬性,表明我們會為這個屬性動態(tài)提供存取方法耕魄,編譯器不會再默認(rèn)為我們生成這個屬性的 setter 和 getter 方法了画恰,需要我們自己提供。

@dynamic propertyName;

這時吸奴,我們可以通過分別重載resolveInstanceMethod:和resolveClassMethod:方法添加實例方法實現(xiàn)和類方法實現(xiàn)允扇。

2.當(dāng) Runtime 系統(tǒng)在 Cache 和類的方法列表(包括父類)中找不到要執(zhí)行的方法時缠局,Runtime 會調(diào)用 resolveInstanceMethod: 或 resolveClassMethod: 來給我們一次動態(tài)添加方法實現(xiàn)的機(jī)會。我們需要用 class_addMethod 函數(shù)完成向特定類添加特定方法實現(xiàn)的操作:

#import "MyClass.h"
#import <objc/runtime.h>
//添加要動態(tài)添加的類方法的實現(xiàn)
void dynamicClassMethodIMP(id self,SEL _cmd){
    NSLog(@"攔截到未實現(xiàn)的類方法考润,并為之添加實現(xiàn)");
}
//添加要動態(tài)添加的實例化方法的實現(xiàn)
void dynamicInstanceMethodIMP(id self,SEL _cmd){
    NSLog(@"攔截到未實現(xiàn)的實例化方法狭园,并為之添加實現(xiàn)");
}
/*
 * 在NSObject中有動態(tài)解析方法,我們所有的類(除NSPoxy之外)都繼承于NSObject糊治,因此唱矛,我們可以所有的類中都可以重載動態(tài)解析方法
 */
@implementation MyClass
//重載類方法的動態(tài)解析方法
+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(unknownClassMethod)) {
        //objc_getMetaClass("MyClass"),根據(jù)類于對象的繼承關(guān)系井辜,給類添加類方法這里需要找MyClass的原類
        class_addMethod(objc_getMetaClass("MyClass"),sel,(IMP)dynamicClassMethodIMP,"v@:");//"v@:"是動態(tài)的添加方法的返回值和參數(shù)類型绎谦,這里是void。
        return YES;
    }
    return [super resolveClassMethod:sel];
}
//重載實例方法的動態(tài)解析方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(unknownInstanceMethod)) {
        class_addMethod([self class],sel,(IMP)dynamicInstanceMethodIMP,"v@:");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}
@end

各種類型標(biāo)示的含義見:Type Encoding
查看完整代碼:https://github.com/lwc1990/DynamicResolveStudyDemo.git

注意:
動態(tài)方法解析會在消息轉(zhuǎn)發(fā)機(jī)制侵入前執(zhí)行粥脚,動態(tài)方法解析器將會首先給予提供該方法選擇器對應(yīng)的 IMP 的機(jī)會窃肠。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機(jī)制,就讓 resolveInstanceMethod: 方法返回 NO刷允。

步驟二冤留、重定向(指定備用能處理相關(guān)事件的接收者)

從第二步開始,真正進(jìn)入消息轉(zhuǎn)發(fā)階段恃锉。

在消息轉(zhuǎn)發(fā)機(jī)制執(zhí)行前搀菩,Runtime系統(tǒng)允許我們替換消息的接受者(執(zhí)行者)為其他對象。

通過實現(xiàn)-forwardingTargetForSelector: 方法將消息(SEL)直接轉(zhuǎn)發(fā)給另一個對象(備用接收者)破托,也就是在另一個對象上重啟消息發(fā)送過程肪跋。

#import "MyClass.h"
/*
 * -(id)forwardingTargetForSelector:(SEL)aSelector;是NSObject的方法,
 * 除NSPoxy類之外土砂,所有的類繼承于NSObject州既,也就是任何類的實例都可以重載這個方法,實現(xiàn)消息的快速轉(zhuǎn)發(fā)萝映。
 */
@implementation MyClass
//重載快速轉(zhuǎn)發(fā)方法吴叶,把消息轉(zhuǎn)發(fā)給能處理的對象
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(unknownInstanceMethod)) {
        return [NSClassFromString(@"MyFastForwardingClass") new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

點擊查看完整代碼:https://github.com/lwc1990/FastForwardingDemo
如果返回nil或self,則會進(jìn)入消息完整轉(zhuǎn)發(fā)階段序臂。

步驟三蚌卤、完整消息轉(zhuǎn)發(fā)

當(dāng)快速轉(zhuǎn)發(fā)不做處理時,則會出發(fā)消息轉(zhuǎn)發(fā)機(jī)制奥秆,在消息完整轉(zhuǎn)發(fā)階段逊彭,-forwardInvocation:方法會被執(zhí)行,我們可以重寫這個方法來自定義我們的轉(zhuǎn)發(fā)邏輯构订。
代碼示例:

#import "ViewController.h"
//消息轉(zhuǎn)發(fā)后的消息執(zhí)行者
@interface TestObj : NSObject
//轉(zhuǎn)發(fā)方法的聲明
-(void)testMethod;
@end
@implementation TestObj
//轉(zhuǎn)發(fā)方法的實現(xiàn)
-(void) testMethod
{
    NSLog(@"%s",__func__);
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //    [self selfTestMethod]; 結(jié)果:[ViewController selfTestMethod]
//    [self performSelector:@selector(testMethod)];結(jié)果:[TestObj testMethod]
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:NSSelectorFromString(@"otherMethod")];
#pragma clang diagnostic pop
}
/*
 * 當(dāng)動態(tài)解析和快速轉(zhuǎn)發(fā)都未能處理時侮叮,該方法就會被執(zhí)行,如果該方法悼瘾,也未能處理囊榜,Runtime系統(tǒng)就會調(diào)用doNotRecognizeSelector:進(jìn)而崩潰审胸。
 * 參數(shù)anInvocation 類型是NSInvocation,這個類里,封裝了小執(zhí)行的方法SEL,和消息原定的接受者卸勺,以及參數(shù)砂沛、返回值等信息;
 * 因此孔庭,我們可以修改其中的消息接受者也就是執(zhí)行者尺上,或者要執(zhí)行方法,實現(xiàn)消息的完整轉(zhuǎn)發(fā)圆到,防止系統(tǒng)崩潰怎抛。
 * 這里的anInvocation對象是要經(jīng)過-(NSMethodSignature *)methodSignatureForSelector:方法生成的簽名和原消息打包來生成的,
 * 因此芽淡,我們要實現(xiàn)消息的完整轉(zhuǎn)發(fā)马绝,是需要該方法與下面的方法協(xié)同實現(xiàn)。
 */
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    if ([self respondsToSelector:sel]){
        //已實現(xiàn)的直接消息轉(zhuǎn)發(fā)
        [anInvocation invoke];
    }else if ([[[TestObj alloc]init] respondsToSelector:anInvocation.selector]){
        //未實現(xiàn)的用其他實現(xiàn)的Target轉(zhuǎn)發(fā)
        [anInvocation invokeWithTarget:[[TestObj alloc]init]];
    }else{
        //都未實現(xiàn)挣菲,用預(yù)定義的方法方法轉(zhuǎn)發(fā)富稻,防止崩潰
        SEL unKnownSel = anInvocation.selector;
        anInvocation.selector = @selector(unKnownSelector:);
        [anInvocation setArgument:unKnownSel atIndex:0];
        anInvocation.target = self;
        [anInvocation invoke];
    }
}
/*
 * 傳遞過來的消息,需要經(jīng)過該方法進(jìn)行簽名白胀,該方法生成的簽名和原消息一起打包到一個NSInvocation對象中椭赋, 以供上面的消息轉(zhuǎn)發(fā)方法進(jìn)行入?yún)ⅰ?* 方法簽名,類似于C++編譯時的函數(shù)簽名或杠,里面包含方法的參數(shù)個數(shù)哪怔,方法返回值信息大多信息是值讀的;可以通過NSObject的methodSignatureForSelector:方法獲取實例化對象向抢,它主要是協(xié)同NSInvocation實現(xiàn)消息轉(zhuǎn)發(fā) ,要保證NSInvocation對象的中的Selector與該方法返回的方法簽名中的Seletor一致
 */
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    //這個方法返回的實例是協(xié)同上面的方法實現(xiàn)消息轉(zhuǎn)發(fā)的
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature){
        if ([[[TestObj alloc]init] respondsToSelector:aSelector]){
            signature = [[[TestObj alloc]init] methodSignatureForSelector:aSelector];
        }else{
            NSString *str = [NSString stringWithFormat:@"%@ hasn't implementation method %@",NSStringFromClass([self class]),NSStringFromSelector(aSelector)];
            NSAssert(!DEBUG,str);
            signature = [self methodSignatureForSelector:@selector(unKnownSelector:)];
        }
    }
    return signature;
}
-(void)unKnownSelector:(SEL)aSelector{
    NSLog(@"%@ hasn't implementation method %@",NSStringFromClass([self class]),NSStringFromSelector(aSelector));
}
-(void)selfTestMethod{
    NSLog(@"%s",__func__);
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
@end

點擊下載完整代碼:https://github.com/lwc1990/DemoForInvocation

完整轉(zhuǎn)發(fā)的詳細(xì)過程
1.通過實現(xiàn)-methodSignatureForSelector:提供方法簽名(提供參數(shù)和返回值的類型)认境。

  • 方法簽名,類似于C++編譯時的函數(shù)簽名挟鸠,里面包含方法的參數(shù)個數(shù)叉信,方法返回值信息,大多信息是只讀的;可以通過NSObject的methodSignatureForSelector:方法獲取實例化對象艘希,它主要是協(xié)同NSInvocation實現(xiàn)消息轉(zhuǎn)發(fā) ,要保證NSInvocation對象的中的Selector與該方法返回的方法簽名中的Seletor一致.

2. 生成的簽名將和原始消息一起打包到一個NSInvocation對象中

  • 通過操作NSInvocation對象的target覆享、selector屬性可以方便地轉(zhuǎn)發(fā)鸠姨,甚至轉(zhuǎn)發(fā)給另一個對象的另一個需要不同參數(shù)的SEL也是可以的;

3. 實現(xiàn)-forwardInvocation: 方法

  • 通過調(diào)用 -invoke方法重新啟動一個消息發(fā)送過程淹真。
  • 不調(diào)用invoke,系統(tǒng)會吞掉這個消息(不做任何處理)

forwardInvocation: 方法就是一個不能識別消息的分發(fā)中心连茧,將這些不能識別的消息轉(zhuǎn)發(fā)給不同的接收對象核蘸,或者轉(zhuǎn)發(fā)給同一個對象巍糯,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息客扎,因此沒有響應(yīng)也不會報錯祟峦。這一切都取決于方法的具體實現(xiàn)。

注意:
forwardInvocation:方法只有在消息接收對象中無法正常響應(yīng)消息時才會被調(diào)用徙鱼。所以宅楞,如果我們向往一個對象將一個消息轉(zhuǎn)發(fā)給其他對象時,要確保這個對象不能有該消息的所對應(yīng)的方法袱吆。否則厌衙,forwardInvocation:將不可能被調(diào)用。

六绞绒、消息的轉(zhuǎn)發(fā)功能模擬了多繼承功能

1.消息轉(zhuǎn)發(fā)如何模擬實現(xiàn)多繼承
我們都知道婶希,Objec不具有多繼承的特性,但由于Objec有消息轉(zhuǎn)發(fā)的機(jī)制蓬衡,一個對象把消息轉(zhuǎn)發(fā)出去喻杈,就好像它把另一個類中的方法“繼承”過來一樣,因此我們可以利用消息轉(zhuǎn)發(fā)的機(jī)制為Objc編程添加一些多繼承的效果狰晚。
如下圖所示:

不同繼承分支之間的方法調(diào)用

這使得在不同繼承體系分支下的兩個類可以實現(xiàn)“繼承”對方的方法筒饰,在上圖中 Warrior 和 Diplomat 沒有繼承關(guān)系,但是 Warrior 將 negotiate 消息轉(zhuǎn)發(fā)給了 Diplomat 后壁晒,就好似 Diplomat 是 Warrior 的超類一樣瓷们。

消息轉(zhuǎn)發(fā)彌補(bǔ)了 Objc 不支持多繼承的性質(zhì),也避免了因為多繼承導(dǎo)致單個類變得臃腫復(fù)雜讨衣。

3.消息轉(zhuǎn)發(fā)與繼承的區(qū)別

雖然轉(zhuǎn)發(fā)可以實現(xiàn)繼承的功能换棚,但是 NSObject 還是比較嚴(yán)謹(jǐn)?shù)模?respondsToSelector: 和 isKindOfClass: 這類方法只會考慮繼承體系反镇,不會考慮轉(zhuǎn)發(fā)鏈固蚤。

如果上圖中的 Warrior 對象被問到是否能響應(yīng) negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )

回答當(dāng)然是 NO, 盡管它能接受 negotiate 消息而不報錯歹茶,因為它靠轉(zhuǎn)發(fā)消息給 Diplomat 類響應(yīng)消息夕玩。

如果你就是想要讓別人以為 Warrior 繼承到了 Diplomat 的 negotiate 方法,你得重新實現(xiàn) respondsToSelector: 和 isKindOfClass: 來加入你的轉(zhuǎn)發(fā)算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了 respondsToSelector: 和__ isKindOfClass:__ 之外惊豺,instancesRespondToSelector: 中也應(yīng)該寫一份轉(zhuǎn)發(fā)算法燎孟。如果使用了協(xié)議,conformsToProtocol: 同樣也要加入到這一行列中尸昧。

如果一個對象想要轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息揩页,它得給出一個方法標(biāo)簽來返回準(zhǔn)確的方法描述methodSignatureForSelector:,這個方法會最終響應(yīng)被轉(zhuǎn)發(fā)的消息烹俗。從而生成一個確定的 NSInvocation 對象描述消息和消息參數(shù)爆侣。這個方法最終響應(yīng)被轉(zhuǎn)發(fā)的消息萍程。它需要像下面這樣實現(xiàn):

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

以下方法只考慮類繼承體系(不含轉(zhuǎn)發(fā)鏈);如需要對象表現(xiàn)得和繼承一樣兔仰,重寫它們并把轉(zhuǎn)發(fā)算法包括進(jìn)來:

  • -respondsToSelector: & +instancesRespondToSelector:
  • -isKindOfClass: & -isMemberOfClass:
  • -conformsToProtocol:

4.轉(zhuǎn)發(fā)與多繼承的區(qū)別
轉(zhuǎn)發(fā)模擬了繼承茫负,所以可以用來為Objc程序提供類似多繼承的功能。轉(zhuǎn)發(fā)和多繼承的區(qū)別如下:

  • 多繼承是將許多功能combine到一個對象中乎赴;
  • 轉(zhuǎn)發(fā)則將功能分解到多個對象忍法,并一種對消息發(fā)送者透明的方式將它們關(guān)聯(lián)起來;
七榕吼、使用場景總結(jié)

點擊查看:
使用場景總結(jié)一

八饿序、示例Demo下載

使用黑魔法對數(shù)組越界預(yù)處理:https://github.com/lwc1990/RunTimeForSwizzlingDemo
使用黑魔法處理對Button頻繁點擊的問題:https://github.com/lwc1990/UIButtonRunTimeDemo
結(jié)合消息完整轉(zhuǎn)發(fā)和NSPoxy處理NSTimer循環(huán)引用的問題:https://github.com/lwc1990/MessageForward_NSProxyDemo/tree/master/WeakProxyDemo
運用運行時實現(xiàn)自己的通知中心:https://github.com/lwc1990/CustomNotification
運用運行時研究KVO:https://github.com/lwc1990/KVOStudyDemo

結(jié)語:我個人認(rèn)為,運行時是一個很好的研究系統(tǒng)提供的各種方法或設(shè)計模式實現(xiàn)的工具友题,除個別場景之外嗤堰,在復(fù)雜的項目中可被運用的方式很少。
但是度宦,我們無法否認(rèn)的是踢匣,對其理解和運用,這種炫技后的滿足感戈抄!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末离唬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子划鸽,更是在濱河造成了極大的恐慌输莺,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裸诽,死亡現(xiàn)場離奇詭異嫂用,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)丈冬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門嘱函,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人埂蕊,你說我怎么就攤上這事往弓。” “怎么了蓄氧?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵函似,是天一觀的道長。 經(jīng)常有香客問我喉童,道長撇寞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮蔑担,結(jié)果婚禮上露氮,老公的妹妹穿的比我還像新娘。我一直安慰自己钟沛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布局扶。 她就那樣靜靜地躺著恨统,像睡著了一般。 火紅的嫁衣襯著肌膚如雪三妈。 梳的紋絲不亂的頭發(fā)上畜埋,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機(jī)與錄音畴蒲,去河邊找鬼悠鞍。 笑死,一個胖子當(dāng)著我的面吹牛模燥,可吹牛的內(nèi)容都是我干的咖祭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼蔫骂,長吁一口氣:“原來是場噩夢啊……” “哼么翰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辽旋,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤浩嫌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后补胚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體码耐,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年溶其,在試婚紗的時候發(fā)現(xiàn)自己被綠了骚腥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡握联,死狀恐怖桦沉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情金闽,我是刑警寧澤纯露,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站代芜,受9級特大地震影響埠褪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一钞速、第九天 我趴在偏房一處隱蔽的房頂上張望贷掖。 院中可真熱鬧,春花似錦渴语、人聲如沸苹威。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牙甫。三九已至,卻和暖如春调违,著一層夾襖步出監(jiān)牢的瞬間窟哺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工技肩, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留且轨,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓虚婿,卻偏偏與公主長得像旋奢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子雳锋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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