前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC底层探索12-消息动态决议,方法慢速、快速转发OC底层探索12-消息动态决议,方法慢速、快速转发

OC底层探索12-消息动态决议,方法慢速、快速转发OC底层探索12-消息动态决议,方法慢速、快速转发

作者头像
用户8893176
发布2021-08-09 11:27:47
5100
发布2021-08-09 11:27:47
举报
文章被收录于专栏:小黑娃Henry

OC底层探索11-objc_msgSend慢速查找流程中解释了对方法的非缓存查询以及方法查找失败之后的系统报错。

如果在2种机制下都没有找到方法imp,苹果也给出了2条建议:

  • 动态方法决议:慢速查找流程未找到后,会执行一次动态方法决议resolveMethod_locked
  • 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发

1. 方法动态决议

代码语言:javascript
复制
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    // 动态方法决议 : 给一次机会 重新查询
    if (! cls->isMetaClass()) {  // 对象 - 类
        resolveInstanceMethod(inst, sel, cls);
    } 
    else { // 类方法 - 元类
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) { 
        // 此处cls已经是元类了。元类调用方法,根据isa关系会在根元类中查找方法。
        //通过根元类的继承链最终找到NSObject,在NSObject中查询`实例方法`的resolve的实现。巧妙的设计!!
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // 在调用一次查询
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
  • 此处的cls已经是指向isa的指针,也就是,元类,此逻辑是经过汇编层的加工。所以判断不是元类就调用实例方法的resolve实现;反之调用类方法的resolve实现.
  • 根据元类的iSA关系,最终会找到根元类(NSObject),因为类的根元类都是NSObject。但(NSObject)类中只存在对象方法,所以需要再调用一次resolveInstanceMethod
  • 在方法动态决议中,开发者会重新实现该selimp所以, 需要重新进行一次查询。而且在本次查询中会优先在缓存中查找。
resolveInstanceMethod

实例方法的resolve具体源码实现

代码语言:javascript
复制
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    //生成动态解析的方法Sel
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    //在当前类的元类中查找resolve方法是否找得到
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        return;
    }
    
    //resolveInstanceMethod消息发送
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    //进行一次方法慢速查询,将当前方法插入缓存中,提高效率
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    //在实现且需要打印是,进行打印
    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 {
            _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调用后,又调用了一次lookUpImpOrNil;我们知道该方法如果找到对应imp之后会插入到对象类的缓存中去,方便后续使用;另一个是方便debug进行打印。

如何打开该打印

可以通过查看log来发现实现了resolveInstanceMethod的方法

resolveClassMethod

类方法的resolve具体源码实现

代码语言:javascript
复制
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    //判断resolveClassMethod方法是否实现
    if (!lookUpImpOrNil(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);
        }
    }
    //resolveClassMethod消息发送
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    //进行一次方法慢速查询,将当前方法插入缓存中,提高效率
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    
    //在实现且需要打印是,进行打印
    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));
        }
    }
}
  • 实现逻辑于resolveInstanceMethod基本相同
使用方式
  1. 实例方法的resolve 为self动态增加sayMaster的实现.使用runtime-api
代码语言:javascript
复制
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
    Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
    const char *type  = method_getTypeEncoding(sayMMethod);
    return class_addMethod(self, sel, imp, type);
}
代码语言:javascript
复制
+ (BOOL)resolveClassMethod:(SEL)sel{
    IMP imp           = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
}
  • 唯一的区别就是类方法是需要添加到元类中的,所以需要先找到元类objc_getMetaClass.
根据观察resolveInstanceMetho会走2次
  1. 第一次是在查询方法时lookupimp中调用的
  1. 第二次是在coreFunction时调用的
  • 在慢速转发过程中会进行第二次调用,后面会换种方式来验证
2.消息转发

在之前有提到apple推荐的快速转发慢速转发,他们是何时调用的呢?是以什么方式调用的呢?现在就来讨论下~

方法一
不知在之前有没有留意一个方法log_and_fill_cache(...)在这个方法中我们发现了一个系统提供的log方法。
代码语言:javascript
复制
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill(cls, sel, imp, receiver);
}
  • 是否打印就是看objcMsgLogEnabled这个参数的值,系统并没有对外提供这个方法,但是我们可以自己导出.
调用方法
  • extern void instrumentObjcMessageSends(BOOL flag);这个方法就是系统提供,但是需要我们手动导出后使用的方法。
  • 在我们调用方法的前后进行,可以看到该方法的所有调用记录
如何查看
结果

看到了熟悉的resolveInstanceMethod,而且出现了2次,也印证了之前的猜测。

与此同时还有些并不熟悉的方法forwardingTargetForSelector,methodSignatureForSelector

方法二

看到了这个调用的堆栈信息,调用的是CoreFoundation库。可是apple爸爸并没有开源这个库,所以想要查看内部的调用就需要拿出最终大招Hopper反汇编。

  • 在lldb调试中使用image list查看CoreFoundation的库的本地地址。然后拖入Hopper中。
  • forwarding在可以看到forwardingTargetForSelector的伪代码调用
  • 这就是所谓快速转发流程
  • 继续跟流程会走到methodSignatureForSelector,以及forwardInvocation
  • 这就是所谓慢速转发流程
  • 查看调用栈这两个方法中间应该会调用resolveInstanceMethod,但是在反汇编中没有看到具体的调用,如果有知道的大佬可以提醒一下小弟。
消息转发简单实现
代码语言:javascript
复制
// 1: 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    //此处返回一个实现该方法sel的对象
    return [super forwardingTargetForSelector:aSelector];
}

// 2: 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    //返回方法的参数编码
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    //更换方法接受者
    anInvocation.target = [LGStudent alloc];
    //更换方法索引
    anInvocation.selector = NSSelectorFromString(@"实现了该IMP的SEL");
    //更换方法参数编码
    anInvocation.methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"]
    // anInvocation 保存 - 方法
    [anInvocation invoke];
}
3. 整体流程图

消息实现未找到后的流程

  • forwardInvocation不处理并不会奔溃。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/12/1 上,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 方法动态决议
    • resolveInstanceMethod
      • resolveClassMethod
        • 使用方式
          • 根据观察resolveInstanceMetho会走2次
            • 2.消息转发
              • 方法一
              • 方法二
              • 消息转发简单实现
            • 3. 整体流程图
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档