本文主要是介绍6、iOS底层分析 - 消息发送(objc_msgeSend)分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
调试分析
OC中的方法到底是怎么调用的呢?之前查资料知道oc的方法调用是通过消息发送的形式进行调用,那就来结合源码探究一下
LGPerson *person = [[LGPerson alloc] init];Class pClass = [LGPerson class];[person sayNB];
汇编查看流程(注意使用真机调试)
Debug - Debug workflow - Always show Disassembly
0x100000b4b <+27>: movq 0x746(%rip), %rsi ; (void *)0x00000001000012d8: LGPerson0x100000b52 <+34>: movq 0x71f(%rip), %rcx ; "alloc"0x100000b59 <+41>: movq %rsi, %rdi0x100000b5c <+44>: movq %rcx, %rsi0x100000b5f <+47>: movq %rax, -0x48(%rbp)0x100000b63 <+51>: callq *0x4a7(%rip) ; (void *)0x0000000100344640: objc_msgSend0x100000b69 <+57>: movq 0x710(%rip), %rsi ; "init"0x100000b70 <+64>: movq %rax, %rdi0x100000b73 <+67>: callq *0x497(%rip) ; (void *)0x0000000100344640: objc_msgSend0x100000b79 <+73>: movq %rax, -0x18(%rbp)0x100000b7d <+77>: movq 0x714(%rip), %rax ; (void *)0x00000001000012d8: LGPerson0x100000b84 <+84>: movq 0x6fd(%rip), %rsi ; "class"0x100000b8b <+91>: movq %rax, %rdi0x100000b8e <+94>: callq *0x47c(%rip) ; (void *)0x0000000100344640: objc_msgSend0x100000b94 <+100>: movq %rax, -0x20(%rbp)0x100000b98 <+104>: movq -0x18(%rbp), %rax0x100000b9c <+108>: movq 0x6ed(%rip), %rsi ; "sayNB"
通过汇编 Debug 可以发现LGPerson 调用的4个方法 alloc 、init、class、sayNB ,
其中init、sayNB 都可以看到 objc_msgSend。继续分析
找到 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(id,sel) id 消息接收者(对象) sel 方法编号
- 得到的 key & mask 得到 index,然后找到对应 imp
- 发送消息是找函数的过程,oc 底层封装的就是 c 就是通过消息发送找到真正函数实现的 imp
- 如果直接调用 c 的函数,并不会有消息发送的过程,是直接调用的。
- // 发送消息 : objc_msgSemd
// 对象方法 - person - sel
// 类方法 - 类 - sel
// 父类 : objc_superMSgSend
control+in进入 进入objc_msgSend方法,找到其所在为libobjc.A.dylib,那么接下来,我们可以通过源码搜索对其进行探索
objc_msgSend 分析
在objc源码中全局搜索 objc_msgSend 方法,在 objc-msg-arm64.s 文件中找到 objc_msgSend 入口
ENTRY _objc_msgSendUNWIND _objc_msgSend, NoFramecmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERSb.le LNilOrTagged // (MSB tagged pointer looks negative)
#elseb.eq LReturnZero
#endif// person - isa - 类ldr p13, [x0] // p13 = isaGetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:CacheLookup NORMAL // calls imp or objc_msgSend_uncached
这个地方是用汇编写的,不是很好读。经过查询简单分析一下
- cmp p0, #0 // nil check and tagged pointer check 空指针标记,也就是判空
- 判断 SUPPORT_TAGGED_POINTERS 操作(支持标记的指针)
- p13 相当于 isa
- 其次通过 GetClassFromIsa_p16 获取 当前的类 class
- 通过 CacheLookup 先到缓存中进行查找。
- 其实和类的结构基本上一样 ,就是通过汇编语言编写,对当前的类进行地地址偏移16个字节( isa 和 superClass 各占据8个字节),找到 cache 的结构体所在,然后找到 buckets 进行缓存方法的查找
- 如果没有查找到,则进行 CheckMiss
.macro CheckMiss// miss if bucket->sel == 0
.if $0 == GETIMPcbz p9, LGetImpMiss
.elseif $0 == NORMALcbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUPcbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
CheckMiss
根据传递参数的不同,返回不同的结果
objc_msgSend 刚才传递的类型为 NORMAL,所以接下来会调用__objc_msgSend_uncached 方法,并且内部只调用了MethodTableLookup 的方法以及回调一个方法指针
STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band p16 is the class to searchMethodTableLookupTailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncached
- 类的结构。先通过isa找到类,再找到类的 cache 的 buckets 中 ,当缓存之中全都没有找到之后,那么接下来就去类原来的存储空间,也就是 class - bits - rw - ro - methodList 中去找方法的定义。
- 消息发送的汇编也是一样的在 MethodTableLookup 方法中,先是做了一系列准备工作,之后调用了__class_lookupMethodAndLoadCache3 方法,进行方法查找和加载缓存,
- 下面汇编结束是 C/C++
macro MethodTableLookup// push frameSignLRstp fp, lr, [sp, #-16]!mov fp, sp// 一系列准备工作// save parameter registers: x0..x8, q0..q7sub 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 x1mov x2, x16// 前面都是准备工作 在一系列准备工作之后,进入到方法的查找以及缓存的加载 bl __class_lookupMethodAndLoadCache3
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
注意 : 从汇编进入到C++阶段的方法,需要把汇编中的方法去掉一个下划线,才能在源码中搜索到。
- __class_lookupMethodAndLoadCache3 之后就有些迷茫不知道下一步该干什么了,这个地方就查看了一下资料。
- control+in 进入 objc_msgSend 方法之后,往下找发现有一个 __objc_msgSend_uncached 方法
- 进入 __objc_msgSend_uncached 内部,在断点处,发现其跳转了一个位于 objc-runtime-new.mm 文件的C++方法
总结:
- objc_msgSend 是使用汇编写的,主要是速度够快,够灵活(C语言做不到写一个函数来保留未知的参数并且跳转到任意的函数指针)
- 汇编可以动态识别,性能更快更高。汇编更加容易被机器识别
- objc_msgSend 首先通过快速路径对缓存进行查找,如果查找不到则进入 MethodTableLookup 方法列表查找,通过_class_lookupMethodAndLoadCache3 进行查找并且缓存
扩展
消息的发送LGStudent *s = [LGStudent new];[s sayCode];// 方法调用底层编译// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)objc_msgSend(s, sel_registerName("sayCode"));// 类方法编译底层
// id cls = [LGStudent class];
// void *pointA = &cls;
// [(__bridge id)pointA sayNB];objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));// 向父类发消息(对象方法)struct objc_super lgSuper;lgSuper.receiver = s;lgSuper.super_class = [LGPerson class];objc_msgSendSuper(&lgSuper, @selector(sayHello));//向父类发消息(类方法)struct objc_super myClassSuper;myClassSuper.receiver = [s class];myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
上边这个向父类发消息(类方法)
调用super_class 父类,然后去元类查找的原因是因为
类方法要到元类去找,元类找不到向元类的父类去找。
s -> s的元类 –> s父类的元类
直接取父类去发送消息的话就是直接
s的父类->s父类的元类
这篇关于6、iOS底层分析 - 消息发送(objc_msgeSend)分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!