Objective-C Runtime 解讀 (二)

Objective-C Runtime 解讀 (二)

關於runtime的解讀, 前一章節主要講解了基本的概念, 其實runtime的運用是無處不在的, 本章節主要解讀runtime在”消息轉發機制”中的體現.

消息發送的理解

其實, 我們一般所說的函數調用, 在OC中我們更習慣叫做消息發送, 一般會這樣寫: [someObj dosomething], 這種[ ]的寫法, 其實就是消息發送, 其實最終是轉換成了下面的形式:

objc_msgSend(someObj,@selector(dosomething));

有參數的是這樣:

objc_msgSend(someObj,@selector(dosomething:),var1);

看到這裏, 其實我們就已經很清楚, 爲什麼說OC是基於消息發送的機制, 其實底層也就是調用了C函數.

消息發送機制

可能我們經常遇到下面的崩潰信息:

2016-04-01 11:41:03.867 RunTimeTestDemo[19405:1760968] -[ViewController clickAction:]: unrecognized selector sent to instance 0x7fff33c27600
2016-04-01 11:41:03.874 RunTimeTestDemo[19405:1760968] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController clickAction:]: unrecognized selector sent to instance 0x7fff33c27600'

出現這種情況就是, 我們發送的消息沒有被正確解析出來, 那麼, 接下來就詳細介紹OC中的動態消息轉發機制.

其實消息的轉發可以大致分爲兩個階段, 一個是動態方法解析, 而是完整的消息轉發, 那麼接下來先看第一個階段:

動態方法解析

在這裏, 就不得不提到OC的強大之處, 因爲OC是動態語言, 因此消息的解析是運行時完成的, 還記得前一章節提到的isa指針, 那麼動態方法解析的時候就需要使用到isa指針了.

首先消息發送之後, 先查詢接受者, 也就是通過isa指針查找相對應的method_list分發表, 如果沒有查找到指定的selector, 那麼會接着延superClass繼續查找, 直到查到根類, 如果這個過程中該選擇子可以被執行了, 那麼動態解析也就結束了, 如果不可以, 那麼接下來就看當前類能否動態添加方法, 來處理這個未知的選擇子了.

對象收到無法解讀的消息之後, 會調用所屬類的下列方法:

//實力方法
+ (BOOL)resolveInstanceMethod:(SEL)sel

//類方法
+ (BOOL)resolveClassMethod:(SEL)sel

在這裏其實我們還是可以處理這個未知的選擇子, 通過動態添加方法的形式, 處理未知的選擇子, 這也是第一個階段, 到最後runtime機制給我們提供的處理未知選擇子的方法.

大致可以這樣解決:

void addMethod(id obj, SEL _cmd)
{
    NSLog(@"Doing add method");
    NSLog(@"%@", obj);
    NSLog(@"%@", NSStringFromSelector(_cmd));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(clickAction:)) {
        class_addMethod([self class],sel,(IMP)addMethod,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

通過動態添加方法的形式來處理這個未知的選擇子, 到這裏第一階段的工作也就基本結束了, 在第一階段中, 其實我們已經可以處理未知的選擇子了, 當然如果沒有在第一階段處理掉, 其實runtime還是給了我們第二次處理的機會, 接下來就說說完整的消息轉發階段:

完整的消息轉發

其實,這個階段也可以分爲兩小節來看待, 首先就是實現備援接受者, 這個其實跟前一個階段的實現形式差不多, 大致實現如下:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    TargetObject *object = [TargetObject new];
    if (aSelector == @selector(clickAction:) && [object respondsToSelector:aSelector]) {
        return object;
    }

    return [super forwardingTargetForSelector:aSelector];
}

這種處理方式, 其實更像向備援接受者”借用”了這個方法, 其實仔細想想, 這個跟繼承的概念有點像, 但又不一樣, 因爲當前類沒有繼承備援類, 但是卻可以使用備援類中的方法, 而繼承也是可以這樣的, 如果當前類有多個備援接受者, 其實就可以近似看成”多繼承”類理解.

還有一點需要注意的是, 如果當前類借用了備援接受者中的很多方法來處理未知的選擇子, 那麼其實就可以直接繼承來實現了, 因爲通過動態消息解析的方式是比較消耗資源的, 因此, 建議這種情況下, 直接繼承實現.

當着一部分還無法處理是, 接下來就需要啓動完整的消息轉發機制了, 此步驟回調用下列方法轉發:

- (void)forwardInvocation:(NSInvocation *)anInvocation

這一階段首先會創建Invocation對象, 將於未處理選擇子相關的信息裝在裏面, 包括: 選擇子, 目標以及參數等, 大致解決方式如下:

//首先需要their method  signatures
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    TargetObject *object = [TargetObject new];
    return [object methodSignatureForSelector:aSelector];
}

//然後, 重定向
-(void)forwardInvocation:(NSInvocation *)invocation
{  
    SEL invSEL = invocation.selector;    
    if([altObject respondsToSelector:invSEL]) {        
        [invocation invokeWithTarget:altObject];    
    } else {        
        [self doesNotRecognizeSelector:invSEL];    
    }
}

這個方法其實比較簡單, 只是改變了調用目標, 是的消息在新目標上執行, 這其實跟備援接受者的實現方式類似, 都是講未知的選擇子轉給可以處理的對象來處理.

下面這張圖描述了消息轉發機制處理消息的每個步驟:

這裏寫圖片描述

該圖選自Effective Objective-C 2.0 這本書, 建議大家閱讀一下, 裏面有很多高效開發的知識點, 還有一些很深入的講解.

上圖中可以看出, 在整個消息轉發機制中, 每個階段都可以處理未知的選擇子, 但是步驟越往後, 處理消息的代價就越大, 最好是能夠在第一步就處理掉, 這樣的話, 其實代價相對較小.

總結

本文主要講解了OC中動態消息轉發的各個階段, 也主要體現了runtime機制的強大之處, 通過運行期的動態方法解析, 我們可以動態添加需要的方法到類中, 也可以使用備援接受者來處理, 最後還可以通過完整的消息轉發機制, 來處理未知消息.

因此, runtime機制使得我們的程序更健壯, 而且更靈活, 可以再運行期做更多的事情.

參考:
Objective-C Runtime Programming Guide
理解 Objective-C Runtime

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章