消息转发机制原理探究
本文我们探寻方法调用的本质,首先通过一段代码,将方法调用代码转为c++代码查看方法调用的本质是什么样的。 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
//person对象调用test方法
[person test];
// -------- c++底层代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
复制代码
从上面转换的C++源码可以看出来,我们OC平时调用对象的方法,在底层其实都是转化为了objc_msgSend函数,即消息发送机制,对象调用方法 就是给 此对象发送消息。即上面的person 调用test方法,其实就是给
person发送一条名为test的消息,person是消息接受者,test是消息名称。
- 方法调用的几个阶段:
消息发送阶段:
会先在当前类的方法缓存列表及方法列表找,没找到会去父类的方法缓存列表及方法列表找,一直到基类。如果最终没找到则会进入动态解析阶段
;动态解析阶段:
这个阶段可以动态的给上面没有找到的方法添加方法实现,如果也没有添加方法实现,那么就会进入消息转发阶段
;消息转发阶段:
这个阶段可以将上面上面的方法(消息)转发给其他可以处理这个消息的对象去实现。如果到 消息转发阶段 也没有去转发给其他对象实现这个方法,那么程序就会报一个经典的崩溃错误 ,消息无法被识别:
unrecognzied selector sent to instance
下面我们来探究一下三个阶段在源码中是怎么执行的:
_class_lookupMethodAndLoadCache3 函数
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码
我们看看lookUpImpOrForward里面的内容:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP methodPC = nil;
Method meth;
bool triedResolver = NO;
methodListLock.assertUnlocked();
//============= 消息发送阶段 ===================
// 1.在当前类的缓存中查找方法实现methodPC(imp)
if (behavior & LOOKUP_CACHE) {
methodPC = _cache_getImp(cls, sel);
//如果找到了 则不继续执行 直接返回methodPC(imp)方法地址
if (methodPC) goto out_nolock;
}
// 检查已释放的类
if (cls == _class_getFreedObjectClass())
return (IMP) _freedHandler;
// 类初始化方法相关 +initialize
if ((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
retry:
methodListLock.lock();
// 1.1防止动态添加方法,缓存会变化,再次查找缓存。
methodPC = _cache_getImp(cls, sel);
// 如果查找到imp, 直接调用done, 返回methodPC(imp)方法地址
if (methodPC) goto done;
// 根据sel去类对象里面查找方法
meth = _class_getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果找到了方法,则在此类缓存方法,
log_and_fill_cache(cls, cls, meth, sel);
methodPC = method_getImplementation(meth);
// 直接调用done, 返回methodPC(imp)方法地址
goto done;
}
// 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
curClass = cls;
//循环: 如果父类缓存列表及方法列表均找不到方法,则去父类的父类去查找。
while ((curClass = curClass->superclass)) {
//父类缓存中找
meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
if (meth) {
if (meth != (Method)1) {
// 如果在父类找到了方法,则在此类缓存方法,
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
// 直接调用done, 返回methodPC(imp)方法地址
goto done;
}
else {
break;
}
}
// 查找父类的方法列表
meth = _class_getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 如果在父类找到了方法,则在此类缓存方法,
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
// 直接调用done, 返回methodPC(imp)方法地址
goto done;
}
}
//---------------- 消息发送阶段完成 ---------------------
//============= 动态解析阶段 ===================
// 上述列表中都没有找到方法实现, 则尝试解析方法 triedResolver上面默认为false
if ((behavior & LOOKUP_RESOLVER) && !triedResolver) {
methodListLock.unlock();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES; //不管动态解析是否成功 triedResolver都会为YES,下次不会再进入动态解析
goto retry;
}
// ---------------- 动态解析阶段完成 ---------------------
// ---------------- 消息转发阶段 ---------------------
_cache_addForwardEntry(cls, sel);
methodPC = _objc_msgForward_impcache;
done:
methodListLock.unlock();
//上面消息发送阶段的out_nolock的实现
out_nolock:
if ((behavior & LOOKUP_NIL) && methodPC == (IMP)_objc_msgForward_impcache) {
return nil;
}
return methodPC;
}
复制代码
通过上面的runtime源码我们可以发现三个阶段的顺序及逻辑:
消息发送阶段
1.结合源码 消息发送阶段我们可以总结如下图:
动态解析阶段
- 当本类包括父类cache包括class_rw_t中都找不到方法时,就会进入动态方法解析阶段。我们来看一下动态解析阶段源码(上面lookUpImpOrForward里面的内容):
if ((behavior & LOOKUP_RESOLVER) && !triedResolver) {
methodListLock.unlock();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES; //不管动态解析是否成功 triedResolver都会为YES,下次不会再进入动态解析
goto retry;
}
复制代码
上述代码中可以发现,动态解析方法之后,会将triedResolver = YES
那么下次就不会在进行动态解析阶段了,之后会重新执行retry,会重新对方法查找一遍。也就是说无论我们是否实现动态解析方法,无论动态解析方法是否成功,retry之后都不会在进行动态的解析方法了。
我们再来看看 _class_resolveMethod
函数,可以发现对类对象或元类对象都有不同的处理
static void
_class_resolveMethod(id inst, SEL sel, Class cls)
{
if (! cls->isMetaClass()) {
_class_resolveInstanceMethod(inst, sel, cls);
}
else {
_class_resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
_class_resolveInstanceMethod(inst, sel, cls);
}
}
}
复制代码
根据源码画出流程图如下:
项目中如何使用动态解析方法
1.动态解析对象方法:
动态解析对象方法时:
会调用+(BOOL)resolveInstanceMethod:(SEL)sel
方法。
动态解析类方法时,会调用+(BOOL)resolveClassMethod:(SEL)sel
方法。
下面通过代码看看具体如何实现:
//写一个Student类
//Student.h 声明study方法
@interface Student : NSObject
-(void)study;
@end
//Student.m
//没有方法实现
@implementation Student
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 动态的添加方法实现
if (sel == @selector(study)){
// 获取其他方法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(play));
// 动态添加test方法的实现
class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
// 返回YES表示有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
-(void)play{
NSLog(@"like playing");
}
@end
//在viewController 创建student对象调用study方法
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
[student study];
}
//最终NSLog打印 "like playing"
复制代码
上述代码中可以看出,student声明study方法,但是并没有去实现study方法,而是动态解析方法去实现了play方法,最终执行了play方法中的内容。
原理我们上面已经从源码层面分析的很清楚:
当本类和父类cache和class_rw_t中都找不到方法时,就会调用类的动态解析方法resolveInstanceMethod。
这里需要注意的是,class_addMethod是用来向具有给定名称和实现的类添加新方法,class_addMethod将添加一个方法实现的覆盖,但是不会替换已有的实现,所以如果Student写了study方法实现,那么动态解析方法将不会被执行,这点源码也有体现。
- 上面我们在动态解析方法过程中使用
class_addMethod
函数,我们来看一下class_addMethod函数的参数分别代表什么:
/**
第一个参数: cls:给哪个类添加方法
第二个参数: SEL name:添加方法的名称
第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
第四个参数: types :方法类型,需要用特定符号,参考API
*/
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
复制代码
上述参数上文中已经详细讲解过,不清楚可以再去看看,我们这里主要探究一下class_getInstanceMethod获取Method的方法
// 获取其他方法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other));
复制代码
其实Method是objc_method类型结构体,可以理解为其内部结构同method_t结构体相同,上文中提到过method_t是代表方法的结构体,其内部包含SEL、type、IMP,我们通过自定义method_t结构体,将objc_method强转为method_t查看方法是否能够动态添加成功。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 动态的添加方法实现
if (sel == @selector(study)) {
// Method强转为method_t
struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(play));
NSLog(@"%s,%p,%s",method->sel,method->imp,method->types);
// 动态添加test方法的实现
class_addMethod(self, sel, method->imp, method->types);
// 返回YES表示有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void) play {
NSLog(@"like playing");
}
复制代码
打印内容如下:
play,0x10de23090,v16@0:8
like playing
复制代码
实际结果和我们猜想的一样,正确的执行了动态解析的方法paly,而且objc_method的内部结构确实和method_t相同。
上述代码中我们通过method_getImplementation函数和method_getTypeEncoding函数,或者是 method->imp,method->types 获取到imp和types,我们当然也可以自己直接写imp和types然后实现:
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(study)) {
class_addMethod(self, sel, (IMP)play, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void play(id self ,SEL _cmd)
{
NSLog(@"like playing");
}
//最终NSLog打印 "like playing"
复制代码
以上动态解析的里面对于方法调用的三种写法的结果都是一样,因为本质都是传class_addMethod里面的四个参数即class(对象),sel(方法名),imp(方法实现),types
2.动态解析类方法:
-当动态解析类方法的时候,会调用+(BOOL)resolveClassMethod:(SEL)sel方法,而我们知道类方法是存储在元类对象里面的,因此cls第一个对象需要传入元类对象,其他几乎一样,代码如下:
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(study)) {
// 第一个参数是object_getClass(self),传入元类对象。
class_addMethod(object_getClass(self), sel, (IMP)play, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
void play(id self, SEL _cmd)
{
NSLog(@"like playing");
}
//最终NSLog打印 "like playing"
复制代码
消息转发阶段
如果方法没有找到也没有对方法进行动态解析,那么将会进行最后消息转发的阶段
_cache_addForwardEntry(cls, sel);
methodPC = _objc_msgForward_impcache;
复制代码
这里主要是调用_objc_msgForward_impcache
进入转发阶段:
通过搜索可以在汇编中找到__objc_msgForward_impcache
函数实现,
__objc_msgForward_impcache
函数中调用__objc_msgForward
进而找到__objc_forward_handler
。
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
复制代码
这里我们发现了一个打印,即方法找不到导致的崩溃,项目中出现这样的错误是不是很熟悉呢:
因为代码没有开源,其他的过程我们是看不到了,但是可以猜想转发可以实现方法的对象,那个对象也会调用objc_msgSend,走一遍消息发送,动态解析,消息转发的过程,最终找到方法进行调用。
我们来看看项目实际应用中如何使用消息转发:
创建一个Friend对象,也有study的方法声明并且有实现:
//Friend.h
@interface Friend : NSObject
-(void)study;
@end
//Friend.m
@implementation Friend
-(void)study{
NSLog(@"Friend study");
}
@end
///在之前的 Student.m 中
@implementation Student
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(study)) {
///把这个study转发给Friend对象去实现
return [[Friend alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
//打印结果:“Friend study”
复制代码
有上面的代码可以看出:
-
在消息转发阶段可以通过实现
(id)forwardingTargetForSelector:(SEL)aSelector
方法将消息转发给可以实现此方法的对象。 -
而如果
(id)forwardingTargetForSelector:(SEL)aSelector
没有实现或者实现了里面的返回对象为nil
的话就会调用(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelectorf
方法,返回一个方法签名。 -
如果 methodSignatureForSelector 方法返回正确的方法签名就会调用forwardInvocation方法,
@implementation Student
-(id)forwardingTargetForSelector:(SEL)aSelector{
return nil;
}
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(study)){
//三种写法:
//可以直接写types实现,但是一定要和 真正实现的方法types保持一致 不然anInvocation里面信息会不准确
// return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
//return [NSMethodSignature signatureWithObjCTypes: "v@:"];
return [[[Friend alloc] init] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
///将方法调用对象设置为可以实现方法的对象Friend
[anInvocation invokeWithTarget: [[Friend alloc] init]];
}
@end
复制代码
NSInvocation的作用
- 理论上来说如果只是单纯实现消息转发我们完全可以利用
forwardingTargetForSelector
方法进行实现,没必要对methodSignatureForSelector和forwardInvocation实现,那么anInvocation对象到底有什么作用呢?
1.methodSignatureForSelector
方法中返回的方法签名,在forwardInvocation
中被包装成NSInvocation对象
2.NSInvocation提供了获取和修改方法名、参数、返回值等方法,也就是说,在forwardInvocation
函数中我们可以对方法进行最后的修改。
//Friend.h和.m
@interface Friend : NSObject
-(int)study:(int)time;
@end
@implementation Friend
-(int)study:(int)time{
NSLog(@"Friend study");
return time * 2;
}
@end
//Student.h 和.m
@interface Student : NSObject
-(int)study:(int)time;
@end
@implementation Student
-(id)forwardingTargetForSelector:(SEL)aSelector{
return nil;
}
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(study:)){
// 添加一个int参数及int返回值types为 i@:i
return [NSMethodSignature signatureWithObjCTypes: "i@:i"];
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
int time;
// 获取方法的参数,方法默认还有self和cmd两个参数,因此新添加的参数下标为2
[anInvocation getArgument:&time atIndex:2];
NSLog(@"修改前参数的值 = %d",time);
time = time + 10; // 30
NSLog(@"修改后参数的值 = %d",time);
// 设置方法的参数 此时将参数设置为30
[anInvocation setArgument: &time atIndex:2];
///将方法调用对象设置为可以实现方法的对象Friend
[anInvocation invokeWithTarget: [[Friend alloc] init]];
// 获取方法的返回值
int result;
[anInvocation getReturnValue: &result];
NSLog(@"获取方法的返回值 = %d",result); // result = 40,说明参数修改成功
result = 100;
// 设置方法的返回值 重新将返回值设置为99
[anInvocation setReturnValue: &result];
// 获取方法的返回值
[anInvocation getReturnValue: &result];
NSLog(@"修改方法的返回值为 = %d",result); // result = 100
}
//调用
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
[student study:10];
}
@end
复制代码
打印结果:
2021-05-21 17:33:12.622222+0800 MessageForwarding[50333:952591] 修改前参数的值 = 10
2021-05-21 17:33:12.622299+0800 MessageForwarding[50333:952591] 修改后参数的值 = 20
2021-05-21 17:33:12.622370+0800 MessageForwarding[50333:952591] Friend study
2021-05-21 17:33:12.622435+0800 MessageForwarding[50333:952591] 获取方法的返回值 = 40
2021-05-21 17:33:12.622527+0800 MessageForwarding[50333:952591] 修改方法的返回值为 = 100
复制代码
-
我们可以发现,在设置tagert为Friend实例对象时,就已经对方法进行了调用,而在forwardInvocation方法结束之后才输出返回值。
-
从结果可以看出
forwardInvocation
方法可以对原有的方法参数及返回值进行修改,即我们可以根据自身的业务需求决定是否需要对方法的参数及返回进行修改。
类方法的消息转发
类方法的消息转发和对象方法的消息转发流程基本一样,
当类对象进行消息转发时,对调用相应的+号的forwardingTargetForSelector
、methodSignatureForSelector
、forwardInvocation
方法,需要注意的是+号方法仅仅没有提示,而不是系统不会对类方法进行消息转发。
// Friend.h和.m
@interface Friend : NSObject
+(void)study;
@end
@implementation Friend
+(void)study{
NSLog(@"Friend study");
}
@end
// Student.h和.m
@interface Student : NSObject
+(void)study;
@end
@implementation Student
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(study)) {
// 这里需要返回类对象
return [Friend class];
}
return [super forwardingTargetForSelector:aSelector];
}
// 如果forwardInvocation函数中返回nil 则执行下列代码
// 方法签名:返回值类型、参数类型
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(study)) {
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation invokeWithTarget: [Friend class]];
}
@end
//调用
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
[Student study];
}
// 打印结果 "Friend study"
复制代码
通过代码验证可以发现类对象方法也可以进行消息转发。需要注意的是类方法的接受者为类对象。其他同对象方法消息转发模式相同。
最后我们将消息转发阶段用一张图总结一下:
对于消息转发的总结:
在OC中的方法调用,底层都是转化为objc_msgSend
函数调用,给receiver(方法调用者)发送了一条消息(selector方法名)。整个方法调用过程也就是objc_msgSend
底层的实现分为三大阶段:消息发送
、动态方法解析
、消息转发
,文章已经对整个流程都从底层源码到项目代码进行了探究~
本文为个人学习总结,有纰漏的地方欢迎指出 共同进步~???