Runtime簡單使用二

通常,我們以[object message]這種方式向一個類或者對象發送一個未定義消息時,在Xcode編譯的時候就會報錯。但是,如果出現以下情況時就會進入方法轉發流程:
1.我們使用[object message]向一個對象發送了只有message定義沒有實現的消息。
2.使用[object performSelector:@selector(message)]向一個未定義(或者未實現)message的對象發送消息時就會進入方法轉發流程。

總之就是object無法響應message消息。一般如果出現無法響應message消息,系統就會崩潰并報出unrecognized selector sent to instance錯誤。但是如何不讓系統直接崩潰了?那么就進入我們下面討論的消息轉發流程。

首先讓我們來了解幾個概覽:

一.類和元類(Meta Class)

1.類

Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針,讓我們來看一下它的結構

typedef struct objc_class *Class;

查看objc/runtime.hobjc_class結構體的定義如下

struct objc_class {
    Class isa;

#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;
  • isa:在object-c中,一個類也是一個對象(類對象),這個類的isa指向它的元類。
  • ivars:存放當前類的成員變量列表。
  • methodLists:方法列表,存放當前類的所有方法。

2.元類(Meta Class)

其實一個類也是一個對象,當我們給一個類發送一個消息的時候,比如[NSArray array]的時候就是給NSArray類對象發送array消息。這個類的isa指針指向包含這個類的objc_class結構體。

類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數據。具體的結構可以查看下圖:

類、元類的關系

二.隱式參數

objc_msgSend方法中有兩個隱式參數,這也是為什么我們可以在方法中調用self的原因。兩個隱藏參數如下:

  • 接受消息的對象(self)
  • 方法選擇器(_cmd指向的內容)

之所以說是隱藏的,是因為它們在定義方法的源代碼中沒有聲明。它們是在編譯期被插入實現代碼的。

三.消息轉發

1.動態方法解析

當我們實現了+resolveInstanceMethod或者+resolveClassMethod方法 ,如果對象或者類無法處理所接受的消息的時候就會首先進入這一步。

類的創建和調用

創建一個類,并使用performSelector調用一個不存在的方法,并使用如下方法進入消息轉發流程

// 拯救了一個實例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(myInstanceMethod)) {
        
        class_addMethod(self, sel, (IMP) funcInstanceM, "@:");
        
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

void funcInstanceM(id self, SEL _cmd) {
    NSLog(@"%@, %p", self, _cmd);
}

funcInstanceM函數包含必須添加的兩個隱藏參數self_cmd

但是如果我們給MyClass發送一個無法響應的類方法的時候,怎么進行動態方法解析了?
我們可以使用+resolveClassMethod進行消息轉發

[MyClass performSelector:@selector(myClassMethod)];

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(myClassMethod)) {
        class_addMethod(object_getClass(self), sel, (IMP) funcClassM, "@:");
        
        return YES;
    }
    
    return [super resolveClassMethod:sel];
}

void funcClassM(id self, SEL _cmd) {
    NSLog(@"%@, %p", self, _cmd);
}

這里需要注意的是,我們使用object_getClass獲取當前類的元類。

2.備用接受者

如果上述方法無法處理,那就使用- forwardingTargetForSelector將消息轉發給能夠處理的對象

// MyClass 定義
@interface MyClass : NSObject

- (void)method;

@end

@implementation MyClass

- (void)method{
    NSLog(@"%@, %p", self, _cmd);
}

@end

// MyRuntimeClass定義
@interface MyRuntimeClass : NSObject

@property (nonatomic, strong) MyClass *myClass;

- (void)test;

@end

@implementation MyRuntimeClass

- (instancetype)init
{
    if (self = [super init]) {
        self.myClass = [MyClass new];
    }
    
    return self;
}

- (void)test
{
    [self performSelector:@selector(method)];
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(method)) {
        return self.myClass;
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

可以在外界調用MyRuntimeClasstest測試。
如果此方法返回nil或self,則會進入完整方法解析(forwardInvocation:);否則將向返回的對象重新發送消息。
可以使用+ forwardingTargetForSelector:返回一個類對象進行類方法的轉發

3.完整方法轉發

如果還沒有使用上述方法處理,那么將會使用- forwardInvocation:進行最后的消息轉發

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([MyClass instancesRespondToSelector:aSelector]) {
            signature = [MyClass instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 返回一個BOOL值用來判斷消息接受者實例能否相應給定的消息
    if ([MyClass instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self.myClass];
    }
}

Runtime系統會向對象發送- methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。因此我們在使用- (void)forwardInvocation:進行消息轉發時必須實現- methodSignatureForSelector:

如果上述方法你都沒有處理,那么只有崩潰并報unrecognized selector sent to instance錯誤。

參考鏈接:
http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
http://southpeak.github.io/2014/11/03/objective-c-runtime-3/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,776評論 0 9
  • 前言 runtime其實在我們日常開發過程中很少使用到,尤其是像我現在比較初級的程序猿就更用不到了。但是去面試很多...
    WolfTin閱讀 667評論 0 2
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,232評論 0 7
  • 文中的實驗代碼我放在了這個項目中。 以下內容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 946評論 0 6
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 770評論 0 2