【OC 底层】方法调用

229 阅读5分钟

基础

  • 编译时:编译器将源代码翻译成机器能识别的代码(或某个中间状态的语言)。包含词法分析、语言分析等。
  • 运行时:将磁盘中的代被装载到内存中执行。运行时的类型检查与**编译时的类型检查(静态类型检查)**不同,不是简单的扫描代码,而是在内存中做些操作与判断。
  • 方法的本质:消息
    • 消息接收者
    • 消息主体

流程探索

首先我们有这样的代码

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        LGPerson *p = [LGPerson alloc] ;
        [p saySomething];
    }
    return 0;
}

在调用saySomething方法时打上断点并开启Always Show Diassembly可以看到 image.png 实际底层调用了objc_msgSend

快速查找(汇编)

我们以arm64架构的汇编代码为例

ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

// 1. cmp 消息接收者是否存在
cmp p0, #0 
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged //  (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa

// 2. 获取消息接收者的class信息(`cache_getImp`存在class中)
GetClassFromIsa_p16 p13, 1, x0 // p16 = class

LGetIsaDone:
// 3. 利用class中的cache进行查找
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

···
END_ENTRY _objc_msgSend

由上面的流程我们可以知道,如果在缓存中没有找到该方法,则会调用CacheLookup传入的第二个参数(方法)__objc_msgSend_uncached

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// 1. 调用_lookUpImpOrForward查找方法列表
MethodTableLookup // bl _lookUpImpOrForward
// 2. 调用方法
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

流程图:

image.png

慢速查找(C/C++)

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    // 汇编层 方法查找
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    ···
    // 准备1. 判断当前class是否已经注册到缓存表中(即注册类)
    checkIsKnownClass(cls);
    
    // 准备2. 类、元类、父类按需递归初始化(便于方法查找)
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    ···
    curClass = cls;

    // 核心!
    
    for (unsigned attempts = unreasonableClassCount();;) {

        if (curClass->cache.isConstantOptimizedCache(true)) {
#if CONFIG_USE_PREOPT_CACHES
            // 1. 若缓存(汇编层级)中查找到imp,返回imp
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
        // 2. 在本类的方法列表中通过sel查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            // 3. 若无,递归查找父类
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            break;
        }
        if (fastpath(imp)) {
            goto done;
        }
    }
    
    // 4. 若递归查找完父类也没找到的话,走到动态方法决议
    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);
    }
    
 done_unlock:
    ··· // 返回imp
}

其他重点代码

除了上面的基本流程外,我还想额外探究一下最开始调用的_objc_msgForward_impcache方法(以arm64架构为例子)

STATIC_ENTRY __objc_msgForward_impcache
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
END_ENTRY __objc_msgForward

我们会发现,直接全局搜__objc_forward_handler是搜不到的,可见这不是汇编方法,而是C/C++方法,因此去掉一个下划线再搜索就可以查到

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

// objc_defaultForwardHandler
__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**);
}

也就是说如果方法找不到,就会有我们经典的unrecognized selector崩溃

动态方法决议

准备

在慢速查找流程中,如果找不到,会走到resolveMethod_locked,在调用时传入的behaviorLOOKUP_INITIALIZE | LOOKUP_RESOLVER,由如下可知

enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

计算值为2,再执行behavior ^= LOOKUP_RESOLVER;可知behavior从现在开始为0,也就是说resolveMethod_locked方法只会调用一遍

流程

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    ···
    if (! cls->isMetaClass()) { // 1. 若不是元类型(即为实例对象),调用resolveInstanceMethod
        resolveInstanceMethod(inst, sel, cls);
    } else {
    // 2. 若是元类型(Class/MetaClass),调用resolveClassMethod
        resolveClassMethod(inst, sel, cls);
        // 3. 若此时缓存中仍找不到该类方法,则会调用resolveInstanceMethod
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

对象方法的动态决议

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    ···
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 由于NSObject中有默认实现,所以这里不会return
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true))) {
    return;
    }
    
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 1. 系统通过objc_msgSend调用resolveInstanceMethod:方法
    bool resolved = msg(cls, resolve_sel, sel);
    
    // 2. 调用完resolveInstanceMethod:方法后,会再查找一次
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform(···);
        } else {
            _objc_inform(···);
        }
    }
}

也就是说,我们在自实现resolveInstanceMethod方法时,需要将方法添加到类信息中,下面是一个小示例

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod!!!!");
    if (sel == @selector(testFunc)) {
        IMP imp = class_getMethodImplementation(self, @selector(testFunc2));
        Method method = class_getInstanceMethod(**self**, **@selector**(testFunc2));
        const char type = method_getTypeEncoding(method);
        return class_addMethod(**self**,sel,imp,type);
    } else {
        return [super resolveInstanceMethod:sel];
    }
}

类方法的动态决议

static void resolveClassMethod(**id** inst, **SEL** sel, Class cls) {
    ···
    // 由于NSObject中有默认实现,所以这里不会return
    if (!lookUpImpOrNilTryCache(inst, **@selector**(resolveClassMethod:), cls)) {
        return;
    }
    
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform(···);
        } else {
            _objc_inform(···);
        }
    }
}

也就是说,我们在自实现resolveClassMethod方法时,需要将方法添加到类信息中 同时我们可以看到,在进行类方法的动态方法决议时,也会再调用一次resolveInstanceMethod

resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    resolveInstanceMethod(inst, sel, cls);
}

所以我们其实在自实现的resolveInstanceMethod中,可以进行对象方法和类方法的处理。

消息转发

动态方法决议这一步如果失败的话,难道就真的没有补救办法了吗?我们可以通过以下方法进行探索

extern void instrumentObjcMessageSends(BOOL flag);

// 使用示例
Demo *demo = [Demo alloc];
instrumentObjcMessageSends(YES);
[demo testFunc]; // 该方法未实现
instrumentObjcMessageSends(NO);

该方法会在根目录的tmp文件夹下生成一个msgSends-xxxx文件

image.png

快速转发

点击进去查看方法的调用流程,可以看到在resolveInstanceMethod之后首先调用了forwardingTargetForSelector方法

image.png 我们来看看苹果官方文档对该方法的解释: image.png 相当于是重定向,返回一个其他对象来接收方法调用的消息。

慢速转发

若没有实现forwardingTargetForSelector方法或返回的对象也没有实现方法的话,则会走到methodSignatureForSelector

image.png 也就是说,我们可以通过实现methodSignatureForSelector,返回一个方法签名。那么谁来解读该方法呢?还是刚刚那个文件,我们可以看到- Demo NSObject forwardInvocation:,也就是说可以通过实现forwardInvocation来解读。所以这两个方法必须成对出现。

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);

}

会有我们经典的unrecognized selector崩溃