1、类中方法的存储
cache_t
中的方法存储
cache_t cache
方法缓存中,方法的存储是以SEL
和IMP
的形式。
class_data_bits_t
中方法存储
在类objc_class
的学习中,明白了实例对象的方法是存放在类的class_data_bits_t bits;
中。取方法的时候,使用class_data_bits_t
中的class_rw_t* data()
。在这个地方,如果是在编译期,或者说在系统调用运行时的realizeClass
方法之前,data()
是class_ro_t
结构体的形式,保存了类中的方法列表、属性列表、成员变量列表等;在系统运行了runtime的realizeClass
方法后,则会生成class_rw_t
结构体,并更新class_rw_t* data()
的地址,换成class_rw_t
结构体的地址。
class_rw_t
:method_array_t
-->method_list_t
-->method_t
.
class_ro_t
:method_list_t
-->method_t
.
方法以 method_t 结构保存,iOS14 以上该结构发生巨大变化。
在 64 位的系统上会占用 24 字节,name、types、imp 分别占用 64 bit 大小,与之前一样。
- 取值时,在地址后取三个64位的数据,就能得到方法的name、type、imp
但是 struct small 占用 12 字节,name、types、imp 分别占用 32 bit 大小。 - 这种情况下,name、types、imp存储的是地址的偏移量。当前地址 + 存储的偏移量才是真正的存储地址。
name、types、imp 分别指向方法的 名称、参数数据、函数指针,苹果考虑到镜像中的方法都是固定的,不会跑到其他镜像中去。其实不需要 64 位寻址的指针,只需要 32 位即可 (多余 32 位寻址,可执行文件在内存中要超过 4G)。small 结构里面的数据,都是相对地址偏移,不是内存中的具体位置。如果要还原,需要进行计算。
该部分主要是以参考学习--iOS 恢复调用栈(适配iOS14),如需详细了解,请移步该文章。
2、消息机制
(1)、方法调用的编译过程
当我们在程序中调用方法时,格式为[obj method]
。经过编译过程,会转换为objc_msgSend
(调用父类方法,会转换为objc_msgSendSuper
)。
/**
* 发送消息给类的实例对象,并有一个简单的返回信息
*
* @param self 接受这个消息的类的实例对象的指针
* @param op 处理消息的方法的selector.
* @note 当发生方法调用的时候,编译器会把方法调用编译为objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, objc_msgSendSuper_stret.方法中的一个
*/
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
/**
* 给一个成员变量的父类发送一条消息
*
* @param super 一个objc_super类型的结构体的指针,用该值判定把消息发送给谁
* @param op 处理这条消息的方法的selector
* @see objc_msgSend
*/
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
(2)、方法的查找过程
方法的查找过程,大概可以分为三个步骤:
a、 查找类的方法缓存cache_t
b、查找类的方法列表cls->data()->methods()
c、递归调用上一级父类,按照a和b的方式,在父类的缓存和方法列表中查找
d、在类及其父类中,没有找到IMP,尝试方法的动态解析过程
下面追个对每个步骤进行说明:
a、 查找类的方法缓存cache_t
在类的缓存中查询使用的是汇编语言,查询过程中更加的快捷高效。
类的方法缓存,查询流程如图所示:
_objc_msgSend
:方法的实现代码(这里以——arm64——
系统进行说明)
//——arm64——
//入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0是objc_msgSend的第一个参数-消息接收者receiver,判断是不是nil或者tagged pointer
//cmp语句:比较指令,其内部就是进行减法运算,但不影响值。使用b语句进行跳转
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //tagged pointer true amr64
//上面cmp的语句的结果,如果是小于等于(le)0,那么消息接受这是空或者是tagged pointer,
//则执行跳转(b)标号为LNilOrTagged的程序
b.le LNilOrTagged // (MSB tagged pointer looks negative)
//b.le这句也包含着,如果cmp的结果大于0,则继续执行下面的代码
#else
//cmp指令为0的情况下,即消息接受者为nil,跳转LReturnZero
b.eq LReturnZero
#endif
//ldr:读取指令,从寄存器读取内容的指令
//x0是self,类偏移0的位置存储的是isa,即把self的isa给到p13
ldr p13, [x0] // p13 = isa
//执行 GetClassFromIsa_p16 参数为(isa,1,self)。
//内部调用ExtractISA,本质是isa & isa_Mask来获得类地址
//从isa中获取cls
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone://通过上面的过程,根据isa获取到了cls
//去cls的缓存cache中查询IMP
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//对cmp结果判断,小于0或者等于0的情况下,都进来
//再次对cmp的结果进行判断,是不是等于0,即消息接受者为nil。是的话,返回为空
b.eq LReturnZero // nil check
//下面的情况就是接受者的地址指针为tagged pointer
//内存平移获取index,再去找到cls
GetTaggedClass
//找到了cls,跳转到LGetIsaDone标号,执行CacheLookup
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(void){}
//检查p0(消息接受者)是不是nil或者tagged pointer。
//result等于0,p0是nil;result<0,p0是tagged pointer
result = p0 - #0;
//如果p0是nil的话,清理寄存器,直接返回空
if(result == 0){
//调用LReturnZero
//返回空
}
//p0不是nil的情况下,是否为tagged pointer对于获取cls的方式是不一样的,所以需要对tagged pointer进行判断。
if(true amr64){
if(result < 0){//tagged pointer
//调用GetTaggedClass方法,根据isa获得cls
GetTaggedClass;
//调用LGetIsaDone标号,进入CacheLookup方法。在CacheLookup方法中,查询类的cache
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
}else if(result > 0){{
//调用GetClassFromIsa_p16方法,根据isa获得cls
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//执行到LGetIsaDone标号,进入CacheLookup方法。在CacheLookup方法中,查询类的cache
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
}
}else{
//调用GetClassFromIsa_p16方法,根据isa获得cls
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//执行到LGetIsaDone标号,进入CacheLookup方法。在CacheLookup方法中,查询类的cache
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
}
}
根据isa获取cls的方法和过程
下面是根据已经找到的cls,去类中cache中,查询缓存方法的流程,调用的是
CacheLookup
方法。
//在类的方法缓存中通过sel去查找imp
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
// cacheLookup的mode,有NORMAL | GETIMP | LOOKUP
// GETIMP:
// 缓存不存在的话,返回null,x0设置为0
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector 即方法的SEL
// - x16 contains the isa 即cls
// - other registers are set as per calling conventions
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
/*****vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv********/
//以下是不同系统中,buckets和mask的取值方式,准备相关数据
//最终获得的结果信息如下
// p10 = buckets
// p11 = mask(arm64真机是_bucketsAndMaybeMask)
// p12 = index
/***************************************************************/
//arm64 64 OSX/SIMULATOR
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//类的isa地址偏移16个字节,(即isa的8个字节、superClass的8个字节),就得到cache的地址,也就是_bucketsAndMaybeMask
//存入到p10
ldr p10, [x16, #CACHE] // p10 = mask|buckets
//也就是_bucketsAndMaybeMask低48位是存放方法的空间首地址;高16位是存放mask的空间的首地址
//p10的第48位,即mask的首地址,存入到p11
lsr p11, p10, #48 // p11 = mask
//_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)进行与操作,把高16位置0,得到方法的首地址
//方法的首地址存放到p10
and p10, p10, #0xffffffffffff // p10 = buckets
//x12 = cmd & mask w1为第二个参数cmd(self,cmd...),w11也就是p11
and w12, w1, w11 // x12 = _cmd & mask
//arm64 64 真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//类的isa地址偏移16个字节,(即isa的8个字节、superClass的8个字节),就得到cache的地址,也就是_bucketsAndMaybeMask
//存入到p11,即这里的p11是_bucketsAndMaybeMask
ldr p11, [x16, #CACHE] // p11 = mask|buckets
////arm64 + iOS + !模拟器 + 非mac应用
#if CONFIG_USE_PREOPT_CACHES
//iphone 12以后指针验证
#if __has_feature(ptrauth_calls)
//tbnz 测试位不为0则跳转
tbnz p11, #0, LLookupPreopt\Function
//_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)进行与操作,把高16位置0,得到方法的首地址
//方法的首地址存放到p10
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11 第0位不为0则跳转 LLookupPreopt\Function。
tbnz p11, #0, LLookupPreopt\Function
#endif
//eor 逻辑异或(^) 格式为:EOR{S}{cond} Rd, Rn, Operand2
//p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12
eor p12, p1, p1, LSR #7
//p12 = p12 & (_bucketsAndMaybeMask >> 48) = p12 & mask = buckets中的下标
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//_bucketsAndMaybeMask和#0xffffffffffff(即_bucketsAndMaybeMask的低48位)进行与操作,把高16位置0,得到方法的首地址
//方法的首地址存放到p10
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
//arm64 32
#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
/***************************************************************/
//以上是不同系统中,buckets和mask的取值方式
//最终获得的结果信息如下
// p10 = buckets
// p11 = mask(arm64真机是_bucketsAndMaybeMask)
// p12 = index
/****^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^*****/
// p13(bucket_t) = buckets + 下标 << 4 PTRSHIFT arm64 为3.
// 4位为16字节,所以 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第index个元素的地址。
//这里p13为buckets中间的一个bucket的寄存器
//所以这里的do-while是一个p13不断前向遍历的过程,遍历到bucket的首地址是停止。即这个do-while是遍历的buckets的前半部分
add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp 寄存器1 寄存器2 寄存器3 #xx:读取寄存器3 --> 内存地址 --> 内存数据,按字节顺序放入寄存器1和寄存器2中。然后寄存器3的内存地址 - xx
//把x13(bucket)的imp和sel读取出来,依次放入p17、p9寄存器中,并且x13的内存地址减小一个bucket的大小
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
//b.hs指令是判断是否无符号小于
b.hs 1b
//在buckets的前半部分[0 - index]没有找到。继续在后半部分机进行查找(index - (buckets.count-)]
/*****vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv********/
//以下是不同系统中根据mask获取最后一个bucket地址,即准备数据
//p13 = 最后一个bucket的地址
#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
//p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。
//因为maskZeroBits的存在 就找到了mask对应元素的地址 这里没搞明白
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
/****^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^*****/
// 根据上面 p13 = 最后一个bucket的地址,也是查询的有边界
//p12 = buckets + (p12<<4) index对应的bucket_t ,也就上此查询的开始地址,用来做查询的左边界
//(p12 p13];
add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
//如果查询到了,跳到2标号,cacheHit
b.eq 2b // goto hit
//循环条件,p12 p13都有值,且p13 > p12
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
//符合循环条件,继续循环执行
b.hi 4b
//遍历全部的buckets,没找到imp,跳转到 __objc_msgSend_uncached
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
/*其他代码*/
.endmacro
根据上述流程写出的伪代码,如下:
//CacheLookup的伪代码
CacheLookup(Mode, Function, MissLabelDynamic, MissLabelConstant){
//1、各个系统架构下,获取到buckets、mask、并且处理后得到遍历的标识位index等信息,
p10 = buckets;
p11 = mask;//(arm64真机是_bucketsAndMaybeMask)
p12 = index;
/***2、buckets中前半部分的遍历操作[0, index]***/
// p13 = buckets + index *16;
//获取到第index个bucket的地址,并且赋值到p13寄存器中
p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT));
do{
//把p13存放的bucket的imp赋值给p17
p17 = p13.imp;
//把p13存放的bucket的sel赋值给p9
p9 = p13.sel;
//即p13的地址向前移一个bucket大小,即p13改为前一个bucket的地址
p13 = p13 - BUCKET_SIZE;
//p1是要查找的方法的SEL
//查找到了方法的IMP。跳转到CacheHit 传值为 NORMAL
if(p9 == p1){
//调用cacheHit方法
CacheHit NORMAL
break;
}
//没有找到的情况下
if(p9 == nil){
//证明cache有问题,跳转到 __objc_msgSend_uncached
__objc_msgSend_uncached
break;
}
}while(p13 >= p10);//即前向遍历,p13的地址到了第一个bucket(p10)的地址
/***3、在buckets的前半部分没有找到方法的IMP,需要查找buckets的后半部分(index, buckets.count]***/
//各个系统架构下,取得mask位的地址,获得buckets最后的bucket的地址,赋值给p13.用来做查询的右边界
p13 = buckets.count - 1;
//p12 index对应的bucket_t ,也就上此查询的开始地址,用来做查询的左边界
p12 = buckets + (p12<<4);
do{
//把p13存放的bucket的imp赋值给p17
p17 = p13.imp;
//把p13存放的bucket的sel赋值给p9
p9 = p13.sel;
//即p13的地址向前移一个bucket大小,即p13改为前一个bucket的地址
p13 = p13 - BUCKET_SIZE;
//p1是要查找的方法的SEL
//查找到了方法的IMP。跳转到CacheHit 传值为 NORMAL
if(p9 == p1){
//调用cacheHit方法
CacheHit NORMAL
break;
}
}while((p9 != nil) && (p13 > p12);//循环条件,p12 p13都有值,且p13 > p12,p9不为nil
/***4、遍历全部的buckets,没有找到方法的IMP,跳转到 __objc_msgSend_uncached***/
__objc_msgSend_uncached;
}
找到IMP后的实现流程
方法列表、查询父类、动态查询相关代码
在当前类的方法缓存中cache
没有找到方法的对应IMP。那么接下来就是要到类的方法列表中,遍历方法列表。
上面的图能显示出来,缓存中找不到方法的情况下,会调用__objc_msgSend_uncached
方法,而这个方法最终会调用到IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
。这个方法中,使用for循环去查询类的缓存或者类的方法列表,并且在上述查不到的情况下,递归调用父类,查询父类的缓存和方法列表。直到找到IMP,或者特殊情况的break,才能跳出for 循环。
下面是IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
方法的源码,查找类的方法列表、查询父类以及方法的动态解析都在这个方法中。
NEVER_INLINE
//在MethodTableLookup方法中,传进来的behavior = 3
//LOOKUP_INITIALIZE | LOOKUP_RESOLVER // LOOKUP_INITIALIZE = 1;LOOKUP_RESOLVER = 2
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//forward_imp赋值
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
//要返回的imp
IMP imp = nil;
//当前查找的cls
Class curClass;
runtimeLock.assertUnlocked();
//如果类没有初始化,behavior会增加 LOOKUP_NOCACHE。
if (slowpath(!cls->isInitialized())) {
//behavior = 3// 011
//LOOKUP_NOCACHE = 8// 1000
//behavior |= LOOKUP_NOCACHE = 0011 | 1000 = 1011
behavior |= LOOKUP_NOCACHE;
}
//加锁
runtimeLock.lock();
//在缓存内、loaded image的数据段内或已使用obj_allocateClassPair分配,则返回true
checkIsKnownClass(cls);
//如果类没有初始化,则对类进行初始化。
//在类的初始化过程中,完成对rw、ro的准备工作,并且对父类及其原类进行数据准备
//这样也为后面的查询,提供了数据信息
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// 解锁
runtimeLock.assertLocked();
//赋值要查找的类
curClass = cls;
/*--循环开始--*/
//在上限范围内循环,除非return/break
//Provides an upper bound for any iteration of classes, to prevent spins when runtime metadata is corrupted.
for (unsigned attempts = unreasonableClassCount();;) {
//先去缓存查找,防止这个时候共享缓存中已经写入了该方法。
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES //defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
//iOS
//这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找缓存
imp = cache_getImp(curClass, sel);
//找到后直接跳转done_unlock
if (imp) goto done_unlock;
//这个操作是要做什么?
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// curClass method list.查找类的方法列表,cls->data()->methods()
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//找到了IMP,跳到done,插入缓存
imp = meth->imp(false);
goto done;
}
//这里curClass 会被赋值为上一级父类,
//也就是自己类没有查找到,就去查找父类的方法列表
//如果到了NSObject的父类,也就是nil,就赋值imp为foreard_imp。break出循环
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
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.");
}
// 递归调用父类的查询机制,显示缓存再是方法裂列表
//_cache_getImp --> CacheLookup --> __objc_msgSend_uncached -->MethodTableLookup --> _lookUpImpOrForward
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.
goto done;
}
}
/*--循环结束--*/
//在方法列表中没有找到实现,开始动态解析
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
//该方法在没有找到动态解析的方法时,也是会返回nil。
//这样就得进入到消息转发流程了
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
//完成
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//把方法存储到缓存中。方法的IMP,是在本次for结束后,下次for开始时查询类的缓存中进行返回的
//也就是在类的方法列表,或者在父类中找到的方法IMP,都要在done_unlock中返回
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
//返回方法的IMP
return imp;
}
该方法的查找流程如下图所示:b、查找类的方法列表cls->data()->methods()
在类的方法列表中查询IMP的代码如下:
// curClass method list.
// curClass method list.查找类的方法列表,cls->data()->methods()
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
调用static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
方法进行的查询
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
最终会调用到static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
方法,该方法使用二分法对method_list_t
进行遍历。
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
//目标sel所在的位置
uintptr_t keyValue = (uintptr_t)key;
//方法列表中的数量
uint32_t count;
//使用二分法查询
//count >>= 1 count右移以一位,相当于count / 2.
for (count = list->count; count != 0; count >>= 1) {
//计算probe查询位置
probe = base + (count >> 1);
//获取到查询位置的sel,与keyValue对比
uintptr_t probeValue = (uintptr_t)getName(probe);
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)getName((probe - 1))) {
probe--;
}
return &*probe;
}
//probe位置的sel不是要查询的sel,并且目标sel的位置比probe要大
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
//probe位置的sel不是要查询的sel,如果目标sel的位置小于等于probe的话,进入到下一次for循环,
//即count>>1后,再次计算probe
}
return nil;
}
下面,我们带入数字走一遍查询流程:
假设现在的method_list_t
中右10条方法,则list->count = 10
,并且假设目标sel的位置在第3位,那么首次遍历的初始条件为:
第1次:
base = 0; count = 10; keyValue = 3;
>>> probe = base + count>>1 = 0 + 10 / 2 = 5;
>>>> 不是要找的sel,并且keyvalue<probe,不做操作。
>>>>>在for结束时执行count>>1
,即:count = 10>>1 = 5;
>>>>>>综上,得出下一次的相关数据:base = 0; count = 5; keyValue = 3;
第2次:
base = 0; count = 5; keyValue = 3;
>>> probe = base + count>>1 = 0 + 5 / 2 = 2;
>>>> 不是要找的sel,并且keyvalue > probe,执行 base = probe+1; count--;
操作。即:base = 2+1 = 3;count = 5-1=4
>>>>> 在for结束时执行count>>1
,即:count = 4>>1 = 2;
>>>>>>综上,得出下一次的相关数据:base = 3; count = 2; keyValue = 3;
第3次:
base = 3; count = 2; keyValue = 3;
>>> probe = base + count>>1 = 3 + 2 / 2 = 4;
>>>> 不是要找的sel,且keyvalue < probe,不做操作。
>>>>> 在for结束时执行count>>1
,即:count = 2>>1 = 1;
>>>>>>综上,得出下一次的相关数据:base = 3; count = 1; keyValue = 3;
第4次:
base = 3; count = 1; keyValue = 3;
>>> probe = base + count>>1 = 3 + 1 / 2 = 3;
>>>> 该位置就是要查询的sel,查找结束。
在查询流程结束后,会进入到一个while
循环,来查看当前位置之前,是不是有相同SEL的方法。如果有的话,调用前面的IMP。这种情况发生在有分类并且分类中也实现了相同的方法。分类的方法是在类的前面的,优先调用分类中的方法。
c、递归调用上一级父类,按照a和b的方式,在父类的缓存和方法列表中查找
- 1、在方法的列表中没有找到方法IMP,把curClass->superClass赋值给curClass
- 2、调用“cache_getImp”方法
- 3、cache_getImp是要调用“CacheLookup”,这样又走了一遍汇编查询cache、查询类的方法列表、查询父类方法列表的流程。
- 4、因此在这里进行了递归调用
- 5、一旦在父类中查询到了方法的实现,需要在方法接受类的缓存中添加该方法。
- 6、在下一次的for循环中,从cache中读取并调用IMP。
d、在类及其父类中,没有找到IMP,尝试方法的动态解析过程
目标sel的方法实现imp没找到,系统会再给一次补救机会:动态方法解析。
下面两个方法,是实现动态解析的实现,在方法的实现中,可以动态的为sel添加IMP
实例方法:+ (BOOL)resolveInstanceMethod:(SEL)sel;
类方法:+ (BOOL)resolveClassMethod:(SEL)sel;
动态解析使用举例:
/*======================================*/
/*********--MethodSend的类声明--**********/
/*=====================================*/
@interface MethodSend : NSObject
//声明实例方法,但不实现该方法
-(void)instanceMethodResolveTest;
//声明类方法,但不实现该方法
+(void)classMethodResolveTest;
//声明一个实例方法,但不实现该方法,并且让另外一个类实现动态解析
-(void)otherCls_InstanceMethodResolveTest;
@end
/*======================================*/
/*********--MethodSend的类实现--**********/
/*=====================================*/
#import "Dog.h"
#import "MethodSend.h"
#include <objc/runtime.h>
@implementation MethodSend
/*--类方法的动态解析--*/
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(classMethodResolveTest)) {
class_addMethod(object_getClass(self),
sel,
class_getMethodImplementation(object_getClass(self), @selector(dynamicClassIMP)),
"v@:");
return true;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+(void)dynamicClassIMP{
NSLog(@"类方法--classMethodResolveTest--的动态解析:IMP");
}
/*--对象方法的动态解析--*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(instanceMethodResolveTest)) {
class_addMethod([self class],
sel,
class_getMethodImplementation([self class],
@selector(dynamicInstanceIMP)),
"v@:");
return true;
}else if (sel == @selector(otherCls_InstanceMethodResolveTest)) {
class_addMethod([self class],
sel,
class_getMethodImplementation([Dog class], @selector(dog_dynamicInstanceResolve)),
"v@:");
return true;
}
return [super resolveInstanceMethod:sel];
}
-(void)dynamicInstanceIMP{
NSLog(@"实例方法--instanceMethodResolveTest--的动态解析:IMP");
}
@end
/*====================================================================*/
/*********--在Dog类中实现“otherCls_InstanceMethodResolveTest”--**********/
/*====================================================================*/
#import "Dog.h"
@implementation Dog
- (void)dog_dynamicInstanceResolve{
NSLog(@"~~~~我是“Dog”类的动态解析方法~~~");
}
@end
打印结果如下:
2022-07-31 11:58:42.412965+0800 schemeUse[3546:138093] 实例方法--instanceMethodResolveTest--的动态解析:IMP
2022-07-31 11:58:42.413031+0800 schemeUse[3546:138093] 类方法--classMethodResolveTest--的动态解析:IMP
2022-07-31 11:58:42.413075+0800 schemeUse[3546:138093] ~~~~我是“Dog”类的动态解析方法~~~
3、消息转发
动态方法解析失败后流程会被标记,随即再次触发消息查询,此时会跳过动态方法解析流程直接进行消息转发。
所谓消息转发,是将当前消息转发到其它对象进行处理。(这里可以做出类的虚假的多继承,即一个类继承了多个类,使用每个类中的方法)
(1)、快速转发
转发实例方法: - (id)forwardingTargetForSelector:(SEL)aSelector
转发类方法,id需要返回类对象: + (id)forwardingTargetForSelector:(SEL)sel
定义在NSObject
类中,默认返回nil
- (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
+ (id)forwardingTargetForSelector:(SEL)sel {
return nil;
}
举例如下:
/*======================================*/
/*********--MethodSend的类声明--**********/
/*=====================================*/
@interface MethodSend : NSObject
//声明实例方法,但不实现该方法
-(void)instanceMethodResolveTest;
//声明类方法,但不实现该方法
+(void)classMethodResolveTest;
@end
/*======================================*/
/*********--MethodSend的类实现--**********/
/*=====================================*/
@implementation MethodSend
//快速forward,实例对象的消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(instanceMethodResolveTest)) {
//转发到Dog类,让Dog类的instanceMethodResolveTest方法,进行实现
return [[Dog alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
//快速forward,类方法的消息转发
+ (id)forwardingTargetForSelector:(SEL)sel {
if (sel == @selector(classMethodResolveTest)) {
//转发类方法,id需要返回类对象
return NSClassFromString(@"Dog");
}
return [super forwardingTargetForSelector:sel];
}
@end
/*======================================*/
/*********--Dog的类声明--**********/
/*=====================================*/
@interface Dog : NSObject
//对象方法
-(void)instanceMethodResolveTest;
//类方法
+(void)classMethodResolveTest;
@end
/*====================================================================*/
/*********--在Dog类中实现对象方法“instanceMethodResolveTest”--**********/
/************--在Dog类中实现类方啊“classMethodResolveTest”--************/
/*====================================================================*/
@implementation Dog
-(void)instanceMethodResolveTest{
NSLog(@"instanceMethodResolveTest 在 Dog 中实现了");
}
+(void)classMethodResolveTest{
NSLog(@"classMethodResolveTest 在 Dog 中实现了");
}
@end
打印结果如下
MethodSend *ms = [[MethodSend alloc] init];
//实例方法消息转发
[ms instanceMethodResolveTest];
//类方法的消息转发
[MethodSend classMethodResolveTest];
2022-08-01 17:30:24.147594+0800 schemeUse[15671:253255] instanceMethodResolveTest 在 Dog 中实现了
2022-08-01 17:30:24.147658+0800 schemeUse[15671:253255] classMethodResolveTest 在 Dog 中实现了
(2)、慢速转发
如果forwardingTargetForSelector没有实现,或返回了nil或self,则会进入另一个转发流程。
它会依次调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,然后runtime会根据该方法返回的值,组成一个NSInvocation对象。如果返回的方法签名为nil,则直接崩溃报错。如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也不会报错
再去调用- (void)forwardInvocation:(NSInvocation *)anInvocation
。注意,当调用到forwardInvocation时,无论我们是否实现了该方法,系统都默认消息已经得到解析,不会引起crash。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(instanceMethodResolveTest)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
}
(3)、doesNotRecognizeSelector
如果没有方法的动态解析过程呢,也没有消息转发过程的话,则直接调用 doesNotRecognizeSelector抛出异常信息。
(4)、图示转发流程
4、调用super
的方法
前面图中说过,调用super
方法的时候,在编译阶段,会编译成 void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
这种形式。并以这种形式完成消息的传递。
调用父类方法的格式为[super xxxx: xx]
, 其中super
会被编译成objc_super *super
结构体。也就是objc_msgSendSuper
的第一个参数。
objc_super *super
结构如下:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
//类的实例变量,也是实际消息的接受者。
//即使使用super的方法,该父类方法的消息最终还是要发送给调用super方法所在的类的实例对象
__unsafe_unretained _Nonnull id receiver;
//下面是对象>类>superClass ,以super
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
当调用super method时,runtime会到super class中找到IMP,然后发送到当前class的实例上。
5、几个特殊方法
1、class
与object_getClass
实例对象的class方法
:
- (Class)class
方法内部直接返回的是object_getClass(self)
,所以实例对象的class
和object_getClass
是一样的。
- (Class)class {
return object_getClass(self);
}
示例代码:从打印结果和地址的输出,可以看出两者是同样的地址
- (void)instanceMethod{
Class cls_Clazz = [self class];
Class cls_Objc = object_getClass(self);
NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}
//打印结果为
2022-08-02 17:28:18.933562+0800 schemeUse[6677:286665] cls_Clazz is MethodSend -- cls_Objc is MethodSend
//对cls_Clazz和cls_Objc 输出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000104fdd380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000104fdd380 MethodSend
类对象的class
方法:
在+ (Class)class
方法内部,返回了self。而如果在类方法中调用object_getClass
,其实返回的是元类。所以在类方法中,使用这两个函数,是完全不同的。
+ (Class)class {
return self;
}
示例代码:从打印结果和地址的输出,打印出来的类是一样但是两者地址是不同的。
+ (void)classMethod{
Class cls_Clazz = [self class];
Class cls_Objc = object_getClass(self);
NSLog(@"cls_Clazz is %@ -- cls_Objc is %@",cls_Clazz,cls_Objc);
}
//打印结果为
2022-08-02 17:36:09.385057+0800 schemeUse[6795:292600] cls_Clazz is MethodSend -- cls_Objc is MethodSend
//对cls_Clazz和cls_Objc 输出:
(lldb) p/x cls_Clazz
(Class) $0 = 0x0000000102ed5380 MethodSend
(lldb) p/x cls_Objc
(Class) $1 = 0x0000000102ed5358
2、isMemberOfClass
实例方法:
用于判断当前对象所在的类是不是和目标类cls相同
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
类方法:
用于判断当前类对应的元类是不是和目标类cls相同
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
3、isKindOfClass
实例方法:
用于判断对象的类或者父类 ,有能与cls匹配即返回true。
- (BOOL)isKindOfClass:(Class)cls {
//取出对象的类,并遍历自己的父类,只要能和cls匹配,就返回true
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
类方法:
用于判断当前类对应的的元类及元类的父类,只要能和cls匹配,就返回true
+ (BOOL)isKindOfClass:(Class)cls {
//当前self为类,类的isa指向为元类
//取出当前类的元类,并遍历元类的父类,只要能和cls匹配,就返回true
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
4、isKindOfClass
和isMemberOfClass
的面试题
isKindOfClass
和isMemberOfClass
的面试题,结合 isa
指针指向:
BOOL result1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL result2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL result3 = [[Person class] isKindOfClass:[Person class]];
BOOL result4 = [[Person class] isMemberOfClass:[Person class]];
BOOL result5 = [[Person class] isKindOfClass:[NSObject class]];
得出的结果为:
2022-08-03 10:19:23.659741+0800 schemeUse[2917:111755] result1 = 1
2022-08-03 10:19:23.659798+0800 schemeUse[2917:111755] result2 = 0
2022-08-03 10:19:23.659836+0800 schemeUse[2917:111755] result3 = 0
2022-08-03 10:19:23.659873+0800 schemeUse[2917:111755] result4 = 0
2022-08-03 10:19:23.659916+0800 schemeUse[2917:111755] result5 = 1