这是我参与更文挑战的第9天,活动详情查看: 更文挑战
前言
我们已经探究过方法缓存中的插入流程,今天我们继续再深入探究下方法缓存的细节及上层触发时机;
方法缓存关键细节
为什么扩容是在容量的 3/4 时进行?
- 3/4作为负载因子是大多数数据结构算法的共识,负载因子在0.75时空间的利用率是相对较大的;
- cache存入方法时是根据hash算法计算出来的值作为存储下标,缓存空间的剩余大小对下标是否冲突至关重要,当3/4作为负载因子发生hash冲突的几率相对较低;
_bucketsAndMaybeMask的作用
_bucketsAndMaybeMask里面存放的是bucket内存的首地址和bucketMask的地址; 官方解释:
// _bucketsAndMaybeMask is a buckets_t pointer;
// _maybeMask is the buckets mask
- 获取
_bucketsAndMaybeMask之后通过内存平移即可获取下一个bucket的地址; buckets()方法实际上也是对_bucketsAndMaybeMask里面的值进行操作,获取的bucket_t; 代码如下
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);//_bucketsAndMaybeMask的地址和bucketsMask进行与运算;
}
static constexpr uintptr_t bucketsMask = ~0ul;//bucketsMask的所有位都为1;
bucketMask定义在不同的架构下取值不同;
#if defined(__arm64__) && __LP64__//真机或__LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR//模拟器或Mac
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS//高16位大地址
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16//高16位
#endif
#elif defined(__arm64__) && !__LP64__//真机或不等于__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4//低4位
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED //1
#endif
buckets取值
buckets()方法实际上也是对_bucketsAndMaybeMask里面的值进行操作,获取的bucket_t;buckets()返回的数据是bucket_t类型的$n,但我们在取值的时候经常使用p $n[1]这种利用下标取值的方式,看起来bucket_t的取值很像数组的取值方式,实际上bucket()并不是数组;buckets()返回的bucket_t只是一个结构体,但存放的bucket_t时候生成的空间是一块连续的内存空间,所以只要知道了当前bucket_t的位置,就可以通过内存平移获得下一个相邻的区域的内存;- 使用
p $n[1]和p $n+1是一样的效果; - 数组也是使用内存平移的方式进行的取值,进行
+1操作时,平移单位是根据数组内部的数据类型决定的;
hash函数(cache_hash)和二次hash函数(cache_next)
- 首次hash函数
cache_hash
mask_t m = capacity - 1; // 容量-1:4-1=3
mask_t begin = cache_hash(sel, m);//根据容量-1和sel作为参数获取hash值
mask_t i = begin;
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;//将sel转为无符号长整型
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);//用sel的转换值和mask进行按位与得到结果;
}
- 二次hash函数
cache_next
cache_next(i, m)//再次哈希使用的是当前的位置和容量-1作为参数进行cache_next计算
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;//如果i不为0,返回i-1,否则返回mask(容量-1);也可以理解为判断发生冲突的位置是不是在buckets的最开头,如果不在最开头就直接前移,如果在最开头就直接跳到容量-1的位置,再依次向前,直到再次遇到一开始的begin位置,此时说明循环了一圈了还没找到空位置插,坏缓存了;
}
insert()的闭环流程
- 为了找到
insert()的插入时机,我们在insert()方法中下断点,查看调用堆栈信息; - 栈是一种
LIFO的数据结构,index = 0是最后调用的,从0开始依次查看调用信息,发现log_and_fill_cache方法调用了cache.insert方法;
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver);
}
- 查看
objc-cache.mm文件,可以根据注释看到insert()的插入时机最上层是通过objc_msgSend触发的,接下来我们就一起看下objc_msgSend;
objc_msgSend
编译时
- 编译时 顾名思义就是正在编译的时候. 编译器把源代码翻译成机器能识别的代码(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔)。编译时通过语法分析、词法分析等编译时类型检查(静态类型检查)来发现代码中的
errors或warning等编译时的错误信息. - 静态检查不会把代码放内存中运⾏起来,⽽只是当作⽂本来扫描检查,⼀些⼈说编译时还分配内存啥的肯定是错误的说法;
运行时
- 运⾏时就是代码通过dyld被装载到内存中执行的过程;运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做操作和判断.
Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)
- 早期版本对应的编程接⼝:
Objective-C 1.0 - 现⾏版本对应的编程接⼝:
Objective-C 2.0 - 早期版本⽤于
Objective-C 1.0,32位的Mac OS X的平台上 - 现⾏版本:
iPhone程序和Mac OS X v10.5及Mac OS X v10.5`以后的系统中的 64 位程序
- 关于OC语言的更多信息可以在苹果官方文档进行查阅
Runtime调用方式一共有3种
OC方法:[p sayNB];NSObject提供的API:isKindofClass...;objc的下层APIobjc_msg_send方法:class_getInstanceSize...;
Runtime3种调用方式对应的发起者信息如下
objc_msgSend
- 利用
clang将OC文件改写为c++文件,通过还原后的cpp文件可以查看到一部分runtime调用方式; OC的方法调用在runtime层面变成了消息发送objc_msgSend(id _Nullable self, SEL _Nonnull op, ...),方法调用包含两个元素:
- 消息接受者
- 消息主体:
sel+参数
//重写前OC方法调用
GCPerson *person = [GCPerson alloc];
[person sayHello];
//重写后变成了objc_msgSend
static void _I_GCTeacher_sayHello(GCTeacher * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fw_k53y7bbd2rx7sdzz_s85z7840000gn_T_main_cd505b_mi_0,__func__);
}
objc_msgSendSuper
- 查看改写后的cpp文件时我们发现除了
objc_msgSend之外,还有一个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_msgSendSuper的参数objc_super定义如下
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;//消息接收者
/* super_class is the first class to search */
__unsafe_unretained _Nonnull Class super_class;//方法最先查找的class是super_class,如果super_class查找不到会查找super_class的父类
};
- 通过构建
objc_super结构体偶为参数,我们可以主动调用objc_msgSendSuper方法
struct objc_super gc_objc_super;
gc_objc_super.receiver = person;消息接受者
gc_objc_super.super_class = GCPerson.class;//方法最先查找的class是super_class,如果super_class查找不到会查找super_class的父类
objc_msgSendSuper(&gc_objc_super,@selector(sayHello));
objc_msgSend汇编查看
由于OC语言的动态特性,基本上所有方法都是通过objc_msgSend进行处理的,这样一个高频调用的底层方法,从效率层面考虑最好是使用接近机器语言的汇编语言来实现,同时汇编语言基于地址也更加安全,所以系统也是用汇编进行实现的;下面一起来看下objc_msgSend在在libobjc库中的汇编层面的实现吧
-
在
objc源码中搜索objc_msgSend -
关闭所有文件,查看后缀为
.s的汇编文件 -
找到
objc_msgSend的汇编实现
4. 分析
_objc_msgSend汇编部分源码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check// 比较p0和#0的差值
#if SUPPORT_TAGGED_POINTERS //判断是否支持Taggedpointer类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)//如果支持Taggedpointer类型按照LNilOrTagged处理
#else
b.eq LReturnZero // 返回nil如果不支持Taggedpointer类型返回nil
#endif
ldr p13, [x0] // p13 = isa,[x0]存放的是class
//src, needs_auth, auth_address
GetClassFromIsa_p16 p13, 1, x0 //从 GetClassFromIsa_p16 中获取clsss:p16 = class
// receiver->class 获取class,去class 中找method cache
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
END_ENTRY _objc_msgSend
bucket中IMP的获取
按位异或操作操作
^也叫做按位异或操作,如果c = a ^ b;则a = c ^ b;b叫做盐,无实际作用,主要是为了生成异或结果而存在;
方法编码
- bucket_t中用一个无符号长整型的数值来存储IMP
explicit_atomic<uintptr_t> _imp;,而IMP本身是地址,所以存储时set方法进行了编码
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
//这里对IMP和Class进行了编码
uintptr_t newIMP = (impEncoding == Encoded
? encodeImp(base, newImp, newSel, cls)
: (uintptr_t)newImp);
if (atomicity == Atomic) {
_imp.store(newIMP, memory_order_relaxed);
if (_sel.load(memory_order_relaxed) != newSel) {
mega_barrier();
_sel.store(newSel, memory_order_relaxed);
}
} else {
_imp.store(newIMP, memory_order_relaxed);
_sel.store(newSel, memory_order_relaxed);
}
}
//IMP编码的方法
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
if (!newImp) return 0;
return (uintptr_t)newImp ^ (uintptr_t)cls;//按位异或
}
IMP的获取方法IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)也需要进行解码,具体操作是用 当前取出的IMP数值与class进行异或操作
return (IMP)(imp ^ (uintptr_t)cls);
class是上述编码解码操作时用到的盐;
class中的delegate
delegate作为class中的一个成员,获取方法跟method property没有太大的区别,也是通过LLDB调试 来从class中获取的;- 但是我们最终通过
protocols()方法获取的protocol_array_t,protocol_array_t里面存储的是protocol_ref_t,protocol_ref_t是一个uintptr_t类型的,同时注释说明了他是未映射的:// protocol_t *, but unremapped,不能直接打印相关信息,而我们需要的是可以查看相关信息的protocol_t类型; - 基于以上信息我们重新搜索
protocol_ref_t,最终发现用来映射protocol_ref_t的方法remapProtocol(),它的内部实现是直接强转,我们也可以强转protocol_ref_t为protocol_t;
static ALWAYS_INLINE protocol_t *remapProtocol(protocol_ref_t proto)
{
runtimeLock.assertLocked();
// Protocols in shared cache images have a canonical bit to mark that they
// are the definition we should use
if (((protocol_t *)proto)->isCanonical())
return (protocol_t *)proto;//直接强转protocol_t返回
protocol_t *newproto = (protocol_t *)
getProtocol(((protocol_t *)proto)->mangledName);
return newproto ? newproto : (protocol_t *)proto;//直接强转protocol_t返回
}
LLDB调试代码
2021-06-26 21:59:08.559358+0800 KCObjcBuild[81571:7062064] GCTeacher
(lldb) p/x pClass //获取当前class地址
(Class) $0 = 0x0000000100004908 GCTeacher
(lldb) p/x 0x0000000100004908+0x20 //内存偏移获取bits
(long) $1 = 0x0000000100004928
(lldb) p (class_data_bits_t *)$1//强转为bits
(class_data_bits_t *) $2 = 0x0000000100004928
(lldb) p $2->data()//获取bits中的data
(class_rw_t *) $3 = 0x0000000101131b70
(lldb) p *$3//打印data目标地址的内容:class_rw_t
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4294984032
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.protocols()//从class_rw_t中获取protocols
(const protocol_array_t) $5 = {
list_array_tt<unsigned long, protocol_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100004270
}
arrayAndFlag = 4294984304
}
}
}
(lldb) p $5.list //获取protocols的list
(const RawPtr<protocol_list_t>) $6 = {
ptr = 0x0000000100004270
}
(lldb) p $6.ptr //获取protocols的ptr
(protocol_list_t *const) $7 = 0x0000000100004270
(lldb) p *$7 //获取ptr中的protocol_list_t
(protocol_list_t) $8 = (count = 1, list = protocol_ref_t [] @ 0x00007f922ab3a268)
(lldb) p $8.list[0]//从protocol_list_t获取index为0的protocol_ref_t
(protocol_ref_t) $10 = 4294986152
(lldb) p (protocol_t *)$10//将protocol_ref_t强转为protocol_t
(protocol_t *) $11 = 0x00000001000049a8
(lldb) p *$11//取出protocol_t中存储的数据
(protocol_t) $12 = {
objc_object = {
isa = {
bits = 4298453192
cls = Protocol
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 537306649
magic = 0
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
}
mangledName = 0x0000000100003e89 "TestDelegate"
protocols = 0x0000000100004338
instanceMethods = 0x0000000000000000
classMethods = 0x0000000000000000
optionalInstanceMethods = 0x0000000100004350
optionalClassMethods = 0x0000000000000000
instanceProperties = 0x0000000000000000
size = 96
flags = 0
_extendedMethodTypes = 0x0000000100004370
_demangledName = 0x0000000000000000
_classProperties = 0x0000000000000000
}
(lldb) p *$12.optionalInstanceMethods//获取protocol_t中的optionalInstanceMethods
(method_list_t) $15 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $15.get(0).big()//打印optionalInstanceMethods中的index=0的数据
(method_t::big) $17 = {
name = "testDelegate"
types = 0x0000000100003ec6 "v16@0:8"
imp = 0x0000000000000000
}
(lldb)
通过以上方式,最终成功获取到了protocol_t的相关信息,也对之前探索的Class内容做了补充;