iOS底层之方法的本质是什么和方法的调用流程(5)

2,645 阅读5分钟

前言

不知不觉,就开始由转到方法这块了,对于方法,可以说是我们平时使用频率最多了,那今天我们就来一起探索一下方法的本质和调用流程吧。

方法的本质

在前面的篇章,我们曾经用过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_msgSendid是消息接受者,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

image.png

消息转发

那有没有办法防止闪退呢,答案是有的,就是消息转发。

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方法。Persongo里面没有实现方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *methodName = NSStringFromSelector(sel);

    NSLog(@"方法:%@",methodName);

    return  [super resolveInstanceMethod:sel];
}

我们运行一下,看回打印什么

Snipaste_2021-12-24_12-14-27.png

可以看到的确是有进入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];
}

这里面就相当于Persongo方法找不到,那我就交给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处理。

消息转发流程图

Snipaste_2021-12-28_17-42-02.png

后记

感觉写的有点乱,理解的东西写出来也不是很容易看懂,但大体流程就如下面:

  1. 方法缓存查找,缓存方法cache_t里面,找出哈希表buckets
  2. 方法表查找,lookUpImpOrForward,父类循环遍历,找到就缓存。
  3. 动态方法解析,resolveInstanceMethod
  4. 快速转发,methodSignatureForSelector,寻找其它接收者。
  5. 慢速转发,methodSignatureForSelector