iOS 底層第6天的學(xué)習(xí)。今天又學(xué)了一個新的技能,原來還能把底層源碼拉出來在局部進行分析,真是受益匪淺啊。
cache 探索
- 我們已經(jīng)知道在類的底層結(jié)構(gòu)中有
isa,superclass,bit,cache。
| name | desc |
|---|---|
isa |
isa指針,objc isa-> Class,Class isa -> metal Class
|
superclass |
父類 |
bit |
bit 里有 ro,rw,里有屬性,方法,協(xié)議 等等 |
cache |
緩存 |
而這里
cache里到底存了有哪些東西呢?
- 我們先
lldb獲取到cache的內(nèi)存地址,經(jīng)過一番操作如下所示
(lldb) p/x pClass
(Class) $0 = 0x00000001000085d0 Person
(lldb) p/x 0x00000001000085d0 + 0x10 // 平移16字節(jié)
(long) $1 = 0x00000001000085e0
(lldb) p (cache_t *)0x00000001000085e0
(cache_t *) $2 = 0x00000001000085e0
(lldb) p *$2
(cache_t) $3 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298515360
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32800
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802000000000
}
}
}
}
- 接下里就是要去分析
cache_t源碼,目的是為了找到相應(yīng)的方法。
struct cache_t {
..... 省略部分代碼
public:
// The following four fields are public for objcdt's use only.
// objcdt reaches into fields while the process is suspended
// hence doesn't care for locks and pesky little details like this
// and can safely use these.
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
#if CONFIG_USE_PREOPT_CACHES
const preopt_cache_t *preopt_cache() const;
#endif
mask_t occupied() const;
void initializeToEmpty();
#if CONFIG_USE_PREOPT_CACHES
bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = (uintptr_t)&_objc_empty_cache) const;
bool shouldFlush(SEL sel, IMP imp) const;
bool isConstantOptimizedCacheWithInlinedSels() const;
Class preoptFallbackClass() const;
void maybeConvertToPreoptimized();
void initializeToEmptyOrPreoptimizedInDisguise();
#else
inline bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = 0) const { return false; }
inline bool shouldFlush(SEL sel, IMP imp) const {
return cache_getImp(cls(), sel) == imp;
}
inline bool isConstantOptimizedCacheWithInlinedSels() const { return false; }
inline void initializeToEmptyOrPreoptimizedInDisguise() { initializeToEmpty(); }
#endif
void insert(SEL sel, IMP imp, id receiver);
}
- 我們看到 有 一個
struct bucket_t *buckets() const;buckets的結(jié)構(gòu)體,
點擊進入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
}
發(fā)現(xiàn)了 _imp 和 _sel ,由此我們可以知道 buckets 里面存儲了 _imp 好 `_sel,

- 接下來我們回到
lldb去驗證一下buckets
(lldb) p $3.buckets()
(bucket_t *) $5 = 0x00000001003623a0
(lldb) p *$5
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
- 為什么這里的
Value是nil呢? 原來是buckets是一個哈希結(jié)構(gòu).
(lldb) p $3.buckets()[1]
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 48880
}
}
}
- 查找
bucket_t源碼 ,發(fā)現(xiàn)2個方法
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
繼續(xù) lldb 輸出 sel & imp
(lldb) p $6.sel()
(SEL) $8 = "doSomething"
(lldb) p $6.imp(nil,pClass)
(IMP) $12 = 0x0000000100003b00 (KCObjcBuild`-[Person doSomething])
- 驗證成功。但是每次都要去
lldb打印 感覺真的好麻煩,有沒有更方便的方法呢?接下來我們用底層局部代碼塊的形式繼續(xù)分析,用NSLog日志方式進行打印。
局部代碼分析 cache
- 把獲取的源碼更改成自己需要的
struct
struct x_bucket_t {
SEL _sel;
IMP _imp;
};
struct x_cache_t {
uintptr_t _bucketsAndMaybeMask;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct x_class_data_bits_t {
uintptr_t bits;
};
struct x_objc_class {
// Class ISA;
Class isa;
Class superclass;
struct x_cache_t cache;
struct x_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
Class p_class = p.class;
// 進行 x_objc_class 的強轉(zhuǎn)
struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
NSLog(@"x_class is %@",x_class);
}
- 打印結(jié)果
x_class is Person
- 說明這樣是可以底層源碼更進行
NSLog輸出的,我們繼續(xù)往下走。 - 我們的目的是要打印
x_cache_t里的buckets,可現(xiàn)在x_cache_t里沒有,我想能不能在x_cache_t加一個x_bucket_t試試呢,我們把_bucketsAndMaybeMask替換成struct x_bucket_t *_buckets;
struct x_cache_t {
struct x_bucket_t *_buckets;
// uintptr_t _bucketsAndMaybeMask;
mask_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
Class p_class = p.class;
[p readBook1]; //
// 進行 x_objc_class 的強轉(zhuǎn)
struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
struct x_bucket_t bucket = x_class->cache._buckets[I];
NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"_occupied is %hu,_maybeMask is %u",x_class->cache._occupied,x_class->cache._maybeMask);
- 打印輸出結(jié)果,我們發(fā)現(xiàn)
_bucketsAndMaybeMask替換成struct x_bucket_t *_buckets可以輸出結(jié)果打印sel和imp - 已經(jīng)成功找到
sel的readBook1了
sel is (null), imp is 0x0f
sel is (null), imp is 0x0f
sel is readBook1, imp is 0xbcb0f
_occupied is 1,_maybeMask is 3
- 我們繼續(xù)測試多打印幾個方法調(diào)用,看看
cache里_buckets會有什么變化
Class p_class = p.class;
[p readBook1]; //
[p readBook2]; //
[p readBook3]; //
// 進行 x_objc_class 的強轉(zhuǎn)
struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
struct x_bucket_t bucket = x_class->cache._buckets[I];
NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
>>> readBook1
>>> readBook2
>>> readBook3
sel is (null), imp is 0x0f // 1
sel is (null), imp is 0x0f // 2
sel is (null), imp is 0x0f // 3
sel is (null), imp is 0x0f // 4
sel is (null), imp is 0x0f // 5
sel is (null), imp is 0x0f // 6
sel is readBook3, imp is 0xbc50f // 7
_occupied is 1,_maybeMask is 7
疑問1:為什么我調(diào)用了
3個 方法,buckets里的maybeMask都變成7個了呢?
疑問2:readBook2,readBook3為什么沒有呢?
- 我們只調(diào)用
readBook1和readBook2看看輸出如何
sel is readBook2, imp is 0xbc78f
sel is (null), imp is 0x0f
sel is readBook1, imp is 0xbc08f
_occupied is 2,_maybeMask is 3
- 我們可以看到如果是打印
2個方法occupied是2, 但maybeMask是3沒有增加, 打印1個方法,occupied是1,maybeMask還是3,也沒有增加。 - 我們帶著上面的機個疑問,去分析下
cache底層源碼到底是如何實現(xiàn)的。 - 分析前先要思考下我們在調(diào)用 方法的時候是在從
cache里讀把,那到底是怎么寫進去的呢?
struct cache_t {
...
void insert(SEL sel, IMP imp, id receiver);
...
}
- 我源碼里找到了
insert插入 = 寫把,我們進入insert方法里繼續(xù)分析。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
// ... 省略部分代碼
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1; // occupied 默認(rèn) 0 + 1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 先從這里開始分析
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE; // 4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) { // <= capacity * 3/4
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION // 1
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else { // newOccupied +1 > capacity * 3/4 會進來
// 4*2 = 8
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1; // 4-1=3
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//. 通過 do while 尋找合適的下標(biāo)操作
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls()); // 賦值操作
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
}
- 先
isConstantEmptyCache判斷是否有緩存,沒緩存直接進入reallocate方法開辟內(nèi)存
ALWAYS_INLINE
/*
oldCapacity = 0
newCapacity = 4
freeOld = false
**/
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// ... 省略部分代碼
// newCapacity = 4
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity); // 回收清空
}
}
當(dāng)
(3 = newOccupied +1) = {4 > 3} = (4 = capacity * 3/4 )時會進入else這個方法里。這里的3/4只針對__arm__||__x86_64__||__i386__當(dāng)
capacity = capacity ? capacity * 2->capacity = 8->reallocate(oldCapacity, capacity, true)->collect_free(oldBuckets, oldCapacity);當(dāng)
capacity = 8->mask_t m = capacity - 1 = 7就能解釋 疑問1:為什么我調(diào)用了3個 方法,buckets里的maybeMask都變成7個了調(diào)用3 個 methods=>occupied = 2->else 代碼->freeOld = true->重新梳理緩存, 這樣就能解釋疑問2:readBook2,readBook3沒有的原因了。
-
cache 分析圖如下
總結(jié)
- 當(dāng)
cache里的occupied + 2>capacity * 3/4, 就會進行一次擴容,并把原有的舊方法都清空。而capacity無值的時默認(rèn)4, 擴容后的capacity=8,擴容的算法就是當(dāng)前capacity * 2。 而maybeMask=capacity - 1
知識點補充
- 脫離源碼進行小規(guī)模底層分析到底有什么好處呢?
1:解決了有時源碼無法調(diào)試的問題。
2:解決了lldb操作麻煩的問題。
3:小規(guī)模取樣 ,讓你研究的東西 更加的簡單和清晰。
ps: 真機架構(gòu) - arm64
模擬器 - i386
Mac - x86_64
_LP64 -> Unix 和 Unix類系統(tǒng)(Linux,Mac OS X)
