OC底层原理08-objc_msgSend方法消息慢速查找(二)

iOS--OC底层原理文章汇总

本章内容是基于上一章OC底层原理07--Runtime以及objc_msgSend分析(一)内容继续扩展,上一章中探究了方法消息快速查找方式,这一章将探索objc_msgSend中的慢速查找流程。

分类方法

在进行方法消息的查找之前,先做一个铺垫,即探究一下分类方法的调用、查找流程。
定义两个类:Book为父类,子类为English,各实现一些方法

  • Book类
    Book.m 中不实现burnBook方法
// Book.h
@interface Book : NSObject
-(void)read;
-(void)burnBook;
+(void)write;
@end
// Book.m
@implementation Book
-(void)read{
    NSLog(@"%s",__func__);
}
+(void)write
{
    NSLog(@"%s",__func__);
}
@end

  • English类
// Englis.h
@interface English : Book
-(void)buy;
+(void)say;
@end
// English.m
@implementation English
-(void)buy{
    NSLog(@"%s",__func__);
}
+(void)say{
    NSLog(@"%s",__func__);
}
@end
  • main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Book * book = [[Book alloc] init];
        English * english = [[English alloc] init];
       
        [book read];
        [Book write];
        
        [English say];
        [english buy];
        // 1.子类调用父类定义未实现方法。
        [english burnBook];
        // 2.子类调用自身未实现方法
      // [English performSelector:@selector(sell)];
#pragma clang diagnostic pop
    }
    return 0;
}

如果这样,会出现什么样的情况呢?父类定义了方法,子类去调用父类定义未实现的方法是否会报错呢?结果如下:

子类调用父类未实现方法报错

报错了,English,Book 各自调用各自的类方法,实例对象调用对象方法都没有问题;当English的实例对象去调用父类Book定义未实现的方法时,报错了。这个其实很好理解,定义方法未实现,当被调用是肯定会报错的。
换第二个情况,当我们定义一个NSObjectCagtegory,该分类中定义一个-sell()方法并实现它。当调用[English performSelector:@selector(sell)];, "万物之主"NSObject中的方法定义的方法,它是否会报错呢?
来看结果

没有报错,English来调用NSObject的实例方法,按我们的认知,当一个类调用父类的实例方法是无法通过编译且会报错的,为什么在这里English能调用到NSObject的分类的是实例方法呢?
这个原因是:类方法存在元类之中,当子类调用一个类方法,就是在调用元类的实例方法。它会先去自己的元类中查找是否有该方法,自身元类中没有,则寻找元类的父类,直到跟元类中查找,而根元类的父类是NSObject,所以在这里,能调用到NSObject的-sell()方法。 这其实就是isa走位图的例子,相关知识在OC底层原理04—类、元类、根元类 与 isa的关联OC底层原理05-成员、实例变量、元类中介绍。
这里我们就再一次体会到了方法消息在调用过程中,经历着很复杂的消息传递和查找过程。接下来我们就继续探索方法消息的慢速查找流程。

objc_msgSend - 慢速查找

快速查找通过汇编在缓存中查找,在上一章已做探究。

objc4-781的源码中继续,定位到objc-msg-arm64.s中,在之前分析中,如果在汇编底层中缓存命中,就说明找到了方法消息。如果未命中则跳到CheckMissorJumpMiss ,结果都是相似,都要进行退出,转至__objc_msgSend_uncached, 进入慢速查找流程。

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

在源码中查找__objc_msgSend_uncached

查找到MethodTableLookup

MethodTableLookup 找到关键代码
lookUpImpOrForward

慢速查找

进行全局搜索 lookUpImpOrForward,这就是慢速查找关键方法

lookUpImpOrForward具体实现

下面的代码是从缓存再次获取Imp,由于有可能存在多线程,方法可能被重置。加之后面的一个操作是耗时操作,所以需要在查找开始前,再次从缓存中获取imp

 // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
  • 确定类、父类、元类集成链,方便后续循环查找
   if (slowpath(!cls->isRealized())) { 
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

找到realizeClassMaybeSwiftAndLeaveLocked -> realizeClassMaybeSwiftMaybyReLocked -> realizeClassWithoutSwift, 进入struct Class realizeClassWithoutSwift,获取数据存入ro、rw。

realizeClassWithoutSwift

再继续走,下面有这样两个变量

supercls = realizeClassWithoutSwift(remapClass(cls->superclass),nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()),nil);

这里的目的是通过循环方式,确定下本类、对象继承链、元类继承链,这也是为后面做循环查找。

  • 初始化方法
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) { 
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }
  • 开始查找类的缓存
    // The code used to lookpu the class's cache again right after (苹果这里有个小疏忽,lookup拼写成了lookpu)
    // 这样的for是死循环--反复在for死循环中查找父类,元类,跟元类,根类NSOBject,直到nil都没有查找到
    for (unsigned attempts = unreasonableClassCount();;) {
        //【一】 查找当前类的方法列表中去查找——二分查找
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        // 查到到方法,取出方法的imp存到变量imp中返回,imp用于后面缓存到cache中, 
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        
        if (slowpath((curClass = curClass->superclass) == nil)) {
            //【四】查找不到,跳出循环
            // 反复在for死循环中查找父类,元类,跟元类,根类NSOBject,直到nil都没有查找到,则就让imp = forward_imp 将消息转发(下一章探索),结束该循环退出。
            imp = forward_imp;
            break;
        }
       
        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
 
        // Superclass cache.
        // 【二】 curClass 在上面已经指向了父类,所以这里是在父类缓存中查找方法,本类-
>父类->NSObject->nil
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        //【三】
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            // 在父类继承链、元类继承链中找到了方法,就将其缓存到此类中,done中就是执行这样
            goto done;
        }
    }

    // 【五】No implementation found. Try method resolver once. 如果方法找不到,就执行动态方法决议
    // 大致的理解是为了
    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;
  • 二分查找入口
    【一】在for循环中,通过查找本类、父类继承链,元类继承链,反复查找方法。
    getMethodNoSuper_nolock --> search_method_list_inline() --> findMethodInSortedMethodList
    findMethodInSortedMethodList()是慢速查找的关键部分,它使用二分查找(在数据取出之后就进行了排序操作)。
    如果找到了就执行done流程,进入
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
   // objc_msgSend -> 二分查找自身 -> cache_fill(查到的方法先存缓存) ->objc_msgSend
    cache_fill(cls, sel, imp, receiver);
}

将查到的方法,而又不是要找的方法先存入缓存,以备下次查找时直接从缓存中取出比对,提高查找效率。
objc_msgSend -> 二分查找自身 -> cache_fill(查到的方法先存缓存) ->objc_msgSend这样形成了一个查找方法的循环。如果再一次循环查找的时候,会先从缓存查找,如果还是没有,就通过父类缓存中去查找方法。

  • 父类去查找
    这里会执行【二、三】中的流程
       // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }

imp = cache_getImp(curClass,sel),OC层是没有其实现,它走的是汇编实现。(缓存查找就是要去汇编操作)

    // cache_getImp汇编查找与跳出
    STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp

CacheLookup 就是会执行上一章讲解的底层缓存查找进入了汇编查找如果依然没有查找到,cache_getImp -> lookup -> lookUpImpOrForward ,再回到lookUpImpOrForward,走第【四】执行imp = forward_imp。将消息转发lookUpImpOrForward: forward_imp = objc_msgForward_impcache(汇编中)

  • 动态方法决议 - 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);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

再检查一次是否有对resolveInstanceMethod方法的实现,是否对imp查找不到做了处理

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

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

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    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 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));
        }
    }
}

如果对imp进行了处理操作,相安无事,继续走一次lookUpImpOrForward,查找到之前处理的方法返回。如果没有做操作,抱歉! please see see the erro ↓

查找不到-抛出错误

xxx :unrecognized selector sent to instance 0x10xxxx,这样的报错方式很常见吧。
但在类中给没有实现的方法实现imp的操作,则就不会再报错.在类中实现以下方法即可

+ resolveInstancMethod:(SEL sel){
      if(sel == @selector(xxx1)){
        Method myMethod = method_getInstanceMethod(self,@selector(xxx2));
        IMP imp = class_getMethodImplementation(self,@selector(xxx2));
         const char * type = method_getTypeEncodeing(myMethod);
        return class_addMethod(self,sel,)
      }
      return [super resolveInstanceMethod:sel];
}

这样就从一定程度上杜绝了多人开发中,定义了方法但是未实现,他人调用时防止了奔溃。可以将这样的方式编写在NSOBject的分类中,可以做切面(后面探索)、AOP。但是这样还是对工程侵入太强,处理方法过于霸道,后面还有更好的解决方案。

总结

本章内容探索了,objc_msgSend在消息发送过程中,继上一章在汇编层快速寻找方法之后,未找到方法,进入到了慢速查找。慢速查找过程中会先进行缓存查找,其实就是快速查找获取到imp,如果imp有效则进行跳转。否则就继续,接下来会将类、父类、元类的继承链关系确定,方便后面不断在继承链中查找方法。
接下来就会开始类的for循环查找,在for循环中,先利用二分查找当前类的方法列表,找到则返回imp跳转done;如果没有找到,就要继续在继承链中反复查找父类的方法列表、父类的缓存;然而还是没有的话就进行实例方法or类方法的动态方法决议,如果还是没有查找到,将imp=forward_imp,退出循环,进入下一步消息转发。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266