iOS底层原理之消息发送

1.方法的本质

1.1 探索
  • 在之前的文章中,我们探索了对象、类、以及isa等的本质,那么今天我们一起来分析一下方法的本质,看看OC所调用的方法究竟是什么
  • 首先进入target目录下,使用clang -rewrite-objc main.m,编译生成main.cpp文件,对照二者的main函数
// oc
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];;
        [person sayNB];
    }
    return 0;
}

// c++
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));;
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
    }
    return 0;
}
  • 对比分析可以发现,我们调用的[person sayNB]方法,在C++中则被编译成为了objc_msgSend((id)person, sel_registerName("sayNB")),由此可以分析出,方法在底层的本质其实就是objc_msgSend,调用方法其实就是调用objc_msgSend向特定的对象发送特定的消息
  • objc_msgSend(对象,方法名)
  • 我们接着定义一个C语言函数,然后继续调用,并且同样使用Clang编译成C++,可以看到run函数直接就调用了
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));;
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
        // run是函数,直接调用了
        run();
    }
    return 0;
}
1.2 分析
  • 所以,oc中方法调用的过程,就是利用objc_msgSend向对象发送消息,然后根据发送的SEL找到具体的函数实现地址imp,最后调用
  • 实例方法:objc_msgSend(对象,sel)
  • 类方法:objc_msgSend(类,sel)
  • 父类:则是使用objc_msgSendSuper
  • 注意,super和self指向一个对象,但是self是类隐藏参数,而super只是预编译指令符号,作用就是不经过本类的方法列表,直接通过superClass的方法列表去查找,然后利用本身(objc_super->receiver)去调用

2. objc_msgSend分析

  • 首先,打开工程,在方法调用那一行,标记断点,并且在Debug-Debug WorkFlow-Always Show Disassembly勾选上,让我们开始跟踪一下当调用方法的时候,到底进行了哪些步骤
2.1 初探
  • 首先,通关一连串看不懂的汇编代码中,找到了我们的init方法,sayNB方法,以及objc_msgSend方法
image.png
  • control+in进入 进入objc_msgSend方法,找到其所在为libobjc.A.dylib,那么接下来,我们可以通过源码搜索对其进行探索
    image.png
2.2 探究 objc_msgSend
  • 打开objc源码,全局搜索objc_msgSend方法,在objc-msg-arm.s文件中找到objc_msgSend入口
    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    // person - isa - 类
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

  • 简单分析一下,首先进行正常的判空以及TAGGED_POINTERS操作
  • 其次通过GetClassFromIsa_p16获取当前的类
  • 通过CacheLookup先到缓存中进行查找,大概就是通过汇编语言,对当前的类进行地地址偏移16个字节(isa和superClass各占据8个字节),找到cache的结构体所在,然后找到buckets进行缓存方法的查找
  • 如果没有查找到,则进行CheckMiss
.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
  • CheckMiss的汇编方法中,我们看到了,根据传递参数的不同,返回不同的结果,我们刚才传递的类型为NORMAL,所以接下来会调用__objc_msgSend_uncached方法,并且内部只调用了MethodTableLookup的方法以及回调一个方法指针
    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
  • 那么看一下之前的步骤,先通过isa找到类,在找到类的cache以及buckets,当缓存之中全都没有找到之后,那么接下来就去类原来的存储空间,也就是class-bits-rw-ro-methodList中去找方法的定义
  • MethodTableLookup方法中,先是做了一系列准备工作,之后调用了__class_lookupMethodAndLoadCache3方法,进行方法查找和加载缓存,到此汇编阶段结束,准备进入C/C++阶段
macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp
        // 一系列准备工作
    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
          // 一系列准备工作之后,进入到方法的查找以及缓存的加载
    bl  __class_lookupMethodAndLoadCache3
  • 这里就是最后从汇编进入到C++阶段的方法,不过这里需要把汇编中的方法去掉一个下划线,才能在源码中搜索到
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
2.4 补充
  • 刚刚探索到最后从汇编到C++的那个步骤,也就是__class_lookupMethodAndLoadCache3,但是从这里想要进入到C++,必须要有一定的上帝视角才行,下面换个断点方法直接找到我们需要找的C++方法

  • 依照第一段中的项目,control+in 进入objc_msgSend方法之后,在一系列看不懂的汇编中找到我们刚才寻找的__objc_msgSend_uncached方法

image.png
  • 继续往下走,进入__objc_msgSend_uncached内部,在断点处,发现其跳转了一个位于objc-runtime-new.mm文件的C++方法

    image.png

  • 我们根据方法定义找到其方法定义,到此,方法查找成功从汇编过渡到C++

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
2.3 小结
  • objc_msgSend是使用汇编写的,主要是速度够快,够灵活(C语言做不到写一个函数来保留未知的参数并且跳转到任意的函数指针)
  • objc_msgSend首先通过快速路径对缓存进行查找,如果查找不到则进入MethodTableLookup方法列表查找,通过_class_lookupMethodAndLoadCache3进行查找并且缓存

3. 方法查找流程

3.1 分析
  • 对象方法查找:SelfClass -> SuperClass ->...->NSObject -> nil
  • 类方法查找:SelfMetaClass -> SuperMetaClass -> ... -> NSObjectMetaClass-> NSObject -> nil
  • 如果找不到,则经典报错
3.2 流程分析
  • objc_msgSend先通过缓存进行快速查找,如果找不到,则进行慢速常规查找
  • 快速查找的流程就是前面分析的消息发送流程,那么现在来看_class_lookupMethodAndLoadCache3普通查找流程
lookUpImpOrForward分析
  • 首先进行一系列前戏,保证当对象方法或者类方法没有找到时,能够有父类或者元类能够去让我们去继续去查找,一系列递归
    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
  • 之后进入正题,可能是当前类也可能是当前类的父类(如果当前类找不到目标方法,那么之前的realizeClass方法已经准备好了即将要查找的父类,继续查找父类即可),具体流程都是一样的,还是先判断一下当前imp有没有被缓存,如果存在缓存那么直接调用即可(这个时候可能有缓存进来)
  • 之后通过getMethodNoSuper_nolock进行方法列表查找
     // Try this class's method lists.
    //当前类的方法列表
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    //当前类的父类方法列表
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
  • 在方法内部,通过对methods列表进行遍历,找到当前sel对应的method,如果找不到则返回nil
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
  • 其中在search_method_list过程之后,通过findMethodInSortedMethodList(sel, mlist)进行二分查找,保证能够快速寻找到目标方法
// 关键二分查找代码
for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
  • 最后,如果方法成功被找到,则进入log_and_fill_cache方法,内部调用cache_fill正式进入缓存流程,之后进入goto done,具体内容可以看一下iOS方法缓存-cache
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
  • 如果方法没有找到,那么首先会进行动态方法解析_class_resolveMethod,在这个过程中,系统会调用一次已经存在的事先定义好的两个类方法,在这里给我们提供了一次容错的机会,
// objc源码
_class_resolveInstanceMethod(cls, sel, inst)
_class_resolveClassMethod(cls, sel, inst)

// NSObject内部方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
  • 如果我们没有做任何处理,那么则进入消息转发阶段,消息转发阶段全局搜索一下该方法。在其汇编方法内部,调用了__objc_msgForward
 imp = (IMP)_objc_msgForward_impcache;
    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    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,全局搜索找到定义,并且找到了objc_defaultForwardHandler方法的具体实现
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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
  • 如果处理失败那么则报出经典错误unrecognized selector sent to instance 0x123456789
3.3 总结
  • 方法查找,先是通过objc_msgSend的快速缓存查找,之后再通过对类以及父类的方法列表进行二分法常规查找
  • 最后如果NSObject中都没有需要实现的方法,那么先进入动态方法解析,之后进入消息的快速转发和常规转发,消息转发处理失败之后则报错crash
  • 在之后的消息转发分析中,也会向大家具体分析一下消息转发的三个阶段,以及每个阶段的作用

结束语

  • iOS底层的消息发送以及方法查找的大致流程已经介绍的差不多了,之后就是底层的另一个大的功能消息转发 objc_msgForward,下次再分析哈~
  • objc_msgSend的部分由于是汇编写的(本人太菜),所以只是简单介绍了一下功能,没有具体深入分析,下面给大家整一个流程图,就到这结束了~
  • 诙谐学习,不干不燥,如有疑问,欢迎回复骚扰~
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容