承接文章iOS底层分析之类的探究-cache篇继续:
前面我们说了cache方法缓存数据通过insert方法来插入的,但从始自终,这都是我们的猜测以及一些推导,我们并没有看到明确的流程走向,接下来我们借助objc源码,查看整个cache从写入到读取的构成,了解整个cache的完整链走向。由于我们目前的切入点只有insert方法,我们也不知道到底是谁调用了insert,所以我们干脆在insert方法打个断点,然后通过bt指令打印堆栈信息:
我们发现了一个跟cache有关系的方法,复制并项目里全局搜索一下“log_and_fill_cache
”,看看这个方法是不是我们insert的上一层调用者:
果不其然,通过comand+单机
insert
可以确定,log_and_fill_cache就是insert的上层调用者,接下来再搜一下“log_and_fill_cache”找到它的上层调用者:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
log_and_fill_cache(cls, imp, sel, inst, curClass);
...
};
lookUpImpOrForward这个里面就涉及到消息的转发知识点,在那之前我们要先知道,我们平时发起消息,有三种方法:oc层面的发起、runtime API层面的发起、framework层面的发起,
针对以上三种,下面我们来一一测试下:
第一种:
void test02(void){
Student *stu = [Student alloc];
[stu run];//实例方法
}
int main(int argc, char * argv[]) {
test02();
...
};
首先,终端或iTerm里cd到main.m所在目录;然后输入以下命令
$xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
目的是在目录下生成一个main.cpp文件,然后我们打开它,全局搜索“test02(void)
”:
void test02(void){
Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("run"));
}
我们可以看到[Student alloc]
在底层变成了
((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc"))
分析可以得到,方法在底层的调用是通过objc_msgSend进行消息的发送,而它需要两个参数:
1、消息接收者
2、方法名
我们有时候会将一些通用的方法,写在父类里,子类在需要的时候直接调用父类方法就好了,又或者调用一些在子类声明却未实现,但在父类里有实现的方法,又会是什么情况呢?
@interface StudentChild:Student
- (void)run;
- (void)sleep;
- (void)SwimmingWithSpeed:(NSInteger)speed sex:(NSInteger)sex;
+ (void)eat;
@end
@implementation StudentChild
- (void)sleep{
NSLog(@"StudentChild--sleep");
}
- (void)SwimmingWithSpeed:(NSInteger)speed sex:(NSInteger)sex{
NSLog(@"Swimming Speed is %ld ;sex is %@",(long)speed,sex==0?@"boy":@"girl");
}
@end
void test03(void){
StudentChild *stuChild = [StudentChild alloc];
[stuChild SwimmingWithSpeed:100 sex:0];//实例方法, 有声明 已经实现
[stuChild run];//实例方法, 有声明 未实现
[stuChild dump];//实例方法 未声明 未实现
[StudentChild eat];//类方法 有声明 未实现
}
打印结果如下:
我们执行同上面一样的xcrun
命令,可能会报警高,因为我们没有在StudentChild里实现方法提,暂时不必理会警告,我们继续查看main.cpp文件:
void test03(void){
StudentChild *stuChild = ((StudentChild *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("StudentChild"), sel_registerName("alloc"));
((void (*)(id, SEL, NSInteger, NSInteger))(void *)objc_msgSend)((id)stuChild, sel_registerName("SwimmingWithSpeed:sex:"), (NSInteger)100, (NSInteger)0);
((void (*)(id, SEL))(void *)objc_msgSend)((id)stuChild, sel_registerName("run"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stuChild, sel_registerName("dump"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("StudentChild"), sel_registerName("eat"));
}
分析得出,方法的调用依旧是objc_msgSend消息发送,内部参数按照:接收者、方法名、参数1、参数2...依次追加。
第二种
既然我们已经知道了方法的发送
在底层是消息发送
,那么我们何不直接调用objc_msgSend
发送消息,来模拟方法的调用呢?
记得引入头文件:#import <objc/message.h>
void test04(void){
StudentChild *stuChild = [StudentChild alloc];
objc_msgSend(stuChild,sel_registerName("run"));
objc_msgSend(stuChild,sel_registerName("SwimmingWithSpeed:sex:"),98,1);
}
如果你们没有对工程进行设置,应该会编译报错:
Too many arguments to function call, expected 0, have 2
需要设置一下
target > Build Settings > Enable Strict Checking of objc_msgSend Calls
修改为 No
,默认情况下Yes.
运行结果如下:
第三种
void test05(void){
StudentChild *stuChild = [StudentChild alloc];
objc_msgSend(stuChild,@selector(run));
objc_msgSend(stuChild,@selector(SwimmingWithSpeed:sex:),66,1);
}
我们在查看main.cpp文件,发现除了objc_msgSend方法,还有一个objc_msgSendSuper方法,顾名思义是给父类发送方法,那么我们也来测试下吧:
//找到objc_msgSendSuper定义,发现有两个参数objc_super结构体指针,以及SEL
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
点击查看objc_super定义如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// 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 */
};
当前是OBJC2
环境,简化代码如下:
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
所以测试代码如下:
//测试objc_msgSendSuper
void test06(void){
StudentChild *stuChild = [StudentChild alloc];//实例化对象
///结构体
struct objc_super ssjobjc;
ssjobjc.receiver = stuChild;
ssjobjc.super_class = Student.class;
objc_msgSendSuper(&ssjobjc,@selector(sleep));
}
打印结果:
接下来我们看一下objc_msgSend里面是如何走的
打开
Debug->Debug workflow 选中always show Disassembly
然后运行~
会进入这个界面
由此我们得知objc_msgSend要去到objc源码去看:
按上图操作,左边的方法列表就被折叠起来了。我们知道,objc_msgSend
是用汇编
写的,所以需要关注后缀是.s
的几个文件,又因为我们主要研究的环境是iPhone真机,所以我们需要关注的是arm64
相关名字的.s文件,我们发现还是不能定位哪个objc_msgSend方法,但是我们留意到有个ENTRY _objc_msgSend
没错,这里就是_objc_msgSend相关代码,我们来分析一下:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
/**
cmp:compare,比较的意思
p0:p0寄存器,存放的是第一个参数,也就是消息接收者
#0:常量0
意思就是p0和0进行比较,判断消息接收者是否为空(0就表示空)
*/
cmp p0, #0 // nil check and tagged pointer check
/**
tagged pointer:为了节省内存和提高执行效率,苹果提出的概念,感兴趣可自行查阅。
当p0为空,如果支持tagged pointer,就会进入b.le LNilOrTagged,否则return 空。
*/
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
/** p0不为空就进入这里
ldr p13, [x0] 解释如下:
从x0开始读取,把它对应的执行码给p13;
x0即消息接收者,即消息接收者首地址,即消息接收者的isa.
*/
ldr p13, [x0] // p13 = isa
//关于GetClassFromIsa_p16,请继续往下看~
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
当前文件搜索“GetClassFromIsa_p16
”,找到一个宏定义:
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
//---- 此处SUPPORT_INDEXED_ISA用于watchOS,不用看它
#if SUPPORT_INDEXED_ISA
...
/**我们分析一下__LP64__*/
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
/**
1、前面调用 GetClassFromIsa_p16 p13, 1, x0,所以
src存放的就是isa,
needs_auth存放的就是1,needs_auth == 0不成立,这个if判断可以不用看了;
2、mov p16, \src的意思就是把src传给p16,所以p16就是isa;
*/
mov p16, \src
.else
// 64-bit packed isa
/**
ExtractISA p16, \src, \auth_address根据ExtractISA定义,可以理解为:
and $0, $1, #ISA_MASK
也就是把$1与上ISA_MASK地址,结构给$0
即p16 = isa & ISA_MASK,也就是p16存的是Class地址
*/
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
objc搜索"ExtractISA
":
解读:
__has_feature
:判断编译器是否支持某个功能。ptrauth_calls
:指针身份验证,针对 arm64e 架构;使用 Apple A12 或更高版本 A 系列处理器的设备(如 iPhone XS以后的设备)支持 arm64e 架构。 a12它并非普遍版本,所以我们要看下面那个:
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
快速查找分析
看到这里,隐约间有了这么一个概念:objc_msgSend通过消息接收者,去获取它的Class类。
那么为什么要这么做呢?首先,我们知道objc_msgSend之后,会有一个cache_t里面的insert方法,而cache_t存在于Class类里面。
我们继续看上面的CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
文件全局搜索“CacheLookup
”:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
///把x16传给x15,x16就是p16,也就是class地址
mov x15, x16
...
//只需要关心iPhone真机模式即可
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/**
ldr p11, [x16, #CACHE]意思是x16平移CACHE大小个单位
CACHE全局搜素可以看到是:#define CACHE (2 * __SIZEOF_POINTER__),即16字节大小;
x16是Class地址,平移16字节就是cache_t地址;
p11 = cache_t地址
*/
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//CONFIG_USE_PREOPT_CACHES的解释看下面,反正等于1,会进入if语句块
#if CONFIG_USE_PREOPT_CACHES
//__has_feature(ptrauth_calls)上面解释过,A12及以后才会进入,直接看else
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
/** 看这里
and p10, p11, #0x0000fffffffffffe意思是
p11与上48位,得到bucket地址,然后传给p10
*/
and p10, p11, #0x0000fffffffffffe // p10 = buckets
///tbnz是比较运算:p11存在,就走LLookupPreopt;不存在就走下面的endif
tbnz p11, #0, LLookupPreopt\Function
#endif
/**
关于为什么要右移7位,再右移48位,下面会有说明
*/
//p1右移7个位置,然后传给p12(p1就是SEL)
eor p12, p1, p1, LSR #7
//p11右移48个位置得到mask地址,然后与上p12得到sel-imp的index下标并传给p12;
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
...
#endif
/**
PTRSHIFT项目全局搜索发现等于3;
p10是buckets
p12是下标
解释:p13 = p10+(p12左移4个单位,即内存平移4个单位),即insert函数里的b[i]
【下文有图解“buckets首地址平移得到index下标对应bucket”】
*/
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//b[i]平移-bucket单位大小,得到b[i]里的内容,
//并将sel赋值给p9,imp赋值给p17;
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//如果p9对应的sel和p1对应的sel一样,一样就进入2对应的那块代码,否则就进入3对应代码
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
//能进入语句块2说明cache缓存找到,CacheHit相关代码看下面,mode参数就是前面的normal
2: CacheHit \Mode // hit: call or return imp
// }
//判断p9对应的sel是否为空,为空就p13和p10(buckets首地址)地址进行比较,
//如果p13地址大于p10地址,就回到1语句块。
//这个循环会一直持续,直到对应的缓存找到。
//如果最终都没找到,就会进入MissLabelDynamic,MissLabelDynamic就是CacheLookup函数的第三个参数,也就是__objc_msgSend_uncached
//关于__objc_msgSend_uncached的分析,下面会讲到;
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
...
.endmacro
全局搜索“CacheHit
”, 项目全局搜索"TailCallCachedImp
"
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
... // return imp via x17
.else
.abort oops
.endif
.endmacro
//TailCallCachedImp搜索结果不止一个,具体看宏定义if else判断
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
//$3相当于一个Class,$0 ^ $3,就得到了imp
eor $0, $0, $3
///跳转执行imp
br $0
.endmacro
项目全局搜索“CONFIG_USE_PREOPT_CACHES
”:
/**
defined(__arm64__) 是否支持arm64
TARGET_OS_IOS 是否支持iOS系统
TARGET_OS_SIMULATOR 是否MacOS模拟器
TARGET_OS_MACCATALYST 是否兼容MacOS代码
所以if语句,判断条件就是 arm64环境+iOS系统+非MacOS模拟器+不兼容MacOS代码,
也就是iOS系统arm64下的真机模式
*/
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
慢速查找分析 __objc_msgSend_uncached
当前objc-msg-arm64.s文件全局搜索“__objc_msgSend_uncached
”:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup //方法列表查找
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
文件全局搜索“MethodTableLookup
”:
.macro MethodTableLookup
SAVE_REGS MSGSEND
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
/**
MethodTableLookup函数就是做了数据的move赋值,还调用了_lookUpImpOrForward函数;
lookUpImpOrForward,顾名思义是查找imp,再结合下面x0,我们知道x0寄存器亦可用于存放返回值。
*/
我们文件内搜索_lookUpImpOrForward
找不到,改为搜索“lookUpImpOrForward
”:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
...
checkIsKnownClass(cls);//检查类是否已经注册,已经注册类才会继续查找方法
...
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.
//getMethodNoSuper_nolock -> 二分法查找方法
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//找到了方法就找到了imp,然后goto done跳出死循环,
//往下翻一点,可以看到done函数
imp = meth->imp(false);
goto done;
}
/**查询当前类的父类并赋值给curClass,
如果curClass为空,则给imp默认赋值forward_imp并跳出循环。
*/
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.");
}
// Superclass cache.
/**进入父类的快速查找->慢速查找流程,
父类找不到就继续找父类的上层父类,递归查找,直到curClass为空*/
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;
}
}
//看这里
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);
}
return imp;
};
二分法查找
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
//list是class的data里的methods方法列表,可以查看getMethodNoSuper_nolock函数内部的调用
ASSERT(list);
auto first = list->begin();
//这里base是第一个元素
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//count是list方法列表的个数
for (count = list->count; count != 0; count >>= 1) {
//count >> 1 就是 count / 2
//probe = probe +
probe = base + (count >> 1);
///获取probe对应的名字,也就是sel方法名
uintptr_t probeValue = (uintptr_t)getName(probe);
//判断查找的cmd等于sel
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
/**因为分类的重写方法在内存中,顺序是插入在类的方法之前,
所以如果前面一个sel与之也是匹配,就返回前面一个sel
*/
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
///走到这里,说明前面一个sel没匹配上,那就返回当前这个sel
return &*probe;
}
//如果keyValue比probeValue大,base就+1
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
假设我们要找的sel别名是6,如图进入查找:
下面解释,为什么p11与上#0x0000fffffffffffe等于bucket地址:
#0x0000fffffffffffe用计算器可以算出刚好等于48
struct cache_t {
// _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
// _maybeMask is unused, the mask is stored in the top 16 bits.
// How much the mask is shifted by.
//翻译:
//_bucketsAndMaybeMask是低48位的buckets_t指针
//_maybeMask未使用,mask存储在高16位
//面具移动了多少
static constexpr uintptr_t maskShift = 48;
};
//所以,cache_t地址平移48位就得到了bucket地址
下面解释为什么要先右移7位,再右移48位:
我们在cache_t里的insert方法里,bucket_t赋值sel和imp是通过一个下标来定位是哪个bucket,下标的获取则是通过哈希函数来得到,具体代码请看:
void cache_t::insert(SEL sel, IMP imp, id receiver){
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
};
/** 函数内部实现,sel先右移7位,然后再与上一个mask,经过哈希算法得到sel-imp下标;
mask则是通过cache_t右移48位得到,
*/
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
buckets首地址平移得到index下标对应bucket
:
简单总结
到此,我们对objc_msgSend(reciver,_cmd)
到找到imp
做一个简单总结:
- class位移16位得到cache_t;
- cache_t与上mask掩码得到mask地址,
- cache_t与上bucket掩码得到bucket地址;
- mask与上sel得到sel-imp下标index,
- 通过bucket地址内存平移,可以得到第index位置的bucket;
- bucket里面有sel、imp;然后拿bucket里的sel和msg_msgSend的_cmd参数进行比较是否相等;
- 如果相等就执行cacheHit,cacheHit里面做的就是拿到sel对应的imp,然后进行imp^Class,得到真正的imp地址,最后调用imp函数 。
- 如果不相等,就拿到bucket进行- - (减减)平移,找到下一个bucket进行比较,如果找到了就进入7,否则就继续缓存查找。如果一直找不到,就进入__objc_msgSend_uncached慢速查找函数。
- 慢速查找流程:lookUpImpOrForward二分法查找imp,找到了就写入缓存;
当前类找遍了没有,就进入递归循环:
- 再从父类开始,快速查找、慢速查找。还是没找到,就从父类的父类开始循环这一步;
- 递归结束条件是class为空,然后给imp一个默认值。
【更新时间】:2021/7/8
【更新内容】:追加->慢速查找分析