正題開(kāi)始之前我們先來(lái)個(gè)開(kāi)胃小菜鞏固一下之前學(xué)習(xí)的內(nèi)容
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; //
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; //
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]; //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
這段代碼輸出會(huì)是咋樣呢

輸出的結(jié)果是1 0 0 0 1 1 1 1下面的四個(gè)都是1這個(gè)應(yīng)該沒(méi)什么問(wèn)題咱們平常開(kāi)發(fā)中經(jīng)常會(huì)對(duì)對(duì)象做這樣的判斷,但是上面的四個(gè)可能就有點(diǎn)問(wèn)題了,類(lèi)的判斷不怎么做,那我們?cè)趺蠢斫膺@個(gè)結(jié)果呢?當(dāng)然是看源碼了 ,直接上源碼
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
通過(guò)源碼可以看到出
- 實(shí)例方法的
- (BOOL)isMemberOfClass:(Class)cls是直接判斷實(shí)例的類(lèi)跟cls相不相同,實(shí)例方法的- (BOOL)isKindOfClass:(Class)cls是比較實(shí)例的類(lèi)和它的所有父類(lèi)跟cls是否相同 - 類(lèi)方法的
+ (BOOL)isMemberOfClass:(Class)cls是直接判斷該類(lèi)的元類(lèi)跟cls相不相同,類(lèi)方法的+ (BOOL)isKindOfClass:(Class)cls是比較類(lèi)的元類(lèi)和元類(lèi)的所有父類(lèi)跟cls是否相同
這樣來(lái)看輸出的結(jié)果就好理解了。但是這里還有一個(gè)問(wèn)題實(shí)際運(yùn)行時(shí)是否真的調(diào)用的是isMemberOfClass和isKindOfClass,下面我們來(lái)看下匯編
image.png
通過(guò)匯編可以看到isMemberOfClass:是正常調(diào)用的,但是isKindOfClass:變成了objc_opt_isKindOfClass這個(gè)方法的調(diào)用,接下來(lái)看下這個(gè)方法的源碼
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
其實(shí)objc_opt_isKindOfClass的核心代碼也是判斷obj的isa指向的類(lèi)以及isa指向的類(lèi)的所有父類(lèi)是否與otherClass相同。
好了開(kāi)胃小菜結(jié)束開(kāi)始正餐
一、cache的數(shù)據(jù)結(jié)構(gòu)探索
通過(guò)《iOS底層原理探究05》我們已經(jīng)了解了類(lèi)的數(shù)據(jù)結(jié)構(gòu)
struct objc_class : objc_object {
...省略無(wú)關(guān)代碼
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
...省略無(wú)關(guān)代碼
}
bits前面的兩篇的探索中已經(jīng)探究過(guò)了,今天我們來(lái)看下class里的cache的數(shù)據(jù)結(jié)構(gòu)是怎樣的,到底存了些啥?網(wǎng)上很多博客說(shuō)cache是方法的緩存是不是這樣呢?cache的數(shù)據(jù)結(jié)構(gòu)又是咋樣的呢?下面就來(lái)探索一下。
先來(lái)看下cache_t的源碼數(shù)據(jù)結(jié)構(gòu)
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;
};
...省略無(wú)關(guān)代碼
struct bucket_t *buckets() const;
}
主要就是一個(gè)聯(lián)合體的結(jié)構(gòu)下面在控制臺(tái)打印一下看看實(shí)際存儲(chǔ)的內(nèi)容
cache 前面是ISA、superclass這兩個(gè)指針類(lèi)型的成員變量所以拿到類(lèi)的地址往后平移16個(gè)字節(jié)(一個(gè)是8字節(jié))也就是加上0x10

來(lái)嘗試輸出cache_t里存儲(chǔ)的內(nèi)容

分別輸出 _bucketsAndMaybeMask、_maybeMask、_originalPreoptCache均沒(méi)打印我們想要的內(nèi)容,前面我們已經(jīng)有了打印方法列表和屬性列表的經(jīng)驗(yàn),一般這些數(shù)據(jù)結(jié)構(gòu)都需要使用源碼里提供的api打印,接下來(lái)看看cache_t中是否也有相應(yīng)的api,通過(guò)上面的源碼可以看到源碼中提供了
struct bucket_t *buckets() const;方法來(lái)獲取buckets下面我們先來(lái)看下bucket_t的結(jié)構(gòu)再在lldb中打印一下
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
...省略無(wú)關(guān)代碼
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
}
...省略無(wú)關(guān)代碼
}
通過(guò)源碼可以看到bucket_t中存儲(chǔ)的是sel和imp,下面在控制臺(tái)里打印試試

通過(guò)buckets()方法我們確實(shí)打印出了bucket,但是打印出來(lái)的數(shù)據(jù)Value是null,這是為啥呢?這個(gè)問(wèn)題先留這個(gè)這兒,這里我們調(diào)用的buckets()既然它帶個(gè)'s'那是不是像數(shù)組一樣存多個(gè)bucket呢,我們來(lái)試一下

我們?nèi)ハ聵?biāo)為1的元素,打印出來(lái)init方法的sel和imp,由此驗(yàn)證了cache里確實(shí)存儲(chǔ)著方法的緩存。
但這里我們也產(chǎn)生了一些問(wèn)題:
- buckets的數(shù)據(jù)結(jié)構(gòu)是咋樣的
- 緩存方法的時(shí)候開(kāi)辟多大的空間,是怎么開(kāi)辟的
- 方法是怎么被存到緩存里去的
我們仿照源碼的數(shù)據(jù)結(jié)構(gòu)自己定義一套結(jié)構(gòu)方便我們繼續(xù)探索
struct sp_bucket_t {//對(duì)應(yīng)bucket_t
SEL _sel;
IMP _imp;
};
struct sp_cache_t {//對(duì)應(yīng)cache_t
struct sp_bucket_t *buckets;
uint32_t _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
struct sp_class_data_bits_t {//對(duì)應(yīng)class_data_bits_t
uintptr_t bits;
};
struct sp_objc_class{//對(duì)應(yīng)objc_class
Class ISA;
Class superclass;
sp_cache_t cache;
sp_class_data_bits_t bits;
};
定義相應(yīng)的數(shù)據(jù)結(jié)構(gòu)之后main方法中就可以添加相應(yīng)的代碼數(shù)據(jù)cache存儲(chǔ)的內(nèi)容了
struct sp_objc_class *sp_class = (__bridge struct sp_objc_class *)(pClass);
for (uint32_t i = 0; i < sp_class->cache._maybeMask; i++) {
struct sp_bucket_t bucket = sp_class->cache.buckets[I];
NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"%hu - %u ",sp_class->cache._occupied,sp_class->cache._maybeMask);
- for循環(huán)打印cache中存儲(chǔ)的方法
-
最后打印出緩存的方法個(gè)數(shù)和開(kāi)辟的空間
image.png -
當(dāng)調(diào)用say1、say2兩個(gè)方法時(shí)開(kāi)辟了三個(gè)位置,并緩存了這兩個(gè)方法,但是0號(hào)位置空出來(lái)了沒(méi)有存東西
在加個(gè)方法試試
image.png - 當(dāng)調(diào)用say1、say2、say3時(shí),cache只緩存了say3,cache開(kāi)辟了7個(gè)位置
lldb調(diào)試過(guò)程中我們遇到了一些問(wèn)題
- 開(kāi)辟的空間和實(shí)際緩存的方法數(shù)并不一致
- 緩存的方法并不是順序存儲(chǔ)的會(huì)有空位的情況
- 當(dāng)開(kāi)辟的空間從3變?yōu)?的時(shí)候之前緩存的方法被清空了只剩了個(gè)say3
帶著這些問(wèn)題我們來(lái)看源碼
cache的插入方法
//入?yún)⑹?需要緩存的方法的SEL IMP 和 方法接收者
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
...省略無(wú)關(guān)代碼
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {//第一次進(jìn)來(lái)緩存是空的
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//計(jì)算申請(qǐng)空間的大小
reallocate(oldCapacity, capacity, /* freeOld */false);//申請(qǐng)空間
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {//已開(kāi)辟的空間還沒(méi)有存滿可以繼續(xù)存
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
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 {//已開(kāi)辟的空間已經(jīng)存滿了 進(jìn)行雙倍擴(kuò)容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);//開(kāi)辟空間
}
bucket_t *b = buckets();//取出buckets
mask_t m = capacity - 1;//計(jì)算maybemask
mask_t begin = cache_hash(sel, m);//使用哈希算法計(jì)算插入的位置
mask_t i = begin;//I表示插入位置
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {//如果插入位置是空的 則插入該方法(避免hash沖突)
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {//判斷其他線程是否緩存過(guò)該方法
// 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));//如果i位置沒(méi)有插入成功 通過(guò)cache_next找下一個(gè)可以插入的位置
bad_cache(receiver, (SEL)sel);//如果while循環(huán)走完都找不到可以插入的位置則緩存失敗
#endif // !DEBUG_TASK_THREADS
}
理一下緩存插入的大致的流程,過(guò)程中調(diào)用的方法在后面附上源碼
-
newOccupied = occupied() + 1這里計(jì)算插入當(dāng)前方法后緩存的方法總數(shù)newOccupied是插入當(dāng)前方法后的總數(shù) 首次進(jìn)來(lái)的時(shí)候occupied()為0 ,newOccupied = 1 -
oldCapacity = capacity()獲取之前緩存的空間大小,capacity = oldCapacity初始化新的空間大小等于老的capacity是新的空間大小 -
isConstantEmptyCache()判斷緩存是否為空 首次insert的時(shí)候緩存是空的,進(jìn)走這個(gè)流程 -
if (!capacity) capacity = INIT_CACHE_SIZE;首次緩存capacity(需要開(kāi)辟的空間)賦值為4 (INIT_CACHE_SIZE = 4 后面附上源碼) -
eallocate(oldCapacity, capacity, /* freeOld */false);開(kāi)辟空間 -
fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))判斷當(dāng)前開(kāi)辟的空間是否夠用 -
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;當(dāng)前開(kāi)辟的空間不夠用的話進(jìn)行兩倍擴(kuò)容 - 下面就是插入緩存的流程了
-
bucket_t *b = buckets();先取出buckets -
m = capacity - 1;mayBeMask設(shè)置成開(kāi)辟的空間減一(這個(gè)后面會(huì)再看源碼) -
begin = cache_hash(sel, m);通過(guò)哈希算法計(jì)算插入的位置,這就是方法不是順序存儲(chǔ)的原因,方法存儲(chǔ)位置的下標(biāo)是通過(guò)hash算法計(jì)算得到的 - 進(jìn)入
do-while循環(huán)把方法插入緩存fastpath(b[i].sel() == 0)判斷當(dāng)時(shí)位置是不是為空,為空則調(diào)用incrementOccupied()使_occupied + 1,通過(guò)b[i].set方法把方法插入緩存 -
b[i].sel() == sel如果之前已經(jīng)緩存過(guò)該方法直接return -
i = cache_next(i, m)如果前面的兩個(gè)判斷都為假則查找下一個(gè)可以插入的位置 -
bad_cache(receiver, (SEL)sel)如果do-while結(jié)束也沒(méi)有找到可以插入的位置則緩存失敗
下面來(lái)看下插入流程中調(diào)用的方法的源碼實(shí)現(xiàn)
mask_t cache_t::occupied() const
{
return _occupied;
}
- 返回
_occupied即已經(jīng)緩存的方法數(shù)
unsigned cache_t::capacity() const
{
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask() const
{
return _maybeMask.load(memory_order_relaxed);
}
-
mask()返回_maybeMask即開(kāi)辟的空間大小 -
capacity()如果_maybeMask有值返回_maybeMask + 1否則返回0,因?yàn)?code>_maybeMask是等于開(kāi)辟的空間減一 所以要加回來(lái)
bool cache_t::isConstantEmptyCache() const
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
- 判斷緩存是否為空
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
- 這個(gè)判斷是否存滿的算法是 判斷有沒(méi)有達(dá)到緩存的3/4,達(dá)到了就需要對(duì)緩存擴(kuò)容了
struct bucket_t *cache_t::buckets() const
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & bucketsMask);
}
-
buckets是通過(guò)_bucketsAndMaybeMask&bucketsMask得到的 -
static constexpr uintptr_t bucketsMask = ~0ul;bucketsMask等于對(duì)0取反就是所有二進(jìn)制位都是1與上_bucketsAndMaybeMask之后得到的還是_bucketsAndMaybeMask本身
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
- cache_hash算法是通過(guò)sel的地址hash得到緩存插入位置的
- 因?yàn)榕c上了m所以得到的位置不會(huì)大于m
- 而
m是外界傳進(jìn)來(lái)的m = capacity - 1,而capacity被初始化成INIT_CACHE_SIZE又通過(guò)INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)和INIT_CACHE_SIZE_LOG2 = 2得到INIT_CACHE_SIZE = 4即m= 3 所以我們?cè)趌ldb中輸出的時(shí)候得到1 - 3的打印
擴(kuò)容之后因?yàn)槭请p倍擴(kuò)容capacity變成了8 ,m變成7所以擴(kuò)容后打印的是1 - 7
void cache_t::incrementOccupied()
{
_occupied++;
}
- 這個(gè)方法比較簡(jiǎn)單就是_occupied自增1
未完待續(xù)


