代码改变世界

Objective-C的消息传递与转发

2018-12-27 16:21  法子  阅读(626)  评论(0编辑  收藏  举报

注意:本文中”消息”和”方法”意思相同。

在读一些比较"秀"的代码时候,遇到声明了方法但没实现,父类也没实现,仍然可以正常运行。这是利用了Objective-C是消息型语言,通过重写系统函数,在运行时实现了消息的转发:

- (id)forwardingTargetForSelector:(SEL)aSelector

在函数里返回实现了方法的对象(参见下面讲解的第2.->(2)->①阶段)

如果不了解Objective-C的消息传递和转发,读这些代码就会难以理解。

Objective-C的消息传递与转发

Objective-C由消息型语言Smalltalk演化而来。在运行时才会去查找所要执行的方法;才会动态绑定,确定接受消息(即方法)的对象。而不是类似C语音的静态绑定,C语音不考虑内联的情况下,编译时期就已经决定了运行时所应调用的函数。

Objective-C虽然在底层都是C语音函数,不过运行期组件构成了一种与开发者所编代码相链接的动态库,负责调用哪个方法、谁接收这个方法。

当调用一个函数的时候,分为两个阶段:1.传递消息(一般我们写的正常函数,在消息传递时期就可以正确实现)。2.(如果消息传递失败)消息转发。

1.传递消息与objc_msgSend方法

比如我们调用一个函数

    id returnValue = [someObject messageName: parameter];

编译器会把它转化为一个C语言函数:

//    void objc_msgSend(id self, SEL cmd, ...)
    id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

该方法在someObject类中搜寻"方法列表",如果找不到,就沿继承体系依次向上在父类们中找,到顶层父类还找不到,就会进入第二步:消息转发。

如果找到,匹配结果会缓存到每个类的"快速映射表",以提高下一次执行相同方法的速度。

整个过程比较复杂,还有其他的相关优化技术。我们只需要了解工作原理,能读懂一些比较"秀"的代码就可以了。

另外,这个C函数在Objective-C中是可以调用的,不过首先要把BuildSettings->Enable strict checking of objc_msgSend Calls改为NO。有些程序员会在使用这个函数来去除"PerformSelector may cause a leak because its selector is unknown"的警告

#import <objc/message.h>

- (id)execute:(SEL)selctor parameter:(id)parameter {
//    [self performSelector:selctor withObject:parameter];//会警告:PerformSelector may cause a leak because its selector is unknown
    id returnValue = objc_msgSend(self, selctor, parameter);
    return returnValue;
}

还有,objc_msgSend是用来处理通常状况的,还有其他的类似函数。比如待发消息的返回值是结构体:objc_msgSend_stret,返回值是浮点数:objc_msgSend_fpret,要给父类发消息:objc_msgSendSuper等。

2.消息转发

分为两个阶段:(1)动态方法解析:征询接受者,看是否能动态添加方法。(2)消息转发机制:①让接收着看看有没有其他对象处理②完整的消息转发机制。

(1)动态方法解析

对象收到无法解析的方法,首先调用所属类的类方法,返回值表示这个类是否能新增一个实例方法来处理:

//尚未实现的方法是实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel 

//尚未实现的方法是类方法
+ (BOOL)resolveClassMethod:(SEL)sel

重写该函数可以在函数里为类添加新方法,此方案常用来实现@dynamic属性。要访问CoreData框架中NSManagedObjects对象的属性时就是这么做的:

#import <objc/runtime.h>

//声明并实现
id autoDictionaryGetter(id self, SEL _cmd);
//声明并实现
void autoDictionarySetter(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if (/*selector is from a @dynamic property */) {
        if ([selectorString hasPrefix:@"set"]) {
            class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

(2)消息转发

如果没有做(1)的工作,就会进入第二阶段消息转发。消息转发也分为两个阶段。

①备援接受者

这一步中,系统会询问,能不能把消息转给其他接受者来处理。也就是文章开头举的例子

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

重写该方法,返回一个备援对象,来处理aSelector方法。

②完整的消息转发

如果上一步没处理,就会进入最后这一步,启用完整的消息转发机制。生成NSInvocation对象,存储了那条未处理的消息的全部细节:方法、目标、参数等。调用:

- (void)forwardInvocation:(NSInvocation *)anInvocation

如果目标子类没有重写实现该方法,它会层层向父类传递调用,直到传递给NSObject的该方法,里面会调用"doesNotRecognizeSelector:",然后抛出异常。

重写该函数,可以在里面做很多事,比如修改目标(像①修改备援接受者一样)、修改方法、修改参数等等。

 

现在回头看开头的问题,就很清楚了,是消息的处理走到了第2.->(2)->①阶段。