将来跳槽用iOS开发笔录

iOS Runtime 运行时之 消息转发

2018-02-24  本文已影响43人  oneday527

unrecognized selector sent to instance 0x604000010c60是我们最常见的错误,是由于调用的方法在接受者中没有实现导致的。但是在控制台中输出这句log之前,运行时系统究竟做了哪些操作呢?我们有没有机会在程序崩溃之前力挽狂澜呢?

答案是肯定的,接下来我们一起学习下消息从发送到输出崩溃log我们都可以做哪些事。

#示例代码
#import <Foundation/Foundation.h>
@interface Dog : NSObject
- (void)bark:(NSString *)bark;
@end

#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>
@implementation Dog
@end


#import <Foundation/Foundation.h>
@interface AnotherDog : NSObject
- (void)bark:(NSString *)bark;
@end

#import "AnotherDog.h"
@implementation AnotherDog
- (void)bark:(NSString *)bark{
    NSLog(@"\nAnotherDog: %@\n",bark);
}
@end

一、消息发送

在OC中,方法调用称为向对象发送消息。

#我们给dog发送一个只有声明并未实现的消息。
[dog bark:@"汪汪"];

那么,[dog bark:@"汪汪"]编译后是什么呢?

objc_msgSend(dog, @selector(bark:), @"汪汪");

objc_msgSend的具体流程如下:

1、通过isa指针找到所属类(参考下图),
2、查找类的cache列表, 如果没有则下一步,
3、查找类的“方法列表”,
4、如果能找到名称相符的方法, 就跳至其实现代码,
5、若找不到, 就沿着继承顺序继续向上查找,
6、如果能找到与方法名相符的方法, 就跳至其实现代码,
7、若找不到, 进入动态方法解析阶段,允许动态给未实现的方法添加方法实现
8、若在动态解析阶段未进行处理,最终进入“消息转发”流程。
9、在转发流程中仍然没有找到能够处理消息的接收者,会抛出unrecognized selector sent to instance异常。

一张类和对象的继承关系的经典图

二、动态方法解析(第一次挽救崩溃的机会)

官方文档说,在启用消息转发机制之前,首先会进行动态方法解析。
这个方法允许通过class_addMethod动态给要处理的sel添加实现。

如果respondstoselectorismemberofclass:或instancesRespondToSelector:被调用,动态方法是有机会先给方法选择器提供一个IMP。

#一个是处理对象方法,一个处理类方法
//sel :要处理的方法选择器
//return :如果找到方法并且添加给接收者则返回YES, 否则返回NO启用消息转发机制
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel

动态添加实现如下:

#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
    NSLog(@"我是动态添加的实现");
}

@implementation Dog

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(bark:))
    {
       //如果是未实现的bark:方法 则将dynamicMethodIMP的实现添加给bark:
        class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

控制台

2018-02-23 16:10:09.188290+0800 runtimeDemo[87666:1689163] 我是动态添加的实现

【注意】关于方法的返回值,如果仅仅是返回YES并不能阻止方法继续进入消息转发流程。只有给bark:添加了方法实现并且返回YES,否则继续进入消息转发流程。

三、消息转发

究竟什么是消息转发呢?
向不能处理该消息的对象发送消息会导致一个错误。然而,在报错之前,运行时系统给接收对象一个处理消息的第二次机会。这就是消息转发。

1、 尝试寻找其他接收者(第二次挽救崩溃的机会)

请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.

动态方法解析失败, 则调用这个方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那个未知的消息
// 返回一个能响应未知方法选择器的对象
#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
    NSLog(@"我是动态添加的实现");
}

@implementation Dog

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    return NO;
}
#返回非空,非self的对象会终止消息转发。
- (id)forwardingTargetForSelector:(SEL)aSelector{
    AnotherDog * anotherDog =  [[AnotherDog alloc] init];
    if ([anotherDog respondsToSelector:aSelector]) {
        return anotherDog;
    }
    return nil;
}

@end

控制台

2018-02-23 18:06:21.359926+0800 runtimeDemo[13562:1928673] 
AnotherDog: 汪汪
2、 完整的消息转发(第三次挽救崩溃的机会)

找不到其他接收者, 这个方法就准备要被包装成一个NSInvocation对象, 在这里要先返回一个方法签名,如果方法签名返回nil,转发终止,程序崩溃。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

最后一次机会处理这个方法, 搞不定就直接程序崩溃!

// invocation : 封装了未知消息所有相关细节的对象
- (void)forwardInvocation:(NSInvocation *)invocation
#import "Dog.h"
#import "AnotherDog.h"
#import <objc/runtime.h>

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
    NSLog(@"我是动态添加的实现");
}

@implementation Dog
+ (BOOL)resolveInstanceMethod:(SEL)sel{

    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSMethodSignature *signature;
    AnotherDog * anotherDog =  [[AnotherDog alloc] init];
    signature = [anotherDog methodSignatureForSelector:aSelector];
    if (signature) return signature;
   
    //如果有其他能够相应的对象,那么生成对应的签名
    //signature = [otherDog methodSignatureForSelector:aSelector];
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    AnotherDog * anotherDog =  [[AnotherDog alloc] init];
    if ([anotherDog respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:anotherDog];
    }
}

控制台

2018-02-24 14:45:54.098264+0800 runtimeDemo[75920:3959070] 
AnotherDog: 汪汪

综上当调用我们一个未实现的方法时,我们有三次挽救的机会。如果不做处理,系统默认调用doesNotRecognizeSelector方法,抛出unrecognized selector sent to instance。

如下是系统对于未实现方法的处理方式,更多请下载源码

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}


+ (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)((id)self, sel);
}

+ (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj);
}

+ (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)((id)self, sel, obj1, obj2);
}

- (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

- (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2);
}


// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

四、通过消息转发模拟“多继承”

注意这里说的不是真正的多继承,而是通过在消息转发阶段将消息分配给能够真正处理的消息的对象,模拟实现多继承。
官方文档给我们提了思路:
在消息转发阶段的forwardingTargetForSelector或者forwardInvocation将消息分发给真正的接收者去处理。

1、这种处理方式的根本前提是:消息能够进入消息转发流程,那么类本身就不能实现需要“继承”的方法。
2、如果使用转发机制来扩展类的功能,则转发机制应该像继承一样透明。我们希望对象表现得好像它们确实继承了它们转发消息的对象的行为,那么需要重新实现respondsToSelector:和isKindOfClass:方法来包含我们的转发算法:

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

除了respondstoselectorismemberofclass:和isKindOfClass: ,instancesRespondToSelector:方法也应该实现转发算法。如果使用了协议,那么conformsToProtocol:方法也应该重写。相应的也需要重写methodSignatureForSelector:方法使得可以返回准确的方法签名;
例如,如果一个对象可以给代理(surrogate)转发消息

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

至此,本章内容结束,欢迎批评交流。

上一篇 下一篇

猜你喜欢

热点阅读