1. 程式人生 > >Objective-C Runtime 總結:訊息機制 篇

Objective-C Runtime 總結:訊息機制 篇

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。

與Runtime互動

Objc 從三種不同的層級上與 Runtime 系統進行互動,分別是通過 Objective-C 原始碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函式的直接呼叫。

  • Objective-C原始碼

大部分情況下你就只管寫你的Objc程式碼就行,runtime 系統自動在幕後辛勤勞作著。
還記得引言中舉的例子吧,訊息的執行會使用到一些編譯器為實現動態語言特性而建立的資料結構和函式,Objc中的類、方法和協議等在 runtime 中都由一些資料結構來定義,這些內容在後面會講到。(比如objc_msgSend函式及其引數列表中的id和SEL都是啥)

  • NSObject的方法

Cocoa 中大多數類都繼承於NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現了一些訊息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕後小弟去幹。
有的NSObject中的方法起到了抽象介面的作用,比如description方法需要你過載它併為你定義的類提供描述內容。NSObject還有些方法能在執行時獲得類的資訊,並檢查一些特性,比如class返回物件的類;isKindOfClass:和isMemberOfClass:則檢查物件是否在指定的類繼承體系中;respondsToSelector:檢查物件能否響應指定的訊息;conformsToProtocol:檢查物件是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。

  • Runtime的函式

Runtime 系統是一個由一系列函式和資料結構組成,具有公共介面的動態共享庫。標頭檔案存放於/usr/include/objc目錄下。許多函式允許你用純C程式碼來重複實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 程式碼時一般不會直接用到這些函式的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函式的詳細文件。

Runtime訊息傳遞

Objc 中傳送訊息是用中括號([])把接收者和訊息括起來,而直到執行時才會把訊息與方法實現繫結。
[receiver message]會被編譯器轉化為:
id objc_msgSend ( id self, SEL op, ... )


如果訊息含有引數,則為:
objc_msgSend(id, op, arg1, arg2, ...)

這裡簡單講一下必要的引數:

  • id:指標,可指向任意物件,定義為typedef struct objc_object *id;
  • SEL:區分方法的 ID,是個對映到方法的C字串,可以用 Objc 編譯器命令@selector()來獲取,定義為typedef struct objc_selector *SEL; ,不同類中相同名字的方法所對應的方法選擇器是相同的。
  • IMP:它就是一個函式指標,指向了這個方法的實現,就是最終要執行的那段程式碼,這是由編譯器生成的。定義為typedef id (*IMP)(id, SEL, ...); ,
    當你發起一個 ObjC 訊息之後,最終它會執行的那段程式碼,就是由這個函式指標指定的。而 IMP 這個函式指標就指向了這個方法的實現。IMP指向的方法與objc_msgSend函式型別相同,引數都包含id和SEL型別,前面說過,不同類中相同名字的方法所對應的方法選擇器是相同的,而每個物件中的SEL對應的方法實現肯定是唯一的,通過一組id和SEL引數就能確定唯一的方法實現地址,即 知道id,SEL便可以確定IMP。

訊息傳送步驟:
1. 檢測這個 selector 是不是要忽略的。
2. 檢測這個 target 是不是 nil 物件。ObjC 的特性是允許對一個 nil 物件執行任何一個方法不會Crash,因為會被忽略掉。
3. 如果上面兩個都過了,那就開始查詢這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函式去執行。
4. 如果 cache 找不到就找一下方法分發表。
5. 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
6. 如果還找不到就要開始進入動態方法解析了,後面會提到

PS:這裡說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯絡起來

這裡寫圖片描述

動態方法解析

當一個訊息傳送過程中,如果找不到對應方法的實現,便會進行動態方法解析,可讓我們動態繫結方法實現
設有B類,聲明瞭方法resolveThisMethodDynamically

@interface B : NSObject
- (void)resolveThisMethodDynamically;
@end

然後直接呼叫該方法

B *b=[B new];
[b resolveThisMethodDynamically];

正常情況下由於沒有方法實現,程式崩潰。然而,我們可以在B類中通過分別過載resolveInstanceMethod:和resolveClassMethod:方法分別新增例項方法實現和類方法實現,
因為當 Runtime 系統在Cache和方法分發表中(包括超類)找不到要執行的方法時,Runtime會呼叫resolveInstanceMethod:或resolveClassMethod:來給程式設計師一次動態新增方法實現的機會

void dynamicMethodIMP(){
    // implementation ....
    NSLog(@"這是dynamicMethodIMP");
}

//動態繫結例項方法實現IMP
 + (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v");//其中 “[email protected]:” 表示返回值和引數,這個符號涉及 Type Encoding
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

結果,呼叫[b resolveThisMethodDynamically]最終會執行void dynamicMethodIMP() 方法,達到動態繫結方法實現的效果。

PS:

  • 前提是沒有找到對應方法的實現,runtime才會呼叫resolveInstanceMethod:或resolveClassMethod:
    說明:class_addMethod最後一個引數是Type
    Encoding

  • 如果 respondsToSelector: 或
    instancesRespondToSelector:方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會
    所以執行

[b respondsToSelector:@selector(resolveThisMethodDynamically)]

是返回YES的。

  • 動態方法解析會在訊息轉發機制浸入前執行,如果你想讓該方法選擇器被傳送到轉發機制,那麼就讓resolveInstanceMethod:返回NO

    在講訊息轉發前我們先看一下整個轉發機制的流程
    這裡寫圖片描述

訊息轉發

重定向

在訊息轉發機制執行前,Runtime 系統會再給我們一次偷樑換柱的機會,即通過過載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換訊息的接受者為其他物件。
前提是,先讓resolveInstanceMethod:返回NO,才會呼叫forwardingTargetForSelector:
我們在B類中過載方法:

//先返回NO
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
        if (aSEL == @selector(resolveThisMethodDynamically))     {
        return NO;
    }
    return [super resolveInstanceMethod:aSEL];
}

//更改接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(resolveThisMethodDynamically)){
        return [OtherObject new]; //返回另外一個物件,將該訊息重定向給別人,變成[otherObject resolveThisMethodDynamically]
    }
    return [super forwardingTargetForSelector:aSelector];
}

返回另外一個物件,將該訊息重定向給別人,變成[otherObject resolveThisMethodDynamically]。
畢竟訊息轉發要耗費更多時間,抓住這次機會將訊息重定向給別人是個不錯的選擇。
PS:如果此方法返回nil或self,則會進入訊息轉發機制(forwardInvocation:);否則將向返回的物件重新發送訊息。

轉發

當動態方法解析不作處理返回NO時,則會呼叫forwardingTargetForSelector更改接受者,若返回nil或self,訊息轉發機制會被觸發。在這時forwardInvocation:方法會被執行,我們可以重寫這個方法來定義我們的轉發邏輯:

//轉發
- (void)forwardInvocation:(NSInvocation *)anInvocation //anInvocation封裝了原始的訊息和訊息的引數
{
    id someOtherObject=[OtherObject new];

    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

該訊息的唯一引數是個NSInvocation型別的物件——該物件封裝了原始的訊息和訊息的引數。我們可以實現forwardInvocation:方法來對不能處理的訊息做一些預設的處理,也可以將訊息轉發給其他物件來處理,而不丟擲錯誤。
這裡需要注意的是引數anInvocation是從哪的來的呢?其實在forwardInvocation:訊息傳送前,Runtime系統會向物件傳送methodSignatureForSelector:訊息,並取到返回的方法簽名用於生成NSInvocation物件。
所以所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,並且返回不為空的methodSignature,否則會crash崩潰

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

    if (aSelector==@selector(resolveThisMethodDynamically)) {
        // Type Encoding: v->void 、 @->id 、 :->SEL
        return [NSMethodSignature signatureWithObjCTypes:"[email protected]:"];//v@: 這裡v代表函式返回型別void,後面三個字元參上
    }else{
        return [super methodSignatureForSelector:aSelector];
    }

}

說明:這裡[NSMethodSignature signatureWithObjCTypes:"[email protected]:"]中的[email protected]: 是Type Encoding,表示了resolveThisMethodDynamically的返回型別和引數型別,但這裡方法並沒有帶引數,為何會有@:呢,下面來解釋一下:
Objective-C中的方法預設被隱藏了兩個引數:self和_cmd。self指向物件本身,_cmd指向方法本身。
被指定為動態實現的方法的引數型別有如下的要求:
A.第一個引數型別必須是id(就是self的型別)
B.第二個引數型別必須是SEL(就是_cmd的型別)
C.從第三個引數起,可以按照原方法的引數型別定義,
如:-(void)setName:(NSString)*name 對應Type Encoding為[email protected]:@
最後的@表示引數name的型別

PS:
1.轉發和繼承相似,可以用於為Objc程式設計新增一些多繼承的效果,就好像繼承了ViewController的方法一樣
2.儘管轉發很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector: 和 isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發鏈

Method Swizzling

之前所說的訊息轉發雖然功能強大,但需要我們瞭解並且能更改對應類的原始碼,因為我們需要實現自己的轉發邏輯。當我們無法觸碰到某個類的原始碼,卻想更改這個類某個方法的實現時,該怎麼辦呢?可能繼承類並重寫方法是一種想法,但是有時無法達到目的。這裡介紹的是 Method Swizzling ,它通過重新對映方法對應的實現來達到“偷天換日”的目的。跟訊息轉發相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。

這裡摘抄一個例子:
將UIViewController類的viewWillAppear:方法和xxx_viewWillAppear:方法的實現相互調換
在ViewController類裡重寫load:

+ (void)load {
    Class aClass = [self class];

    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);

    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

    // When swizzling a class method, use the following:
    // Class aClass = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(aClass, originalSelector);
    // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
    //object_getClass((id)self) 與 [self class] 返回的結果型別都是 Class,但前者為元類,後者為其本身,因為此時 self 為 Class 而不是實

    BOOL didAddMethod =
    class_addMethod(aClass,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));

    //如果類中不存在要替換的方法,那就先用class_addMethod和class_replaceMethod函式新增和替換兩個方法的實現
    if (didAddMethod) {
        class_replaceMethod(aClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        //如果類中已經有了想要替換的方法,那麼就呼叫method_exchangeImplementations函式交換了兩個方法的 IMP
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }

}

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

Swizzling 應該在+load方法中實現,因為+load是在一個類最開始載入時呼叫。
xxx_viewWillAppear:方法的定義看似是遞迴呼叫引發死迴圈,其實不會的。因為[self xxx_viewWillAppear:animated]訊息會動態找到xxx_viewWillAppear:方法的實現,而它的實現已經被我們與viewWillAppear:方法實現進行了互換,所以這段程式碼不僅不會死迴圈,如果你把[self xxx_viewWillAppear:animated]換成[self viewWillAppear:animated]反而會引發死迴圈

PS:如果類中沒有想被替換實現的原方法時,class_replaceMethod相當於直接呼叫class_addMethod向類中新增該方法的實現;否則呼叫method_setImplementation方法,types引數會被忽略。method_exchangeImplementations方法做的事情與如下的原子操作等價

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

相關推薦

Objective-C Runtime 總結訊息機制

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。 與Runtime互動 Objc 從

Objective-C runtime機制(2)——訊息機制

當我們用中括號[]呼叫OC函式的時候,實際上會進入訊息傳送和訊息轉發流程: 訊息傳送(Messaging),runtime系統會根據SEL查詢對用的IMP,查詢到,則呼叫函式指標進行方法呼叫;若查詢不到,則進入訊息轉發流程,如果訊息轉發失敗,則程式crash並記錄日誌。

ios學習路線—Objective-C(Runtime訊息機制)

RunTime簡稱執行時。就是系統在執行的時候的一些機制,其中最主要的是訊息機制。對於C語言,函式的呼叫在編譯的時候會決定呼叫哪個函式( C語言的函式呼叫請看這裡 )。編譯完成之後直接順序執行,無任何二義性。OC的函式呼叫成為訊息傳送。屬於動態呼叫過程。在編譯的時候並不能決定真正呼叫哪個函式(事實證明,在編

Objective-C Runtime 執行時之三方法與訊息

前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL

iOS學習筆記56(Runtime)-Objective-C Runtime 執行時之三方法與訊息

前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL

Objective-C runtime機制

Objective-C runtime機制 先來看看怎麼理解發送訊息的含義: 曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文件和呼叫。還記得初學 Objective-C 時把[receiver message]當成簡單的方法呼叫,而無視了“傳送訊息

Objective-C runtime機制(7)——SideTables, SideTable, weak_table, weak_entry_t

在runtime中,有四個資料結構非常重要,分別是SideTables,SideTable,weak_table_t和weak_entry_t。它們和物件的引用計數,以及weak引用相關。 關係 先說一下這四個資料結構的關係。 在runtime記憶體空間中,SideTables是

Objective-C runtime機制(6)——weak引用的底層實現原理

前言 提起弱引用,大家都知道它的作用: (1)不會新增引用計數 (2)當所引用的物件釋放後,引用者的指標自動置為nil 那麼,圍繞它背後的實現,是怎麼樣的呢?在許多公司面試時,都會問到這個問題。那麼,今天就帶大家一起分析一下weak引用是怎麼實現的,希望能夠搞清楚每一個細節。 S

Objective-C runtime機制(5)——iOS 記憶體管理

概述 當我們建立一個物件時: SWHunter *hunter = [[SWHunter alloc] init]; 上面這行程式碼在棧上建立了hunter指標,並在堆上建立了一個SWHunter物件。目前,iOS並不支援在棧上建立物件。 iOS 記憶體分割槽 iOS

Objective-C runtime機制(4)——深入理解Category

在平日程式設計中或閱讀第三方程式碼時,category可以說是無處不在。category也可以說是OC作為一門動態語言的一大特色。category為我們動態擴充套件類的功能提供了可能,或者我們也可以把一個龐大的類進行功能分解,按照category進行組織。 關於category的使用

Objective-C runtime機制(3)——method swizzling

方法替換,又稱為method swizzling,是一個比較著名的runtime黑魔法。網上有很多的實現,我們這裡直接講最正規的實現方式以及其背後的原理。 Method Swizzling 在進行方法替換前,我們要考慮兩種情況: 要替換的方法在target class

Objective-C runtime機制(1)——基本資料結構:objc_object & objc_class

前言 從本篇文章開始,就進入runtime的正篇。 什麼是runtime? OC是一門動態語言,與C++這種靜態語言不同,靜態語言的各種資料結構在編譯期已經決定了,不能夠被修改。而動態語言卻可以使我們在程式執行期,動態的修改一個類的結構,如修改方法實現,繫結例項變數等。

Objective-C runtime機制(前傳2)——Mach-O格式和runtime

在前傳1中,我們分析瞭解了XNU核心所支援的二進位制檔案格式Mach-O。同時還留了一個小尾巴,就是Mach-O檔案中和Objective-C以及runtime相關的Segment section。今天,就來了解一下它們。 OC之源起 我們知道,程式的入口點在iOS中被稱之為ma

Objective-C runtime機制(前傳)——Mach-O格式

Mach-O Mach-O是Mach Object檔案格式的縮寫。它是用於可執行檔案,動態庫,目的碼的檔案格式。作為a.out格式的替代,Mach-O格式提供了更強的擴充套件性,以及更快的符號表資訊訪問速度。 Mach-O格式為大部分基於Mach核心的作業系統所使用的,包括NeX

Objective-C Runtime 執行時之五協議與分類

Objective-C中的分類允許我們通過給一個類新增方法來擴充它(但是通過category不能新增新的例項變數),並且我們不需要訪問類中的程式碼就可以做到。 Objective-C中的協議是普遍存在的介面定義方式,即在一個類中通過@protocol定義介面,在另外

Objective-C Runtime 執行時之六拾遺

前面幾篇基本介紹了runtime中的大部分功能,包括對類與物件、成員變數與屬性、方法與訊息、分類與協議的處理。runtime大部分的功能都是圍繞這幾點來實現的。 本章的內容並不算重點,主要針對前文中對Objective-C Runtime Reference內容遺漏

Objective-C Runtime 執行時之一類與物件

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。 這種特性意味著Objective-C不僅需要一

Objective-C Runtime 執行時之二成員變數與屬性

在前面一篇文章中,我們介紹了Runtime中與類和物件相關的內容,從這章開始,我們將討論類實現細節相關的內容,主要包括類中成員變數,屬性,方法,協議與分類的實現。 本章的主要內容將聚集在Runtime對成員變數與屬性的處理。在討論之前,我們先介紹一個重要的概念:型別

Objective-C Runtime 執行時之四Method Swizzling

理解Method Swizzling是學習runtime機制的一個很好的機會。在此不多做整理,僅翻譯由Mattt Thompson發表於nshipster的Method Swizzling一文。 Method Swizzling是改變一個selector的實際實現的

Objective-C runtime原始碼學習之IMP定址(不包括訊息轉發部分)

寫在前面 前段時間寫了一篇部落格runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法),這是在看《招聘一個靠譜的iOS》時回答第22題時總結的一篇部落格,不過這篇部落格中並沒有牽涉到底層的程式碼,而且也留下了幾個沒有解決的