詳解OC中的Runtime

簡(jiǎn)介

Runtime 又叫運(yùn)行時(shí)铐维,是一套底層的 C 語(yǔ)言 API,其為 iOS 內(nèi)部的核心之一慎菲,我們平時(shí)編寫(xiě)的 OC 代碼嫁蛇,底層都是基于它來(lái)實(shí)現(xiàn)的。比如:

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

以上你可能看不出它的價(jià)值露该,但是我們需要了解的是 Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言睬棚,它會(huì)將一些工作放在代碼運(yùn)行時(shí)才處理而并非編譯時(shí)。也就是說(shuō)解幼,有很多類(lèi)和成員變量在我們編譯的時(shí)是不知道的抑党,而在運(yùn)行時(shí),我們所編寫(xiě)的代碼會(huì)轉(zhuǎn)換成完整的確定的代碼運(yùn)行撵摆。

因此底靠,編譯器是不夠的,我們還需要一個(gè)運(yùn)行時(shí)系統(tǒng)(Runtime system)來(lái)處理編譯后的代碼特铝。

Runtime 基本是用 C 和匯編寫(xiě)的暑中,由此可見(jiàn)蘋(píng)果為了動(dòng)態(tài)系統(tǒng)的高效而做出的努力。蘋(píng)果和 GNU 各自維護(hù)一個(gè)開(kāi)源的 Runtime 版本鲫剿,這兩個(gè)版本之間都在努力保持一致鳄逾。

點(diǎn)擊這里下載蘋(píng)果維護(hù)的開(kāi)源代碼。


Runtime 的作用

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

  1. 通過(guò) Objective-C 源代碼
  2. 通過(guò) Foundation 框架的 NSObject 類(lèi)定義的方法
  3. 通過(guò)對(duì) Runtime 庫(kù)函數(shù)的直接調(diào)用

Objective-C 源代碼

多數(shù)情況我們只需要編寫(xiě) OC 代碼即可灵莲,Runtime 系統(tǒng)自動(dòng)在幕后搞定一切雕凹,還記得簡(jiǎn)介中如果我們調(diào)用方法,編譯器會(huì)將 OC 代碼轉(zhuǎn)換成運(yùn)行時(shí)代碼,在運(yùn)行時(shí)確定數(shù)據(jù)結(jié)構(gòu)和函數(shù)请琳。

通過(guò) Foundation 框架的 NSObject 類(lèi)定義的方法

Cocoa 程序中絕大部分類(lèi)都是 NSObject 類(lèi)的子類(lèi),所以都繼承了 NSObject 的行為赠幕。(NSProxy 類(lèi)時(shí)個(gè)例外俄精,它是個(gè)抽象超類(lèi))

一些情況下,NSObject 類(lèi)僅僅定義了完成某件事情的模板榕堰,并沒(méi)有提供所需要的代碼竖慧。例如 -description 方法,該方法返回類(lèi)內(nèi)容的字符串表示逆屡,該方法主要用來(lái)調(diào)試程序圾旨。NSObject 類(lèi)并不知道子類(lèi)的內(nèi)容,所以它只是返回類(lèi)的名字和對(duì)象的地址魏蔗,NSObject 的子類(lèi)可以重新實(shí)現(xiàn)砍的。

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

  • -class方法返回對(duì)象的類(lèi)廓鞠;
  • -isKindOfClass:-isMemberOfClass: 方法檢查對(duì)象是否存在于指定的類(lèi)的繼承體系中(是否是其子類(lèi)或者父類(lèi)或者當(dāng)前類(lèi)的成員變量);
  • -respondsToSelector: 檢查對(duì)象能否響應(yīng)指定的消息谣旁;
  • -conformsToProtocol:檢查對(duì)象是否實(shí)現(xiàn)了指定協(xié)議類(lèi)的方法床佳;
  • -methodForSelector: 返回指定方法實(shí)現(xiàn)的地址。

通過(guò)對(duì) Runtime 庫(kù)函數(shù)的直接調(diào)用

Runtime 系統(tǒng)是具有公共接口的動(dòng)態(tài)共享庫(kù)榄审。頭文件存放于/usr/include/objc目錄下砌们,這意味著我們使用時(shí)只需要引入objc/Runtime.h頭文件即可。

許多函數(shù)可以讓你使用純 C 代碼來(lái)實(shí)現(xiàn) Objc 中同樣的功能搁进。除非是寫(xiě)一些 Objc 與其他語(yǔ)言的橋接或是底層的 debug 工作浪感,你在寫(xiě) Objc 代碼時(shí)一般不會(huì)用到這些 C 語(yǔ)言函數(shù)。對(duì)于公共接口都有哪些饼问,后面會(huì)講到篮撑。我將會(huì)參考蘋(píng)果官方的 API 文檔。


一些 Runtime 的術(shù)語(yǔ)的數(shù)據(jù)結(jié)構(gòu)

要想全面了解 Runtime 機(jī)制匆瓜,我們必須先了解 Runtime 的一些術(shù)語(yǔ)赢笨,他們都對(duì)應(yīng)著數(shù)據(jù)結(jié)構(gòu)。

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 類(lèi))驮吱。selector 是方法選擇器茧妒,其實(shí)作用就和名字一樣,日常生活中左冬,我們通過(guò)人名辨別誰(shuí)是誰(shuí)桐筏,注意 Objc 在相同的類(lèi)中不會(huì)有命名相同的兩個(gè)方法。selector 對(duì)方法名進(jìn)行包裝拇砰,以便找到對(duì)應(yīng)的方法實(shí)現(xiàn)梅忌。它的數(shù)據(jù)結(jié)構(gòu)是:

typedef struct objc_selector *SEL;

我們可以看出它是個(gè)映射到方法的 C 字符串狰腌,你可以通過(guò) Objc 編譯器器命令@selector() 或者 Runtime 系統(tǒng)的 sel_registerName 函數(shù)來(lái)獲取一個(gè) SEL 類(lèi)型的方法選擇器。

注意:
不同類(lèi)中相同名字的方法所對(duì)應(yīng)的 selector 是相同的牧氮,由于變量的類(lèi)型不同琼腔,所以不會(huì)導(dǎo)致它們調(diào)用方法實(shí)現(xiàn)混亂。

id

id 是一個(gè)參數(shù)類(lèi)型踱葛,它是指向某個(gè)類(lèi)的實(shí)例的指針丹莲。定義如下:

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

以上定義,看到 objc_object 結(jié)構(gòu)體包含一個(gè) isa 指針尸诽,根據(jù) isa 指針就可以找到對(duì)象所屬的類(lèi)甥材。

注意:
isa 指針在代碼運(yùn)行時(shí)并不總指向?qū)嵗龑?duì)象所屬的類(lèi)型,所以不能依靠它來(lái)確定類(lèi)型性含,要想確定類(lèi)型還是需要用對(duì)象的 -class 方法洲赵。

PS:KVO 的實(shí)現(xiàn)機(jī)理就是將被觀察對(duì)象的 isa 指針指向一個(gè)中間類(lèi)而不是真實(shí)類(lèi)型。相當(dāng)于在調(diào)用KVO添加觀察者時(shí)商蕴,動(dòng)態(tài)創(chuàng)建了一個(gè)子類(lèi)板鬓,isa指向了這個(gè)被觀察者的子類(lèi)

Class

typedef struct objc_class *Class;

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

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

objc_class 可以看到究恤,一個(gè)運(yùn)行時(shí)類(lèi)中關(guān)聯(lián)了它的父類(lèi)指針俭令、類(lèi)名、成員變量部宿、方法、緩存以及附屬的協(xié)議暂幼。

其中 objc_ivar_listobjc_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;
}

由此可見(jiàn),我們可以動(dòng)態(tài)修改 *methodList 的值來(lái)添加成員方法叮姑,這也是 Category 實(shí)現(xiàn)的原理耕拷,同樣解釋了 Category 不能添加屬性的原因碧查。這里可以參考下:深入理解 Objective-C: Category卦方。

objc_ivar_list 結(jié)構(gòu)體用來(lái)存儲(chǔ)成員變量的列表,而 objc_ivar 則是存儲(chǔ)了單個(gè)成員變量的信息泰佳;同理盼砍,objc_method_list 結(jié)構(gòu)體存儲(chǔ)著方法數(shù)組的列表,而單個(gè)方法的信息則由 objc_method 結(jié)構(gòu)體存儲(chǔ)逝她。

值得注意的時(shí)衬廷,objc_class 中也有一個(gè) isa 指針,這說(shuō)明 Objc 類(lèi)本身也是一個(gè)對(duì)象汽绢。為了處理類(lèi)和對(duì)象的關(guān)系吗跋,Runtime 庫(kù)創(chuàng)建了一種叫做 Meta Class(元類(lèi)) 的東西,類(lèi)對(duì)象所屬的類(lèi)就叫做元類(lèi)。Meta Class 表述了類(lèi)對(duì)象本身所具備的元數(shù)據(jù)跌宛。

我們所熟悉的類(lèi)方法酗宋,就源自于 Meta Class。我們可以理解為類(lèi)方法就是類(lèi)對(duì)象的實(shí)例方法疆拘。每個(gè)類(lèi)僅有一個(gè)類(lèi)對(duì)象蜕猫,而每個(gè)類(lèi)對(duì)象僅有一個(gè)與之相關(guān)的元類(lèi)。

當(dāng)你發(fā)出一個(gè)類(lèi)似 [NSObject alloc](類(lèi)方法) 的消息時(shí)哎迄,實(shí)際上回右,這個(gè)消息被發(fā)送給了一個(gè)類(lèi)對(duì)象(Class Object),這個(gè)類(lèi)對(duì)象必須是一個(gè)元類(lèi)的實(shí)例漱挚,而這個(gè)元類(lèi)同時(shí)也是一個(gè)根元類(lèi)(Root Meta Class)的實(shí)例翔烁。所有元類(lèi)的 isa 指針最終都指向根元類(lèi)。

所以當(dāng) [NSObject alloc] 這條消息發(fā)送給類(lèi)對(duì)象的時(shí)候旨涝,運(yùn)行時(shí)代碼 objc_msgSend() 會(huì)去它元類(lèi)中查找能夠響應(yīng)消息的方法實(shí)現(xiàn)蹬屹,如果找到了,就會(huì)對(duì)這個(gè)類(lèi)對(duì)象執(zhí)行方法調(diào)用白华。

image

上圖實(shí)現(xiàn)是 super_class 指針慨默,虛線時(shí) isa 指針。而根元類(lèi)的父類(lèi)是 NSObject弧腥,isa指向了自己厦取。而 NSObject 沒(méi)有父類(lèi)。

最后 objc_class 中還有一個(gè) objc_cache 管搪,緩存虾攻,它的作用很重要,后面會(huì)提到抛蚤。

Method

Method 代表類(lèi)中某個(gè)方法的類(lèi)型

typedef struct objc_method *Method;

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

objc_method 存儲(chǔ)了方法名台谢,方法類(lèi)型和方法實(shí)現(xiàn):

  • 方法名類(lèi)型為 SEL
  • 方法類(lèi)型 method_types 是個(gè) char 指針,存儲(chǔ)方法的參數(shù)類(lèi)型和返回值類(lèi)型
  • method_imp 指向了方法的實(shí)現(xiàn)岁经,本質(zhì)是一個(gè)函數(shù)指針

Ivar

Ivar 是表示成員變量的類(lèi)型朋沮。

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é)

IMP

IMP在objc.h中的定義是:

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

它就是一個(gè)函數(shù)指針,這是由編譯器生成的缀壤。當(dāng)你發(fā)起一個(gè) ObjC 消息之后樊拓,最終它會(huì)執(zhí)行的那段代碼,就是由這個(gè)函數(shù)指針指定的塘慕。而 IMP 這個(gè)函數(shù)指針就指向了這個(gè)方法的實(shí)現(xiàn)筋夏。

如果得到了執(zhí)行某個(gè)實(shí)例某個(gè)方法的入口,我們就可以繞開(kāi)消息傳遞階段图呢,直接執(zhí)行方法条篷,這在后面 Cache 中會(huì)提到骗随。

你會(huì)發(fā)現(xiàn) IMP 指向的方法與 objc_msgSend 函數(shù)類(lèi)型相同,參數(shù)都包含 idSEL 類(lèi)型赴叹。每個(gè)方法名都對(duì)應(yīng)一個(gè) SEL 類(lèi)型的方法選擇器鸿染,而每個(gè)實(shí)例對(duì)象中的 SEL 對(duì)應(yīng)的方法實(shí)現(xiàn)肯定是唯一的,通過(guò)一組 idSEL 參數(shù)就能確定唯一的方法實(shí)現(xiàn)地址乞巧。

而一個(gè)確定的方法也只有唯一的一組 idSEL 參數(shù)涨椒。

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)實(shí)例對(duì)象接收到一個(gè)消息時(shí)绽媒,它不會(huì)直接在 isa 指針指向的類(lèi)的方法列表中遍歷查找能夠響應(yīng)的方法蚕冬,因?yàn)槊看味家檎倚侍土耍莾?yōu)先在 Cache 中查找是辕。

Runtime 系統(tǒng)會(huì)把被調(diào)用的方法存到 Cache 中囤热,如果一個(gè)方法被調(diào)用,那么它有可能今后還會(huì)被調(diào)用免糕,下次查找的時(shí)候就會(huì)效率更高赢乓。就像計(jì)算機(jī)組成原理中 CPU 繞過(guò)主存先訪問(wèn) Cache 一樣忧侧。

Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個(gè)更常用

可以通過(guò)class_copyPropertyListprotocol_copyPropertyList 方法獲取類(lèi)和協(xié)議中的屬性:

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

注意:
返回的是屬性列表石窑,列表中每個(gè)元素都是一個(gè) objc_property_t 指針

#import <Foundation/Foundation.h>

@interface Person : NSObject

/** 姓名 */
@property (strong, nonatomic) NSString *name;

/** age */
@property (assign, nonatomic) int age;

/** weight */
@property (assign, nonatomic) double weight;

@end

以上是一個(gè) Person 類(lèi),有3個(gè)屬性蚓炬。讓我們用上述方法獲取類(lèi)的運(yùn)行時(shí)屬性松逊。

    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);
    }

打印結(jié)果如下:

2014-11-10 11:27:28.473 test[2321:451525] 3
2014-11-10 11:27:28.473 test[2321:451525] name--------T@"NSString",&,N,V_name
2014-11-10 11:27:28.473 test[2321:451525] age--------Ti,N,V_age
2014-11-10 11:27:28.474 test[2321:451525] weight--------Td,N,V_weight

property_getName 用來(lái)查找屬性的名稱(chēng),返回 c 字符串肯夏。property_getAttributes 函數(shù)挖掘?qū)傩缘恼鎸?shí)名稱(chēng)和 @encode 類(lèi)型经宏,返回 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_getPropertyprotocol_getProperty 通過(guò)給出屬性名在類(lèi)和協(xié)議中獲得屬性的引用驯击。


消息

一些 Runtime 術(shù)語(yǔ)講完了烁兰,接下來(lái)就要說(shuō)到消息了。體會(huì)蘋(píng)果官方文檔中的 messages aren’t bound to method implementations until Runtime徊都。消息直到運(yùn)行時(shí)才會(huì)與方法實(shí)現(xiàn)進(jìn)行綁定沪斟。

這里要清楚一點(diǎn),objc_msgSend 方法看清來(lái)好像返回了數(shù)據(jù)暇矫,其實(shí)objc_msgSend 從不返回?cái)?shù)據(jù)主之,而是你的方法在運(yùn)行時(shí)實(shí)現(xiàn)被調(diào)用后才會(huì)返回?cái)?shù)據(jù)。下面詳細(xì)敘述消息發(fā)送的步驟(如下圖):

image
  1. 首先檢測(cè)這個(gè) selector 是不是要忽略李根。比如 Mac OS X 開(kāi)發(fā)槽奕,有了垃圾回收就不理會(huì) retain,release 這些函數(shù)房轿。
  2. 檢測(cè)這個(gè) selector 的 target 是不是 nil粤攒,Objc 允許我們對(duì)一個(gè) nil 對(duì)象執(zhí)行任何方法不會(huì) Crash所森,因?yàn)檫\(yùn)行時(shí)會(huì)被忽略掉。
  3. 如果上面兩步都通過(guò)了夯接,那么就開(kāi)始查找這個(gè)類(lèi)的實(shí)現(xiàn) IMP必峰,先從 cache 里查找,如果找到了就運(yùn)行對(duì)應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼钻蹬。
  4. 如果 cache 找不到就找類(lèi)的方法列表中是否有對(duì)應(yīng)的方法吼蚁。
  5. 如果類(lèi)的方法列表中找不到就到父類(lèi)的方法列表中查找,一直找到 NSObject 類(lèi)為止问欠。
  6. 如果還找不到肝匆,就要開(kāi)始進(jìn)入動(dòng)態(tài)方法解析了,后面會(huì)提到顺献。

在消息的傳遞中旗国,編譯器會(huì)根據(jù)情況在 objc_msgSendobjc_msgSend_stret 注整, objc_msgSendSuper 能曾, objc_msgSendSuper_stret 這四個(gè)方法中選擇一個(gè)調(diào)用。如果消息是傳遞給父類(lèi)肿轨,那么會(huì)調(diào)用名字帶有 Super 的函數(shù)寿冕,如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡(jiǎn)單值時(shí),會(huì)調(diào)用名字帶有 stret 的函數(shù)椒袍。

方法中的隱藏參數(shù)

疑問(wèn):
我們經(jīng)常用到關(guān)鍵字 self 驼唱,但是 self 是如何獲取當(dāng)前方法的對(duì)象呢?

其實(shí)驹暑,這也是 Runtime 系統(tǒng)的作用玫恳,self 實(shí)在方法運(yùn)行時(shí)被動(dòng)態(tài)傳入的。

當(dāng) objc_msgSend 找到方法對(duì)應(yīng)實(shí)現(xiàn)時(shí)优俘,它將直接調(diào)用該方法實(shí)現(xiàn)京办,并將消息中所有參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí)帆焕,它還將傳遞兩個(gè)隱藏參數(shù):

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

因?yàn)樵谠创a方法的定義中视搏,我們并沒(méi)有發(fā)現(xiàn)這兩個(gè)參數(shù)的聲明审孽。它們時(shí)在代碼被編譯時(shí)被插入方法實(shí)現(xiàn)中的。盡管這些參數(shù)沒(méi)有被明確聲明浑娜,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?/p>

這兩個(gè)參數(shù)中佑力, self更實(shí)用。它是在方法實(shí)現(xiàn)中訪問(wèn)消息接收者對(duì)象的實(shí)例變量的途徑筋遭。

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

struct objc_super { id receiver; Class class; };

這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類(lèi)。 receiver 仍然是 self 本身编饺,當(dāng)我們想通過(guò) [super class] 獲取父類(lèi)時(shí)乖篷,編譯器其實(shí)是將指向 selfid 指針和 class 的 SEL 傳遞給了 objc_msgSendSuper 函數(shù)。只有在 NSObject 類(lèi)中才能找到 class 方法透且,然后 class 方法底層被轉(zhuǎn)換為 object_getClass()撕蔼, 接著底層編譯器將代碼轉(zhuǎn)換為 objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個(gè)參數(shù)是指向 selfid 指針秽誊,與調(diào)用 [self class] 相同鲸沮,所以我們得到的永遠(yuǎn)都是 self 的類(lèi)型。因此你會(huì)發(fā)現(xiàn):

// 這句話(huà)并不能獲取父類(lèi)的類(lèi)型锅论,只能獲取當(dāng)前類(lèi)的類(lèi)型名
NSLog(@"%@", NSStringFromClass([super class]));

獲取方法地址

NSObject 類(lèi)中有一個(gè)實(shí)例方法:methodForSelector讼溺,你可以用它來(lái)獲取某個(gè)方法選擇器對(duì)應(yīng)的 IMP 灯萍,舉個(gè)例子:

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í)瘦赫,兩個(gè)隱藏參數(shù)也必須明確給出循头,上面的例子調(diào)用了1000次函數(shù)艇棕,你也可以嘗試給 target 發(fā)送1000次 setFilled: 消息會(huì)花多久。

雖然可以更高效的調(diào)用方法转培,但是這種做法很少用硕糊,除非時(shí)需要持續(xù)大量重復(fù)調(diào)用某個(gè)方法的情況倾贰,才會(huì)選擇使用以免消息發(fā)送泛濫束析。

注意:
methodForSelector:方法是由 Runtime 系統(tǒng)提供的艳馒,而不是 Objc 自身的特性


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

你可以動(dòng)態(tài)提供一個(gè)方法實(shí)現(xiàn)憎亚。如果我們使用關(guān)鍵字 @dynamic 在類(lèi)的實(shí)現(xiàn)文件中修飾一個(gè)屬性员寇,表明我們會(huì)為這個(gè)屬性動(dòng)態(tài)提供存取方法,編譯器不會(huì)再默認(rèn)為我們生成這個(gè)屬性的 setter 和 getter 方法了第美,需要我們自己提供蝶锋。

@dynamic propertyName;

這時(shí),我們可以通過(guò)分別重載 resolveInstanceMethod:resolveClassMethod: 方法添加實(shí)例方法實(shí)現(xiàn)和類(lèi)方法實(shí)現(xiàn)什往。

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

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子為 resolveThisMethodDynamically 方法添加了實(shí)現(xiàn)內(nèi)容别威,就是 dynamicMethodIMP 方法中的代碼躯舔。其中 "v@:" 表示返回值和參數(shù),這個(gè)符號(hào)表示的含義見(jiàn):Type Encoding

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


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

image

重定向

消息轉(zhuǎn)發(fā)機(jī)制執(zhí)行前惜互,Runtime 系統(tǒng)允許我們替換消息的接收者為其他對(duì)象布讹。通過(guò) - (id)forwardingTargetForSelector:(SEL)aSelector 方法。

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

如果此方法返回 nil 或者 self训堆,則會(huì)計(jì)入消息轉(zhuǎn)發(fā)機(jī)制(forwardInvocation:)描验,否則將向返回的對(duì)象重新發(fā)送消息。

轉(zhuǎn)發(fā)

當(dāng)動(dòng)態(tài)方法解析不做處理返回 NO 時(shí)坑鱼,則會(huì)觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制膘流。這時(shí) forwardInvocation: 方法會(huì)被執(zhí)行,我們可以重寫(xiě)這個(gè)方法來(lái)自定義我們的轉(zhuǎn)發(fā)邏輯:

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

唯一參數(shù)是個(gè) NSInvocation 類(lèi)型的對(duì)象鲁沥,該對(duì)象封裝了原始的消息和消息的參數(shù)睡扬。我們可以實(shí)現(xiàn) forwardInvocation: 方法來(lái)對(duì)不能處理的消息做一些處理。也可以將消息轉(zhuǎn)發(fā)給其他對(duì)象處理黍析,而不拋出錯(cuò)誤卖怜。

注意:參數(shù) anInvocation 是從哪來(lái)的?
forwardInvocation: 消息發(fā)送前阐枣,Runtime 系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector: 消息马靠,并取到返回的方法簽名用于生成 NSInvocation 對(duì)象。所以重寫(xiě) forwardInvocation: 的同時(shí)也要重寫(xiě) methodSignatureForSelector: 方法蔼两,否則會(huì)拋異常甩鳄。

當(dāng)一個(gè)對(duì)象由于沒(méi)有相應(yīng)的方法實(shí)現(xiàn)而無(wú)法相應(yīng)某消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過(guò) forwardInvocation: 消息通知該對(duì)象额划。每個(gè)對(duì)象都繼承了 forwardInvocation: 方法妙啃。但是, NSObject 中的方法實(shí)現(xiàn)只是簡(jiǎn)單的調(diào)用了 doesNotRecognizeSelector:俊戳。通過(guò)實(shí)現(xiàn)自己的 forwardInvocation: 方法揖赴,我們可以將消息轉(zhuǎn)發(fā)給其他對(duì)象。

forwardInvocation: 方法就是一個(gè)不能識(shí)別消息的分發(fā)中心抑胎,將這些不能識(shí)別的消息轉(zhuǎn)發(fā)給不同的接收對(duì)象燥滑,或者轉(zhuǎn)發(fā)給同一個(gè)對(duì)象,再或者將消息翻譯成另外的消息阿逃,亦或者簡(jiǎn)單的“吃掉”某些消息铭拧,因此沒(méi)有響應(yīng)也不會(huì)報(bào)錯(cuò)。這一切都取決于方法的具體實(shí)現(xiàn)恃锉。

注意:
forwardInvocation:方法只有在消息接收對(duì)象中無(wú)法正常響應(yīng)消息時(shí)才會(huì)被調(diào)用搀菩。所以,如果我們向往一個(gè)對(duì)象將一個(gè)消息轉(zhuǎn)發(fā)給其他對(duì)象時(shí)破托,要確保這個(gè)對(duì)象不能有該消息的所對(duì)應(yīng)的方法肪跋。否則,forwardInvocation:將不可能被調(diào)用炼团。

轉(zhuǎn)發(fā)和多繼承

轉(zhuǎn)發(fā)和繼承相似澎嚣,可用于為 Objc 編程添加一些多繼承的效果疏尿。就像下圖那樣,一個(gè)對(duì)象把消息轉(zhuǎn)發(fā)出去易桃,就好像它把另一個(gè)對(duì)象中的方法接過(guò)來(lái)或者“繼承”過(guò)來(lái)一樣褥琐。

image

這使得在不同繼承體系分支下的兩個(gè)類(lèi)可以實(shí)現(xiàn)“繼承”對(duì)方的方法,在上圖中 WarriorDiplomat 沒(méi)有繼承關(guān)系晤郑,但是 Warriornegotiate 消息轉(zhuǎn)發(fā)給了 Diplomat 后敌呈,就好似 DiplomatWarrior 的超類(lèi)一樣。

消息轉(zhuǎn)發(fā)彌補(bǔ)了 Objc 不支持多繼承的性質(zhì)造寝,也避免了因?yàn)槎嗬^承導(dǎo)致單個(gè)類(lèi)變得臃腫復(fù)雜磕洪。

轉(zhuǎn)發(fā)與繼承

雖然轉(zhuǎn)發(fā)可以實(shí)現(xiàn)繼承的功能,但是 NSObject 還是必須表面上很?chē)?yán)謹(jǐn)诫龙,像 respondsToSelector:isKindOfClass: 這類(lèi)方法只會(huì)考慮繼承體系析显,不會(huì)考慮轉(zhuǎn)發(fā)鏈。

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

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

回答當(dāng)然是 NO签赃, 盡管它能接受 negotiate 消息而不報(bào)錯(cuò)谷异,因?yàn)樗哭D(zhuǎn)發(fā)消息給 Diplomat 類(lèi)響應(yīng)消息。

如果你就是想要讓別人以為 Warrior 繼承到了 Diplomatnegotiate 方法锦聊,你得重新實(shí)現(xiàn) respondsToSelector:isKindOfClass: 來(lái)加入你的轉(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)該寫(xiě)一份轉(zhuǎn)發(fā)算法。如果使用了協(xié)議孔庭,conformsToProtocol: 同樣也要加入到這一行列中尺上。

如果一個(gè)對(duì)象想要轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息,它得給出一個(gè)方法標(biāo)簽來(lái)返回準(zhǔn)確的方法描述 methodSignatureForSelector:圆到,這個(gè)方法會(huì)最終響應(yīng)被轉(zhuǎn)發(fā)的消息怎抛。從而生成一個(gè)確定的 NSInvocation 對(duì)象描述消息和消息參數(shù)。這個(gè)方法最終響應(yīng)被轉(zhuǎn)發(fā)的消息构资。它需要像下面這樣實(shí)現(xiàn):

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

健壯的實(shí)例變量(Non Fragile ivars)

在 Runtime 的現(xiàn)行版本中抽诉,最大的特點(diǎn)就是健壯的實(shí)例變量了。當(dāng)一個(gè)類(lèi)被編譯時(shí)吐绵,實(shí)例變量的內(nèi)存布局就形成了,它表明訪問(wèn)類(lèi)的實(shí)例變量的位置河绽。實(shí)例變量一次根據(jù)自己所占空間而產(chǎn)生位移:

image

上圖左是 NSObject 類(lèi)的實(shí)例變量布局己单。右邊是我們寫(xiě)的類(lèi)的布局。這樣子有一個(gè)很大的缺陷耙饰,就是缺乏拓展性纹笼。哪天蘋(píng)果更新了 NSObject 類(lèi)的話(huà),就會(huì)出現(xiàn)問(wèn)題:

image

我們自定義的類(lèi)的區(qū)域和父類(lèi)的區(qū)域重疊了苟跪。只有蘋(píng)果將父類(lèi)改為以前的布局才能拯救我們廷痘,但這樣導(dǎo)致它們不能再拓展它們的框架了蔓涧,因?yàn)槌蓡T變量布局被固定住了。在脆弱的實(shí)例變量(Fragile ivar)環(huán)境下笋额,需要我們重新編譯繼承自 Apple 的類(lèi)來(lái)恢復(fù)兼容元暴。如果是健壯的實(shí)例變量的話(huà),在健壯的實(shí)例變量下兄猩,編譯器生成的實(shí)例變量布局跟以前一樣茉盏,但是當(dāng) Runtime 系統(tǒng)檢測(cè)到與父類(lèi)有部分重疊時(shí)它會(huì)調(diào)整你新添加的實(shí)例變量的位移,那樣你再子類(lèi)中新添加的成員變量就被保護(hù)起來(lái)了枢冤。

注意:
在健壯的實(shí)例變量下鸠姨,不要使用 siof(SomeClass),而是用 class_getInstanceSize([SomeClass class]) 代替淹真;也不要使用 offsetof(SomeClass, SomeIvar)讶迁,而要使用 ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar")) 來(lái)代替。


總結(jié)

我們讓自己的類(lèi)繼承自 NSObject 不僅僅是因?yàn)榛?lèi)有很多復(fù)雜的內(nèi)存分配問(wèn)題核蘸,更是因?yàn)檫@使得我們可以享受到 Runtime 系統(tǒng)帶來(lái)的便利添瓷。

雖然平時(shí)我們很少會(huì)考慮一句簡(jiǎn)單的調(diào)用方法,發(fā)送消息底層所做的復(fù)雜的操作值纱,但深入理解 Runtime 系統(tǒng)的細(xì)節(jié)使得我們可以利用消息機(jī)制寫(xiě)出功能更強(qiáng)大的代碼鳞贷。

如果學(xué)會(huì)Runtime不是為了裝逼,那將毫無(wú)意義虐唠!
如果Runtime你不會(huì)搀愧,那你還拿什么裝逼!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疆偿,一起剝皮案震驚了整個(gè)濱河市咱筛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杆故,老刑警劉巖迅箩,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異处铛,居然都是意外死亡饲趋,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)撤蟆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)奕塑,“玉大人,你說(shuō)我怎么就攤上這事家肯×渑椋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)换棚。 經(jīng)常有香客問(wèn)我式镐,道長(zhǎng),這世上最難降的妖魔是什么固蚤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任娘汞,我火速辦了婚禮,結(jié)果婚禮上颇蜡,老公的妹妹穿的比我還像新娘价说。我一直安慰自己,他們只是感情好风秤,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布鳖目。 她就那樣靜靜地躺著,像睡著了一般缤弦。 火紅的嫁衣襯著肌膚如雪领迈。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天碍沐,我揣著相機(jī)與錄音狸捅,去河邊找鬼。 笑死累提,一個(gè)胖子當(dāng)著我的面吹牛尘喝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斋陪,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼朽褪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了无虚?” 一聲冷哼從身側(cè)響起缔赠,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎友题,沒(méi)想到半個(gè)月后嗤堰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡度宦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年踢匣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斗埂。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡符糊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呛凶,到底是詐尸還是另有隱情,我是刑警寧澤行贪,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布漾稀,位于F島的核電站模闲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崭捍。R本人自食惡果不足惜尸折,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望殷蛇。 院中可真熱鬧实夹,春花似錦、人聲如沸粒梦。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)匀们。三九已至缴淋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泄朴,已是汗流浹背重抖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祖灰,地道東北人钟沛。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像局扶,于是被迫代替她去往敵國(guó)和親恨统。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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