这是我参与更文挑战的第9天,活动详情查看: 更文挑战
cache拓展补充
内存平移
我们在上一篇文章中查找sel
和imp
是使用了下边这样的方式来查找数据
在$12
中无法找到数据时,我们采用了指针平移的方式,最终找到了数据:
(lldb) p $9.buckets()[4]
(bucket_t) $19 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 45536
}
}
}
复制代码
那么就有一个疑问:$9.buckets()[4]
这种取值方式我们经常是在数组中这样通过索引获取数据,那么bucket_t
是不是数组呢,我们查看源码发现,bucket_t
是一个结构体:
那么通过$9.buckets()
的打印数据中我们可以明白,$12
是一个结构体指针,针对一个结构体指针的下标操作$9.buckets()[4]
,我们称为取内存平移
。$9.buckets()[4]
等同于$12+4
。
_bucketsAndMaybeMask
解析
在之前的操作中我们有这样的数据结构:
那么,这个_bucketsAndMaybeMask
是什么东西呢?我们通过lldb
来看一下:
查看buckets()
方法的实现:
通过_bucketsAndMaybeMask
获取到地址addr
,然后和bucketsMask
进行与
操作之后,强制转换为十六进制地址
(还原原来地址)
_bucketsAndMaybeMask
存储的就是buckets()
这一段内存的首地址
cache读取流程分析
在介绍cache_t
是我们知道了方法是通过insert
操作进行插入缓存的,那么是什么时候进行的insert
操作呢,我们在insert
方法中打上断点,执行代码:
根据左边的调用栈分析,是log_and_fill_cache
调用了insert
方法进行缓存插入操作,我们看一下log_and_fill_cache
的实现:
调用栈显示,是lookUpImpOrForward
调用了log_and_fill_cache
:
这是C++
底层的消息流程
runtime的运行时理解
runtime 概述
想要理解消息流程,那么必须先了解runtime
的相关知识
编译时编译器会帮我们把源代码翻译成机器能识别的代码,或者是某个中间状态的代码。在这个过程中编译器会帮我们进行语法分析、类型检查等工作;而运行时
是我们的程序已经运行起来,被加载到内存中去的时候,此时所有的操作都是在内存中进行的。
runtime
有两个版本:
Legacy
版本为早期版本,对应Objective-C 1.0
,用于32
位的MacOS X
系统中Modern
为现行版本,对应Objective-C 2.0
,用于iPhone
和MacOS X v10.5
之后的64
位系统中
Objective-C Runtime Programming Guide
调用runtime 的三种方式
- OC的方法;比如
[peron run]
- NSObject的接口;比如
isKindoOfClass
- objc下层Api;比如
class_getInstanceSize
我们来看下边代码
@interface Teacher : NSObject
- (void)say:(NSString *)string;
- (void)run;
@end
@implementation Teacher
- (void)say:(NSString *)string {
NSLog(@"%s-->%@", __func__,string);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Teacher *teacher = [Teacher alloc];
[teacher say:@"hello"];
[teacher run];
}
return 0;
}
复制代码
类Teacher
有两个方法say:
和run
,say:
方法有实现,而run
方法没有实现;这段代码在编译时可以编译成功,但是在运行时就会崩溃,报错:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Teacher run]: unrecognized selector sent to instance 0x1007aa0a0'
terminating with uncaught exception of type NSException
复制代码
来看一下它在cpp
文件文件中的代码:
Teacher *teacher = ((Teacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Teacher"), sel_registerName("alloc"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)teacher, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_7z_qx6zs9gj2s58dv3ps88mf9pr0000gn_T_main_388285_mi_1);
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("run"));
复制代码
为方便观看,简化如下:
Teacher *teacher = ((Teacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Teacher"), sel_registerName("alloc"));
objc_msgSend)(teacher, sel_registerName("say:"), __NSConstantStringImpl__var_folders_7z_qx6zs9gj2s58dv3ps88mf9pr0000gn_T_main_388285_mi_1);
objc_msgSend(teacher, sel_registerName("run"));
复制代码
我们常用的OC代码都会在底层使用runtime
进行解释
OC方法的调用实质上是消息发送的流程
objc_msgSend(id self, SEL _cmd)
复制代码
id self
为消息的接收者SEL _cmd
为消息的主体sel
+参数
objc_msgSend和objc_msgSendSuper
明白了其本质之后,那么我们就可以直接通过objc_msgSend
实现方法调用:
需要将
Build Settings
中的Enable Strict Checking of objc_msgSend Calls
改为NO
,默认为YES
我们给Teacher
类添加一个方法talk
,并且实现:
- (void)talk;
- (void)talk {
NSLog(@"-->%s", __func__);
}
复制代码
方法调用成功
我们创建一个Person
类,让Teacher
继承自Person
类,在Person
类中实现run
方法,代码如下:
@interface Person : NSObject
@end
@implementation Person
- (void)run {
NSLog(@"-->%s", __func__);
}
@end
@interface Teacher : Person
- (void)say:(NSString *)string;
- (void)run;
- (void)talk;
@end
@implementation Teacher
- (void)say:(NSString *)string {
NSLog(@"%s-->%@", __func__,string);
}
- (void)talk {
NSLog(@"-->%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Teacher *teacher = [Teacher alloc];
[teacher say:@"hello"];
[teacher run];
}
return 0;
}
复制代码
运行项目:
没有崩溃,虽然子类Teacher
没有run
方法的实现,但是直接找到了父类Person
的run
方法
此时生成cpp
文件:
Teacher *teacher = ((Teacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Teacher"), sel_registerName("alloc"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)teacher, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_7z_qx6zs9gj2s58dv3ps88mf9pr0000gn_T_main_54da99_mi_3);
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("run"));
复制代码
cpp
文件并没有什么异常,依然是通过objc_msgSend
发送的消息,那么我们怀疑是不是objc_msgSend
在底层会去找父类的方法呢,我们在cpp
文件中发现还有一个向父类发送消息的方法objc_msgSendSuper
;从objc
源码中可以找到objc_msgSendSuper
的方法定义:
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码
他有两个参数:
- 结构体
objc_super
SEL
objc
源码中关于结构体objc_super
的定义:
由于当前环境为满足__OBJC2__
—Objective-C 2.0
,所以结构体可以简化为:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};
复制代码
我们在工程中,尝试使用objc_msgSendSuper
去调用run
方法:
解释:super_class
是第一个需要查找的类,如果找不到对应的方法,那么向上查找父类,然后查找;因为NSObject
没有run
方法,而NSObject
的父类是nil
,所以此处不能传NSObject
;
_objc_msgSend流程分析
我们来分析一下_objc_msgSend流程分析
的底层实现,它是用汇编实现的,我们在objc
的源码中去分析:
代码不多,解释如下:
_objc_msgSend
ENTRY _objc_msgSend // 汇编常用方法进入ENTRY
UNWIND _objc_msgSend, NoFrame // 常用语法
cmp p0, #0 // 寄存器p0是objc_msgSend的第一个参数`id self`,和0比较判断有没有消息接收者
#if SUPPORT_TAGGED_POINTERS // 判断条件是否支持 tagged pointer
b.le LNilOrTagged // 执行LNilOrTagged开始向下执行
#else
b.eq LReturnZero // p0 = 0,没有消息接收者,消息为空
#endif
ldr p13, [x0] // x0寄存器为消息接收者,将x0赋值给p13,p13 = 接收者的isa
GetClassFromIsa_p16 p13, 1, x0 // GetClassFromIsa_p16的解释在下方,最终结果 p16 =接收者的class
LGetIsaDone: // 指令完成 其实是一个通过receiver找class的过程,因为class中有cache
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero: // return nil
// 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结束
复制代码
GetClassFromIsa_p16
// GetClassFromIsa_p16 p13, 1, x0; src=p13=isa needs_auth=1 auth_address=x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA // 不满足直接看else
// Indexed isa
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:
#elif __LP64__ // linux MacOS X的系统 此处看else分支arm64系统
.if \needs_auth == 0 // needs_auth=1 不满足 _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address // 操作结束之后,p16是接收者的class
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
复制代码
ExtractISA
// ExtractISA p16, \src, \auth_address
.macro ExtractISA
and $0, $1, #ISA_MASK // $0=p16,$1=src=isa,解释:$1与ISA_MASK进行与操作得到的class,存到$0=p16
.endmacro
复制代码
下一章继续……