iOS底层分析之objc_msgSend消息动态决议

935 阅读8分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前面文章iOS底层分析之类的探究-cache之 insert、objc_msgSend已经分析了objc_msgSend从快速消息查找到慢速查找的流程,在慢速查找的函数是lookUpImpOrForward,里面的查找流程是:

  • 当前类方法列表查找
  • 父类缓存查找
  • 父类方法列表查找
  • 继续从父类的父类开始循环第二步的操作,直到父类为nil,给imp返回一个默认的forward_imp lookUpImpOrForward部分代码如下
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    ...
    for (unsigned attempts = unreasonableClassCount();;) {
        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                ...
                ///当遍历所有父类都找不到,给imp赋值forward_imp
                imp = forward_imp;
                break;
            }
    }
    ...
}

那么我们来探索下_objc_msgForward_impcache的流程实现:

(搜索思路:当前文件优先搜索,找不到了再全局搜索,必要时可以去掉前面的“_”或"_ _"然后再搜索)
STATIC_ENTRY __objc_msgForward_impcache
	b	__objc_msgForward
END_ENTRY __objc_msgForward_impcache

本文件搜索“__objc_msgForward

ENTRY __objc_msgForward
	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
        /** TailCallFunctionPointer就是跳转进入$0参数也就是x17,
        x17就是__objc_forward_handler,
        所以__objc_msgForward就是进入__objc_forward_handler
    */
	TailCallFunctionPointer x17
	
END_ENTRY __objc_msgForward

全局搜索“_objc_forward_handler

__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);
}
//_objc_forward_handler就是objc_defaultForwardHandler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

看到这里,是不是很熟悉,我们平时写OC代码的时候,如果调用了一个只声明未实现的方法,控制台就会打印这段话,说归说,还是要动手写一下的:

  • 实例方法 image.png

  • 类方法 image.png

我们甚至还在源码里看到,控制台打印的报错方法的“+”“-”都是手动添加上去,所以这也说明了,在底层源码里,没有类方法这一说,都是对象方法。

OK,到这里我们已经知道了缓存imp找不到就去方法列表找,方法列表也找不到,就返回一个默认的imp并且抛出异常,那么有没有什么办法可以应对这种找不到imp的情况?接下来,我们回到lookUpImpOrForward函数,我们看到这样一段代码:

消息动态决议 探索

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
// No implementation found. Try method resolver once.
/** 从注释可以看出,当imp找不到的时候,会有一次解决的应对措施,
    而且这个if语句还是个单例,感兴趣的朋友可以看看下面的“单例解释”
*/
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        //计算结果:behavior = 1;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
...
}

搜索“resolveMethod_locked

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    ...
    ///如果不是元类就调用resolveInstanceMethod
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        ///如果是元类就调用resolveInstanceMethod
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    ///此刻的behavior = 1
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
/**

*/

下面看看resolveInstanceMethod都干了啥,

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    ...
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    ...
    /** 给 cls类发送消息,判断是否实现 resolveInstanceMethod:方法,参数是当前的sel
        常规操作是在resolveInstanceMethod:方法里,对当前类添加sel;
        比如 class_addMethod([self class],sel, (IMP)sayHello,"v@:@");
     */
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    ///然后再查找一遍,看看有没有sel
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    /** PrintResolving是配置的其中一种环境变量,
    目的是打印 resolveClassMethod: 和 +resolveInstanceMethod: 的方法日志;
    目前PrintResolving返回都是false,所以下面的if可以暂时不用取看它*/
    if (resolved  &&  PrintResolving) {
            if (imp) {
                _objc_inform("RESOLVE: method %c[%s %s] "
                             "dynamically resolved to %p", 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel), imp);
            }
            else {
                // Method resolver didn't add anything?
                _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                             ", but no new implementation of %c[%s %s] was found",
                             cls->nameForLogging(), sel_getName(sel), 
                             cls->isMetaClass() ? '+' : '-', 
                             cls->nameForLogging(), sel_getName(sel));
            }
        }
        /**
        resolveInstanceMethod函数干了两件事情:
        1、判断是否实现resolveInstanceMethod
        2、继续调用lookUpImpOrNilTryCache查找缓存里sel对应的imp
        */
}

下面看看lookUpImpOrNilTryCache都干了啥,

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    //查询缓存里的imp,如果behavior不传,那behavior就等于0
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    //如果cls类从来没调用过Initialized函数,就调用lookUpImpOrForward方法
    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    // 如果cls已经初始化过,就调用cache_getImp获取缓存里的imp
    IMP imp = cache_getImp(cls, sel);
    
    if (imp != NULL) goto done;// 如果imp不为空,就调用done
    //如果imp为空,继续下面if判断
#if CONFIG_USE_PREOPT_CACHES    //CONFIG_USE_PREOPT_CACHES:是否有缓存
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    // 如果imp为空,就调用lookUpImpOrForward
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    // 如果(behavior & LOOKUP_NIL)为真,且imp等于_objc_msgForward_impcache就返回nil
    // _objc_msgForward_impcache就是lookUpImpOrForward函数里创建的forward_imp
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

这里解释 为什么resolveMethod_locked调用了两次_lookUpImpTryCache

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    ...
    resolveInstanceMethod(inst, sel, cls);
    ...
    //lookUpImpOrForwardTryCache里面会调用_lookUpImpTryCache
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    ///lookUpImpOrNilTryCache里面会调用_lookUpImpTryCache
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    ...
    done:
    /**
        * 我们发现:
        * 由resolveInstanceMethod进入到这里,behavior=4;
        * 由lookUpImpOrForwardTryCache,behavior=1;
        
        * behavior=4即表示前面系统已经给了一次补救机会,如果此时查到的imp
        仍旧是默认的_objc_msgForward_impcache,
        那说明根本就没有实现resolveInstanceMethod: 或 resolveClassMethod: 方法,
        所以会返回nil;
    */
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

我们发现:

  • resolveInstanceMethod进入到_lookUpImpTryCache,behavior传入的是4,这是因为要做非空判判断,因为调用之前系统给了一次补救imp找不到的机会;
  • lookUpImpOrForwardTryCache进入到_lookUpImpTryCache,behavior传入的是1,这个1是由前面lookUpImpOrForward函数带过来的

单例解释

    //接下来解释,为什么这个if内部语句只会调用一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    解释如下:
    (补0的位置不参与比较)
    |运算是把左右两边的相加
    举例:210010 + 0001 = 0011,也就是3
    
    & 运算是把左右两边的值,每一位进行比较,相同输出1,不同输出0
    举例:3&20011 & 0010 ,第三位相同,输出1,第四位不同,输出0,结果是0010也就是2;
    
    ^ 运算跟&刚好相反,不同输出1,相同输出0
    举例:3&20011 & 0010 ,第三位相同,输出0,第四位不同,输出1,结果是0001也就是1;
    简单记忆:
        |  相加
        &   找相同
        ^   找不同
        
    回到上面的问题,
    _lookUpImpOrForward全局搜索可知等于3LOOKUP_RESOLVER是一个常量23&2 = 2,所以if判断结果为true;
    behavior = behavior^LOOKUP_RESOLVER = 3^2 = 0011^0010 = 0001 = 1
    下次进来的时候,if判断就变成1&2 = 0001&0010 = 0,不会进入判断
枯燥的源码分析终于告一段落,接下来开始实战测试:

这个是没有实现resolveInstanceMethod:image.png

接下来在DirectionChild里加上代码:

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    NSLog(@"resolveInstanceMethod class:%@ - selName:%@",self,NSStringFromSelector(sel));
   
    return [super resolveInstanceMethod:sel];
}

image.png

我们发现resolveInstanceMethod:执行了两次,这里大胆儿猜测第二次是元类,但是控制台打印发现两个self地址一样,这个猜测被否定了:

image.png

我们继续回到源码分析原因,在resolveInstanceMethod函数里,在图片所在“msg(cls, resolve_sel, sel)”这一行打上端点,然后运行,进入断点的时候打印下cls的地址:

image.png

通过分析,我们发现resolveInstanceMethod函数里,执行了好几遍objc_msgSend消息发送,其实两次是对DirectionChild及其元类发送消息,所以也就解释了上面“+ (BOOL)resolveInstanceMethod:(SEL)sel”为什么会调用两次。

后续发现,这里的解释有点不那么通顺,这里再补充解释下: 在resolveInstanceMethod:方法里打上断点,因为我们好奇的是为什么第二次会进来,所以第二次进入断点的时候,控制台输入bt打印一下堆栈信息:

image.png

我们发现,在返回main之前,还调用了一下CoreFoundation框架里的___forwarding___函数,(关于___forwarding___的探索,可以看消息转发文章拓展部分),而我们知道,___forwarding___内部是调用了class_respondsToSelector函数,我们打开objc源码搜索class_respondsToSelector可以看到:

image.png

image.png

image.png 继续点击_lookUpImpTryCache

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    ...
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    ...
}

会发现其内部,当imp为空的时候,调用了lookUpImpOrForward,所以我们可以理解为,假如imp不为空,就不会进入这里,也就是不会二次调用resolveInstanceMethod:,要实现这个目的那就在resolveInstanceMethod:返回一个可以新的消息接收者

完善上述resolveInstanceMethod代码:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod class:%@ - selName:%@",self,NSStringFromSelector(sel));
    if(sel == @selector(sayHello)) {
        Method method =class_getInstanceMethod(self,@selector(saySomething));
        ///添加对象方法
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)saySomething{
    NSLog(@"What are you doing ?");
}

控制台能够正常打印,且resolveInstanceMethod只执行一次;

类方法也同样如此,可以实现以下动态决议:

+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"resolveClassMethod class:%@ - selName:%@",self,NSStringFromSelector(sel));
    if(@selector(sayHowAreYou) == sel){
        Class metaClass = objc_getMetaClass("DirectionChild");
        IMP imp = class_getMethodImplementation(metaClass, @selector(saySSJ));
        Method md = class_getInstanceMethod(metaClass, @selector(saySSJ));
        const char * type = method_getTypeEncoding(md);
        
        return class_addMethod(metaClass,sel, imp,type);
    }
    return [super resolveClassMethod:sel];
}

+ (void)saySSJ{
        NSLog(@"=========saySSJ !!");
}

那么问题来了,如果resolveInstanceMethod:resolveClassMethod:都没有实现,又该怎么办呢?

欲知后事,请看下篇文章《iOS底层分析之objc_msgSend消息转发

动态决议代码
百度网盘:pan.baidu.com/s/1_1B0sSeq…
密码:zsxn

更新日志:

2021.07.15 17:45 -> 增加 resolveInstanceMethod:执行了两次的解释