iOS底层原理_07消息流程分析之快速查找(上)

第七节课 消息流程分析之快速查找(上)

在上篇文章我们分析了cacheinsert流程,但是在insert之前呢?还有一个cache读取流程,即objc_msgSendcache_getImp。在分析之前,首先了解什么是Runtime

Runtime的运行时理解

编译时
是将源代码翻译成机器能识别的代码的过程,简单的一些翻译工作,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段
运行时
代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段

runtime的使用有以下三种方式:
OC代码: [persion sayHi]
NSObject: isKindOfClass
Runtime API: class_getInstanceSize

07-Runtime.png

其三种实现方法与编译层和底层的关系如上图所示
其中的compiler就是我们了解的编译器,即LLVM,例如OCalloc 对应底层的objc_allocruntime system libarary就是底层库

探索方法的本质

写两个方法,只实现一个,并在main函数进行调用

HZMPerson *person = [HZMPerson alloc];
[person sayNB];
[person sayHello];
07-报错.png

编译的时候是成功的,但是运行起来后却崩溃了,这其实就是我们上面说到过的编译时与运行时的区别

添加上未实现的方法,编译之后,clang编译的源码查看cpp文件。可以看到,再cpp文件中的main代码翻译成如下代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        HZMPerson *person = ((HZMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HZMPerson"), sel_registerName("alloc"));
        HZMTeacher *teach = ((HZMTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HZMTeacher"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));



        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_nd_w4zbcj291zn3gc4kvv8vv7bc0000gn_T_main_23e09d_mi_2);
    }
    return 0;
}

可以发现,上层的代码都会翻译成一个解释代码,可以看到我们调用方法的过程翻译过来是
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));这样一段代码,我们简化一下就是
调用方法 = 消息的发送: objc_msgsend(消息的接收者,消息的主体(sel+参数))

为了验证下,我们通过手动调用objc_msgsend来模仿方法调用
注意:
1、直接调用objc_msgSend,需要导入头文件#import <objc/message.h>
2、需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错

[person sayNB];
objc_msgSend(person, @selector(sayNB));
07-objc_msgSend调用方法打印.png

结论:方法的本质就是objc_msgSend消息发送

除了验证,我们还可以尝试让person的调用执行父类中实现,通过objc_msgSendSuper实现

07-自定义类.png

查看objc_msgSendSuper的源码,看一下需要的参数。

07-objc_msgSendSuper.png

objc_msgSendSuper方法中有两个参数(结构体,sel),其结构体类型是objc_super定义的结构体对象,且需要指定receiver 和 super_class两个属性

struct objc_super zm_objc_super;
zm_objc_super.receiver = person;
zm_objc_super.super_class = HZMPerson.class;

objc_msgSendSuper(&zm_objc_super,@selector(sayHello));

按照objc_msgSendSuper的格式创建zm_objc_super的结构体,再补全两个属性,发现可以直接使用了,与我们之前直接调用方法的打印一模模一样样。

发现不论是[person sayHello]还是objc_msgSendSuper都执行的是父类中sayHello的实现,所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。

接下来我们就来看看objc_msgSend的源码实现

objc_msgSend流程上

掏出我们的源码吧

07-源码搜索.png

搜索objc_msgSend,因为objc_msgSend是用汇编来实现的,所以我们直接找.s文件进入arm64.s

objc_msgSend 汇编源码

//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend 
//---- 无窗口
    UNWIND _objc_msgSend, NoFrame 
    
//---- p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receiver
    cmp p0, #0          // nil check and tagged pointer check 
//---- le小于 --支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) 
#else
//---- p0 等于 0 时,直接返回 空
    b.eq    LReturnZero 
#endif 
//---- p0即receiver 肯定存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器
    ldr p13, [x0]       // p13 = isa 
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
    GetClassFromIsa_p16 p13     // p16 = class 
LGetIsaDone:
    // calls imp or objc_msgSend_uncached 
//---- 如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//---- 等于空,返回空
    b.eq    LReturnZero     // nil check 

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

我们梳理一下主要的流程:
【第一步】判断objc_msgSend方法的第一个参数receiver是否为空
如果支持tagged pointer,跳转至LNilOrTagged
如果小对象为空,则直接返回空,即LReturnZero
如果小对象不为空,则处理小对象的isa,走到【第二步】
如果即不是小对象,receiver也不为空,有以下两步
receiver中取出isa存入p13寄存器
通过 GetClassFromIsa_p16中,arm64架构下通过 isa & ISA_MASK获取shiftcls位域的类信息,即class,GetClassFromIsa_p16的汇编实现如下,然后走到【第二步】

.macro GetClassFromIsa_p16 /* src */
//---- 此处用于watchOS
#if SUPPORT_INDEXED_ISA 
    // Indexed isa
//---- 将isa的值存入p16寄存器
    mov p16, $0         // optimistically set dst = src 
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa -- 判断是否是 nonapointer isa
    // isa in p16 is indexed
//---- 将_objc_indexed_classes所在的页的基址 读入x10寄存器
    adrp    x10, _objc_indexed_classes@PAGE 
//---- x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根据 偏移量 进行 内存偏移
    add x10, x10, _objc_indexed_classes@PAGEOFF
//---- 从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index 
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

//--用于64位系统
#elif __LP64__ 
    // 64-bit packed isa
//---- p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
    and p16, $0, #ISA_MASK 

#else
    // 32-bit raw isa ---- 用于32位系统
    mov p16, $0

#endif

.endmacro

【第二步】获取isa完毕,进入慢速查找流程CacheLookup NORMAL

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容