前言
上一篇文章类的底层探究-cache中提到cache_t插入bucket调用的是cache_t::insert方法,全局搜索我们看到下面的注释:
从注释中看出写入cache的方法有
copyCacheNolock,eraseNolock,collectNolock,insert,destroy
我们好奇的是cache的读取为什么有objc_msgSend,cache_getImp,本文主要研究objc_msgSend,针对方法的本质,objc_msgSend的底层源码,以及objc_msgSend的真机调试进行探究
方法的本质
运行时runtime的调用方法
OC代码直接的方法调用,比如[person run]NSObject系统库,接口调用,比如isKindOfClassruntime API, 比如objc_msgSend,objc_msgSendSuper
前面两种方式比较简单,采用第三种方式调用的时候需要注意的是引入objc/message.h,以及禁止objc_msgSend调用的严格检查
下面是模拟器的调用,可以看到已经能成功调用objc_msgSend了
但是使用真机运行后报错了:
所以可以得到objc_msgSend的调用跟指令集有关的结论。
在arm指令集下,我们采用Apple官方建议的方式调用api:
可以正常调用了😄
方法的本质
对于代码
SPObject *obj = [[SPObject alloc] init];
[obj runSP];
我们xcrun一下(clang也一样)得到cpp文件,查看相关代码
SPObject *obj = ((SPObject *(*)(id, SEL))(void *)objc_msgSend)((id)((SPObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("SPObject"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("runSP"));
化简一下就是objc_msgSend(obj,sel_registerName("runSP")
那么对于有参数的方法调用是怎么样的呢
((void (*)(id, SEL, id))(void *)objc_msgSend)((id)obj, sel_registerName("runSP2:"), (id)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2));
化简一下就是objc_msgSend(obj,sel_registerName("runSP2:"), @2)
这里我们可以得出结论,方法的本质就是objc_msgSend
objc_msgSend源码分析
我们通过跟进汇编流程,发现objc_msgSend的调用是在libobjc.A.dylib里面:
同时在
libobjc里面也找到了objc_msgSend的声明:
找到了声明,但是点击定位不到方法的实现。我们想要是objc_msgSend方法的实现是c或者是c++方法应该直接可以定位,于是我们大胆猜想objc_msgSend方法的实现是汇编的方法,汇编的方法需要前面加下划线(为了防止符号冲突)全局搜_objc_msgSend
我们看到有好多文件用到了_objc_msgSend这个符号,且都是汇编代码,不同的文件区分不同的架构,这里笔者选择arm64架构的代码进行探索
_objc_msgSend汇编中用到的一些宏定义说明 MSG_ENTRY, ENTRY, STATIC_ENTRY, UNWIND
.macro MSG_ENTRY /*name*/
// 伪操作 它不是真正的指令
// 定义代码段 只读和可执行的 后面那些指令都属于.text段
.text
// .align提供的参数作为对齐目标 圆整对象为 2^10
// 作用在于对指令或者数据的存放地址进行对齐,
// 有些CPU架构要求固定的指令长度并且存放地址相对于2的幂指数圆整,否则程序无法正常 运行,比如ARM
.align 10
//告诉汇编器 $0(也就是_objc_msgSend的函数地址)要被链接器用到
.globl $0
$0:
.endmacro
.macro ENTRY /* name */
// 定义代码段 只读和可执行的 后面那些指令都属于.text段
.text
// .align提供的参数作为对齐目标 圆整对象为 2^5
.align 5
//告诉汇编器 $0(也就是_objc_msgSend的函数地址)要被链接器用到
.globl $0
$0:
.endmacro
.macro STATIC_ENTRY /*name*/
.text
.align 5
//private_extern可以把symbol_name变成私有外部符号。
//当链接编辑器将此模块与其他模块组合在一起
//(且keep_private_externs 命令行选项未指定)时,该符号会将它从全局更改为静态
.private_extern $0
$0:
.endmacro
.macro END_ENTRY /* name */
// 定义方法出口
LExit$0:
.endmacro
.macro UNWIND
//定义section端
.section __LD,__compact_unwind,regular,debug
PTR $0
// 计算函数占的空间大小
.set LUnwind$0, LExit$0 - $0
// 分配函数空间
.long LUnwind$0
// 分配NoFrame或者FrameWithNoSaves的空间大小
.long $1
// 非私有的
PTR 0 /* no personality */
// LLVM对于LSDA的解释,不明觉厉
// Language Specific Data Area. C++ “zero cost” unwinding is built on top a generic unwinding mechanism.
// As the unwinder walks each frame, it calls a “personality” function to do language specific analysis.
// Each function’s FDE points to an optional LSDA which is passed to the personality function.
// For C++, the LSDA contain info about the type and location of catch statements in that function.
PTR 0 /* no LSDA */
// 定义代码段 后面要开始执行代码了
.text
.endmacro
#define NoFrame 0x02000000 // no frame, no SP adjustment
#define FrameWithNoSaves 0x04000000 // frame, no non-volatile saves
_objc_msgSend源码分析
//定义方法入口
MSG_ENTRY _objc_msgSend
//准备section段,分配方法空间,开始执行text段的代码
UNWIND _objc_msgSend, NoFrame
//p0=x0寄存器,也就是_objc_msgSend的第一个参数self
//p1=x0寄存器,也就是_objc_msgSend的第二个参数sel
//我们假定是对象发送消息,p0就是消息的接受者
//比较p0是否是nil
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//支持小对象模式的话小于0就是空对象或者小对象,走LNilOrTagged流程
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//不支持小对象模式的话对象为空,走LReturnZero流程
b.eq LReturnZero
#endif
//这里的流程是对象不为空的流程
//对象的指针指向的是对象的isa, p14=对象的isa
ldr p14, [x0] // p14 = raw isa
//对象的isa指向类,这个方法执行完后 p16=类
GetClassFromIsa_p16 p14, 1, x0 // p16 = class
LGetIsaDone:
// 这里开始查找类里面的缓存的cache_t里面的方法获取imp
// 根据传入的模式返回imp或者调用imp或者走__objc_msgSend_uncached缓存未命中的流程
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//TaggedPionter流程
//0的话是空对象走,LReturnZero流程
b.eq LReturnZero // nil check
//不然就是小于0的小对象走GetTaggedClass获取小对象
//GetTaggedClass流程暂时不分析,看最后的返回应该也是拿到类存到p16寄存器
GetTaggedClass
//也是走LGetIsaDon流程去类的cache_t里面找imp
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
//p0是nil才回来这个流程
//这方法把参数x0,x1和返回值d0,d1,d2,d3这几个寄存器置0,然后返回
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
p0=x0的原因是有相关宏定义,这里定义了17个寄存器的别名
分析_objc_msgSend整个流程大概是这样的:
1:对于方法接受者p0判断是否为空,空的话走LReturnZero流程,把参数和返回值置0,然后返回
2:如果p0不为空,判断是否是小对象,是的话走GetTaggedClass获取小对象的类存在p16
3:不是小对象的话,获取对象的isa存到p14
4:根据对象的isa获取类,存到p16
5:根据类查找cache,调用CacheLookup,找到imp返回或者调用,没找到走__objc_msgSend_uncached流程去类的bits的methods里面找,或者进行消息动态决议或转发
GetClassFromIsa_p16
接下来我们再分析里面的GetClassFromIsa_p16流程,探究怎么从对象的isa获取类的:
// 如果是个Indexed isa流程是跟iwatch相关的,暂时不做探究
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//不是Indexed isa并且是64位指令的话
#elif __LP64__
//needs_auth=0说明是纯isa,src指向的就是类
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
//64位非纯isa的话需要通过auth_address(isa_mask)与上isa获取类
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
其中SUPPORT_INDEXED_ISA的说明:
__ARM_ARCH_7K__是个啥东西,源码里找不到定义,我们通过clang看出了一些端倪
// Unfortunately, __ARM_ARCH_7K__ is now more of an ABI descriptor. The CPU
// happens to be Cortex-A7 though, so it should still get __ARM_ARCH_7A__.
if (getTriple().isWatchABI())
Builder.defineMacro("__ARM_ARCH_7K__", "2");
综上看SUPPORT_INDEXED_ISA是iwatch的判断
对于ExtractISA的定义:
#if __has_feature(ptrauth_calls)(iponex以上的设备)的条件下会走:
非iphonex的会走:
这里的$0=p16,$1=src=p14=isa, ISA_MASK就是isa的掩码,isa与上isa掩码获取类,p16=类
CacheLookup的流程
//Mode有GETIMP,NORMAL,LOOKUP,_objc_msgSend传入的是NORMAL
//Function是_objc_msgSend
//MissLabelDynamic=__objc_msgSend_uncached
//MissLabelConstant只有Mode=GETIMP才会用到,这里不做探究
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function label we may have
// loaded an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd\Function,
// then our PC will be reset to LLookupRecover\Function which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
//我们探索真机下的这个架构的,其他流程类似
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//#define CACHE (2 * __SIZEOF_POINTER__)
//类内存平移16个字节得到cache_t的指针地址
//cache_t的内存付给p11,p11=_bucketsAndMaybeMask
//p11的64位在真机下后48位存buckets,前16位存mask
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
//高16位抹0,得到低48位的buckets给到p10
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
//p1是_objc_mesSend的第二个参数sel
//这里面是执行cache_hash函数通过mask和sel获取sel对应的初始化下标p12
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//获取初始下标的bucket,下标p12左移4位等于乘以16(16是因为一个bucket包含8字节IMP合8字节的SEL)
// p13=初始的bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
// #define BUCKET_SIZE (2 * __SIZEOF_POINTER__)
// BUCKET_SIZE是16个字节,也就是一个bucket的内存大小
// cache_next函数往前遍历第一遍循环找到buckets的首地址
// p9=SEL p17=IMP
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
//p17和参数p1对比,相等的话找到了,不然就继续走3的分支
2: CacheHit \Mode // hit: call or return imp
// }
//如果sel==0说明没找到,走MissLabelDynamic流程
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//从初始下标往前遍历找知道buckets的头部位置
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//获取buckets的尾地址
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
//mask是capacity-1,到这边p13正好是buckets中最后一个bucket
//p12是初始下标,p10是buckets,经过这个操作后p12 =buckets的第一个bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
//从buckets尾地址开始继续循环往前找知道找到p12这个初始位置,循环终止,
//这样前面部分和后面部分的bucket都进行了一次遍历
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
分析对于真机整个流程大概是这样的:
- 1.类
内存平移16个字节得到cache_t的指针地址 - 2.根据架构从
_bucketsAndMaybeMask(_mask)获取buckets以及mask - 3.根据参数sel进行
hash函数,算出初始的下标 - 4.根据初始下标跟
buckets的首地址算出初始的bucket - 5.往前遍历
bucket直到buckets的首地址 - 6.从
buckets的最后一个元素往前遍历直到初始的bucket - 7.循环过程中如果缓存命中走
CacheHit流程 - 8.如果没找到sel走
MissLabelDynamic流程
缓存命中CacheHit流程
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
// TailCallCachedImp是认证IMP和调用IMP的过程,objc_msgSend找到IMP就调用了
// TailCallCachedImp根据是否是iphoneX以上的机型有不同的定义
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
#if __has_feature(ptrauth_calls) //iphoneX以上
.macro TailCallCachedImp IMP, IMPAddress, SEL, ISA
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor \IMPAddress, \IMPAddress, \SEL // mix SEL into ptrauth modifier
eor \IMPAddress, \IMPAddress, \ISA // mix isa into ptrauth modifier
.ifndef LTailCallCachedImpIndirectBranch
LTailCallCachedImpIndirectBranch:
.endif
brab \IMP, \IMPAddress
.endmacro
//其他机型
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
.ifndef LTailCallCachedImpIndirectBranch
LTailCallCachedImpIndirectBranch:
.endif
br $0
.endmacro
缓存命中的代码比较简单,对于MODE=NORMAL来说是认证IMP和调用 IMP的过程
缓存命中MissLabelDynamic流程
从CacheLookup的参数得知MissLabelDynamic=__objc_msgSend_uncached,定位__objc_msgSend_uncached方法
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
//快速查找没找到调用_lookUpImpOrForward进行慢速查找,并且循环继承链查找
//还是没找到就动态决议,消息快速转发和慢速转发流程
MethodTableLookup
//TailCallFunctionPointer就是跳转到x17
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
// x0和x1本身就是objc_msgSend的参数就不用处理了
// x2是cls,之前的流程已经存在了p16了
// x3是参数behavior,这里写死是3
//其中LOOKUP_RESOLVER=2,用来控制动态决议走一次resolveMethod_locked
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
缓存没命中配置好_lookUpImpOrForward的参数然后调用_lookUpImpOrForward方法,返回的结果在x0,赋值给x17,然后TailCallFunctionPointer x17,直接调用方法
真机探索_objc_msgSend流程
随便对一个OC方法加断点打开汇编,看到结果如下:
到这里我们进入了
objc_msgSend的真机汇编流程
这里看到
x0是消息接受者,x1是sel
这里
x14=对象的isa,x16=类
这里
p11 = mask|buckets,p10 = buckets
这里获取
初始下标 p12以及初始bucket=p13,我们看到这里已经存了IMP和SEL了,但不是我们要找的,继续循环找
笔者尝试的方法是没有实现的,所以几次循环后最终会走缓存没命中的流程,直到
crash。读者可以根据汇编源码的不同分支尝试探索不同的案例,比如缓存命中 TaggedPionter,模拟器,iWatch等case进行探索,思路是一样的,这里不再赘述
总结
cache_t::insert是写入cache,cache的读取在objc_msgSend,cache_getImp- 运行时
runtime的调用方法OC代码直接的方法调用NSObject系统库,接口调runtime API
- 方法的本质就是
objc_msgSend _objc_msgSend源码流程:- 方法接受者
p0判断是否为空,空的话走LReturnZero流程 - 不是的话判断是否是
小对象,是的话走GetTaggedClass流程 - 不是的话根据对象的
isa获取类,存到p16 - 根据类调用
CacheLookup
- 方法接受者
CacheLookup流程:- 类
内存平移16个字节得到cache_t的指针地址 - 获取
buckets以及mask - 哈希函数算出初始下标,得到初始
bucket的地址 - 从初始位置往前到头,然后从尾部到初始位置循环遍历查找sel
- 找到的话走
缓存命中,直接调用方法 - 没找到走
_lookUpImpOrForward,对返回的imp进行调用
- 类
- 结合汇编源码进行
真机汇编断点调试