前言
不知不觉,就开始由类转到方法这块了,对于方法,可以说是我们平时使用频率最多了,那今天我们就来一起探索一下方法的本质和调用流程吧。
方法的本质
在前面的篇章,我们曾经用过clang来观察对象的属性,那现在我们也用来观察一下对象的方法。
我们建一个Person类,然后在main.m内容下初始化,并调用方法。
Person *person = [Person alloc];
[person test];
然后我们先把main.m转为main.cpp。
clang -rewrite-objc main.m -o main.cpp
接着我们打开main.cpp,直接搜索int main。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
}
return 0;
}
很显然,我们是调用了objc_msgSend,id是消息接受者,sel是消息编号。
我们去源码objc-msg-arm64.s文件那里查看一下。
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************
还有很多源码没有展示出来,都是一些汇编的东西。
大概流程就是先进行快速查找,直接去到缓存方法cache_t里面,找出哈希表buckets(这里的cache_t可以看上一篇类的结构分析),如果有存储,就直接返回函数实现_IMP。如果没有,就进行正常的方法表查找,也是我们常说的慢速查找。
大概罗列一下汇编的重要代码
CacheLookup NORMAL, _objc_msgSend
CacheLookup LOOKUP, _objc_msgLookup
CacheLookup NORMAL, _objc_msgSendSuper
MethodTableLookup
_lookUpImpOrForward
方法表查找
我们先看下方法表查找的源码。
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
// 查询是否有缓存
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
// 加锁
runtimeLock.lock();
// 查询当前的类是否注册到缓存列表中
checkIsKnownClass(cls);
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
// 循环找方法
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
// 当前类进行二分查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
// 找到方法就退出
if (meth) {
imp = meth->imp;
goto done;
}
// 没有找到,就去父类里面找,找到就退出循环
if (slowpath((curClass = curClass->superclass) == nil)) {
imp = forward_imp;
break;
}
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
// 去父类的缓存里面找
imp = cache_getImp(curClass, sel);
// 找到就退出循环
if (slowpath(imp == forward_imp)) {
break;
}
if (fastpath(imp)) {
goto done;
}
}
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
// 动态方法解析
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
// 找到方法后就对方法进行缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
找到就缓存方法。
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
cache_fill(cls, sel, imp, receiver);
}
这里的cahe_fill是不是就是我们上一篇写的方法缓存入口。
那如果一直找不到呢,是不是就会闪退呢。明显是的。
方法闪退分析
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
我们可以看下forward_imp。
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
所以我们直接去查找_objc_msgForward_impcache。
全局搜索一下,又回到我们objc-msg-arm64.s汇编的地方。
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
先跳转到__objc_msgForward,然后又来到__objc_forward_handler,
我们再来一次全局搜索_objc_forward_handler
// Default forward handler halts the process.
__attribute__ ((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self**), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
看到这个了吗,unrecognized selector sent to instance,这个就是我们找不到方法就会报错的地方。
慢速查找总结
- 当调用类方法的时候,会沿着继承链往上,进行二分查找方法,找到就会进行缓存,并返回IMP。
- 如果一直没有找到就会报错
unrecognized selector sent to instance
消息转发
那有没有办法防止闪退呢,答案是有的,就是消息转发。
resolveInstanceMethod
动态方法解析:resolveInstanceMethod。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
// 动态方法解析
return resolveMethod_locked(inst, sel, cls, behavior);
}
在lookUpImpOrForward里面,如果找不到方法,还会调用resolveMethod_locked。
static** NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
这里还调用了resolveInstanceMethod。然后在来一次查询。
我们看一下它在NSObject.mm的实现方式。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
那我们是不是在我们的类里面对它进行重写。
Person *p = [Person alloc];
[p go];
先调用给一个go方法。Person在go里面没有实现方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *methodName = NSStringFromSelector(sel);
NSLog(@"方法:%@",methodName);
return [super resolveInstanceMethod:sel];
}
我们运行一下,看回打印什么
可以看到的确是有进入resolveInstanceMethod,然后闪退了。
那我们就可以改下代码了
- (void)run
{
NSLog(@"%s", **__FUNCTION__** );
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(go)) {
IMP runIMP = class_getMethodImplementation(self, @selector(run));
Method runMethod = class_getInstanceMethod(self, @selector(run));
const char *runType = method_getTypeEncoding(runMethod);
NSLog(@"动态添加一个方法 - %p",sel);
return class_addMethod(self, sel, runIMP, runType);
}
return [super resolveInstanceMethod:sel];
}
输出结果:
[80155:4607416] 动态添加一个方法 - 0x7fff7cd8d1ba
[80155:4607416] -[Person run]
总结:在resolveInstanceMethod里面,我们对sel动态添加一个IMP,这时候重新查找方法,就能找到对应的IMP。
forwardingTargetForSelector
快速转发:forwardingTargetForSelector。
如果我们没有对动态解析进行任何处理,这时候就会进来快速流程。
我们先在Person.h里面添加go方法,不实现。
Person *p = [Person alloc];
[p go];
然后在Person2里面添加go方法实现。
这时候,我们在Person里面实现转发。
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(go)) {
return [Person2 alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
这里面就相当于Person的go方法找不到,那我就交给Person2进行处理。
methodSignatureForSelector
慢速转发:methodSignatureForSelector。
如果前面2步我们都没有实现,那就会来到慢速转发。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(go)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
想到于对go方法添加了事务。但是具体谁来实现还不确定。
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL aSelector = [anInvocation selector];
if ([[Person2 alloc] respondsToSelector:aSelector]) {
[anInvocation invokeWithTarget:[Person2 alloc]];
} else {
[super forwardInvocation:anInvocation];
}
}
对于事务,如果Person2响应go方法,就交给Person2处理。
消息转发流程图
后记
感觉写的有点乱,理解的东西写出来也不是很容易看懂,但大体流程就如下面:
- 方法缓存查找,缓存方法
cache_t里面,找出哈希表buckets。 - 方法表查找,
lookUpImpOrForward,父类循环遍历,找到就缓存。 - 动态方法解析,
resolveInstanceMethod。 - 快速转发,
methodSignatureForSelector,寻找其它接收者。 - 慢速转发,
methodSignatureForSelector。