注:本文不是原创,只是在学习中做的整理和笔记,以便自己以后更好的复习。原文来自runtime从入门到精通系列以及深入理解Objective-C的Runtime机制。
前面从objc_msgSend作为入口,逐步深入分析Runtime的数据结构,了解每个数据结构的作用和它们之间关系后,我们正式转入消息发送和消息转发这两个正题。
消息发送
当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
例如:
[person read: book];
// 会被编译成:
objc_msgSend(person, @selector(read:), book);
现在让我们看一下objc_msgSend它具体是如何发送消息:
- 首先根据receiver对象的isa指针获取它对应的class;
- 优先在class的cache查找message方法,如果找不到,再到methodLists查找;
- 如果没有在class找到,就沿着继承体系继续向上到super_class查找;
- 一旦找到message这个方法,就执行它实现的IMP;
- 找不到, 执行”消息转发”。
方法解析与消息转发
[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to…类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序,即消息转发,消息转发的流程如下:
- 动态方法解析(Method Resolution) : 先问接收者所属的类, 你看能不能动态添加个方法来处理这个”未知的消息”? 如果能, 则消息转发结束.
- 后备接收者(Fast Forwarding): 请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.
- 完整的消息转发(Normal Forwarding) : 首先调用 methodSignatureForSelector:进行方法签名 ,获取函数的参数和返回值,如果返回为nil,程序会Crash掉。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。 再问接收者一次, 快想办法把这个搞定了. 到了这个地步如果还无法处理, 消息转发机制也无能为力了。
1. 动态方法解析(Method Resolution):
对象在收到无法解读的消息后, 首先在运行时调用+ resolveInstanceMethod:
或+ resolveClassMethod:
方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
// selector : 那个未知的选择子
// 返回YES则结束消息转发
// 返回NO则进入备胎
+ (BOOL)resolveInstanceMethod:(SEL)selector
/// 尚未实现的方法不是实例方法而是类方法, 则会调用另一个方法
+ (BOOL)resolveClassMethod:(SEL)selector
举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:
// .h文件
@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end
// .m文件
@implementation Message
- (void)sendMessage:(NSString *)word
{
NSLog(@"normal way : send message = %@", word);
}
@end
如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:
- (void)viewDidLoad {
[super viewDidLoad];
Message *message = [Message new];
[message sendMessage:@"Sam Lau"];
}
控制台会打印以下信息:
normal way : send message = Sam Lau
但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:
#pragma mark - Method Resolution
/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(sendMessage:)) {
class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
NSLog(@"method resolution way : send message = %@", word);
}), "v@*");
}
return YES;
}
控制台就会打印以下信息:
method resolution way : send message = Sam Lau
注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings。
如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)。
2. 后备接收者(Fast Forwarding):
动态方法解析失败, 目标对象实现- forwardingTargetForSelector:
方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。
// selector : 那个未知的消息
// 返回一个能响应该未知选择子的后备对象
- (id)forwardingTargetForSelector:(SEL)selector
继续上面Message类的例子,将sendMessage和resolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:
#pragma mark - Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(sendMessage:)) {
return [MessageForwarding new];
}
return nil;
}
此时还缺一个转发消息的后备接收类MessageForwarding,这个类的设计与实现如下:
@interface MessageForwarding : NSObject
- (void)sendMessage:(NSString *)word;
@end
@implementation MessageForwarding
- (void)sendMessage:(NSString *)word
{
NSLog(@"fast forwarding way : send message = %@", word);
}
@end
此时,控制台会打印以下信息:
fast forwarding way : send message = Sam Lau
这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。
3. 完整的消息转发(Normal Forwarding)
如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用- methodSignatureForSelector:
返回一个方法签名,来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用- forwardInvocation:
方法。
// NSMethodSignature : 该selector对应的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// invocation : 封装了与那条尚未处理的消息相关的所有细节的对象
- (void)forwardInvocation:(NSInvocation *)invocation
继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelector和forwardInvocation方法的实现:
#pragma mark - Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
MessageForwarding *messageForwarding = [MessageForwarding new];
if ([messageForwarding respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:messageForwarding];
}
}
上面这4个方法均是模板方法,开发者可以override,由runtime来调用.
三种方法的选择
Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
- 动态方法解析(Method Resolution):由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
- 后备接收者(Fast Forwarding):它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
- 完整的消息转发(Normal Forwarding):它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
runtime其他补充
Associated Objects(关联)
当使用Category对某个类进行扩展时,有时需要存储属性,Category是不支持的,这时需要使用Associated Objects来给已存在的类Category添加自定义的属性。Associated Objects提供三个API来向对象添加、获取和删除关联值:
void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy )
id objc_getAssociatedObject (id object, const void *key )
void objc_removeAssociatedObjects (id object )
其中objc_AssociationPolicy是个枚举类型,它可以指定Objc内存管理的引用计数机制。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
下面有个关于NSObject+AssociatedObject Category添加属性associatedObject的示例代码:
// NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (strong, nonatomic) id associatedObject;
@end
// NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
- (void)setAssociatedObject:(id)associatedObject
{
objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject
{
return objc_getAssociatedObject(self, _cmd);
}
@end
Associated Objects的key要求是唯一并且是常量,而SEL是满足这个要求的,所以上面的采用隐藏参数_cmd作为key。_cmd表示当前调用方法,其实它就是一个方法选择器SEL。
Method Swizzling
Method Swizzling就是在运行时将一个方法的实现代替为另一个方法的实现。如果能够利用好这个技巧,可以写出简洁、有效且维护性更好的代码。可以参考两篇关于Method Swizzling技巧的文章:
- nshipster Method Swizzling
- Method Swizzling和AOP实践
危险性
Method Swizzling就像一把瑞士小刀,如果使用得当,它会有效地解决问题。但使用不当,将带来很多麻烦。在stackoverflow上有人已经提出这样一个问题:What are the Dangers of Method Swizzling in Objective C?,它的危险性主要体现以下几个方面:
- Method swizzling is not atomic
- Changes behavior of un-owned code
- Possible naming conflicts
- Swizzling changes the method's arguments
- The order of swizzles matters
- Difficult to understand (looks recursive)
- Difficult to debug
总结
虽然在平时项目不是经常用到Objective-C的Runtime特性,但当你阅读一些iOS开源项目时,你就会发现很多时候都会用到。所以深入理解Objective-C的Runtime数据结构、消息转发机制有助于你更容易地阅读和学习开源项目。
扩展阅读
- 玉令天下博客的Objective-C Runtime
- 顾鹏博客的Objective-C Runtime
- Associated Objects
- Method Swizzling
- Method Swizzling和AOP实践
- Objective-C Runtime Reference
- What are the Dangers of Method Swizzling in Objective C?
- iOS程序员6级考试(答案和解释)