iOS开发底层探究之路
在对Objective-C
底层的探究过程中,已经探究过objc_class
结构中的isa
及bits
,那么本篇文章我们来见见这个cache
是个啥玩意:
struct objc_class : objc_object {
// 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
class_rw_t *data() const {
return bits.data();
}
}
以及cache_t
结构体类型的cache
源码:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
// Ensure we have enough bits for the buckets pointer.
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
// _maskAndBuckets stores the mask shift in the low 4 bits, and
// the buckets pointer in the remainder of the value. The mask
// shift is the value where (0xffff >> shift) produces the correct
// mask. This is equal to 16 - log2(cache_size).
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskBits = 4;
static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
// 省略下方方法
}
其中,在不同运行环境解释为:
-
CACHE_MASK_STORAGE_OUTLINED
模拟器
或者macOS
-
CACHE_MASK_STORAGE_HIGH_16
64位真机
-
CACHE_MASK_STORAGE_LOW_4
非64位真机
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 _imp;
explicit_atomic _sel;
#else
explicit_atomic _sel;
explicit_atomic _imp;
#endif
void set(SEL newSel, IMP newImp, Class cls);
}
上面源码分析可以看出:
- 类底层结构体中的
cache
是cache_t
结构体类型的,在cache_t
结构体中,在不同的运行环境下,内部的结构也不一样,在模拟器
或macOS
下内部属性有_buckets
和_mask
,在真机下将_buckets
和_mask
存放在一起,为了节省内存
,以_maskAndBuckets
属性存放。 -
_buckets
中存放的是bucket_t
类型的数据,从bucket_t
结构中可以看出只有属性_imp
和_sel
,所以可以看出cache
就是用来存放方法
的。
cache例子分析
首先在可编译源码工程中创建一个LGPerson
类,并添加属性及方法:
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)sayHello{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayCode{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayMaster{
NSLog(@"LGPerson say : %s",__func__);
}
- (void)sayNB{
NSLog(@"LGPerson say : %s",__func__);
}
@end
main.m
文件main
入口函数中创建实例并方法调用:
如图,断点停在22
行,在对象p
调用实例方法之前,我们利用lldb
打印当前pClass
相关类信息,因为实例方法存放在类信息里面:
通过对当前
pClass
类指针偏移16
位的地址进行打印,我们得到当前类的cache
信息,其中_occupied = 0
,_mask = 0
,好,我们继续下一个断点,来到了23
行,此时只进行了sayHello
方法的调用,我们再次打印当前类的cache
信息:
可以看到:
- 此时
cache
信息中_occupied = 1
,那我们可以猜测这个属性是否就是表示当前方法调用的数量
? -
_mask
此时值为3
- 通过
cache_t
结构体中的函数buckets()
,我们可以获得buckets()
指针地址,打印当前地址信息,得到一个bucket_t
结构体数据$5
,再通过bucket_t
结构体中的sel()
和imp(Class)
两个函数方法我们可以打印出刚刚调用的sayHello
方法的方法名
及方法实现
函数的地址信息。
好,接下来我们再调用第二个方法sayCode,再继续查看当前类的cahce信息:
- 此时
cache
信息中的_occupied = 2
,那是否就证实我们上面所说的调用方法个数呢?下面继续探究!嘻嘻 -
_mask
值不变依旧为3
- 因为
buckets
是数组
,所以可以按地址偏移
的方式进行遍历打印,也可以调用数组buckets()
下标[0][1]...
的方式获取。如上打印情况,偏移一位打印出$15 bucket_t
结构体信息,继而打印sel
及imp
信息,发现正是第二个方法sayCode
。
继续接着第三个方法sayMaster
方法调用,那么此时再打印类信息又会是怎样的:
上图可以看出,在调用第三个方法之后,
cache
信息有了很大变化:
- 此时
_occupied = 1
,_mask = 7
,首先这个_occupied = 1
就推翻了我们之前的猜想_occupied
为 调用方法的个数;_mask
由先前的3
变成了现在的7
- 此时
buckets
第一个位置的bucket_t
的sel
为null
且imp = 0
,那么我们利用上面所说的地址偏移或者下标打印是否还能打印出第三个方法的sel
和imp
信息?
地址偏移1
,刚好就找到了sayMaster
的sel
和imp
,而且发现其余位置打印出来的sel
都为null
,imp
都为0
。
最后调用第四个方法sayNB
,来看看结果又是怎样的:
继续利用
地址偏移
,最后找到sayNB
方法sel
和imp
信息:
从上面两个打印结果看出:
-
_occupied = 2
,当我们调用第四个方法后,又加了1
,然而经过第三个方法我们又证明了_occupied
与我们调用的方法数不想等,其实从字面意思上看得出_occupied
是占用的意思,这到底代表啥呢? -
_mask = 7
,此时与第三个方法调用之后没啥改变,_mask
又是啥呢? - 通过地址偏移,遍历打印,我们发现在
buckets
数组中有些的确存储了实例方法的sel
及imp
信息,但是有些是没存储任何方法信息(sel
为null
,imp
为0
),以及是否buckets
中存储的方法信息一定是按方法调用先后顺序来存储的?
接下来我们通过分析cache
缓存方法的源码来一探究竟。
cache 源码分析
拓展
:查看源码原则,一般属性
只有存储信息
的作用,能改变信息
的一般我们认准函数
方法,函数方法才大有可能是改变信息的地方。
首先我们来看文章开始cache_t
源码中隐藏的函数方法部分:
public:
static bucket_t *emptyBuckets();
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
unsigned capacity();
bool isConstantEmptyCache();
bool canBeFreed();
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void insert(Class cls, SEL sel, IMP imp, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn, cold));
从上面所有函数方法中探究,前四个函数,查看源码,发现只是简单的获取cache
的相对应的属性。我们发现第五个函数incrementOccupied()
:
void cache_t::incrementOccupied()
{
_occupied++;
}
可以看出此函数正式改变(增加)_occupied 值
的,那我们就从此函数入手,全局搜索incrementOccupied()
,发现只有void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
方法中找到此函数的调用位置:
从参数上来看,insert
方法可以理解为向cache
中插入方法的sel
及imp
信息,全局搜索发现只有在cache_fill
方法中进行了cache->insert
调用:
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
#if !DEBUG_TASK_THREADS
// Never cache before +initialize is done
if (cls->isInitialized()) {
cache_t *cache = getCache(cls);
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
cache->insert(cls, sel, imp, receiver); //向cache插入方法sel、imp信息
}
#else
_collecting_in_critical();
#endif
}
全局搜索cache_fill
,发现在cache_fill
方法之前,还进行了Cache
中的sel
与imp
查找:
insert方法分析
下面着重分析 cache
缓存方法的核心函数insert
源码:
我们可以将上面三个断点作为insert
方法的三个阶段:
- 阶段一:
mask_t newOccupied = occupied() + 1
;不管当前的cache
是否已经初始化过,newOccupied
都为当前的_occupied
值加上1
,表示当前cache
内存所占用个数
。 - 阶段二:
初始化
及扩容阶段
- 先判断当前
cache
是否是空的(未初始化),小概率事件判断,此时capacity()
函数返回0
,oldCapacity = 0
,capacity = 0
。如果cache
为空,进入if
,capacity
赋值为宏定义4
,reallocate
去内存请求cache
空间。 - 如果当前
cache
已经初始化过了,newOccupied = _occupied + 1
,capacity()
函数返回当前的cache
容量大小,判断当前的newOccupied + CACHE_END_MARKER(1) <= capacity / 4 * 3
是否满足 ,如果<=
,继续下面的存储,如果>
,则进入下一步的对cache
扩容 - 对当前
cache
的容量大小扩大两倍,此时有最大值MAX_CACHE_SIZE(2^15)
,然后重新初始化当前的cache
,并对原有的cahce
存储信息进行清理。接着进行下面存储工作。
- 先判断当前
- 阶段三:真正的方法信息
sel
、imp
存储步骤,bucket_t
存放sel
、imp
,buckets
存放bucket_t
,所以cache
缓存方法实质就是将bucket_t
通过hash算法
插入到buckets
中的过程。
reallocate 初始化方法分析
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
首先去内存中申请buckets
内存空间,allocateBuckets
函数:
bucket_t *allocateBuckets(mask_t newCapacity)
{
// Allocate one extra bucket to mark the end of the list.
// This can't overflow mask_t because newCapacity is a power of 2.
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);
bucket_t *end = cache_t::endMarker(newBuckets, newCapacity);
#if __arm__ //真机
// End marker's sel is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->set((SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else //模拟器或macOS(当前环境)
// End marker's sel is 1 and imp points to the first bucket.
end->set((SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
通过calloc
函数去内存中申请newCapacity
大小的容量的newBuckets
,此方法返回的newBuckets
只是一个临时变量。setBucketsAndMask
就是 将newBuckets
保存在cache
中:
-
CACHE_MASK_STORAGE_OUTLINED
模拟器
或macOS
_buckets.store(newBuckets, memory_order::memory_order_release); // ensure other threads see new buckets before new mask _mask.store(newMask, memory_order::memory_order_release); _occupied = 0;
-
CACHE_MASK_STORAGE_HIGH_16
64位真机
uintptr_t buckets = (uintptr_t)newBuckets; uintptr_t mask = (uintptr_t)newMask; ASSERT(buckets <= bucketsMask); ASSERT(mask <= maxMask); _maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed); _occupied = 0;
-
CACHE_MASK_STORAGE_LOW_4
32位真机
uintptr_t buckets = (uintptr_t)newBuckets; unsigned mask = (unsigned)newMask; ASSERT(buckets == (buckets & bucketsMask)); ASSERT(mask <= 0xffff); // The shift amount is equal to the number of leading zeroes in // the last 16 bits of mask. Count all the leading zeroes, then // subtract to ignore the top half. uintptr_t maskShift = __builtin_clz(mask) - (sizeof(mask) * CHAR_BIT - 16); ASSERT(mask == (0xffff >> maskShift)); _maskAndBuckets.store(buckets | maskShift, memory_order::memory_order_relaxed); _occupied = 0; ASSERT(this->buckets() == newBuckets); ASSERT(this->mask() == newMask);
对比可以看出,真机环境下,bucket
和mask
的位置存储,并将occupied
占用设置为0
,模拟器
或者macOS
下,正常存储bucket
和mask
,并将occupied
占用设置为0
。
最后判断是否需要清理oldBuckets
,如果需要则调用cache_collect_free
将旧的buckets
清理掉。
hash 算法存储sel、imp
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
cache_hash
通过(SEL sel, mask_t mask)
中的sel
& mask
,把得到的作为下标在buckets
获取当前的bucket_t
,如果当前的bucket
中的sel == 0
,说明当前位置暂无插入任何sel
、imp
,即插入当前的sel
及imp
,并且_occupied
占用加1
;如果当前bucket
中的sel
与要插入的sel
相等,说明已经存在了当前的sel
,就直接返回,避免重复插入
;如果当前bucket
已经存在sel
,即已经保存了别的方法的sel
及imp
信息,那就通过cache_next
哈希冲突方法重新查找别的位置进行插入。
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
至此,cache
源码分析告一段落!
问题解释及LGPerson例子解释
-
_mask
: 是指掩码
数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask
等于capacity - 1
,所以例子中刚开始未调用方法时,_mask = 0
,调用第一个方法时候capacity
大小为4
,_mask = 4 - 1 = 3
,当调用第三个方法时,此时cache
容量扩大了,capacity
大小为8,_mask = 8 - 1 = 7
。 -
_occupied
: 表示哈希表中bucket
的占用大小
(即可以理解为当前cache
的buckets
中已经存储了bucket_t
的的个数),一开始未调用方法_occupied = 0
,调用第一个时,增加1
,调用第二个时,增加1
变为2
,此时当调用第三个方法时,由于此时newOccupied = _occupied + 1 = 3
,占用大于capacity
的四分之三
,所以需要扩容,同时清理之前存储的,及_occupied = 0
,然后插入第三那个方法,增加_occupied
值。所以得出结论,_occupied
即为当前buckets
中所占用的bucket_t
个数。 - 因为
sel-imp
的存储是通过哈希算法
计算下标的,其计算的下标有可能已经存储了sel
,所以又需要通过哈希冲突
算法重新计算哈希下标,所以导致下标是随机
的,并不是固定的,也不是
按方法调用顺序来存放的。