iOS 消息发送与转发

810 阅读4分钟

一、探索方法的本质

1.通过clang编译成cpp文件可以看到底层代码,得到方法的本质

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XXX.m

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk XXX.m

2.代码转换

Person *p = [Person alloc];
[p fly];
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("fly"));

((Person *(*)(id, SEL))(void *)是类型强转

(id)objc_getClass("Person")获取Person类对象

sel_registerName("alloc")等同于@selector()

最后可以简写为((类型强转)objc_msgSend)(对象, 方法调用);

3.方法的本质

一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),所以 方法的本质是通过objc_msgSend发送消息

id objc_msgSend(id self, SEL op, ...); id self是消息的接收者, SEL op是消息的方法名,c字符串...是参数列表

二、用msg_send发送消息

  1. 先给Father和Son分别定义实例方法和类方法
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface Father: NSObject
- (void)walk;
+ (void)run;
@end

@implementation Father
- (void)walk { NSLog(@"%s",__func__); }
+ (void)run { NSLog(@"%s",__func__); }
@end

@interface Son: Father
- (void)jump;
+ (void)swim;
@end
  1. msg_send发送消息
1.发送实例方法(消息接收者——实例对象)
Son *s = [Son new];
objc_msgSend(s, sel_registerName("jump"));

2.发送类方法(消息接收者——类对象)
objc_msgSend(objc_getClass("Son"), sel_registerName("swim"));

3.向父类发送实例方法(receiver——实例对象;super_class——父类类对象)
struct objc_super superInstanceMethod;
superInstanceMethod.receiver = s;
superInstanceMethod.super_class = objc_getClass("Father");
objc_msgSendSuper(&superInstanceMethod, sel_registerName("walk"));

3.向父类发送类方法(receiver——类对象;super_class——父类元类对象)
struct objc_super superClassMethod;
superClassMethod.receiver = [s class];
superClassMethod.super_class = class_getSuperclass(object_getClass([s class]));
objc_msgSendSuper(&superClassMethod, sel_registerName("run"));

如果出现Too many arguments to function call, expected 0, have 2问题,来到BuildSetting把配置修改成如下图

image.png

三、消息发送流程

核心流程

objc_msgSend函数内部如何给对象发送消息的(消息发送流程,方法调用流程)

  1. 进入objc _msgSend函数,系统会首先判断消息接收者是不是nil, 如果是nil直接return,结束该方法的调用,程序并不会崩掉。

  2. 如果不是nil, 则根据对象的isa指针找到该对象所属的类,去这个类的方法缓存一cache里查找方法,方法缓存是通过散列表实现的,所以查找效率非常高,如果找到了就直接调用,如果没有找到,则会去类的方法列表methods里查找,这里对于已排过序的方法列表采用二分查找,对于未排过序的方法列表则采用遍历查找,如果在类的方法列表找到了方法,则首先把该方法缓存到当前类的cache中,然后调用该方法,如 果没有找到,则会根据当前类的superclass指针找到它的父类,去父类里查找。

  3. 找到父类后,会首先去父类的方法缓存一cache里查找方法,如果找到了,则首先把该方法缓存到当前类的cache中(注意不是父类的cache),然后调用该方法,如果没有找到,则会去父类的方法列表一methods里查找。如果在父类的方法列表找到了方法,则首先把该方法缓存到当前类的cache中(注意不是父类的cache),然后调用该方法,如果没有找到,则会一层一层往上,直到根类,直到nil

  4. 如果到了nil, 还是没有找到方法,就会触发动态方法解析。

流程图

image.png

image.png

四、消息转发流程

如果方法列表(methodLists)没找到对应的selector,系统会提供三次补救的机会。

1. 第一次

+ (BOOL)resolveInstanceMethod:(SEL)sel {} (实例方法)
+ (BOOL)resolveClassMethod:(SEL)sel {}  (类方法)

我们只需要在 resolveInstanceMethod: 方法中,利用 class_addMethod 方法,将未实现的 myTestPrint: 绑定到 myMethod 上就能完成转发,最后返回 YES。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(myTestPrint:)) {
        class_addMethod([self class],sel,(IMP)myMethod,"v@:@");
        return YES;
    }else {
        return [super resolveInstanceMethod:sel];
    }
}

void myMethod(id self, SEL _cmd,NSString *nub) {
	NSLog(@"ifelseboyxx%@",nub);
}

2. 第二次

- (id)forwardingTargetForSelector:(SEL)aSelector {}

这个方法要求返回一个 id。使用场景一般是将 A 类的某个方法,转发到 B 类的实现中去。

// Son.m 中
- (id)forwardingTargetForSelector:(SEL)aSelector {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    if (aSelector == @selector(myTestPrint:)) {
#pragma clang diagnostic pop
        return [Person new];
    }else{
        return [super forwardingTargetForSelector:aSelector];
    }
}

//Person.m中
@interface Person : NSObject
@end
@implementation Person
- (void)myTestPrint:(NSString *)str {
	NSLog(@"ifelseboyxx%@",str);
}
@end

3. 第三次

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {}
- (void)forwardInvocation:(NSInvocation *)anInvocation {}

第一个要求返回一个方法签名,第二个方法转发具体的实现。二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。 这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活,forwardingTargetForSelector: 只能固定的转发到一个对象;forwardInvocation: 可以让我们转发到多个对象中去。

// Son.m 中
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
	if (aSelector == @selector(myTestPrint:)) {
	return [NSMethodSignature  signatureWithObjCTypes:"v@:@"];
}
	return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
	Person *person = [Person new];
	Animal *animal = [Animal new];
	if ([person respondsToSelector:anInvocation.selector]) {
		[anInvocation invokeWithTarget:person];
	}
	if ([animal respondsToSelector:anInvocation.selector]) {
		[anInvocation invokeWithTarget:animal];
	}
}
@interface Person : NSObject
@end

@implementation Person
- (void)myTestPrint:(NSString *)str {
	NSLog(@"ifelseboyxx%@",str);
}
@end
@interface Animal : NSObject
@end

@implementation Animal
- (void)myTestPrint:(NSString *)str {
	NSLog(@"tiger%@",str);
}
@end

⚠️ 如果到了第三次机会,还没找到对应的实现,就会 crash:

unrecognized selector sent to instance 0x7f9f817072b0

小结:流程图

msg_send.png

五、整个消息发送与转发

image.png