Runtime系列文章
Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime
????本文篇幅比較長(zhǎng),創(chuàng)作的目的并不是為了在簡(jiǎn)書上刷贊和閱讀量,而是為了自己日后溫習(xí)知識(shí)所用。如果有幸被你發(fā)現(xiàn)這篇文章,并且引起了你的閱讀興趣,請(qǐng)休息充分,靜下心來,精力充足地開始閱讀,希望這篇文章能對(duì)你有所幫助。如發(fā)現(xiàn)任何有誤之處,肯請(qǐng)留言糾正,謝謝。????
承接上一篇的內(nèi)容,我們回過頭去看Class的定義
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 用于獲取具體的類信息
};
這里面還有一個(gè)cache_t cache
沒有解讀過,一起來看一看這個(gè)東西。看名字很好理解,就是緩存的意思,緩存什么呢?——緩存方法。
它的底層是通過散列表(哈希表)
的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)的,用于緩存曾經(jīng)調(diào)用過的方法,可以提高方法的查找速度。
首先,回顧一下正常情況下方法調(diào)用的流程。假設(shè)我們調(diào)用一個(gè)實(shí)例方法[bj XXXX];
obj
->isa
->obj
的Class
對(duì)象 ->method_array_t methods
-> 對(duì)該表進(jìn)行遍歷查找,找到就調(diào)用,沒找到繼續(xù)往下走obj
->superclass
->obj
的父類 ->isa
->method_array_t methods
-> 對(duì)父類的方法列表進(jìn)行遍歷查找,找到就調(diào)用,沒找到就重復(fù)本步驟- 找到就調(diào)用,沒找到重復(fù)流程
- 找到就調(diào)用,沒找到重復(fù)流程
- 找到就調(diào)用,沒找到重復(fù)流程
- 直到
NSObject
->isa
->NSObject
的Class
對(duì)象 -> method_array_t methods ......
如果XXXX
方法在程序內(nèi)會(huì)被頻繁的調(diào)用,那么這種逐層便利查找的方式肯定是效率低下的,因此蘋果設(shè)計(jì)了cache_t cache
,當(dāng)XXXX
第一次被調(diào)用的時(shí)候,會(huì)按照常規(guī)流程查找,找到之后,就會(huì)被加入到cache_t cache
中,當(dāng)再次被調(diào)用的時(shí)候,系統(tǒng)就會(huì)直接現(xiàn)到cache_t cache
來查找,找到就直接調(diào)用,這樣便大大提升了查找的效率。
剛才介紹了cache_t cache
是通過散列表
來實(shí)現(xiàn)的,下面就來著重分析一下,方法是如何被緩存的。散列/哈希表,想必大部分iOS開發(fā)這至少應(yīng)該聽過,而我們常用的NSDictionary
其實(shí)就是一種散列表數(shù)據(jù)結(jié)構(gòu)。來看一下cache_t cache
的定義
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
-
struct bucket_t *_buckets;
—— 用來緩存方法的散列/哈希表 -
mask_t _mask;
—— 這個(gè)值 = 散列表長(zhǎng)度 - 1 -
mask_t _occupied;
—— 表示已經(jīng)緩存的方法的數(shù)量
上面介紹的_buckets
散列表里面的存儲(chǔ)單元是bucket_t
,來看看它包含了方法的什么信息
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
-
cache_key_t _key;
—— 這個(gè)key實(shí)際上就是方法的SEL,也就是方法名 -
IMP _imp;
—— 這個(gè)就是方法對(duì)應(yīng)的函數(shù)的內(nèi)存地址
想一想我們平時(shí)是怎么使用NSDictionary的,通過一堆Key-Value鍵值對(duì)來進(jìn)行存儲(chǔ)的,NSDictionary的底層就是散列表,這個(gè)剛才說過。方法緩存的時(shí)候,key就是上面的cache_key_t _key;
,value就是上面的bucket_t
結(jié)構(gòu)體對(duì)象。
但是散列表的運(yùn)作原理到底如何呢,這個(gè)屬于數(shù)據(jù)結(jié)構(gòu)問題,這里簡(jiǎn)要介紹一下。首先散列表本質(zhì)上就是一個(gè)數(shù)組
key
計(jì)算出一個(gè)index,然后再將元素插入散列表的index位置那么從散列表里面取值就顯而易見了,根據(jù)一個(gè)key,計(jì)算出index,然后到散列表對(duì)應(yīng)位置將值取出
這里的查詢方法的時(shí)候(也就是取值操作),時(shí)間復(fù)雜度為O(1), 對(duì)比我們一開始從方法列表的遍歷查詢,它的時(shí)間復(fù)雜度為O(n),因此通過緩存方法,可以極大的提高方法查詢的效率,從而提高了方法調(diào)用機(jī)制的效率。
根據(jù)key計(jì)算出index值的這個(gè)算法稱作散列算法,這個(gè)算法可以由你自己設(shè)計(jì),總之目的就是盡可能減少不同的key得出相同index的情況出現(xiàn),這種情況被稱作哈希碰撞,同時(shí)還要保證得出的index值在合理的范圍。index越大,意味著對(duì)應(yīng)的散列表的長(zhǎng)度越長(zhǎng),這是需要占用實(shí)際物理空間的,而我們的內(nèi)存是有限的。散列表是一種通過犧牲一定空間,來換取時(shí)間效率的設(shè)計(jì)思想。
我們通過key計(jì)算出的index大小是隨機(jī)的,無順序的,因此在方法緩存的過程中,插入的順序也是無順序的而且可以預(yù)見的是,散列表里面再實(shí)際使用中會(huì)有很多位置是空著的,比如散列表長(zhǎng)度為16,最終值存儲(chǔ)了10個(gè)方法,散列表長(zhǎng)度為64,最終可能只會(huì)存儲(chǔ)40個(gè)方法,有一部分空間終究是要被浪費(fèi)的。但是卻提高查找的效率。這既是所謂的空間換時(shí)間。
再介紹一下蘋果這里所采用的散列算法,其實(shí)很簡(jiǎn)單,如下
index = @selector(XXXX) & mask
根據(jù)&運(yùn)算的特點(diǎn),可以得知最終index <= mask
,而mask
= 散列表長(zhǎng)度 - 1,也就是說 0 <= index <= 散列表長(zhǎng)度 - 1
,這實(shí)際上覆蓋了散列表的索引范圍。而剛剛我們還提到過一個(gè)問題——哈希碰撞,也就是不同的key得到相同的index,該怎么處理呢?我們看一下源碼,在objc源碼里面搜索cache_t,可以發(fā)現(xiàn)一個(gè)跟查找相關(guān)的方法
bucket_t * cache_t::find(cache_key_t k, id receiver) //根據(jù)key值 k 進(jìn)行查找
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m); //通過cache_hash函數(shù)【begin = k & m】計(jì)算出key值 k 對(duì)應(yīng)的 index值 begin,用來記錄查詢起始索引
mask_t i = begin; // begin 賦值給 i,用于切換索引
do {
if (b[i].key() == 0 || b[i].key() == k) {
//用這個(gè)i從散列表取值,如果取出來的bucket_t的 key = k,則查詢成功,返回該bucket_t,
//如果key = 0,說明在索引i的位置上還沒有緩存過方法,同樣需要返回該bucket_t,用于中止緩存查詢。
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// 這一步其實(shí)相當(dāng)于 i = i-1,回到上面do循環(huán)里面,相當(dāng)于查找散列表上一個(gè)單元格里面的元素,再次進(jìn)行key值 k的比較,
//當(dāng)i=0時(shí),也就i指向散列表最首個(gè)元素索引的時(shí)候重新將mask賦值給i,使其指向散列表最后一個(gè)元素,重新開始反向遍歷散列表,
//其實(shí)就相當(dāng)于繞圈,把散列表頭尾連起來,不就是一個(gè)圈嘛,從begin值開始,遞減索引值,當(dāng)走過一圈之后,必然會(huì)重新回到begin值,
//如果此時(shí)還沒有找到key對(duì)應(yīng)的bucket_t,或者是空的bucket_t,則循環(huán)結(jié)束,說明查找失敗,調(diào)用bad_cache方法。
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
*********************************** cache_hash(k, m);
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
*********************************** cache_next(i, m)
static inline mask_t cache_next(mask_t i, mask_t mask) {
// return (i-1) & mask; // 非arm64
return i ? i-1 : mask; // arm64
}
cache_t::find
函數(shù)還被源碼里面的另一個(gè)函數(shù)調(diào)用過——cache_fill_nolock
,緩存填充(插入)操作,源碼如下
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
上面源碼的最后一段以及它的注釋說明可以明白,當(dāng)通過cache->find
返回的bucket->key() == 0
,就說明該位置上是空的,沒有緩存過方法,是一個(gè)unused slot
(未使用的槽口),因此可以進(jìn)行插入操作bucket->set(key, imp);
,也就是將方法緩存到這個(gè)位置上。
根據(jù)上面的分析,下面用圖示來總結(jié)一下方法存入cache_t中,以及從cache_t中取方法的整體流程
向cache_t存入方法
從cache_t查詢方法
你可能還會(huì)有一個(gè)疑問,如果不斷的往緩存里添加方法,緩存滿了怎么辦?我們回到剛才看過的一段代碼cache_fill_nolock
函數(shù),直接用截圖解讀一下
expand()
,源碼如下
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
上面代碼里面uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
說的很明白,擴(kuò)容就是將當(dāng)前緩存容量* 2
,如果是首次調(diào)用這個(gè)函數(shù),會(huì)使用一個(gè)初始容量值INIT_CACHE_SIZE
來設(shè)定緩存容量
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
從INIT_CACHE_SIZE
的定義顯示它的值是4,也就是說蘋果給cache_t設(shè)定的初始容量是4。
你可能還會(huì)問,重置緩存之后,原來老緩存里面的內(nèi)容還要不要呢,expand()
函數(shù)里面調(diào)用的最后一個(gè)函數(shù)是reallocate(oldCapacity, newCapacity);
,我們?cè)谶M(jìn)入它的源碼看看
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
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);
cache_collect(false);
}
}
很明顯,在最對(duì)舊的緩存空間進(jìn)行了釋放,但是條件是freeOld = true
,函數(shù)開頭給出了freeOld
的由來,通過canBeFreed()
函數(shù)獲得
bool cache_t::isConstantEmptyCache()
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
bool cache_t::canBeFreed()
{
return !isConstantEmptyCache();
}
canBeFreed()
函數(shù)其實(shí)很簡(jiǎn)單,就是判斷一下緩存是不是空的,如果空的,舊沒必要釋放空間了,如果原來的緩存不是空的,就直接釋放掉,并且我們發(fā)現(xiàn),擴(kuò)容的操作里面,并沒有對(duì)舊的緩存空間里面的內(nèi)容進(jìn)行復(fù)制保留,就是很粗暴的直接分配一塊新的緩存空間,然后直接釋放掉舊的緩存空間,這意味著,每次進(jìn)行擴(kuò)容操作之后,原來緩存過的方法就會(huì)全部丟失,而上面的cache_fill_nolock
函數(shù)里面,在進(jìn)行完expand()
擴(kuò)容操作之后,也僅僅是把當(dāng)前處理的方法放到緩存空間里面,因此,擴(kuò)容之前曾經(jīng)被緩存過的方法,如果下次再次調(diào)用的話,有需要被重新緩存了。這里好好體會(huì)一下。
父類的方法被調(diào)用的時(shí)候,會(huì)如何緩存?
現(xiàn)在,我們知道,當(dāng)對(duì)一個(gè)對(duì)象發(fā)送消息后,會(huì)通過對(duì)象的isa找到它的Class對(duì)象,在Class對(duì)象里面先從方法緩存cache_t查找該方法,沒有的話再對(duì)Class對(duì)象的方法列表進(jìn)行遍歷查找,如果找到了方法,就進(jìn)行緩存并且調(diào)用,那么這里肯定是將方法緩存到了該對(duì)象的Class對(duì)象的cache_t里面。
如果在當(dāng)前Class對(duì)象里面沒有找到該方法,那么會(huì)通過Class對(duì)象的superclass進(jìn)入其父類的Class對(duì)象里面,同樣,會(huì)先查找它的cache_t,如果沒有找到方法,會(huì)對(duì)其方法列表進(jìn)行遍歷查找,問題就在這里,如果此時(shí)在方法列表里面找到了方法,進(jìn)行緩存操作的時(shí)候,是會(huì)將方法存入當(dāng)前父類的Class對(duì)象的cache_t里面呢,還是會(huì)存到接收消息的對(duì)象的Class對(duì)象的cache_t里面呢?
要搞清楚這個(gè)問題,首先可以看一下哪些地方調(diào)用了static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
函數(shù),因?yàn)樗膮?shù)里面?zhèn)魅肓艘粋€(gè)Class cls
我們只需要搞清楚這個(gè)Class cls
到底是誰。
根據(jù)下圖的操作進(jìn)入上層調(diào)用函數(shù)cache_fill
用同樣方法查看一下
cache_fill
的上層調(diào)用函數(shù),如下圖首先來看一下這個(gè)
log_and_cache()
lockUpImpOrForward()
函數(shù)調(diào)用的。
接下來我們?cè)谙瓤匆幌?code>lookUpMethodInClassAndLoadCache()函數(shù)
最后在來看一下剩下的那個(gè) lookUpImpOrForward
函數(shù),下面代碼請(qǐng)看??????處標(biāo)記的中文注解即可
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.-------------------------------->??????標(biāo)準(zhǔn)的IMP查找流程
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {//------------------------------>??????查詢當(dāng)前Class對(duì)象的緩存,如果找到方法,就返回該方法
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.read();
if (!cls->isRealized()) {//------------------------------>??????當(dāng)前Class如果沒有被realized,就進(jìn)行realize操作
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {//-------------->??????當(dāng)前Class如果沒有初始化,就進(jìn)行初始化操作
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertReading();
// Try this class's cache.//------------------------------>??????嘗試從該Class對(duì)象的緩存中查找,如果找到,就跳到done處返回該方法
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.//---------------->??????嘗試從該Class對(duì)象的方法列表中查找,找到的話,就緩存到該Class的cache_t里面,并跳到done處返回該方法
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.------>??????進(jìn)入當(dāng)前Class對(duì)象的superclass對(duì)象
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;//------>??????該for循環(huán)每循環(huán)一次,就會(huì)進(jìn)入上一層的superclass對(duì)象,進(jìn)行循環(huán)內(nèi)部方法查詢流程
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.------>??????在當(dāng)前superclass對(duì)象的緩存進(jìn)行查找
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;//------>??????如果在當(dāng)前superclass的緩存里找到了方法,就調(diào)用log_and_fill_cache進(jìn)行方法緩存,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對(duì)象所對(duì)應(yīng)的Class對(duì)象的cache_t中,然后跳到done處返回該方法
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;//---->??????如果緩存里找到的方法是_objc_msgForward_impcache,就跳出該輪循環(huán),進(jìn)入上一層的superclass,再次進(jìn)行查找
}
}
// Superclass method list.---->??????如過畫緩存里面沒有找到方法,則對(duì)當(dāng)前superclass的方法列表進(jìn)行查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//------>??????如果在當(dāng)前superclass的方法列表里找到了方法,就調(diào)用log_and_fill_cache進(jìn)行方法緩存,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對(duì)象所對(duì)應(yīng)的Class對(duì)象的cache_t中,然后跳到done處返回該方法
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.//------>??????如果到基類還沒有找到方法,就嘗試進(jìn)行方法解析
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help. //------>??????如果方法解析不成功,就進(jìn)行消息轉(zhuǎn)發(fā)
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
關(guān)于上面再方法列表查找的函數(shù)Method meth = getMethodNoSuper_nolock(cls, sel);
還需要說明一下,進(jìn)入它的實(shí)現(xiàn)
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);//---??????核心函數(shù)
if (m) return m;
}
return nil;
}
再進(jìn)入其核心函數(shù)search_method_list(*mlists, sel)
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
//---??????如果方法列表是經(jīng)過排序的,則進(jìn)行二分查找
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
//---??????如果方法列表沒有進(jìn)行排序,則進(jìn)行線性遍歷查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
}
根據(jù)代碼中的邏輯,如果方法列表是經(jīng)過排序的,會(huì)使用findMethodInSortedMethodList
進(jìn)行查找,而這里面實(shí)際上是用二分法進(jìn)行查找的,具體代碼如下
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//---??????count >>= 1相當(dāng)于 count/=2,說明是從數(shù)組中間開始查找,也就是二分查找發(fā)
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
經(jīng)過上述解讀,我們已經(jīng)基本了解方法查詢和方法緩存所涉及到的細(xì)節(jié),現(xiàn)在可以把方法查找和方法緩存流程結(jié)合起來描述一下 Runtime消息機(jī)制 當(dāng)中的 消息發(fā)送 流程
- (1) 當(dāng)一個(gè)對(duì)象接收到消息時(shí)
[obj message];
,首先根據(jù)obj
的isa
指針進(jìn)入它的類對(duì)象cls
里面。- (2) 在
obj
的cls
里面,首先到緩存cache_t
里面查詢方法message
的函數(shù)實(shí)現(xiàn),如果找到,就直接調(diào)用該函數(shù)。- (3) 如果上一步?jīng)]有找到對(duì)應(yīng)函數(shù),在對(duì)該
cls
的方法列表進(jìn)行二分/遍歷查找,如果找到了對(duì)應(yīng)函數(shù),首先會(huì)將該方法緩存到obj
的類對(duì)象cls
的cache_t
里面,然后對(duì)函數(shù)進(jìn)行調(diào)用。- (4) 在每次進(jìn)行緩存操作之前,首先需要檢查緩存容量,如果緩存內(nèi)的方法數(shù)量超過規(guī)定的臨界值(
設(shè)定容量的3/4
),需要先對(duì)緩存進(jìn)行2倍擴(kuò)容,原先緩存過的方法全部丟棄,然后將當(dāng)前方法存入擴(kuò)容后的新緩存內(nèi)。- (5) 如果在
obj
的cls
對(duì)象里面,發(fā)現(xiàn)緩存和方法列表都找不到mssage
方法,則通過cls
的superclass
指針進(jìn)入它的父類對(duì)象f_cls
里面- (6) 進(jìn)入
f_cls
后,首先在它的cache_t
里面查找mssage
,如果找到了該方法,那么會(huì)首先將方法緩存到消息接受者obj
的類對(duì)象cls
的cache_t
里面,然后調(diào)用方法對(duì)應(yīng)的函數(shù)。- (7) 如果上一步?jīng)]有找到方法,將會(huì)對(duì)
f_cls
的方法列表進(jìn)行遍歷二分/遍歷查找,如果找到了mssage
方法,那么同樣,會(huì)首先將方法緩存到消息接受者obj
的類對(duì)象cls
的cache_t
里面,然后調(diào)用方法對(duì)應(yīng)的函數(shù)。需要注意的是,這里并不會(huì)將方法緩存到當(dāng)前父類對(duì)象f_cls
的cache_t里面。- (8) 如果還沒找到方法,則會(huì)通過
f_cls
的superclass
進(jìn)入更上層的父類對(duì)象里面,按照(6)->(7)->(8)
步驟流程重復(fù)。如果此時(shí)已經(jīng)到了基類對(duì)象NSObject
,仍沒有找到mssage
,則進(jìn)入步驟(9)
- (9) 接下來將會(huì)轉(zhuǎn)到消息機(jī)制的 動(dòng)態(tài)方法解析 階段
消息發(fā)送流程
至此,OC Runtime里面的消息發(fā)送流程
和方法緩存策略
就分析完畢。
Runtime系列文章
Runtime原理探究(一)—— isa的深入體會(huì)(蘋果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime