iOS底层探索之类的结构—cache分析(上)
Posted 卡卡西Sensei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之类的结构—cache分析(上)相关的知识,希望对你有一定的参考价值。
1. 回顾
在iOS底层探索之类的结构(上) 中介绍了类中的isa
,在iOS底层探索之类的结构(中)介绍了类中的bits
,还有一个cache
没有探索和分析,这次主要是分析cache
属性。
2. cache 结构
我们的目的是探索cache
,首先得先去了解它的结构,然后再具体分析。
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...此处省略代码...
}
从类的结构中,可以看到cache
是cache_t
类型的,那么我们去cache_t
里面看看。
2.1 cache_t
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
// _bucketsAndMaybeMask is a buckets_t pointer
// _maybeMask is the buckets mask
static constexpr uintptr_t bucketsMask = ~0ul;
static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
static constexpr uintptr_t maskShift = 48;
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
static constexpr uintptr_t preoptBucketsMarker = 1ul;
static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
...此处省略代码...
}
从底层源码我们很容易看出,
cache_t
的结构,我们也可以代码测试lldb
查看
从控制台输出可以看到结构是一模摸一样样。查看cache_t
的源码,我们还发现底层分成了3个架构来处理,其中真机的架构中mask
和bucket
是写在一起,目的是为了优化,可以通过各自的掩码来获取相应的数据。
CACHE_MASK_STORAGE_OUTLINED
: 表示运行的环境是模拟器
或者macOS
系统CACHE_MASK_STORAGE_HIGH_16
: 表示运行环境是64
位的真机CACHE_MASK_STORAGE_LOW_4
:表示运行环境是非64位
的真机
在cache_t
的结构里面还发现了如下代码
static bucket_t *emptyBuckets();
static bucket_t *allocateBuckets(mask_t newCapacity);
static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));
从这些代码中可以,知道是对bucket_t
的进行了操作,那么这个bucket_t
是个什么重要角色呢?
2.2 bucket_t
以下是bucket_t
核心代码
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(bucket_t *base, SEL newSel, Class cls) const {
return (uintptr_t)base ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
...此处省略代码...
}
从上面👆的bucket_t
的结构体源码可以看出,bucket_t
里面存储的是SEL
和 IMP
,同样分为两个版本,真机 和 非真机,区别在于SEL
和IMP
的顺序不一致。
到此大概知道了cache
是和方法有关的了,这家伙就是方法缓存嘛😁。
由此可以画出一个简单的结构图,如下
那么到底是不是方法缓存呢?又是如何进行方法缓存的呢?我接着往下探索分析
2.2.1 buckets()
从源码中发现,有个buckets()
方法可以获取bucket_t
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x00000001003623c0
(lldb) p *$3
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb)
好尴尬啊!什么都么有啊!_sel
值为nil
?不是方法缓存吗?方法哪里去了啊!
这是因为我们都没有调用方法,哪里来的缓存那!那就调用一个方法再看看
(lldb) p [p sayHello]
2021-06-25 15:37:47.401935+0800 JPBuild[18788:5401308] -[JPPerson sayHello]
(lldb) p/x pClass
(Class) $5 = 0x0000000100008688 JPPerson
(lldb) p (cache_t*)0x0000000100008698
(cache_t *) $6 = 0x0000000100008698
(lldb) p *$6
(cache_t) $7 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4301295648
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32808
_occupied = 1
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802800000007
}
}
}
}
(lldb) p *$7.buckets()
(bucket_t) $8 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb)
2.2.2 sel()、imp()
什么鬼👻???纳尼?还是没有啊!但是我们发现了_maybeMask
、_flags
、_occupied
是有值的。于是继续查看源码,发现了,bucket_t
里面的sel()
方法,这不就打印方法名的啊!
(lldb) p $7.buckets()[1]
(bucket_t) $10 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $7.buckets()
(bucket_t *) $11 = 0x0000000100609020
(lldb) p *$11
(bucket_t) $12 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $12.sel()
(SEL) $13 = (null)
(lldb) p $7.buckets()[2]
(bucket_t) $14 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p $7.buckets()[3]
(bucket_t) $15 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 49128
}
}
}
(lldb) p $15.sel()
(SEL) $16 = "sayHello"
(lldb)
可以看到输出了我们调用的方法"sayHello"
,IMP
也是可以输出的,使用下面这个方法
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
输出结果如下
(lldb) p $15.imp(nil,pClass)
(IMP) $17 = 0x0000000100003960 (JPBuild`-[JPPerson sayHello])
(lldb)
3. 脱离源码分析
在上面是在底层源码里面查看结构,并且结合LLDB
调试来分析的,那么我们如果源码调式不了呢?改怎么办呢?那么接下来就通过,模仿源码结构,直接代码分析。
小规模取样,模仿源码结构
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct jp_bucket_t {
SEL _sel;
IMP _imp;
};
struct jp_cache_t {
struct jp_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct jp_class_data_bits_t {
uintptr_t bits;
};
// cache class
struct jp_objc_class {
Class isa;//在源码中,objc_class的ISA属性是继承自objc_object的,
//但在我们将其拷贝过来时,去掉了objc_class的继承关系,
//需要将这个属性明确,否则打印的结果是有问题的
Class superclass;
struct jp_cache_t cache; // formerly cache pointer and vtable
struct jp_class_data_bits_t bits;
};
方法
@implementation LGPerson
- (void)say1{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)say2{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)say3{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)say4{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)say5{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)say6{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)say7{
NSLog(@"LGPerson say : %s",__func__);
}
+ (void)sayHappy{
NSLog(@"LGPerson say : %s",__func__);
}
@end
调用两个方法,打印看看,是否缓存了方法
调用了两个方法,都打印出来了,那么我们多调用几个方法看看
从以上两个测试打印的结果来看,_occupied
和_maybeMask
的值有变化,方法调用的数量不同值会变大。那么_occupied
和_maybeMask
这两个家伙又是什么呢?
请看下一篇博客分析
iOS底层探索之类的结构—cache分析(下)
更多内容持续更新
🌹 请动动你的小手,点个赞👍🌹
🌹 喜欢的可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹
以上是关于iOS底层探索之类的结构—cache分析(上)的主要内容,如果未能解决你的问题,请参考以下文章