笔者也是一名菜鸟,从头开始学习
Runtime
,所以有些东西可能不正确。而且这个简书排版也不怎么会,有问题各位大佬可以直接指出,不用留情面。
关于消息转发有几个问题,带着问题去寻找答案我觉得更高效。
1、消息转发是如何触发的?
2、消息转发都分为几步?
3、消息转发有什么作用?
先思考一下这3个问题,然后带着疑问去看看
Runtime
中发生了什么。
1、消息转发是如何触发的?
当前创建了一个类,类名Book
,Book.h
声明了一个方法- (void)sell;
但是没有实现该方法。Xcode
会友好的提示我们Method definition for 'sell' not found
。
//
// Book.h
// 消息转发
//
// Created by -- on 2019/8/2.
// Copyright © 2019 --. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Book : NSObject
- (void)sell;
@end
NS_ASSUME_NONNULL_END
//
// Book.m
// 消息转发
//
// Created by -- on 2019/8/2.
// Copyright © 2019 --. All rights reserved.
//
#import "Book.h"
#import <objc/runtime.h>
@implementation Book
//Method definition for 'sell' not found
@end
接下来在 ViewController.m
的viewDidLoad
中调用 [book sell]
,很显然这个会崩溃的。
//
// ViewController.m
// 消息转发
//
// Created by -- on 2019/8/1.
// Copyright © 2019 --. All rights reserved.
//
#import "ViewController.h"
#import "Book.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//Class Book
Book *book = [Book new];
[book sell];
}
@end
然后控制台打印如下:
2019-08-02 09:57:30.059674+0800 消息转发[1205:43257] -[Book sell]: unrecognized selector sent to instance 0x600002ff0fc0
2019-08-02 09:57:30.067159+0800 消息转发[1205:43257] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Book sell]: unrecognized selector sent to instance 0x600002ff0fc0'
*** First throw call stack:
(
0 CoreFoundation 0x000000010e5888db __exceptionPreprocess + 331
1 libobjc.A.dylib 0x000000010db2bac5 objc_exception_throw + 48
2 CoreFoundation 0x000000010e5a6c94 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation 0x000000010e58d623 ___forwarding___ + 1443
4 CoreFoundation 0x000000010e58f418 _CF_forwarding_prep_0 + 120
5 消息转发 0x000000010d25571a -[ViewController viewDidLoad] + 106
控制台打印出来了找不到方法实现的崩溃的栈,但是有意思的是在[ViewController viewDidLoad]
之后接连发生了_CF_forwarding_prep_0
和___forwarding___
的方法调用。
网上一番搜索之后发现了一片文章Objective-C 消息发送与转发机制原理(二)对这此讲解。
文章之后有这么一段:
**__CF_forwarding_prep_0和 forwarding_prep_1函数都调用了forwarding只是传入参数不同。
forwarding有两个参数,第一个参数为将要被转发消息的栈指针(可以简单理解成 IMP),第二个参数标记是否返回结构体。__CF_forwarding_prep_0第二个参数传入 0
,forwarding_prep_1传入的是 1,从函数名都能看得出来。下面是这两个函数的伪代码:
从图中可以看出来,但我们调用
[book sell]
的时候,Runtime
找不到方法实现
,之后进行了消息转发
。转发之后给我们抛出了异常(-[Book sell]: unrecognized selector sent to instance 0x600002ff0fc0'
);
那么Runtime
是怎么寻找方法实现的?
Runtime
有一张这样的图是我们需要牢记在心的图。图如下:
上图实线是
Superclass
指针,虚线是 isa
指针。 Runtime
先在我们的类Book
中寻找我们的sell
方法,如果Book
中找不到方法实现,就会一层一层沿着父类寻找,最后找不到会调用doesNotRecognizeSelector
方法,如果该方法不处理,Runtime
就会抛出异常。
既然知道了Runtime
的查找方式图,那具体的查找方式呢?
查阅了Runtime ObjC2.0
源码,找到了如下代码:
struct objc_object {
private:
isa_t isa;
...
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
...
}
在class_rw_t
下找到了我们想要的东西method_array_t
。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
...
}
struct method_t {
//方法名
SEL name;
//返回类型
const char *types;
//方法实现的指针地址
MethodListIMP imp;
};
看了method_array_t
中存储的method_t
这就是我们的方法在类中的存储位置,根据上方关系图,沿着父类寻找,最终因为Book
及其父类没有 - (void)sell;
的方法实现而崩溃并打印了异常信息。
2、消息转发都分为几步?
了解了上面Runtime
底层的底层源码,对方法查找有初步的了解了。留意到一个特别有意思的方法__forwarding__
,然后看看NSObject.h
中相关的OC
方法了。
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
发现这两个方法貌似和__forwarding__
有关系。我们可以试试,到底有没有关系?
-
我们断点一下
发现第一个断点进入了,说明这个是消息转发中一个步骤。之后我们查阅了官方文档,
- (id)forwardingTargetForSelector:(SEL)aSelector
返回首先应将无法识别的消息定向到的对象。就是说我们需要一个实现了 -(void)sell
新的对象来接收消息。
创建了个新的对象BookStore
,BookStore.h
中没有声明方法,BookStore.m
实现了- (void)sell
方法
//
// BookStore.h
// 消息转发
//
// Created by -- on 2019/8/2.
// Copyright © 2019 --. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BookStore : NSObject
@end
NS_ASSUME_NONNULL_END
//
// BookStore.m
// 消息转发
//
// Created by -- on 2019/8/2.
// Copyright © 2019 --. All rights reserved.
//
#import "BookStore.h"
@implementation BookStore
- (void)sell{
NSLog(@"卖书了~");
}
@end
我们重写了- (id)forwardingTargetForSelector:(SEL)aSelector
方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *selName = NSStringFromSelector(aSelector);
if ([selName isEqualToString:@"sell"]) {
return [BookStore new];
}
return [super forwardingTargetForSelector:aSelector];
}
看一下控制台打印,这个步骤正确。
2019-08-02 12:31:57.238626+0800 消息转发[4032:301841] 书店卖书了~
- 我们把实现注释掉
接着又报了-[Book sell]: unrecognized selector sent to instance 0x6000032dc6d0
相同的错误,难道- (void)forwardInvocation:(NSInvocation *)anInvocatio;
不是消息转发中的一个步骤吗?
忽然发现forwardInvocation
方法中有一个NSInvocation
对象。
@interface NSInvocation : NSObject {
@private
void *_frame;
void *_retdata;
id _signature;
id _container;
uint8_t _retainedArgs;
uint8_t _reserved[15];
}
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
@property (readonly, retain) NSMethodSignature *methodSignature;
然后点进去看发现了一个+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
这样的方法,原来NSInvocation
需要一个方法签名。
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
就是生成方法签名的方法。
当看NSObject.h
的方法时,也看到了- (void)forwardInvocation:(NSInvocation *)anInvocation
下方有一个- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
看来这个就是方法签名生成后返回给Runtime
的实现了。接下来尝试一下:
那
const char *types
需要什么呢?继续查看官方文档在这里看到了关于const char *types
生成方法:Book.m
先写一个方法实现,这方法实现等价于OC- (void)sell
的方法。
void sell(id self, SEL _cmd){
NSLog(@"Book把自己卖了~");
}
根据上方规则,生成const char *types
为@"v@:"
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"sell"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = [anInvocation selector];
BookStore *bookStore = [BookStore new];
if ([bookStore respondsToSelector:sel]) {
[anInvocation invokeWithTarget:bookStore];
} else {
//走继承树
[super forwardInvocation:anInvocation];
}
}
可以看到消息转发成功了。
2019-08-02 13:20:53.789565+0800 消息转发[4032:301841] 书店卖书了~
到这里,有一个疑问。Runtime
所有的工作都在运行期
发生,那能不能在运行的时候动态添加方法呢?继续查看NSObject.h
文件, 发现有一个+ (BOOL)resolveInstanceMethod:(SEL)sel;
的方法,这个看样子就是动态解析实例方法。
重写改方法,然后断点。
看来就是我们需要的方法,那这个方法里面都该实现点什么?查阅官方文档resolveInstanceMethod
这个太清晰了,照抄~ 哈哈哈。Book.m
实现如下:
void sell(id self, SEL _cmd){
NSLog(@"Book把自己卖了~");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *methodName = NSStringFromSelector(sel);
if ([methodName isEqualToString:@"sell"]) {
class_addMethod(self, sel, (IMP)sell, "v@:");
return YES;
}
//走继承树
return [super resolveInstanceMethod:sel];
}
运行结果:
2019-08-02 13:49:16.766800+0800 消息转发[6926:541823] Book把自己卖了~
当一步步研究发现OC
的消息转发实现方式后,接下来屡一下消息转发的顺序,图如下:
整理:
1、动态消息转发resolveInstanceMethod:
,动态添加一个方法实现;
2、快速消息转发forwardingTargetForSelector:
,转发给一个实现了方法的类对象;
3、完整消息转发,首先先获取方法签名methodSignatureForSelector:
然后在forwardInvocation:
中设置消息转发的对象。
- 完整实现代码如下:
#import "Book.h"
#import <objc/runtime.h>
#import "BookStore.h"
void sell(id self, SEL _cmd){
NSLog(@"Book把自己卖了~");
}
@implementation Book
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *methodName = NSStringFromSelector(sel);
if ([methodName isEqualToString:@"sell"]) {
class_addMethod(self, sel, (IMP)sell, "v@:");
return YES;
}
//走继承树
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"sell"]) {
return [BookStore new];
}
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"sell"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = [anInvocation selector];
BookStore *bookStore = [BookStore new];
if ([bookStore respondsToSelector:sel]) {
[anInvocation invokeWithTarget:bookStore];
} else {
//走继承树
[super forwardInvocation:anInvocation];
}
}
- (void)doesNotRecognizeSelector:(SEL)aSelector{
NSLog(@"找不到方法实现:%@",NSStringFromSelector(aSelector));
}
@end
3、消息转发的作用
1>崩溃日志的搜集
2>增强程序的健壮性
3>实现多重代理
利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。
https://blog.csdn.net/kingjxust/article/details/49559091
结束语:
Runtime
慢慢开始研究了,这是Runtime
的第一篇文章,尽我所能写出的东西不出错误,但是学习总有错的地方,有问题欢迎指出,感谢各位大佬。