iOS 消息机制——基于官方文档的笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Q52077987/article/details/82014348

动态绑定

在OC中,一个调用一个对象的方法被叫做发送消息。[receiver message],就是给receiver发送message的消息。为什么这么说呢,因为OC在编译的时候没有决定调用的message方法就是receiver实现的message方法,receiver可能没有实现自己的message方法,也可能receiver得实际类型也不是声明的类型,可能是子类,也可能仅仅是实现了一个协议。这就是所谓的运行时绑定。

[receiver message]会被编译器转换成如下调用:

objc_msgSend(receiver, selector);
objc_msgSend(receiver, selector, arg1, arg2, ...)

objc_msgSend,会找到selector真正要调用的函数地址,然后把reveiver和参数都传过去。

寻找的过程是这样的:

When a message is sent to an object, the messaging function follows the object’s isa pointer to the class structure where it looks up the method selector in the dispatch table. If it can’t find the selector there, objc_msgSend follows the pointer to the superclass and tries to find the selector in its dispatch table. Successive failures cause objc_msgSend to climb the class hierarchy until it reaches the NSObject class. Once it locates the selector, the function calls the method entered in the table and passes it the receiving object’s data structure.

通过isa指针,找到类对象(class structure),类对象中有一个dispatch_table,在这个table中找;如果没有找到,通过类对象找到父类的类对象(类对象中有一个指针superClass指向父类的类对象),然后在父类的类对象中的dispatch_table中找;如果还没有就一直找到NSObject的类对象中。

为了优化查找速度,每个类对象还会缓存继承而来的方法,所以如果App启动太慢,可以检查是不是继承的层次太多了。(个人感觉一般都不会是这个原因,除非你在写一个基础库,只是面试得时候可以装一装)

不管怎么说,OC提供了绕过动态绑定的方法。通过NSObject的methodForSelector:可以找到方法的地址,直接调用:

void (*methodCall)(id, SEL);

- (void)viewDidLoad {
    [super viewDidLoad];
    methodCall = (void (*)(id, SEL))[self methodForSelector:
                                     @selector(didReceiveMemoryWarning)];
    methodCall(self, _cmd);
}

先定义一个函数指针,签名要和要调用的方法一致,2个默认参数加在前边,然后通过methodForSelector:获取函数地址,直接调用。

实际上,使用这种方式的时候要测试下是否真的瓶颈在这里:

methodCall = (void (*)(id, SEL))[self methodForSelector:
                                 @selector(didReceiveMemoryWarning)];

NSLog(@"time1 start: %@", [NSDate date]);
for(long i =  0; i < 10000000000; i++){
    methodCall(self, _cmd);
}
NSLog(@"time1 end: %@", [NSDate date]);

NSLog(@"time3 start: %@", [NSDate date]);
for(long i = 0; i < 10000000000; i++){
    [self didReceiveMemoryWarning];
}
NSLog(@"time3 end: %@", [NSDate date]);

/* output
2018-08-22 16:46:32.791027+0800 test[5719:298472] time1 start: Wed Aug 22 16:46:32 2018
2018-08-22 16:47:14.515247+0800 test[5719:298472] time1 end: Wed Aug 22 16:47:14 2018
2018-08-22 16:47:14.515468+0800 test[5719:298472] time3 start: Wed Aug 22 16:47:14 2018
2018-08-22 16:48:19.101642+0800 test[5719:298472] time3 end: Wed Aug 22 16:48:19 2018
*/

在Mac模拟器上测试,10000000000次调用也没差出来一个数量级,真机有可能会明显一些。

动态解析

上述的动态绑定是正常的方法调用流程。如果动态绑定过程没有找到方法,则启动动态解析流程。

通常来讲,如果一个方法没有被声明而被调用,编译器就不会通过编译,但是因为OC是动态绑定的,有很多方法可以绕过编译器的检查,比如把对象强转成一个声明了方法的类型,或者只声明而不实现方法,还有声明了动态属性@dynamic propertyName。

此时可以在resolveInstanceMethod:中动态的给类添加上这个方法。

void dynamicMethodIMP(id self, SEL _cmd){
    // implementation ....
}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL{
    if (aSEL == @selector(resolveThisMethodDynamically)){
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSel];
}

class_addMethod方法中[self class]或者直接self都行,因为此时self是被调用方法的真实类型,无论当前方法被被定义在哪个类中。

此处还是有些疑问,如果是resolveClassMethod方法中呢?如果子类和父类都实现了resolveInstanceMethod方法呢?好像也没有问题。

resolveInstanceMethod: 这个方法还有一段注解:

This method is called before the Objective-C forwarding mechanism is invoked. If respondsToSelector: or instancesRespondToSelector: is invoked, the dynamic method resolver is given the opportunity to provide an IMP for the given selector first.

就是说动态提供的方法也可以让responseToSelector:找到。

消息转发

如果调用了对象的一个没有实现的方法,动态解析也没有处理,就进入消息转发阶段。消息转发就是让对象的其他方法来处理这个消息,或者让其他对象来处理这个消息。

第一步的方法叫做forwardingTargetForSelector:

- (id)forwardingTargetForSelector:(SEL)aSelector;

在这个方法中返回一个对象,aSelector会被直接发送给这个返回的对象。

第二步叫做

- (void)forwardInvocation:(NSInvocation *)anInvocation;

在这一步,可以通过修改anInvocation来修改调用函数的签名。比第一步能控制的事情更多一些,当然开销也更大一些。

要走第二步的机制,还必须实现另外一个方法:

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

This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature

消息转发的用途:

第一个用途是吞掉“unrecognized selector sent to instance”异常。但是仅仅吞掉异常不是一个正确的解决方案,应该配以日志和处理措施。还有当没有办法拿到调用处的源码时(比如在SDK中),可以通过消息转发兼容更低版本的API。

第二个应用是代理对象。比如一个组件的入口,可能提供很多功能,但是这些功能都是由组件中其他类提供的。这个入口仅仅是一个代理。但是这种情况也不是刚性需求,不通过消息转发机制也能实现。

官方文档中对消息转发的另外一种理解就是,模拟多继承,就是提供其他类型的功能而不加入继承体系。实际上通过简单的对象组合也能实现。

真正需要消息转发的地方就是无法在编译或者运行时选择的处理方案,否则都可以用if…else…解决。现在能想到的有两种情况,一种是云控,消息的分发由服务端控制,并且服务端可能会下发代码,比如JSPatch热修复;第二种是你在写一个框架,具体实现留给使用者。

NSInvocation

NSInvocation主要是满足这样一个需求:在对象A上调用方法b,但是并不是立即调用,而是把这个执行命令保存起来,或者发送给其他模块,在合适的时候调用。NSInvocation对象封装了一个方法调用的全部内容,target,selector,arguments和return value。除了方法签名,其他的部分都可以替换。

NSInvocation does not support invocations of methods with either variable numbers of arguments or union arguments.

NSInvocation对象的创建不能用alloc init,而是要使用类方法通过NSMethodSignature创建:

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

NSMethodSignature对象保存了方法签名,也就是函数的参数类型和返回值类型。NSMethodSignature可以通过Type Encoding的方式创建,也可以NSObject的methodSignatureForSelector:创建,前者可以凭空创建一个NSMethodSignature对象,后者通过已经实现的selector创建。

NSInvocation对象默认是不保存参数对象的,如果参数对象在调用之前可能会被释放,可以通过retainArguments方法显示的保存一份参数对象。

- (void)retainArguments;

For efficiency, newly created NSInvocation objects don’t retain or copy their arguments, nor do they retain their targets, copy C strings, or copy any associated blocks. You should instruct an NSInvocation object to retain its arguments if you intend to cache it, because the arguments may otherwise be released before the invocation is invoked. NSTimer objects always instruct their invocations to retain their arguments, for example, because there’s usually a delay before a timer fires.

猜你喜欢

转载自blog.csdn.net/Q52077987/article/details/82014348