探索消息流程
- Runtime介绍
- `OC`三大核心动态特性
- 动态类型
- 动态绑定
- 动态语言
- 方法的本质
- 代码转换
- `objc_msgSend`
- SEL
- IMP
- Method
- 父类方法在子类中的实现
- 消息查找流程
- 开始查找
- 快速查找流程
- 慢速查找流程
- 二分查找方法列表
- 父类缓存查找
- 动态方法解析
- 动态方法决议
- 实例方法
- 类方法
- 优化
- 消息转发机制
- 快速转发流程
- 慢速转发
Runtime介绍
runtime
是一套API,由C、C++、汇编一起写成的,为OC提供运行时,区别于编译时:
- 运行时是指在代码跑起来,被装载到内存中的一个过程;在这个时候如果出错的话,程序就会崩溃,这是一个动态阶段
- 编译时是源代码翻译成机器能识别的代码的一个过程,这个时候主要是对语言进行最基本的检查报错,即词法分析、语法分析等,这是一个静态的阶段
runtime
的使用有以下三种方式,三种实现方法与编译层和底层的关系如图所示:
- 通过OC代码,例如
[person sayNB]
- 通过NSObject方法,例如
isKindOfClass
- 通过Runtime API,例如
class_getInstanceSize
其中的compiler
就是我们了解的编译器
,即LLVM
,例如OC的alloc
对应底层的objc_alloc
, runtime system libarary
就是底层库
。
OC
三大核心动态特性
动态类型
OC中的对象是动态类型的,这就意味着在运行的时候可以发送消息给对象,而后对象可以根据收到的消息来执行相应的方法。与静态语言类型不同,静态类型在编译时就必须要确定引用哪种对象,而动态类型则更加广泛。
id someObject = [[NSString alloc] initWithString:@"hello"];
someObject = [[NSData alloc] init];//运行时someObject的类型转换成了NSDate
动态绑定
动态绑定是指方法调用可以在运行时解析,而不是在编译时。这就意味着OC运行时决定要执行对象的哪个方法,而不是在编译时。这种机制是通过消息传递实现的,这使得可以在程序运行期间改变对象的调用方法。
动态语言
OC被称为动态语言的一个核心点就是消息转发机制,消息转发机制允许开发者截取并处理未被对象识别的消息。这使得即使某个方法或者函数没有被实现,编译的时候也不会报错,因为在运行时还可以动态的添加方法。
方法的本质
这里我们先使用Clang将mian.m
编译成cpp文件
CJLPerson* person = [CJLPerson alloc];
CJLTeacher* teacher = [CJLTeacher alloc];
[person sayHello];
//使用Clang编译
CJLPerson* person = ((CJLPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLPerson"), sel_registerName("alloc"));
CJLTeacher* teacher = ((CJLTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
通过上述代码我们可以看出,方法的本质就是objc_msgSend
函数给对象发送消息。
代码转换
((CJLTeacher *(*)(id, SEL))
是类型强转,将objc_msgSend
强制转换为特定返回类型和参数的函数指针((id)objc_getClass("CJLTeacher")
是获取CJLTeacher
对象sel_registerName("alloc"))
等同于@selector()
objc_msgSend
函数原型:
id objc_msgSend(id self, SEL op, ...);
SEL
在源码中,其定义如下所示:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
SEL
一个不透明的类型,代表方法选择子,定义如下所示:
// GNU OC 中的 objc_selector
struct objc_selector {
void *sel_id;
const char *sel_types;
};
实际上,SEL就是一个方法选择器,他负责告诉编译器当前我们想要调用哪一个方法:
在运行时,方法选择器用来表示方法的名字,一个方法选择器就预示一个C字符串,在OC运行的时候被注册,编译器生成选择器在类加载时由运行时自动映射。
可以在运行时 添加新的选择器,并使用sel_registerName
函数检索现有的选择器。
获取SEL的三种方式:
SEL selA = @selector(study);
SEL selB = sel_registerName(@"study");
SEL selC = NSSelectorFromString(@"study");
注意:
OC在编译时会根据方法名字生成唯一一个区分的ID,这个ID是SEL类型的,只要方法名字相同,SEL返回就相同
在Runtime
中维护一个SEL的表,该表按照NSSet
来存储,只要相同的SEL就会被看作是同一个方法并且被加载到表中,故而OC中需要避免方法重载
IMP
指向方法实现的首地址的指针,这里我们可以看到一个他的定义:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP的数据类型是指针,指向方法实现开始的位置
Method
这是一个不透明的类型,表示类中定义的方法,定义如下所示:
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; //表示方法名的字符串
char * _Nullable method_types OBJC2_UNAVAILABLE; //char* 类型的,表示方法的类型,包含返回值和参数的类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; //IMP类型,指向方法实现地址的指针
} OBJC2_UNAVAILABLE;
这里我们可以看出Method
是一个结构体类型指针
当向对象发送消息的时候,调用
SEL
在对象的类以及父类方法列表中进行查找Method
,由于Method
结构体中包含IMP
指针,故而一旦找到对应的Method
就直接调用IMP
去实现方法
父类方法在子类中的实现
这里我们定义两个类,一个类继承于另一个类
-(void)study {
NSLog(@"%@ say: %s",[self class], __func__);
NSLog(@"%@ say: %s",[super class], __func__);
// [super study];
}
我们使用Clang
将这段代码编译成cpp
代码:
static void _I_MyTeacher_study(MyTeacher * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_07_67glw_2n3csgg41v0tztm7w00000gn_T_MyTeacher_a75b57_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")), __func__);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_07_67glw_2n3csgg41v0tztm7w00000gn_T_MyTeacher_a75b57_mi_1,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyTeacher"))}, sel_registerName("class")), __func__);
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyTeacher"))}, sel_registerName("study"));
}
我们可以发现在编译期间[super class]
转化成了objc_msgSendSuper
的方式发送消息,其中消息的接受者依旧是self
消息查找流程
开始查找
这里我们主要研究arm64结构
的汇编实现,来到objc-msg-arm64.s
,这里先给出其汇编的整体流程图:
//----消息发送 -- 汇编入口 -- objc_msgSend主要是拿到接受者的isa信息
ENTRY _objc_msgSend
//--- 流程开始,无需frame
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
GetTaggedClass
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
是否为空
- 如果支持
taggedpointer(小对象类型)
,跳转到LNilOrTagged
:- 如果小对象为空,则直接返回空,即
LReturnZero
。 - 如果小对象不为空,则处理小对象的isa,走到第二步
- 如果小对象为空,则直接返回空,即
- 如果既不是小对象,
receiver
也不为空,就有以下两步:- 从
receiver
中取出isa存入p13寄存器 - 通过
GetClassFromIsa_p16
中,arm64
架构下通过isa & ISA_MASK
获取shiftcls
位域的类信息,即class
,而后走到第二步
- 从
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
//这里用于watchOS
#if SUPPORT_INDEXED_ISA
// Indexed isa
//将isa的值存入p16寄存器
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:
//用于64位系统
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa即用于32位系统
mov p16, \src
#endif
- 获取isa结束,进入查找流程
CacheLookup
快速查找流程
.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//在64位真机
//从x16(isa)中平移16个字节,取出cache存入p11寄存器 -- isa距离cache刚好16字节
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
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
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
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 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
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
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)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets
autdb x10, x16 // auth as early as possible
#endif
// x12 = (_cmd - first_shared_cache_sel)
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...)
lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
// keep the remaining 38 bits for the IMP offset, which may need to reach
// across the shared cache. This offset needs to be shifted << 2. We did this
// to give it even more reach, given the alignment of source (the class data)
// and destination (the IMP)
ldr x17, [x10, x9, LSL #3] // x17 == (sel_offs << 38) | imp_offs
cmp x12, x17, LSR #38
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2
sub x0, x16, x17 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2
sub x17, x16, x17 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
add x16, x16, x9 // compute the fallback isa
b LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES
.endmacro
总结:
- 检查消息接受者是否存在,若为nil则不做任何的处理
- 通过
receiver
的isa指针找到对应的class对象 - 找到
class
类对象进行内存平移找到cache
,而后从其中获取buckets
- 从
buckets
中对比参数SEL
,查看缓存中有没有同名的方法 - 如果
buckets
中有对应的sel -->cacheHit
--> 调用imp
- 若在缓存中没有找到匹配的方法选择子
sel
,就执行慢速查找,调用_objc_msgSend_uncached
函数,并进一步调用_lookUpImpOrForward
函数进行全局方法查找
消息转发会先通过缓存进行查找方法实现,如果在缓存中没有找到方法实现,就会进入慢速查找过程,在类的方法列表以及父类链中进行循环查找
慢速查找流程
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//定义的消息转发
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
//检查当前线程是否未持有runtimeLock锁
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
//加锁,目的是保证读取线程安全
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
//判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类
checkIsKnownClass(cls);
//判断类是否实现,如果没有,需要先实现;目的是为了确定父类链,方法后续的循环
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
//----查找类的缓存
// unreasonableClassCount -- 表示类的迭代的上限,防止出现无限循环
//(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限范围内,所以可以继续递归)
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {//检查当前类的方法缓存是否是预优化常量
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
//---当前类方法列表(采用二分查找算法),若找到,则返回,将方法缓存到cache中
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {//当前类=当前类的父类,判断父类是否为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.");
}
// Superclass cache.
//--父类循环
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;
}//如果在父类中找到了forword,就停止查找,并且不缓存,首先调用此类的方法解析器
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}//在父类中找到方法,存储在cache中
}
// No implementation found. Try method resolver once.--没有找到方法实现,尝试一次方法解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//动态方法决议的控制条件,表示流程只走一次
behavior ^= LOOKUP_RESOLVER;
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
//存储到缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
//解锁
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
总结:
-
cache
缓存中进行查找,即快速查找,找到则直接返回imp
-
判断
cls
:- 类是否实现,若没有则先实现,确定父类链。这时实例化的目的是为了确定父类链、ro、以及rw等内容
- 是否初始化,若没有则进行初始化
-
进行for循环,按照类继承链或者元类继承链的顺序去查找
-
当前cls的方法列表中使用二分查找算法查找方法,若找到,就进入cache写入流程,并且返回imp,没有找到就返回nil
-
父类链中若存在循环,直接报错终止循环
-
父类缓存中查找方法没找到则返回nil,继续循环查找;找到的话,直接返回imp,执行写入流程
-
-
判断是否执行过动态方法解析:
- 若没有,执行动态方法解析
- 若执行过一次,走消息转发流程
以上就是方法慢速查找流程,下面分别详细解释一下二分查找原理和父类缓存查找详细步骤
二分查找方法列表
查找方法列表流程
getMethodNoSuper_nolock
->search_method_list_inline
->findMethodInSortedMethodList
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;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//base相当于low,count是max,probe是middle,这就是一个二分算法
for (count = list->count; count != 0; count >>= 1) {
//从首地址+下标移动到中间位置
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
//如果查找的key的keyValue等于中间位置的probeValue,就直接返回中间位置
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;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
这里笔者附图详细说明二分查找方法列表的步骤
父类缓存查找
cache_getImp
方法是通过汇编_cache_getImp实现
,传入的$0
是 GETIMP
,如下所示
- 如果
父类缓存
中找到了方法实现,则跳转至CacheHit
即命中,则直接返回imp
- 如果在
父类缓存
中,没有找到
方法实现,则跳转至CheckMiss
或者JumpMiss
,通过判断$0
跳转至LGetImpMiss
,直接返回nil
动态方法解析
当我们在使用objc_msgSend
的快速查找与慢速查找都没有找到方法实现的一个情况下,Apple官方给出了两种建议:
- 动态方法决议:慢速查找结束后,会执行一次动态方法决议
- 消息转发:若使用动态方法决议韩式没有找到实现,就会进行消息转发
动态方法决议
当慢速查找流程未找到方法实现的时候,会先尝试一次动态方法决议
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//对象 -- 类
if (! cls->isMetaClass()) {//若不是元类则直接调用对象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {//若是元类就调用类的解析方法, 类 -- 元类
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//如果方法解析中将其实现指向其他方法,则继续走方法查找流程
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
动态决议方法的意义在于在运动时动态地为为实现方法提供实现
步骤详细:
- 判断类是否为元类
- 如果是类,执行实例方法的动态方法决议
resolveInstanceMethod
- 如果是元类,执行类方法的动态方法决议
resolveClassMethod
,如果在元类中没找到或者为空,就在元类的实例方法的动态方法决议resolveInstanceMethod
中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
- 如果是类,执行实例方法的动态方法决议
- 如果动态方法决议中,将其实现指向了其他方法,则继续指定的imp,即继续慢速查找
lookUpImpOrForward
流程如下所示:
实例方法
针对实例方法的调用,当我们最后尝试动态方法决议的时候,会走到resolveInstanceMethod
这一步:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//这一步相当于发送消息前的容错处理
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);//发送resolve_sel消息
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//查找详细方法,例如sayHello
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
如下步骤所示:
- 在发送
resolveInstanceMethod
消息前,需要查找cls
类中是否有该方法的实现,通过lookUpImpOrNil
方法再次进入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法- 若没有则直接返回
- 若有,则发送
resolveInstanceMethod
消息
- 再次慢速查找实例方法的实现,通过
lookUpImpOrNil
方法进入lookUpImpOrForward
慢速查找流程查找实例方法
崩溃修改:
针对实例方法sayHello未实现的报错崩溃,可以通过类中重写resolveInstanceMethod
类方法,将其指向其他方法的实现,在CJLPerson
中重写resolveInstanceMethod
将实例方法sayHello的实现指向say666方法实现就可以了,如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHello)) {
NSLog(@"%@哦no", NSStringFromSelector(sel));
//获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(say666));
//获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(say666));
//获取sayMaster的丰富签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveClassMethod:sel];
}
这样就可以打印正确的结果了
类方法
对于类方法而言,与实例方法其实是相同的,同样通过重写resolveClassMethod
类方法来解决问题,下面代码举例:
+ (BOOL)resolveClassMethod:(SEL)sel:(SEL)sel {
if (sel == @selector(sayMethod)) {
NSLog(@"%@哦no", NSStringFromSelector(sel));
//获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(objc_getMetaClass("CJLPerson"), @selector(say666));
//获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(objc_getMetaClass("CJLPerson"), @selector(say666));
//获取sayMaster的丰富签名
const char *type = method_getTypeEncoding(say666);
//将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveClassMethod:sel];
}
这里我们需要注意的是,在resolveClassMethod
中传入cls不是类,而是元类,可以通过objc_getMetaClass
来获取类的元类,这时由于类方法在元类中是实例方法
优化
我们通过方法慢速查找流程可以方法查找的两条路径:
- 实例方法:类 – 父类 – 根类 – nil
- 类方法:元类 – 根元类 – 根类 – nil
看这两者的路径,我们可以发现他们都会在跟类即NSObject中来查找,所以我们是不是可以将上述的两个方法统一整合,由于类方法在元类中是实例方法,这里我们就可以使用resolveInstanceMethod
来实现:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(sayNB)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return NO;
}
这个方法的实现,与源码中针对类方法的处理逻辑是一致的,完美的阐述了为什么调用类方法动态方法决议,还要调用对象方法动态方法决议,原因是因为类方法在元类中的实例方法。
当然,上面这种写法还是会有其他的问题,比如
系统方法也会被更改
,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀
,根据前缀来判断是否是自定义方法,然后统一处理自定义方法
,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理
,提升用户的体验。
消息转发机制
快速转发流程
若是在动态方法解析中仍没有找到方法实现,那么就会进入消息转发中快速转发(消息接受者替换),给开发者一个机会返回一个能够响应该方法的对象,该方法签名如下所示:
- (id)forwardingTargetForSelector:(SEL)aSelector;
这里我们需要返回一个实现了该方法的对象,使该对象能接收并且处理该消息,返回的对象用于接收消息,并且执行对应的方法。若是返回nil,则进入慢速转发
// 备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(say666)) {
return [CJLTeacher new];
}
return [super forwardingTargetForSelector:aSelector];
}
慢速转发
若是快速转发中还是没有找到,就会进入最后一次挽救机会,在CJLPerson
中重写methodSignatureForSelector
,如下所示:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayBye)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 返回方法签名
}
return [super methodSignatureForSelector:aSelector];
}
// 消息转发逻辑
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
// 检查备用对象是否响应方法
if ([self.backupObject respondsToSelector:selector]) {
[anInvocation invokeWithTarget:self.backupObject];
} else {
// 调用父类默认处理(抛出异常)
[super forwardInvocation:anInvocation];
}
}
- 第一个方法中,为未实现的方法提供方法签名,避免运行时直接崩溃
v@:
在这里表示方法返回值类型为void
- 第二个方法中,将无法处理的消息转发给备用对象
_backupObject
- 这里我们必须要注意,这两个方法必须顺序实现,仅调用第二个方法会产生报错