方法的本质4_消息转发机制


本页所使用的objc runtime 756.2,来自GITHUB

1、回顾

上文消息方法的本质3_消息查找流程 一文总结了方法查找的流程,得到结论如下:

  1. 类的缓存里查找——找到——结束查找
  2. 类的方法列表里查找——找到——结束查找
  3. 递归父类缓存、方法列表查找——找到——结束查找
  4. 动态方法解析——(未分析)
  5. 以上未解决——异常崩溃——结束

下面继续学习源码,结合业务代码,深挖第四步 动态方法决议,来更深一步的学习消息转发机制

2、问题

这篇文章,希望弄清楚下面的问题

  • 消息转发是什么

  • 消息转发的场景

  • 如何进行消息转发

    3、动态方法决议

3.1 入口 - class_resolveMethod

之前的分析,知道了当查找缓存未命中后,会执行objc_msgSend_uncached 的方法,进行方法查找。

具体执行的是方法 _class_lookupMethodAndLoadCache3

runtime 源码如下:

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

在源码中,知道了具体执行的方法是 lookUpImpOrForward,这个方法执行了众多流程,缓存、递归查找等。

如果都未命中,则会找到方法决议实现,代码如下:

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // 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_resolveMethod(cls, sel, inst) 前后,使用了runtimeLock 进行了锁定与解锁,具体实现时互斥锁(参见Cooci老师的互斥锁-读写锁-条件锁),来保证内存安全。

查看一下class_resolveMethod 方法

Summary

_class_resolveMethod Call +resolveClassMethod or +resolveInstanceMethod. Returns nothing; any result would be potentially out-of-date already. Does not check if the method already exists.

class_resolveMethod 调用 +resolveClassMethod 或 +resolveInstanceMethod。

返回值为空;任何结果将可能已经过期。

如果方法已经存在,不会检查/执行。

由此可见resolveMethod 只是一个入口,会具体调用如下方法:

  • 类方法:+resolveInstanceMethod
  • 实例方法:+resolveInstanceMethod

具体的源码,见下面的:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
代码 分析 备注
! cls->isMetaClass() 是否元类 否,说明主体是实例
_class_resolveInstanceMethod 实例方法决议实现
_class_resolveClassMethod 类方法决议实现
_class_resolveInstanceMethod [第2个] 查看NSObject 决议实现 发生在lookUpImpOrNil 失败后

分析:

进入决议后,先确定是否是元类;

如果是元类,由于已知类方法存储在元类里,知道该方法主体是类,则会进入类方法决议

如果不是元类,即方法主题是实例,进入实例方法决议。

如果元类方法决议查找失败,根元类亦查找失败,由于根元类的isa 是指向自身的,所以用到了 _class_resolveInstanceMethod 的实例决议方法。

具体逻辑见下图:

001

3.2 类方法决议 - _class_resolveClassMethod

捋清楚入口后,继续看类方法的决议,源码如下:

/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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 resolveClassMethod:%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));
        }
    }
}

分析上文的源码,除了一些断言、异常的保护代码外,关键语句来到了这一句

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

这里的函数msg 调用了3个参数

  • _class_getNonMetaClass(cls, inst) :即类的本身
  • SEL_resolveClassMethod : 决议的类方法
  • sel :需要查找的方法编号

所以,关键的行为就是SEL_resolveClassMethod 方法,通过搜索,源码代码中均找不到。

回头看苹果给该方法写的注释:

_class_resolveClassMethod

Call +resolveClassMethod, looking for a method to be added to class cls.

意思是C 源码里写的方法,在OC 实现里,会执行+resolveClassMethod 的方法,即通过resolveClassMethod 来对该类进行决议——添加相应的类方法。

在OC里的实现即这个方法

+ (BOOL)resolveClassMethod:(SEL)sel{
    return NO;
}

至此,已经大概明白了决议的意义——当方法找不到实现时,转发到类/实例决议方法里,看看开发者是否有自行实现。其最大的意义,恐怕是收集各种崩溃了。

3.3 实例方法决议 - class_resolveInstanceMethod

实例方法决议的源码实现,与类方法决议类似,差别仅仅在决议执行方法,换成了SEL_resolveInstanceMethod,源码如下:

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

当决议方法得到实现后,会提示,方法XX 已经被决议成为新的实现YY—— 否则会提示决议未找到:

    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));
        }
    }

4、快速转发阶段

4.1 快速转发方法

forwardingTargetForSelector :即将本类未实现的方法,交由指定的其他类来实现。

4.2 业务代码

实现如下:

  • Person, 与未实现的方法teach

    @interface Person : NSObject
    
    - (void)teach;
    
    @end
  • 类的转发方法——指定Teacher 这个类来执行

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(teach)) {
            return [Teacher alloc];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
  • Teacher 以及实现的方法teach

    NS_ASSUME_NONNULL_BEGIN
    
    @interface Teacher : NSObject
    
    - (void)teach;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    @implementation Teacher
    
    - (void)teach{
        NSLog(@"%s", __func__);
    }
    @end
  • 主业务执行,使类执行它为实现的方法teach:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            Person *person = [[Person alloc] init];
    
            [person teach];
    
        }
        return 0;
    }
    
  • 执行结果,teacher类实现了teach

    2020-03-20 16:12:51.477020+0800 debug-objc[8565:349779] -[Teacher teach]

    由此可见,forwardingTargetForSelector 的作用,是自身无法处理,交由其他对象(可能是类)来处理。

5、慢速转发阶段

5.1 慢速转发方法

methodSignatureForSelector,看看开发文档的描述:

methodSignatureForSelector:

Returns an NSMethodSignature object that contains a description of the method identified by a given selector.

返回一个NSMethodSignature 的签名对象,包含一个给定选择器的方法的描述

  • 获取方法签名: methodSignatureForSelector
  • 通过获取的签名,转发信息 –
    执行方法是 forwardInvocation往指定的对象传递方法和参数

5.2 业务代码实现:

基础代码与快速转发一致,只是删除了forwardingTargetForSelector 的实现代码。

并在Person 类里实现如下代码

  • 实现方法签名

    // 获得方法签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSString *sel = NSStringFromSelector(aSelector);
        //生成签名
        if ([sel isEqualToString:@"run"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }else {
            return [super methodSignatureForSelector:aSelector];
        }
    
    }

    “v” 是方法描述,更多的在这里:

    * 代表 char *

    char BOOL 代表 c

    : 代表 SEL

    ^type 代表 type *

    @ 代表 NSObject * 或 id

    ^@ 代表 NSError **

    # 代表 NSObject

    v 代表 void

  • 获取签名,进行配发信息

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
        NSLog(@"-----%@-----",anInvocation);
        //取得消息
        SEL selector = [anInvocation selector];
        //转发
        SomePerson *someP = [SomePerson new];
        if ([someP respondsToSelector:selector]) {
            //调用对象,进行转发
            [anInvocation invokeWithTarget:someP];
        } else {
    
            return [super forwardInvocation:anInvocation];
        }
    }
  • 执行teach 结果

    2020-03-20 16:37:41.639718+0800 debug-objc[9325:370827] -[Teacher teach]
    

6、查找失败

消息没有得到处理 崩溃退出,这里从forwardInvocation 的实现,可以追踪到相关的代码:

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

7、 总结

又到了最受欢迎的总结时间。

答案:

  • 消息转发是什么:向指定的对象或类,发送需要消息,请求交由对方执行
  • 消息转发的场景:自我无法实现的场景下。
  • 如何进行消息转发
    • 快速转发:使用 forwardingTargetForSelector 进行定向转发
    • 慢速转发:获取签名,使用forwardInvocation 转发

有图

另外,总结经验再多,不如有图有真相……

消息转发流程简图

下面是objc_msgSend 查找总流程:

objc_msgSend 流程图


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【底层探索】dyld浅析 【底层探索】dyld浅析
本页所使用的objc runtime 756.2,来自GITHUB 一、引言前文研究了对象、方法的基础,知道了对象/类的结构,类方法的生成和传递原理。下面该进入到核心环节,就是类的加载了,在这一个模块,需要了解的有下面几点: OC 的
2020-03-23 李佳
下一篇 
【休闲】西湖一瞥 【休闲】西湖一瞥
逛次逛次的西湖今天过来平海路的Apple 直营店给电脑做大保健,顺便看了一眼西湖。 湖边景色依旧宜人,可惜游人寥寥。 期待一切顺利,然后恢复到游人如织的景色。
2020-03-09 李佳
  目录