Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
有关消息发送和消息转发机制的原理,可以查看这篇文章。
objc_msgSend 函数
在引言中已经对objc_msgSend
进行了一点介绍,看起来像是objc_msgSend
返回了数据,其实objc_msgSend
从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
1、检测这个selector
是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
2、检测这个 target
是不是nil
对象。ObjC
的特性是允许对一个 nil 对象执行任何一个方法不会 Crash
,因为会被忽略掉。
3、如果上面两个都过了,那就开始查找这个类的IMP
,先从cache
里面找,完了找得到就跳到对应的函数去执行。
4、如果cache
找不到就找一下方法分发表。
5、如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject
类为止。
6、如果还找不到就要开始进入动态方法解析了,后面会提到。
PS:这里说的分发表其实就是Class
中的方法列表,它将方法选择器和方法实现地址联系起来。
其实编译器会根据情况在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret
函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend
不再适用,于是objc_msgSend_fpret
被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。
PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。
方法中的隐藏参数
我们经常在方法中使用self
关键字来引用实例本身,但从没有想过为什么self
就能取到调用当前方法的对象吧。其实self
的内容是在方法运行时被偷偷的动态传入的。
当objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是
self
指向的内容) - 方法选择器(_cmd指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在这两个参数中,self
更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的super
关键字接收到消息时,编译器会创建一个objc_super
结构体:
struct objc_super { id receiver; Class class; };
这个结构体指明了消息应该被传递给特定超类的定义。但receiver
仍然是self
本身,这点需要注意,因为当我们想通过[super class]
获取超类时,编译器只是将指向self
的id
指针和class
的SEL
传递给了objc_msgSendSuper
函数,因为只有在NSObject
类才能找到class
方法,然后class方法调用object_getClass()
,接着调用objc_msgSend(objc_super->receiver, @selector(class))
,传入的第一个参数是指向self的id指针,与调用[self class]
相同,所以我们得到的永远都是self的类型。
获取方法地址
在IMP
那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject
类中有个methodForSelector:
实例方法,你可以用它来获取某个方法选择器对应的IMP
,举个栗子:
void (*setter)(id, SEL, BOOL);
int I;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
当方法被当做函数调用时,上节提到的两个隐藏参数就需要我们明确给出了。上面的例子调用了1000次函数,你可以试试直接给target发送1000次setFilled:消息会花多久。
PS:methodForSelector:
方法是由Cocoa
的 Runtime
系统提供的,而不是 Objc 自身的特性。
动态方法解析
你可以动态地提供一个方法的实现。例如我们可以用@dynamic
关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:
和propertyName
方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
上面的例子为resolveThisMethodDynamically
方法添加了实现内容,也就是dynamicMethodIMP
方法中的代码。其中 “v@:
” 表示返回值和参数,这个符号涉及 Type Encoding
PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:
或 instancesRespondToSelector:
方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP
的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:
返回NO
。
评论区有人问如何用 resolveClassMethod:
解析类方法,我将他贴出有问题的代码做了纠正和优化后如下,可以顺便将实例方法和类方法的动态方法解析对比下:
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}
- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end
需要深刻理解[self class]
与object_getClass(self)
甚至 object_getClass([self class])
的关系,其实并不难,重点在于 self
的类型:
1、当
self
为实例对象时,[self class]
与object_getClass(self)
等价,因为前者会调用后者。object_getClass([self class]) 得到元类。2、当
self
为类对象时,[self class]
返回值为自身,还是 self。object_getClass(self) 与 object_getClass([self class]) 等价。
凡是涉及到类方法时,一定要弄清楚元类、selector、IMP 等概念,这样才能做到举一反三,随机应变。
消息转发
重定向
在消息转发机制执行前,Runtime
系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector
方法替换消息的接受者为其他对象:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self
,因为那样会死循环。 如果此方法返回nil
或self
,则会进入消息转发机制(forwardInvocation:
);否则将向返回的对象重新发送消息。
如果想替换类方法的接受者,需要覆写+ (id)forwardingTargetForSelector:(SEL)aSelector
方法,并返回类对象:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
转发
当动态方法解析不作处理返回NO
时,消息转发机制会被触发。在这时forwardInvocation:
方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
该消息的唯一参数是个NSInvocation
类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:
方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。
这里需要注意的是参数anInvocation
是从哪的来的呢?其实在forwardInvocation:
消息发送前,Runtime
系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation
对象。所以我们在重写forwardInvocation:
的同时也要重写methodSignatureForSelector:
方法,否则会抛异常。
当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:
消息通知该对象。每个对象都从NSObject
类中继承了forwardInvocation:
方法。然而,NSObject
中的方法实现只是简单地调用了doesNotRecognizeSelector:
。通过实现我们自己的forwardInvocation:
方法,我们可以在该方法实现中将消息转发给其它对象。
forwardInvocation:
方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:
方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意:forwardInvocation:
方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate
消息转发给其它对象,则这个对象不能有negotiate
方法。否则,forwardInvocation:
将不可能会被调用。
转发和多继承
转发和继承相似,可以用于为Objc
编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中Warrior
和Diplomat
没有继承关系,但是Warrior
将negotiate
消息转发给了Diplomat
后,就好似Diplomat
是Warrior
的超类一样。
消息转发弥补了Objc
不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。
替代者对象(Surrogate Objects)
转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档。
转发与继承
尽管转发很像继承,但是NSObject
类不会将两者混淆。像respondsToSelector:
和 isKindOfClass:
这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior
对象如果被问到是否能响应negotiate
消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
结果是NO
,尽管它能够接受negotiate
消息而不报错,因为它靠转发消息给Diplomat
类来响应消息。
如果你为了某些意图偏要“弄虚作假”让别人以为Warrior
继承到了Diplomat
的negotiate
方法,你得重新实现 respondsToSelector:
和isKindOfClass:
来加入你的转发算法:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了respondsToSelector:
和isKindOfClass:
之外,instancesRespondToSelector:
中也应该写一份转发算法。如果使用了协议,conformsToProtocol:
同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:
来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector::
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
健壮的实例变量 (Non Fragile ivars)
在Runtime
的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了
,它表明访问类的实例变量的位置。从对象头部开始,实例变量依次根据自己所占空间而产生位移:
上图左边是NSObject
类的实例变量布局,右边是我们写的类的布局
,也就是在超类
后面加上我们自己类的实例变量
,看起来不错。但试想如果哪天苹果更新了NSObject类,发布新版本的系统的话,那就悲剧了:
我们自定义的类被划了两道线,那是因为那块区域跟超类重叠了。唯有苹果将超类改为以前的布局才能拯救我们,但这样也导致它们不能再拓展它们的框架了,因为成员变量布局被死死地固定了。在脆弱的实例变量(Fragile ivars) 环境下我们需要重新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下会发生什么呢?
在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。
需要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
来代替。
优化 App 的启动时间 讲过加载 Mach-O 文件时有个步骤是通过 fix-up 修改偏移量来解决 fragile base class。
Objective-C Associated Objects
在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:
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 );
这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。有关 Objective-C 引用计数机制的原理,可以查看这篇文章。
Method Swizzling
之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。
PS: 对于熟练使用 Method Swizzling 的开发者,可以跳过此章节,看看我另一篇『稍微深入』一点的文章 Objective-C Method Swizzling。
这里摘抄一个 NSHipster 的例子:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
上面的代码通过添加一个Tracking
类别到UIViewController
类中,将UIViewController
类的viewWillAppear:
方法和Tracking
类别中xxx_viewWillAppear:
方法的实现相互调换。Swizzling
应该在+load
方法中实现,因为+load
是在一个类最开始加载时调用。dispatch_once
是GCD
中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全
是很重要的。
如果类中不存在要替换的方法,那就先用class_addMethod
和class_replaceMethod
函数添加和替换两个方法的实现;如果类中已经有了想要替换的方法,那么就调用method_exchangeImplementations
函数交换了两个方法的 IMP,这是苹果提供给我们用于实现 Method Swizzling
的便捷方法。
可能有人注意到了这行:
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
object_getClass((id)self)
与 [self class]
返回的结果类型都是 Class,但前者为元类,后者为其本身,因为此时 self 为 Class 而不是实例.注意 [NSObject class] 与 [object class] 的区别:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
PS:如果类中没有想被替换实现的原方法时,class_replaceMethod
相当于直接调用class_addMethod
向类中添加该方法的实现;否则调用method_setImplementation
方法,types参数会被忽略。method_exchangeImplementations
方法做的事情与如下的原子操作等价:
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
最后xxx_viewWillAppear:
方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]
消息会动态找到xxx_viewWillAppear:
方法的实现,而它的实现已经被我们与viewWillAppear:
方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]
换成[self viewWillAppear:animated]
反而会引发死循环。
看到有人说+load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 stackoverflow 上也有大神给出了另一个 Method Swizzling 的实现:
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
NSLog(@"arg1 is %@", arg1);
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}
上面的代码同样要添加在某个类的类别中,相比第一个种实现,只是去掉了dispatch_once
部分。
Method Swizzling 的确是一个值得深入研究的话题,找了几篇不错的资源推荐给大家:
- Objective-C的hook方案(一): Method Swizzling
- Method Swizzling
- How do I implement method swizzling?
- What are the Dangers of Method Swizzling in Objective C?
- JRSwizzle
在用 SpriteKit 写游戏的时候,因为 API 本身有一些缺陷(增删节点时不考虑父节点是否存在啊,很容易崩溃啊有木有!),我在 Swift 上使用 Method Swizzling弥补这个缺陷:
extension SKNode {
class func yxy_swizzleAddChild() {
let cls = SKNode.self
let originalSelector = #selector(SKNode.addChild(_:))
let swizzledSelector = #selector(SKNode.yxy_addChild(_:))
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
class func yxy_swizzleRemoveFromParent() {
let cls = SKNode.self
let originalSelector = #selector(SKNode.removeFromParent)
let swizzledSelector = #selector(SKNode.yxy_removeFromParent)
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
@objc func yxy_addChild(_ node: SKNode) {
if node.parent == nil {
self.yxy_addChild(node)
}
else {
print("This node has already a parent!\(String(describing: node.name))")
}
}
@objc func yxy_removeFromParent() {
if parent != nil {
DispatchQueue.main.async(execute: { () -> Void in
self.yxy_removeFromParent()
})
}
else {
print("This node has no parent!\(String(describing: name))")
}
}
}
然后其他地方调用那两个类方法:
SKNode.yxy_swizzleAddChild()
SKNode.yxy_swizzleRemoveFromParent()
因为 Swift 中的 extension 的特殊性,最好在某个类的load() 方法中调用上面的两个方法.我是在AppDelegate 中调用的,于是保证了应用启动时能够执行上面两个方法.
总结
我们之所以让自己的类继承 NSObject
不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的 [receiver message]
背后发生了什么,而只是当做方法或函数调用。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。
Update 20170820: 使用 objc4-709 源码重写部分章节,更新至 Swift 4 代码示例。
参考链接: