当我们用中括号[]调用OC函数的时候,实际上会进入消息发送和消息转发流程.
消息发送(Messaging),runtime系统会根据SEL查找对应的IMP,查找到,则调用函数指针进行方法调用;若查找不到,则进入动态消息解析和转发流程,如果动态解析和消息转发失败,则程序crash并记录日志。
消息相关数据结构
SEL
SEL被称之为消息选择器,它相当于一个key,在类的消息列表中,可以根据这个key,来查找到对应的消息实现。
在runtime中,SEL的定义是这样的:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
它是一个不透明的定义,似乎苹果故意隐藏了它的实现。目前SEL仅是一个字符串。
里要注意,即使消息的参数类型不同(注意,不是指参数数量不同)或方法所属的类也不同,但只要方法名相同,SEL也是一样的。所以,SEL单独并不能作为唯一的Key,必须结合消息发送的目标Class,才能找到最终的IMP。
我们可以通过OC编译器命令@selector()或runtime函数sel_registerName,来获取一个SEL类型的方法选择器。
method_t
当需要发送消息的时候,runtime会在Class的方法列表中寻找方法的实现。在方法列表中方法是以结构体method_t存储的。
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
可以看到method_t包含一个SEL作为key,同时有一个指向函数实现的指针IMP。method_t还包含一个属性const char *types;
types是一个C字符串,用于表明方法的返回值和参数类型。一般是这种格式的:
v24@0:8@16
关于SEL type,可以参考 Type Encodings
IMP
IMP实际是一个函数指针,用于实际的方法调用。在runtime中定义是这样的:
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP是由编译器生成的,如果我们知道了IMP的地址,则可以绕过runtime消息发送的过程,直接调用函数实现。关于这一点,我们稍后会谈到。
在消息发送的过程中,runtime就是根据id和SEL来唯一确定IMP并调用之的。
消息
当我们用[]向OC对象发送消息时,编译器会对应的代码修改为objc_msgSend, 其定义如下:
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
其实,除了objc_msgSend,编译器还会根据实际情况,将消息发送改写为下面四个msgSend之一:
objc_msgSend
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
当我们将消息发送给super class的时候,编译器会将消息发送改写为**SendSuper的格式,如调用[super viewDidLoad],会被编译器改写为objc_msgSendSuper的形式。
objc_msgSendSuper 的定义如下:
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
可以看到,调用super方法时,msgSendSuper的第一个参数不是id self,而是一个objc_super * 。objc_super定义如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained _Nonnull Class super_class;
};
objc_super包含两个数据,receiver指调用super方法的对象,即子类对象,而super_class表示子类的Super Class。
这就说明了在消息过程中调用了 super方法和没有调用super方法,还是略有差异的。我们将会在下面讲解。
至于**msgSend中以_stret结尾的,表明方法返回值是一个结构体类型。
在objc_msgSend的内部,会依次执行:
- 检测selector是否是应该忽略的,比如在Mac OS X开发中,有了垃圾回收机制,就不会响应retain,release这些函数。
- 判断当前receiver是否为nil,若为nil,则不做任何响应,即向nil发送消息,系统不会crash。
- 检查Class的method cache,若cache未命中,则进而查找Class 的 method list。
- 若在Class 的method list中未找到对应的IMP,则进行消息转发
- 若消息转发失败,程序crash
objc_msgSend
objc_msgSend 的伪代码实现如下:
id objc_msgSend(id self, SEL cmd, ...) {
if(self == nil)
return 0;
Class cls = objc_getClass(self);
IMP imp = class_getMethodImplementation(cls, cmd);
return imp?imp(self, cmd, ...):0;
}
而在runtime源码中,objc_msgSend方法其实是用汇编写的。为什么用汇编?一是因为objc_msgSend的返回值类型是可变的,需要用到汇编的特性;二是因为汇编可以提高代码的效率。
对应arm64,其汇编源码是这样的(有所删减):
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
b.eq LReturnZero // nil check
END_ENTRY _objc_msgSend
虽然不懂汇编,但是结合注释,还是能够猜大体意思的。
首先,系统通过cmp x0, #0检测receiver是否为nil。如果为nil,则进入LNilOrTagged,返回0;
如果不为nil,则现将receiver的isa存入x13寄存器;
在x13寄存器中,取出isa中的class,放到x16寄存器中;
调用CacheLookup NORMAL,在这个函数中,首先查找class的cache,如果未命中,则进入objc_msgSend_uncached。
objc_msgSend_uncached 也是汇编,实现如下:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup
br x17
END_ENTRY __objc_msgSend_uncached
其内部调用了MethodTableLookup, MethodTableLookup是一个汇编的宏定义,其内部会调用C语言函数_class_lookupMethodAndLoadCache3:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
最终,会调用到lookUpImpOrForward来寻找class的IMP实现或进行消息转发。
lookUpImpOrForward
lookUpImpOrForward方法的目的在于根据class和SEL,在class或其super class中找到并返回对应的实现IMP,同时,cache所找到的IMP到当前class中。如果没有找到对应IMP,lookUpImpOrForward会进入消息转发流程。
lookUpImpOrForward 的简化版实现如下:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 先在class的cache中查找imp
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();
// 如果class没有被relize,先relize
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
// 如果class没有init,则先init
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
// relaized并init了class,再试一把cache中是否有imp
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 先在当前class的method list中查找有无imp
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 在当前class中没有找到imp,则依次向上查找super class的方法列表
{
unsigned attempts = unreasonableClassCount();
// 进入for循环,沿着继承链,依次向上查找super class的方法列表
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 先找super class的cache
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 在super class 的cache中找到imp,将imp存储到当前class(注意,不是super class)的cache中
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// 在Super class的cache中没有找到,调用getMethodNoSuper_nolock在super class的方法列表中查找对应的实现
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// 在class和其所有的super class中均未找到imp,进入动态方法解析流程resolveMethod
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// 如果在class,super classes和动态方法解析 都不能找到这个imp,则进入消息转发流程,尝试让别的class来响应这个SEL
// 消息转发结束,cache结果到当前class
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
通过上的源码,我们可以很清晰的知晓runtime的消息处理流程:
尝试在当前receiver对应的class的cache中查找imp
尝试在class的方法列表中查找imp
尝试在class的所有super classes中查找imp(先看Super class的cache,再看super class的方法列表)
上面3步都没有找到对应的imp,则尝试动态解析这个SEL
动态解析失败,尝试进行消息转发,让别的class处理这个SEL
在查找class的方法列表中是否有SEL的对应实现时,是调用函数getMethodNoSuper_nolock:
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
方法实现很简单,就是在class的方法列表methods中,根据SEL查找对应的imp。
PS:这里顺便说一下Category覆盖类原始方法的问题,由于在methods中是线性查找的,会返回第一个和SEL匹配的imp。而在class的realizeClass方法中,会调用methodizeClass来初始化class的方法列表。在methodizeClass方法中,会将Category方法和class方法合并到一个列表,同时,会确保Category方法位于class方法前面,这样,在runtime寻找SEL的对应实现时,会先找到Category中定义的imp返回,从而实现了原始方法覆盖的效果。 关于Category的底层实现,我们会Objective-C runtime机制(4)——深入理解Category中讲解。
关于消息的查找,可以用下图更清晰的解释:
runtime用isa找到receiver对应的class,用superClass找到class的父类。
这里用
蓝色的表示实例方法的消息查找流程:通过类对象实例的isa查找到对象的class,进行查找。
用紫色表示类方法的消息查找流程: 通过类的isa找到类对应的元类, 沿着元类的super class链一路查找。
关于元类,我们在上一章中已经提及,元类是“类的类”。因为在runtime中,类也被看做是一种对象,而对象就一定有其所属的类,因此,类所属的类,被称为类的元类(meta class)。
我们所定义的类方法,其实是存储在元类的方法列表中的。
我们所定义的类方法,其实是存储在元类的方法列表中的。
这里有一个很好玩的地方,注意到在SEL查找链的最上方:Root Class和Root meta Class。
我们上面说到,对于类方法,是沿着紫色的路线依次查找Super类方法列表。一路上各个节点,都是元类(meta class)。而注意到,Root meta Class的super class竟然是Root class! 也就是说,当在Root meta Class中找不到类方法时,会转而到Root class中查找类方法。而在Root class中存储的,其实都是实例方法。
换句话说,我们在通过类方法的形式调用Root class中的实例方法,在OC中, 也是可以被解析的!
比如,在NSObject中,有一个实例方法:
@interface NSObject <NSObject> {
…
- (IMP)methodForSelector:(SEL)aSelector;
…
}
然后,我们自定义类:
@interface MyObj : NSObject
- (void)showY;
@end
@implementation MyObj
- (void)showY {
NSLog(@"AB");
}
@end
我们分别通过类方法的形式调用methodForSelector 和showY
[MyObj methodForSelector:@selector(test)];
[MyObj showY];
会发现,编译器允许methodForSelector的调用,并能够正常运行。
而对于showY,则会编译错误。
至于原因,就是因为对于Root meta class,其实会到Root class中寻找对应的SEL实现。
类似的,还有一个好玩的例子,在子类中,我们重写NSObject的respondsToSelector方法,然后通过类方法和实例方法两种形式来调用,看看分别会发生什么情况:
类似的,还有一个好玩的例子,在子类中,我们重写NSObject的respondsToSelector方法,然后通过类方法和实例方法两种形式来调用,看看分别会发生什么情况:
@interface NSObject <NSObject> {
...
- (BOOL)respondsToSelector:(SEL)aSelector;
...
}
@interface MyObj : NSObject
@end
@implementation MyObj
- (BOOL)respondsToSelector:(SEL)aSelector {
NSLog(@"It is my overwrite");
return YES;
}
@end
然后通过类方法和实例方法分别调用
[MyObj methodForSelector:@selector(test)];
MyObj *obj = [[MyObj alloc] init];
[obj showY];
这两种调用方式会有什么不同?
这当做一个思考题,如果大家理解了上面IMP的查找流程,那么应该能够知道答案。
objc_msgSendSuper
看完了objc_msgSend方法的调用流程,我们再来看一下objc_msgSendSuper是如何调用的。
当我们在代码里面显示的调用super 方法时,runtime就会调用objc_msgSendSuper来完成消息发送。
objc_msgSendSuper 的定义如下:
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
当我使用super关键字时,在这里,super并不代表某个确定的对象,而是编译器的一个符号,编译器会将super替换为objc_super *类型来传入objc_msgSendSuper方法中。
而objc_super 结构体定义如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained _Nonnull Class super_class;
};
第一个成员id receiver,表明要将消息发送给谁。它应该是我们的类实例(注意,是当前类实例,而不是super)
第二个成员Class super_class,表明要到哪里去寻找SEL所对应的IMP。它应该是我们类实例所对应类的super class。(即要直接到super class中寻找IMP,而略过当前class的method list)
简单来说,当调用super method时,runtime会到super class中找到IMP,然后发送到当前class的实例上。因此,虽然IMP的实现是用的super class,但是,最终作用对象,仍然是当前class 的实例。这也就是为什么
NSLog(@"%@ %@",[self class], [super class]);
会输出同样的内容,即[self class]的内容。
我们来看一下objc_msgSendSuper的汇编实现:
ENTRY _objc_msgSendSuper
MESSENGER_START
ldr r9, [r0, #CLASS] // r9 = struct super->class
CacheLookup NORMAL
// cache hit, IMP in r12, eq already set for nonstret forwarding
ldr r0, [r0, #RECEIVER] // load real receiver
MESSENGER_END_FAST
bx r12 // call imp
CacheLookup2 NORMAL
// cache miss
ldr r9, [r0, #CLASS] // r9 = struct super->class
ldr r0, [r0, #RECEIVER] // load real receiver
MESSENGER_END_SLOW
b __objc_msgSend_uncached
END_ENTRY _objc_msgSendSuper
可以看到,它就是在struct super->class的method list 中寻找对应的IMP,而real receiver则是super->receiver,即当前类实例。
如果在super class的cache中没有找到IMP的话,则同样会调用__objc_msgSend_uncached,这和objc_msgSend是一样的,最终都会调用到
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
只不过,这里传入lookUpImpOrForward里面的cls,使用了super class而已。
动态解析
如果在类的继承体系中,没有找到相应的IMP,runtime首先会进行消息的动态解析。所谓动态解析,就是给我们一个机会,将方法实现在运行时动态的添加到当前的类中。然后,runtime会重新尝试走一遍消息查找的过程:
// 在class和其所有的super class中均未找到imp,进入动态方法解析流程resolveMethod
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
在源码中,可以看到,runtime会调用_class_resolveMethod,让用户进行动态方法解析,而且设置标记triedResolver = YES,仅执行一次。当动态解析完毕,不管用户是否作出了相应处理,runtime,都会goto retry, 重新尝试查找一遍类的消息列表。
根据是调用的实例方法或类方法,runtime会在对应的类中调用如下方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel // 动态解析实例方法
+ (BOOL)resolveClassMethod:(SEL)sel // 动态解析类方法
resolveInstanceMethod
- (BOOL)resolveInstanceMethod:(SEL)sel用来动态解析实例方法,我们需要在运行时动态的将对应的方法实现添加到类实例所对应的类的消息列表中:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(singSong)) {
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(unrecoginzedInstanceSelector)), "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)unrecoginzedInstanceSelector {
NSLog(@"It is a unrecoginzed instance selector");
}
resolveClassMethod
+(BOOL)resolveClassMethod:(SEL)sel用于动态解析类方法。 我们同样需要将类的实现动态的添加到相应类的消息列表中。
但这里需要注意,调用类方法的‘对象’实际也是一个类,而类所对应的类应该是元类。要添加类方法,我们必须把方法的实现添加到元类的方法列表中。
在这里,我们就不能够使用[self class]了,它仅能够返回当前的类。而是需要使用object_getClass(self),它其实会返回isa所指向的类,即类所对应的元类(注意,因为现在是在类方法里面,self所指的是Class,而通过object_getClass(self)获取self的类,自然是元类)。
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(payMoney)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(unrecognizedClassSelector)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (void)unrecognizedClassSelector {
NSLog(@"It is a unrecoginzed class selector");
}
这里主要弄清楚,类,元类,实例方法和类方法在不同地方存储,就清楚了。
关于class方法和object_getClass方法的区别:
当self是实例对象时,[self class]与object_getClass(self)等价,因为前者会调用后者,都会返回对象实例所对应的类。
当self是类对象时,[self class]返回类对象自身,而object_getClass(self)返回类所对应的元类。
消息转发
当动态解析失败,则进入消息转发流程。所谓消息转发,是将当前消息转发到其它对象进行处理。
- (id)forwardingTargetForSelector:(SEL)aSelector // 转发实例方法
+ (id)forwardingTargetForSelector:(SEL)aSelector // 转发类方法,id需要返回类对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
如果forwardingTargetForSelector没有实现,或返回了nil或self,则会进入另一个转发流程。
它会依次调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,然后runtime会根据该方法返回的值,组成一个NSInvocation对象,并调用- (void)forwardInvocation:(NSInvocation *)anInvocation 。注意,当调用到forwardInvocation时,无论我们是否实现了该方法,系统都默认消息已经得到解析,不会引起crash。
注意,和动态解析不同,由于消息转发实际上是将消息转发给另一种对象处理。而动态解析仍是尝试在当前类范围内进行处理。
消息转发 & 多继承
通过消息转发流程,我们可以模拟实现OC的多继承机制。详情可以参考 官方文档 。
2018060417455566.png直接调用IMP
runtime的消息解析,究其根本,实际上就是根据SEL查找到对应的IMP,并调用之。如果我们可以直接知道IMP的所在,就不用再走消息机制这一层了。似乎不走消息机制会提高一些方法调用的速度,但现实是这样的吗?
我们比较一下:
CGFloat BNRTimeBlock (void (^block)(void)) {
mach_timebase_info_data_t info;
if (mach_timebase_info(&info) != KERN_SUCCESS) return -1.0;
uint64_t start = mach_absolute_time ();
block ();
uint64_t end = mach_absolute_time ();
uint64_t elapsed = end - start;
uint64_t nanos = elapsed * info.numer / info.denom;
return (CGFloat)nanos / NSEC_PER_SEC;
} // BNRTimeBlock
Son *mySon1 = [Son new];
setter ss = (void (*)(id, SEL, BOOL))[mySon1 methodForSelector:@selector(setFilled:)];
CGFloat timeCost1 = BNRTimeBlock(^{
for (int i = 0; i < 1000; ++i) {
ss(mySon1, @selector(setFilled:), YES);
}
});
CGFloat timeCost2 = BNRTimeBlock(^{
for (int i = 0; i < 1000; ++i) {
[mySon1 setFilled:YES];
}
});
将timeCost1和timeCost2打印出来,你会发现,仅仅相差0.000001秒,几乎可以忽略不计。这样是因为在消息机制中,有缓存的存在。
总结
在本文中,我们了解了OC语言中方法调用实现的底层机制——消息机制。并了解了self method和super method的异同。
最后,让我们回答文章开头的两个问题:
类实例可以调用类方法吗? 类可以调用实例方法吗? 为什么?
类实例不可用调用类方法,因为类实例查找消息IMP的流程仅会沿着继承链查找class 的method list,而对于类方法来说,是存于meta class的method list 的,因此类实例通过objc_msgSend方法是找不到对应的实现的。
类大多数情况下是不能够调用实例方法的,除非实例方法定义在root class——NSObject中。因为,当调用类方法时,会在meta class的继承链的method list 查找对应的IMP,而root meta class对应的super class是NSObject,因此在NSObject中定义的实例方法,其实是可以通过类方法形式来调用的。
下面代码输出什么?
@interface Father : NSObject
@end
@implementation Father
@end
@interface Son : Father
- (void)showClass;
@end
@implementation Son
- (void)showClass {
NSLog(@"self class = %@, super class = %@", [self class], [super class]);
}
...
Son *son = [Son new];
[son showClass]; // 这里输出什么?
...
会输出
self class = Son, super class = Son
网友评论