一,前言
在iOS开发过程中,我们都知道不管是什么方法的执行
,对象的创建
,以及代理
和Bloc
k的实现都离不开runtime
,所以runtime
可以说是iOS开发过程中的生命存在, 运行时 存在 动态决议
的作用,例如我们在一个类的声明中声明了相关的方法,但是并没有进行实现时,进行编译是不会有任何问题的,但是运行时就会报错,告知我们没有实现该方法。接下来我们就重点研究一下运行时为什么会只能的告诉我们没有实现该方法。
二,环境配置
首先我们在main.m 中声明一个类LGTeacher集成自NSObject,声明一个方法sayHello,再次声明一个类LGPerson继承自LGTeacher,在LGPerson中重写了父类的sayHello,以及从新声明了sayNB,
代码如下
@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
NSLog(@"666");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
此时我们在main.m 中进行创建相应的对象;并调用相应的方法:
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
[person sayNB];
此时我们对该文件进行相应的clang
命令,生成应的.cpp文件,对该文件进行查看,我们能发现在运行时runtime
对我们的所有方法进行相关的处理;这就是runtime的作用;
以上的代码在运行时编译成
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
LGTeacher *teacher = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
也就是说我们在创建对象的时候存在两种对应的关系;
- alloc 对应的是
sel_registerName("alloc")
- 方法调用 对应是
sel_registerName("方法名"));
我们在控制台打印的结果是
2020-09-20 23:01:12.225822+0800 001-运行时感受[13893:304936] 666
2020-09-20 23:01:12.226333+0800 001-运行时感受[13893:304936] 666
然后我们在用runtime的形式进行创建方法,改写原来的方法调用过程;
[teacher sayHello];
objc_msgSend(teacher, sel_registerName("sayHello"));
[person sayNB];
objc_msgSend(person, sel_registerName("sayNB"));
我们能看到这两组打印结果完全是一样的
所以我们得出的结论是
[teacher sayHello]
和 objc_msgSend(teacher, sel_registerName("sayHello"));
完全等价;那么为什么会是这样的,这就是我们接下来重点研究的对象objc_msgSend
的查找流程
三,查找流程
在objc_msgSend
的方法查找过程中存在两种查找流程,一种是带缓存的,一种是不带缓存,也即是快速和慢速的两种情况,因为我们上一节学习了一个类的中的相关的cache_t
的部分,并做了相关的详细的介绍;所以里边涉及了很多方法的存储和对应的imp的存储过程。那么接下来我们分别对两个查找流程进行一个学习和分析。
1、快速查找
我们知道不管我们是何种语言实现的代码,在底层都会编译为计算机能识别的语言,也就是二进制的代码;那么计算机为什么能将我们写的代码转换为二进制语言了,这就是计算机最高效的一种语言汇编
,这就是计算机能快速识别我们代码的根本原因,那么我们就对汇编查找流程进行一个分析和学习吧;
首先引入一个相关汇编指令介绍
我们打开开源代码0bjc781
,进行编译过后进入相关的汇编代码;进入汇编文件
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
汇编解析:
- 1
ENTRY _objc_msgSend
进入汇编进行方法查找入口; - 2
NoFrame
进入无窗口调试模式; - 3
cmp p0, #0
进行判断类是否是空;如果是空这进入LReturnZero
,否则进入LNilOrTagged
- 4
ldr p13, [x0]
将[x0]
中的信息读取到p13
即将isa赋值给 p13, p13= 该类的isa - 5 获取isa 进行关联类对象,相当于
alloc
流程中的initWithIsa
- 6
CacheLookup NORMAL, _objc_msgSend
获取完isa 后,进行正常的消息转发过程;
CacheLookup
的定义是什么? 所以带着刨根问底的理念全局搜索这个关键字,
CacheLookup
的查找有三种格式
- 1 NORMAL
- 2 GETIMP
- 3 LOOKUP
接下来我们着重分析一下 NORMAL 其他两种情况剩余时间再去学习总结,也算是一个学习的过程
我们找到相关的CacheLookup
定义 ;代码如下
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
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 p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
.endmacro
汇编分析
- 1
ldr p11, [x16, #CACHE]
我们找到#CACHE
的定义是
#define CACHE (2 * __SIZEOF_POINTER__)
而 __SIZEOF_POINTER__
我们都知道是8
,所以 #CACHE
是16;
根据汇编语句也就是 ldr p11, [x16, 16]
也就是 x16
平移16位也就是顺着ISA平移16位到Cache_t的位置,也就是正如备注说的 p11 = mask|buckets
也就相当于上次文章中介绍的找到相关的buckets的索引位置;index
的内部结构
- 2
p10, p11, #0x0000ffffffffffff
通过掩码找出取出缓存中的buckets 也就是p11 = mask|buckets
; - 3
and p12, p1, p11, LSR #48
因为p11, LSR #48
就相当于
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) -
取出mask 在和p12
进行按位与操作
也就是x12 = _cmd & mask
- 4
add p12, p10, p12, LSL #(1+PTRSHIFT)
因为p12, LSL #(1+PTRSHIFT)
我们找到PTRSHIFT
的定义如下
#define PTRSHIFT 3
也就是p12, LSL #4
也就是 p12
左移4位也就是 左移16; p12 = *(buckets + index) *16 也就是找到相应位置的bucket;
5
ldp p17, p9, [x12] // {imp, sel} = *bucket
取出第四步骤中取出的buckect中的sel
和imp
6 拿到相应的Sel 和IMP 和我们调用的方法进行对比,如果没有找到则跳转到
2
找到那么就进入CacheHit
定义
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, 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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
- 7 再次没查找到的时候向前查找所有的buckets,如果都没找到,则进入
CheckMiss
:定义
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
这就是整个快速查找过程,
2、慢速查找
在此过程中我们知道调用方法就是获取某个方法的IMP
.所以我们找到项目的 class_getMethodImplementation
方法,在详细研究相关的流程情况,代码定义如下
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
再次寻根究底的探索相关的获取imp
的重点方法lookUpImpOrNil
;再次进入
static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}
再次顺腾摸瓜进入 lookUpImpOrForward
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
if (slowpath((curClass = curClass->superclass) == 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;
}
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;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
代码解析过程;
- 1 首先从缓存中取出相应的
imp
,我们知道快速查找流程就是没找到相应的缓存;所以此处不可能存在imp
.所以直接将IMP设置为nil
; - 2 再次检查是否是我们当前的类对象;
- 3 如果当前类是否是运行相应的查找行为权限,如果有就继续查找;
- 4 如果当前类中没找到相应的方法,继续从父类方法列表中去查找,直到找到NSObject 为止,
- 5 如果找到;那么就将该方法列表缓存起来,为了下次能快速的查找,
- 6 如果慢速都没找到,直到返回nil的时候,那么就要进行动态方法解析。
这就是所有慢速查找流程的核心,通过上述流程就能体现objec_msgSend的流程执行。
四,总结;
慢速查找的原理就是在c/c++层面去进行,只是做的事情是找到就进行缓存操作,也是反复的递归找出我们所需的方法实现imp ,如果没找到包括快速和慢速都没有,那么后期将会继续判断是否允许动态解析,方法决议等,如果不允许,则程序就会报错,告知我们这个方法没有实现,这就是这个objc_msgSend的流程,