承接文章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进行比较,判断消息接收者是否为nil
*/
cmp p0, #0 // nil check and tagged pointer check
/**
tagged pointer:为了节省内存和提高执行效率,苹果提出的概念,感兴趣可自行查阅。
如果支持tagged pointer,就会进入b.le LNilOrTagged,否则return 空。
*/
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
/**
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;
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
“:
.macro ExtractISA
and $0, $1, #ISA_MASK
复制代码
看到这里,隐约间有了这么一个概念:objc_msgSend通过消息接收者,去获取它的Class类。
那么为什么要这么做呢?首先,我们知道objc_msgSend之后,会有一个cache_t里面的insert方法,而cache_t存在于Class类里面。