当我们用中括号[]
调用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
的简化版实现如下:
MP 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的底层实现,我们会在其他章节中讲解。
关于消息的查找,可以用下图更清晰的解释:
runtime用isa找到receiver对应的class,用superClass找到class的父类。
这里用蓝色的表示实例方法的消息查找流程:通过类对象实例的isa查找到对象的class,进行查找。
用紫色表示类方法的消息查找流程: 通过类的isa找到类对应的元类, 沿着元类的super class链一路查找
关于元类,我们在上一章中已经提及,元类是“类的类”。因为在runtime中,类也被看做是一种对象,而对象就一定有其所属的类,因此,类所属的类,被称为类的元类(meta 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所指向的类,即类所对应的元类
。
+ (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的多继承机制。详情可以参考官方文档。
直接调用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秒,几乎可以忽略不计。这样是因为在消息机制中,有缓存的存在。
参考文献
Objective-C Runtime
Objective-C 消息发送与转发机制原理
object_getClass与objc_getClass的不同