Runtime總結(jié)

目錄

  • 簡介
  • Runtime的作用
    • 通過 Objective-C源代碼
    • 通過 Foundation 框架的 NSObject類定義的方法
    • 通過對 Runtime 庫函數(shù)的直接調(diào)用
  • Runtime 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
    • SEL
    • id
    • Class
    • Method
    • Ivar
    • IMP
    • Cache
    • objc_property_t
    • protocol_t
    • Category
  • 消息
    • objc_msgSend函數(shù) 消息發(fā)送步驟
    • 方法中的隱藏參數(shù)
    • 獲取方法地址
  • 動態(tài)方法解析
  • 消息轉(zhuǎn)發(fā)
    • 重定向 轉(zhuǎn)發(fā)目標(biāo) 備用接收者
    • 完整轉(zhuǎn)發(fā) 轉(zhuǎn)發(fā)調(diào)用 替換消息的方法
    • 轉(zhuǎn)發(fā)與多繼承
      • 替代者對象 使輕量級對象代表重量級對象
      • 轉(zhuǎn)發(fā)與繼承
  • 健壯的實例變量 (Non Fragile ivars)
  • 總結(jié)

簡介

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, ...)

以上你可能看不出它的價值,但是我們需要了解的是Objective-C 是一門動態(tài)語言,它會將一些工作放在代碼運行時才處理而并非編譯時。也就是說,有很多類和成員變量在我們編譯的時是不知道的,而在運行時,我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運行。

因此,編譯器是不夠的,我們還需要一個運行時系統(tǒng)(Runtime system)來處理編譯后的代碼。

Runtime基本是用C和匯編寫的,可見蘋果為了動態(tài)系統(tǒng)的高效而作出的努力。你可以在蘋果開源代碼中心下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。

Runtime 的作用

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

  • 1.通過 Objective-C源代碼
  • 2.通過 Foundation 框架的 NSObject類定義的方法
  • 3.通過對 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)中獲取信息,允許對象進行自我檢查。例如:

  • -class方法返回對象的類;
  • -isKindOfClass:-isMemberOfClass:方法檢查對象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當(dāng)前類的成員變量);
    • isKindOfClass用來判斷某個對象是否屬于某個類,或者屬于某個派生類
    • isMemberOfClass用來判斷某個對象是否為當(dāng)前類的實例
    • isMemberOfClass不能檢測任何的類都是基于NSObject類這一事實,而isKindOfClass可以。
  • -respondsToSelector:檢查對象能否響應(yīng)指定的消息;
  • -conformsToProtocol:檢車對象是否實現(xiàn)了指定協(xié)議類的方法;
  • -methodForSelector:返回指定方法實現(xiàn)的地址。

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

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

許多函數(shù)可以讓你使用純C代碼來實現(xiàn)來實現(xiàn)Objc中同樣的功能。除非是寫一些Objc中同樣的功能。除非是寫一些Objc與其他語言的橋接或者是底層的debug工作,你在寫Objc代碼時一般不會用到這些C語言函數(shù)。對于公共接口都有哪些,后面會講到。我將會參考蘋果官方的API文檔。

Runtime 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)

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

SEL

objc_msgSend函數(shù)第二個參數(shù)類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區(qū)分方法的ID,而這個ID的數(shù)據(jù)結(jié)構(gòu)是SEL

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

其實它就是個映射到方法的C字符串,你可以用Objc編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來獲得一個SEL類型的方法選擇器。

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

id

objc_msgSend 第一個參數(shù)類型為id,大家對它都不陌生,它是一個指向類實例的指針:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

以上定義,看到objc_object結(jié)構(gòu)體包含一個isa指針,isa指向?qū)ο笏鶎俚念悺?/p>

注意:
isa指針在代碼運行時并不能總指向?qū)嵗龑ο笏鶎俚念愋?,所以不能依靠它來確定類型,要想確定類型還是需要用對象的-class方法。

PS:KVO 的實現(xiàn)機理就是將被觀察對象的 isa 指針指向一個中間類而不是真實類型。這是一種叫做 isa-swizzling 的技術(shù),詳見官方文檔。

Class

Class 其實是一個指向objc_class 結(jié)構(gòu)體的指針:

typedef struct objc_class *Class;

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可以看到,一個運行時類中關(guān)聯(lián)了它的父類指針,類名、成員變量、方法、緩存以及附屬的協(xié)議。

// 成員變量列表
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;
}

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

我們再來看看objc-private.h文件中objc_class的源碼:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    ... 省略其他方法
}

objc_class繼承于objc_object,也就是說一個ObjC類本身同時也是一個對象,為了處理類和對象的關(guān)系,runtime庫創(chuàng)建了一種叫做元類(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)用。

類結(jié)構(gòu)圖.png

上圖實線是 super_class 指針,虛線時 isa 指針。而根元類的父類是 NSObject,isa指向了自己。而 NSObject 沒有父類。

最后 objc_class 中還有一個 objc_cache ,緩存,它的作用很重要,后面會提到。

Method

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

typedef struct objc_method *Method;

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

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

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

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é),ivar_name是成員變量名稱,ivar_type是成員變量類型。

IMP

IMP在objc.h中的定義是:

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

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

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

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

而一個確定的方法也只有唯一的一組id和SEL參數(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)用的性能進行優(yōu)化,每當(dāng)實例對象接收到一個消息時,它不會直接在isa指針指向的類的方法列表中遍歷查找能夠響應(yīng)的方法,因為每次都要查找效率太低了,而是優(yōu)先在Cache中查找。

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

objc_property_t

@property標(biāo)記了類中的屬性,它是一個指向property_t結(jié)構(gòu)體的指針:

typedef struct property_t *objc_property_t;

struct property_t {
    const char *name;
    const char *attributes;
};

從上面Runtime的源代碼可以看出,property_t有兩個成員變量,其中name是屬性名稱,attributese是屬性的@encode類型字符串;

我們可以用下面的代碼獲取屬性列表:

id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

也可以用property_getName函數(shù)來查找屬性名稱:

const char *property_getName(objc_property_t property)

也可以用property_getAttributes函數(shù)來發(fā)掘?qū)傩缘拿Q和@encode類型字符串:

const char *property_getAttributes(objc_property_t property)

把上面的代碼放一起,你就能從一個類中獲取它的屬性啦:

id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

對比下class_copyIvarList函數(shù),使用class_copyPropertyList函數(shù)只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。

protocol_t

typedef struct objc_object Protocol;

從上面可以看出Protocolobjc_object類型的結(jié)構(gòu)體,我們再來看看Runtimeobjc-runtime-new.h中的源碼:

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;

    ... 省略一些封裝的便捷 get 方法
};

flags32位指針最后兩位是給加載Mach-Ofix-up階段使用的,前16位預(yù)留給Swift用的。

protocol主要內(nèi)容其實是(可選)方法,其次就是繼承其他protocol。Swift 還支持 protocol 多繼承,所以需要 protocols 數(shù)組來做兼容。

Category

Category 為現(xiàn)有的類提供了拓展性,它是 category_t 結(jié)構(gòu)體的指針。

typedef struct category_t *Category;

category_t 存儲了類別中可以拓展的實例方法、類方法、協(xié)議、實例屬性和類屬性。類屬性是 Objective-C 2016 年新增的特性,沾 Swift 的光。所以 category_t 中有些成員變量是為了兼容 Swift 的特性,Objective-C 暫沒提供接口,僅做了底層數(shù)據(jù)結(jié)構(gòu)上的兼容。

struct category_t {
    const char *name; //類的名字
    classref_t cls; //類
    struct method_list_t *instanceMethods; //category中所有給類添加的實例方法的列表
    struct method_list_t *classMethods; //category中所有添加的類方法的列表
    struct protocol_list_t *protocols; //category實現(xiàn)的所有協(xié)議的列表
    struct property_list_t *instanceProperties; //category中添加的所有屬性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; // 類屬性

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

category_t的成員變量結(jié)構(gòu)可以看出Category可以添加實例方法,類方法,實現(xiàn)協(xié)議和添加屬性,卻沒有ivars,所以不能為類添加成員變量。

消息

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

objc_msgSend函數(shù)

在引言中已經(jīng)對objc_msgSend進行了一點介紹,看起來像是objc_msgSend返回了數(shù)據(jù),其實objc_msgSend從不返回數(shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細敘述下消息發(fā)送步驟:

  • 1.檢測這個selector是不是要忽略的。比如Mac OS X開發(fā),有了垃圾回收就不理會retain,release這些函數(shù)了。
  • 2.檢測這個target是不是nil對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉。
  • 3.如果上面兩個都過了,那就開始查找這個類的IMP,先從cache里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。
  • 4.如果cache找不到從類的methodLists方法列表中去找。
  • 5.如果在類的方法列表中找不到,則到super_class父類的方法列表中找,一直找,直到找到NSObject類為止。
  • 6.如果還找不到就要開始進入動態(tài)方法解析了,后面會提到。
消息發(fā)送步驟.png

其實編譯器會根據(jù)情況在objc_msgSendobjc_msgSend_stret,objc_msgSendSuper,或objc_msgSendSuper_stret四個方法中選擇一個來調(diào)用。果消息是傳遞給超類,那么會調(diào)用名字帶有Super的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時,那么會調(diào)用名字帶有stret的函數(shù)。排列組合正好四個方法。

PS:有木有發(fā)現(xiàn)這些函數(shù)的命名規(guī)律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”。

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

我們經(jīng)常在方法中使用self關(guān)鍵字來引用實例本身,但從沒有想過為什么self就能取到調(diào)用當(dāng)前方法的對象吧。其實self的內(nèi)容是在方法運行時被偷偷的動態(tài)傳入的。

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

  • 接收消息的對象(也就是self指向的內(nèi)容)
  • 方法選擇器(_cmd指向的內(nèi)容)

之所以說它們是隱藏的,是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)。它們是在代碼被編譯時被插入實現(xiàn)的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?br> 而當(dāng)方法中的super關(guān)鍵字接收到消息時,編譯器會創(chuàng)建一個objc_super結(jié)構(gòu)體:

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

struct objc_super { id receiver; Class class; };

這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。但receiver仍然是self本身,這點需要注意,因為當(dāng)我們想通過[super class]獲取超類時,編譯器只是將指向selfid指針和class的SEL傳遞給了objc_msgSendSuper函數(shù),因為只有在NSObject才能找到class方法,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數(shù)是指向selfid指針,與調(diào)用[self class]相同,所以我們得到的永遠都是self的類型。

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

獲取方法地址

IMP那節(jié)提到過可以避開消息綁定而直接獲取方法的地址并調(diào)用方法。這種做法很少見,除非是需要持續(xù)大量重復(fù)調(diào)用某方法的極端情況,避開消息發(fā)送泛濫而直接調(diào)用該方法會更高效。

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)用時,上節(jié)提到的兩個隱藏參數(shù)就需要我們明確給出了。上面的例子調(diào)用了1000次函數(shù),你可以試試直接給target發(fā)送1000次setFilled:消息會花多久。

PS:methodForSelector:方法是有Cocoa的Runtime系統(tǒng)提供的,而不是ObjC自身的特性。

動態(tài)方法解析

你可以動態(tài)的提供一個方法的實現(xiàn)。例如我們可以用@dynamic關(guān)鍵字在類的實現(xiàn)文件中修飾一個屬性:

@dynamic propertyName;

這表明我們會為這個屬性動態(tài)提供存取方法,也就是說編譯器不會再默認我們生成setPropertyName:propertyName方法,而需要我們動態(tài)提供。我們可以通過分別重載resolveInstanceMethod:resolveClassMethod:方法分別添加實例方法實現(xiàn)和類方法實現(xiàn)。因為當(dāng)Runtime系統(tǒng)在Cache和方法列表(包括超類)找不到執(zhí)行的方法時,Runtime會調(diào)用resolveInstanceMethod:resolveClassMethod:來給程序員一次動態(tài)添加方法實現(xiàn)的機會。我們需要用class_addMethod函數(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方法添加了實現(xiàn)內(nèi)容,也就是dynamicMethodIMP方法中的代碼。其中v@:表示返回值和參數(shù),這個符合設(shè)計Type Encoding。

PS:動態(tài)方法解析會在消息轉(zhuǎn)發(fā)機制進入前執(zhí)行。如果respondsToSelector:instancesRespondToSelector:方法被執(zhí)行,動態(tài)方法解析器將會被首先給與一個提供該方法選擇器對應(yīng)的IMP的機會。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機制,那么就讓resolveInstanceMethod:返回NO

那有如何用resolveClassMethod:解析類方法呢,我們可以將實例方法和類方法的動態(tài)方法解析對比下:

頭文件:

#import <Foundation/Foundation.h>

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

.m文件:

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

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(learnClass:)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(goToSchool:)) {
        class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
    NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
    NSLog(@"myInstanceMethod = %@", string);
}
@end

需要深刻理解[self class]object_getClass(self)甚至object_getClass([self class])的關(guān)系,其實并不難,重點在與self的類型:

  • 當(dāng)self為實例對象時,[self class]object_getClass(self)等價,因為前者會調(diào)用后者。object_getClass([self class])得到元類。
  • 當(dāng)self為類對象時,[self class]返回值為自身,還是self。object_getClass(self)object_getClass([self class])等價。

凡是涉及到類方法時,一定要弄清楚元類、selector、IMP等概念,這樣才能做到舉一反三,隨機應(yīng)變。

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

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

重定向 轉(zhuǎn)發(fā)目標(biāo) 備用接收者

在消息轉(zhuǎn)發(fā)機制執(zhí)行前,Runtime系統(tǒng)會再給我們一次偷梁換柱的機會,通過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接收者為其他對象:

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

畢竟消息轉(zhuǎn)發(fā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇。如果此方法返回nilself,則會進入消息轉(zhuǎn)發(fā)機制forwardInvocation:;否則將向返回的對象重新發(fā)送消息。

如果想替換類方法的接收者,需要重載+ (id)forwardingTargetForSelector:(SEL)aSelector方法,并返回類對象:

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

完整轉(zhuǎn)發(fā) 轉(zhuǎn)發(fā)調(diào)用 替換消息的方法

當(dāng)動態(tài)方法解析不作處理返回NO時,消息轉(zhuǎn)發(fā)機制會被觸發(fā)。在這時forwardInvocation:方法會被執(zhí)行,我們可以重寫這個方法來定義我們的轉(zhuǎn)發(fā)邏輯:

/// 方法簽名
+ (NSMethodSignature* ) methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

    //NSLog(@"%s", __func__);

   // 轉(zhuǎn)發(fā)給別的對象
   // [anInvocation invokeWithTarget:[TZDog new]];

    /// 轉(zhuǎn)發(fā)給別的對象,同時替換方法
    anInvocation.selector = @selector(run);
    //anInvocation.target = self;
    [anInvocation invokeWithTarget:[TZDog new]];
}

該消息的唯一參數(shù)是個NSInvocation類型的對象--該對象封裝了原始的消息和消息的參數(shù)。我們可以實現(xiàn)forwardInvocation:方法來對不能處理的消息做一些默認的處理,也可以將消息轉(zhuǎn)發(fā)給其他對象來處理,而不拋出錯誤。

這里需要注意的是參數(shù)anInvocation是從哪里來的呢?其實在forwardInvocation:消息發(fā)送前,Runtime系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,否則會拋出異常。

當(dāng)一個對象由于沒有相應(yīng)的方法實現(xiàn)而無法響應(yīng)某消息時,運行時系統(tǒng)將通過forwardInvocation:消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現(xiàn)只是簡單地調(diào)用了doesNotRecognizeSelector:。通過實現(xiàn)我們自己的forwardInvocation:方法,我們可以在該方法實現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對象。

forwardInvocation:方法就像一個不能識別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對象?;蛘咚部梢韵褚粋€運輸站將所有的消息都發(fā)送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的吃掉某些消息,因此沒有響應(yīng)也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應(yīng),這一切都取決于方法的具體實現(xiàn)。該方法所提供是將不同的對象鏈接到消息鏈的能力。

注意:forwardInvocation:方法只有在消息接收對象中無法正常響應(yīng)消息時才會被調(diào)用。所以,如果我們希望一個對象將negotiate消息轉(zhuǎn)發(fā)給其它對象,則這個對象不能有negotiate方法。否則,forwardInvocation:將不可能會被調(diào)用。

PS: 一個NSMethodSignature對象記錄著某個方法的返回值類型信息以及參數(shù)類型信息。它用于合成一個方法調(diào)用者NSInvocation。參考文章NSMethodSignature

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

轉(zhuǎn)發(fā)和繼承相似,可以用于為Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉(zhuǎn)發(fā)出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。

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

這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中WarriorDiplomat沒有繼承關(guān)系,但是Warriornegotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似DiplomatWarrior的超類一樣。

消息轉(zhuǎn)發(fā)彌補了ObjC不支持多繼承的性質(zhì),也避免了因為多繼承導(dǎo)致單個類變得臃腫復(fù)雜。它將問題分解的很細,只針對想要借鑒的方法才轉(zhuǎn)發(fā),而且轉(zhuǎn)發(fā)機制是透明的。

替代者對象

轉(zhuǎn)發(fā)不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背后是強大的男人,畢竟女人遇到難題都把它們轉(zhuǎn)發(fā)給男人來做了。這里有一些適用案例,可以參看官方文檔

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

盡管轉(zhuǎn)發(fā)很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector:isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉(zhuǎn)發(fā)鏈。比如上圖中一個Warrior對象如果被問到是否能響應(yīng)negotiate消息:

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

結(jié)果是NO,盡管它能夠接受negotiate消息而不報錯,因為它靠轉(zhuǎn)發(fā)消息給Diplomat類來響應(yīng)消息。

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

在 Runtime 的現(xiàn)行版本中,最大的特點就是健壯的實例變量。當(dāng)一個類被編譯時,實例變量的布局也就形成,它表明訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據(jù)自己所占空間而產(chǎn)生位移:

NonFragileIvars1.png

上圖左邊是NSObject類的實例變量布局,右邊是我們寫的類的布局,也就是在超類后面加上我們自己類的實例變量,看起來不錯。但試想如果哪天蘋果更新了NSObject類,發(fā)布新版本的系統(tǒng)的話,那就悲劇了:

NonFragileIvars2.png

我們自定義的類被劃了兩道線,那是因為那塊區(qū)域跟超類重疊了。唯有蘋果將超類改為以前的布局才能拯救我們,但這樣也導(dǎo)致它們不能再拓展它們的框架了,因為成員變量布局被死死地固定了。在脆弱的實例變量(Fragile ivars) 環(huán)境下我們需要重新編譯繼承自 Apple 的類來恢復(fù)兼容性。那么在健壯的實例變量下會發(fā)生什么呢?

NonFragileIvars3.png

在健壯的實例變量下編譯器生成的實例變量布局跟以前一樣,但是當(dāng)runtime系統(tǒng)檢測到與超類有部分重疊時它會調(diào)整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。

需要注意的是在健壯的實例變量下,不要使用sizeof(SomeClass),而是class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))來代替。

優(yōu)化App的啟動時間文件時有個步驟是通過fix-up修改偏移量來解決fragile base class。

總結(jié)

我們之所以讓自己的類繼承NSObject不僅僅因為蘋果幫我們完成了復(fù)雜的內(nèi)存分配問題,更是因為這使得我們能夠用上Runtime系統(tǒng)帶來的便利??赡芪覀兤綍r寫代碼時可能很少會考慮一句簡單的[receiver message]背后發(fā)生了什么,而只是當(dāng)做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細節(jié)更有利于我們利用消息機制寫出功能更強大的代碼,比如 Method Swizzling 等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容