引入
Objective-C程序有三种途径和运行时系统交互
1.通过 Objective-C 源代码
2.通过Foundation框架中NSObject的方法
3.通过调用运行时系统给我们提供的API接口
我们很想看一看main.m文件中经过clang编译后会变成什么东西,能否从里面得到有用的东西
使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp,由于得到的文件非常庞大,故只截取比较重要的部分来参考
这里可以知道在
Objective-C中方法的本质是objc_msgSend,并且默认带两个参数,第一个是方法接受者,第二个是方法编号,那么是如何通过sel找到方法的实现IMP?
从这里可以知道
Objective-C对象的本质是结构体,其模板是objc_object,每个对象都有一个isa。
方法的调用
方法的调用这里分为三种:实例调用方法、类调用方法、父类调用方法
实例调用方法objc_msgSend(p, @selector(run));类调用方法 1.用指针的方法
id class = [Person class];
void *pointClass = &class;
[(__bridge id)pointClass walk];
2.消息发送
objc_msgSend(objc_getClass("Person"), @selector(walk));
父类调用实例方法
Student *s = [Student new];
struct objc_super mySuper;
mySuper.receiver = s;
mySuper.super_class = class_getSuperclass([s class]);
objc_msgSendSuper(&mySuper, @selector(run));
父类调用类方法
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([Student class]));
objc_msgSendSuper(&myClassSuper, @selector(walk));
之前的文章有分析过,这里直接说结论:实例方法存储于类中,类方法存储于元类中同样以实例方法的形式存在。那么发送消息的流程是如何呢?
方法的查找
objc_msgSend的两种方式
- 快速的方式:通过查找
缓存中的汇编 - 慢速的方式:
C、C++配合汇编一起查找
cache中包含sel和IMP的缓存,会去查找一张由sel和IMP组成的哈希表,如果能直接从里面找到就会直接返回,速度很快,但是如果缓存中没有的话,就会走入慢速的方式,并且找到后会存入这张哈希表中。
objc_msgSend
直接在arm-64的汇编文件中 查找objc_msgSend
1.
LNilOrTagged这步在判断当前对象是否为Tagged-Point对象,此处先不对这个做解释
2.LGetIsaDone对Isa进行处理
3.CacheLookup NORMAL缓存中查找IMP
这里可以看出CacheLookup中又可以分为三种情况来讨论,既然是在缓存中查找无非是找的到或者找不到
CacheLookup
CacheHit
在缓存中命中
此时会直接call IMP
CheckMiss
在缓存中未曾找到
因为参数是
NORMAL则会走入__objc_msgSend_uncached
MethodTableLookup在方法列表中查找到后,直接调用函数指针
这里会从汇编直接回到C,以上是大致的流程分析,之后会对快速查找流程及慢速查找流程做出详细分析。
CacheLookup快速查找流程
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 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$1,
// then our PC will be reset to LLookupRecover$1 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
//
LLookupStart$1:
// p1 = SEL, p16 = isa
/*
找到定义 #define CACHE (2 * __SIZEOF_POINTER__)
p1 = SEL
p16 = isa
isa偏移16个字节正好是cache
将 cache 存入 p11
cache(mask高16位 + buckets低48位)
*/
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/*
p11 存储 cache
高16位置0,得到 buckets 存入 p10
*/
and p10, p11, #0x0000ffffffffffff // p10 = buckets
/*
p11 存储 cache
右移48位得到mask
p1 存储 SEL
_cmd & mask得到sel-imp的下标index(即搜索下标) 结果存入 p12
*/
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
/*
p12 存储 搜索下标
p10 存储 buckets首地址
LSL #(1+PTRSHIFT) 就是得到一个bucket占用的内存大小
左移4位,相当于*16,一个bucket占有的内存16
通过下标找到对应bucket存入 p12
*/
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
/*
p12 存储 查找到的bucket
p17 存储 imp
p9 存储 sel
*/
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 比较 sel 与 p1(传入的参数cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 如果不相等跳转到 2f
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
/*
设置到最后一个bucket
p12 buckets首地址
p11 mask 右移44位,相当于mask*16,再用buckets首地址偏移,得到最后一个bucket
*/
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)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
/*
p12 存储 最后一个bucket
p17 存储 imp
p9 存储 sel
*/
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
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
整体流程大致分为以下几步
第一步
找到定义 #define CACHE (2 * SIZEOF_POINTER)
p1 = SEL
p16 = isa
isa偏移16个字节正好是cache
将 cache 存入 p11
cache(mask高16位 + buckets低48位)
第二步
从p11中即cache中取出mask和buckets
- 由于
cache中的结构是mask占高16位,buckets在低48位,故采用与0x0000ffffffffffff进行与运算清除高16位,直接得到buckets存入p10 - 同样将
cache右移48位直接得到高16位的mask,将p1中的sel与mask进行与运算得到sel-imp的下标index(即搜索下标)结果存入p12,至于为什么要这样运算,因为存储时运用了同样的算法。
第三步
根据所得的搜索下标从找到buckets找到对应的bucket
-PTRSHIFT为3,则1+PTRSHIFT为4,对p12即搜索下标左移4位,其实就是对应下标*16字节,那么一个bucket占用16字节空间,从buckets的首地址偏移即可得到对应的bucket
第四步
根据获取的bucket,取出其中的imp存入p17,取出sel存入p9
第五步进入第一层递归循环查找
-
比较查找到的
bucket中的sel与objc_msgSend中的参数_cmd是否相等 -
如果相等则命中,走入
CacheHit,返回IMP -
若不相等,将再次分为两种情况来讨论
-
如果一直找不到会走入
CheckMiss,并且由于参数是NORMAL,会跳转到__objc_msgSend_uncached,至此进入慢速查找流程
-
如果
bucket为buckets的首地址,即第一个bucket,就会通过p11中的mask右移44位,相当于mask*16,再用buckets首地址偏移,得到最后一个bucket,至此开始进行第二层的递归循环查找。 -
如果
bucket不是buckets的首个元素,则持续向前查找,走入第一层的的递归循环查找。
第六步进入第二层递归循环查找
-
比较查找到的
bucket中的sel与objc_msgSend中的参数_cmd是否相等 -
如果相等则命中
CacheHit,返回IMP -
如果不相等会一直向前偏移查找,直至
bucket是buckets的首个元素,直接跳转至JumpMiss,同样跳转至__objc_msgSend_uncached,参数NORMAL
到这里快速查找流程分析完成,上面也分析到如果快速查找流程都没有找到的话,都会跳转至__objc_msgSend_uncached这里
MethodTableLookup在方法列表中查找到后,直接调用函数指针
从这里开始就将从汇编部分跳转至C、C++,也就是我们熟悉的地方
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 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.
//
// TODO: this check is quite costly during process startup.
/*
保证这个类是已经加载过的类
*/
checkIsKnownClass(cls);
/*
判断类是否实现了,若没有则要实现,这里是确保父类链方法的调用
*/
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
/*
判断类是否初始化,如果没有要先初始化
*/
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookpu 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().
//查找类的缓存
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
//当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
//当前类 = 当前类的父类,并判断父类是否为nil
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.
// 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
//如果在父类中,找到了此方法,将其存储到cache中
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;
}
同样这里也分为以下几步
第一步
在cache中进行查找,如果找到直接返回,如果没有就进入慢速查找流程,其实这里会有疑问,因为走到这里的时候是已经进入了慢速查找流程,为什么还会去cache查找?其实这里是一个优化的地方,为的是防止在多线程的操作时,调用到此方法时,缓存已经有了就不必进入慢速查找流程了。
第二步先对cls进行判断
-
判断
cls是否为已知类,即保证cls是已经加载过的类 -
判断类是否实现了,若没有则要实现,这里是确保父类链方法的调用,这里实现的目的是为了确定父类链、
ro、以及rw等,方法后续数据的读取以及查找的循环 -
判断类是否初始化,如果没有要先初始化
第三步
开始循环查找,依据类循环链(类--->父类--->根类--->nil)以及元类循环链(元类--->根源类--->根类--->nil)开始循环查找
-
当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到
cache中 -
当前类 = 当前类的父类,并判断父类是否为nil,如果为nil,imp = forward_imp,进行消息转发(第四步),并结束循环 -
如果父类链中存在循环,则停止
-
在父类缓存中查找
- 如果找到,返回
imp,并写入cache - 如果在父类中找到了
forward,则停止查找,且不缓存,首先调用此类的方法解析器 - 如果未找到,则继续进入循环查找
第四步
判断是否执行过动态方法解析,如果没有就执行一次动态方法解析,如果已经执行过动态方法解析,则进行消息转发流程。
总结
实例方法在慢速查找的过程中,是在类中查找,父类链为类--父类--根类--nil类方法在慢速查找的过程中,是在元类中查找,父类链为元类--根源类--根类--nil- 如果
快速查找、慢速查找也没有找到方法实现,则尝试动态方法决议 - 若
动态方法决议仍然没有找到,则进行消息转发