Runtime系列文章
Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優(yōu)化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime
????本文篇幅比較長,創(chuàng)作的目的并不是為了在簡書上刷贊和閱讀量,而是為了自己日后溫習知識所用。如果有幸被你發(fā)現(xiàn)這篇文章,并且引起了你的閱讀興趣,請休息充分,靜下心來,精力充足地開始閱讀,希望這篇文章能對你有所幫助。如發(fā)現(xiàn)任何有誤之處,肯請留言糾正,謝謝。????
承接上一篇的內容,我們回過頭去看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 用于獲取具體的類信息
};
這里面還有一個cache_t cache
沒有解讀過,一起來看一看這個東西。看名字很好理解,就是緩存的意思,緩存什么呢?——緩存方法。
它的底層是通過散列表(哈希表)
的數(shù)據(jù)結構來實現(xiàn)的,用于緩存曾經調用過的方法,可以提高方法的查找速度。
首先,回顧一下正常情況下方法調用的流程。假設我們調用一個實例方法[bj XXXX];
obj
->isa
->obj
的Class
對象 ->method_array_t methods
-> 對該表進行遍歷查找,找到就調用,沒找到繼續(xù)往下走obj
->superclass
->obj
的父類 ->isa
->method_array_t methods
-> 對父類的方法列表進行遍歷查找,找到就調用,沒找到就重復本步驟- 找到就調用,沒找到重復流程
- 找到就調用,沒找到重復流程
- 找到就調用,沒找到重復流程
- 直到
NSObject
->isa
->NSObject
的Class
對象 -> method_array_t methods ......
如果XXXX
方法在程序內會被頻繁的調用,那么這種逐層便利查找的方式肯定是效率低下的,因此蘋果設計了cache_t cache
,當XXXX
第一次被調用的時候,會按照常規(guī)流程查找,找到之后,就會被加入到cache_t cache
中,當再次被調用的時候,系統(tǒng)就會直接現(xiàn)到cache_t cache
來查找,找到就直接調用,這樣便大大提升了查找的效率。
剛才介紹了cache_t cache
是通過散列表
來實現(xiàn)的,下面就來著重分析一下,方法是如何被緩存的。散列/哈希表,想必大部分iOS開發(fā)這至少應該聽過,而我們常用的NSDictionary
其實就是一種散列表數(shù)據(jù)結構。來看一下cache_t cache
的定義
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
-
struct bucket_t *_buckets;
—— 用來緩存方法的散列/哈希表 -
mask_t _mask;
—— 這個值 = 散列表長度 - 1 -
mask_t _occupied;
—— 表示已經緩存的方法的數(shù)量
上面介紹的_buckets
散列表里面的存儲單元是bucket_t
,來看看它包含了方法的什么信息
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
-
cache_key_t _key;
—— 這個key實際上就是方法的SEL,也就是方法名 -
IMP _imp;
—— 這個就是方法對應的函數(shù)的內存地址
想一想我們平時是怎么使用NSDictionary的,通過一堆Key-Value鍵值對來進行存儲的,NSDictionary的底層就是散列表,這個剛才說過。方法緩存的時候,key就是上面的cache_key_t _key;
,value就是上面的bucket_t
結構體對象。
但是散列表的運作原理到底如何呢,這個屬于數(shù)據(jù)結構問題,這里簡要介紹一下。首先散列表本質上就是一個數(shù)組
key
計算出一個index,然后再將元素插入散列表的index位置那么從散列表里面取值就顯而易見了,根據(jù)一個key,計算出index,然后到散列表對應位置將值取出
這里的查詢方法的時候(也就是取值操作),時間復雜度為O(1), 對比我們一開始從方法列表的遍歷查詢,它的時間復雜度為O(n),因此通過緩存方法,可以極大的提高方法查詢的效率,從而提高了方法調用機制的效率。
根據(jù)key計算出index值的這個算法稱作散列算法,這個算法可以由你自己設計,總之目的就是盡可能減少不同的key得出相同index的情況出現(xiàn),這種情況被稱作哈希碰撞,同時還要保證得出的index值在合理的范圍。index越大,意味著對應的散列表的長度越長,這是需要占用實際物理空間的,而我們的內存是有限的。散列表是一種通過犧牲一定空間,來換取時間效率的設計思想。
我們通過key計算出的index大小是隨機的,無順序的,因此在方法緩存的過程中,插入的順序也是無順序的而且可以預見的是,散列表里面再實際使用中會有很多位置是空著的,比如散列表長度為16,最終值存儲了10個方法,散列表長度為64,最終可能只會存儲40個方法,有一部分空間終究是要被浪費的。但是卻提高查找的效率。這既是所謂的空間換時間。
再介紹一下蘋果這里所采用的散列算法,其實很簡單,如下
index = @selector(XXXX) & mask
根據(jù)&運算的特點,可以得知最終index <= mask
,而mask
= 散列表長度 - 1,也就是說 0 <= index <= 散列表長度 - 1
,這實際上覆蓋了散列表的索引范圍。而剛剛我們還提到過一個問題——哈希碰撞,也就是不同的key得到相同的index,該怎么處理呢?我們看一下源碼,在objc源碼里面搜索cache_t,可以發(fā)現(xiàn)一個跟查找相關的方法
bucket_t * cache_t::find(cache_key_t k, id receiver) //根據(jù)key值 k 進行查找
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m); //通過cache_hash函數(shù)【begin = k & m】計算出key值 k 對應的 index值 begin,用來記錄查詢起始索引
mask_t i = begin; // begin 賦值給 i,用于切換索引
do {
if (b[i].key() == 0 || b[i].key() == k) {
//用這個i從散列表取值,如果取出來的bucket_t的 key = k,則查詢成功,返回該bucket_t,
//如果key = 0,說明在索引i的位置上還沒有緩存過方法,同樣需要返回該bucket_t,用于中止緩存查詢。
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// 這一步其實相當于 i = i-1,回到上面do循環(huán)里面,相當于查找散列表上一個單元格里面的元素,再次進行key值 k的比較,
//當i=0時,也就i指向散列表最首個元素索引的時候重新將mask賦值給i,使其指向散列表最后一個元素,重新開始反向遍歷散列表,
//其實就相當于繞圈,把散列表頭尾連起來,不就是一個圈嘛,從begin值開始,遞減索引值,當走過一圈之后,必然會重新回到begin值,
//如果此時還沒有找到key對應的bucket_t,或者是空的bucket_t,則循環(huán)結束,說明查找失敗,調用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ù)還被源碼里面的另一個函數(shù)調用過——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);
}
上面源碼的最后一段以及它的注釋說明可以明白,當通過cache->find
返回的bucket->key() == 0
,就說明該位置上是空的,沒有緩存過方法,是一個unused slot
(未使用的槽口),因此可以進行插入操作bucket->set(key, imp);
,也就是將方法緩存到這個位置上。
根據(jù)上面的分析,下面用圖示來總結一下方法存入cache_t中,以及從cache_t中取方法的整體流程
向cache_t存入方法
從cache_t查詢方法
你可能還會有一個疑問,如果不斷的往緩存里添加方法,緩存滿了怎么辦?我們回到剛才看過的一段代碼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;
說的很明白,擴容就是將當前緩存容量* 2
,如果是首次調用這個函數(shù),會使用一個初始容量值INIT_CACHE_SIZE
來設定緩存容量
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
從INIT_CACHE_SIZE
的定義顯示它的值是4,也就是說蘋果給cache_t設定的初始容量是4。
你可能還會問,重置緩存之后,原來老緩存里面的內容還要不要呢,expand()
函數(shù)里面調用的最后一個函數(shù)是reallocate(oldCapacity, newCapacity);
,我們在進入它的源碼看看
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);
}
}
很明顯,在最對舊的緩存空間進行了釋放,但是條件是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ù)其實很簡單,就是判斷一下緩存是不是空的,如果空的,舊沒必要釋放空間了,如果原來的緩存不是空的,就直接釋放掉,并且我們發(fā)現(xiàn),擴容的操作里面,并沒有對舊的緩存空間里面的內容進行復制保留,就是很粗暴的直接分配一塊新的緩存空間,然后直接釋放掉舊的緩存空間,這意味著,每次進行擴容操作之后,原來緩存過的方法就會全部丟失,而上面的cache_fill_nolock
函數(shù)里面,在進行完expand()
擴容操作之后,也僅僅是把當前處理的方法放到緩存空間里面,因此,擴容之前曾經被緩存過的方法,如果下次再次調用的話,有需要被重新緩存了。這里好好體會一下。
父類的方法被調用的時候,會如何緩存?
現(xiàn)在,我們知道,當對一個對象發(fā)送消息后,會通過對象的isa找到它的Class對象,在Class對象里面先從方法緩存cache_t查找該方法,沒有的話再對Class對象的方法列表進行遍歷查找,如果找到了方法,就進行緩存并且調用,那么這里肯定是將方法緩存到了該對象的Class對象的cache_t里面。
如果在當前Class對象里面沒有找到該方法,那么會通過Class對象的superclass進入其父類的Class對象里面,同樣,會先查找它的cache_t,如果沒有找到方法,會對其方法列表進行遍歷查找,問題就在這里,如果此時在方法列表里面找到了方法,進行緩存操作的時候,是會將方法存入當前父類的Class對象的cache_t里面呢,還是會存到接收消息的對象的Class對象的cache_t里面呢?
要搞清楚這個問題,首先可以看一下哪些地方調用了static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
函數(shù),因為它的參數(shù)里面?zhèn)魅肓艘粋€Class cls
我們只需要搞清楚這個Class cls
到底是誰。
根據(jù)下圖的操作進入上層調用函數(shù)cache_fill
用同樣方法查看一下
cache_fill
的上層調用函數(shù),如下圖首先來看一下這個
log_and_cache()
lockUpImpOrForward()
函數(shù)調用的。
接下來我們在先看一下lookUpMethodInClassAndLoadCache()
函數(shù)
最后在來看一下剩下的那個 lookUpImpOrForward
函數(shù),下面代碼請看??????處標記的中文注解即可
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.-------------------------------->??????標準的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) {//------------------------------>??????查詢當前Class對象的緩存,如果找到方法,就返回該方法
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()) {//------------------------------>??????當前Class如果沒有被realized,就進行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()) {//-------------->??????當前Class如果沒有初始化,就進行初始化操作
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對象的緩存中查找,如果找到,就跳到done處返回該方法
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.//---------------->??????嘗試從該Class對象的方法列表中查找,找到的話,就緩存到該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.------>??????進入當前Class對象的superclass對象
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;//------>??????該for循環(huán)每循環(huán)一次,就會進入上一層的superclass對象,進行循環(huán)內部方法查詢流程
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.------>??????在當前superclass對象的緩存進行查找
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;//------>??????如果在當前superclass的緩存里找到了方法,就調用log_and_fill_cache進行方法緩存,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對象所對應的Class對象的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),進入上一層的superclass,再次進行查找
}
}
// Superclass method list.---->??????如過畫緩存里面沒有找到方法,則對當前superclass的方法列表進行查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//------>??????如果在當前superclass的方法列表里找到了方法,就調用log_and_fill_cache進行方法緩存,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對象所對應的Class對象的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.//------>??????如果到基類還沒有找到方法,就嘗試進行方法解析
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. //------>??????如果方法解析不成功,就進行消息轉發(fā)
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
關于上面再方法列表查找的函數(shù)Method meth = getMethodNoSuper_nolock(cls, sel);
還需要說明一下,進入它的實現(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;
}
再進入其核心函數(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)) {
//---??????如果方法列表是經過排序的,則進行二分查找
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
//---??????如果方法列表沒有進行排序,則進行線性遍歷查找
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ù)代碼中的邏輯,如果方法列表是經過排序的,會使用findMethodInSortedMethodList
進行查找,而這里面實際上是用二分法進行查找的,具體代碼如下
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相當于 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;
}
經過上述解讀,我們已經基本了解方法查詢和方法緩存所涉及到的細節(jié),現(xiàn)在可以把方法查找和方法緩存流程結合起來描述一下 Runtime消息機制 當中的 消息發(fā)送 流程
- (1) 當一個對象接收到消息時
[obj message];
,首先根據(jù)obj
的isa
指針進入它的類對象cls
里面。- (2) 在
obj
的cls
里面,首先到緩存cache_t
里面查詢方法message
的函數(shù)實現(xiàn),如果找到,就直接調用該函數(shù)。- (3) 如果上一步沒有找到對應函數(shù),在對該
cls
的方法列表進行二分/遍歷查找,如果找到了對應函數(shù),首先會將該方法緩存到obj
的類對象cls
的cache_t
里面,然后對函數(shù)進行調用。- (4) 在每次進行緩存操作之前,首先需要檢查緩存容量,如果緩存內的方法數(shù)量超過規(guī)定的臨界值(
設定容量的3/4
),需要先對緩存進行2倍擴容,原先緩存過的方法全部丟棄,然后將當前方法存入擴容后的新緩存內。- (5) 如果在
obj
的cls
對象里面,發(fā)現(xiàn)緩存和方法列表都找不到mssage
方法,則通過cls
的superclass
指針進入它的父類對象f_cls
里面- (6) 進入
f_cls
后,首先在它的cache_t
里面查找mssage
,如果找到了該方法,那么會首先將方法緩存到消息接受者obj
的類對象cls
的cache_t
里面,然后調用方法對應的函數(shù)。- (7) 如果上一步沒有找到方法,將會對
f_cls
的方法列表進行遍歷二分/遍歷查找,如果找到了mssage
方法,那么同樣,會首先將方法緩存到消息接受者obj
的類對象cls
的cache_t
里面,然后調用方法對應的函數(shù)。需要注意的是,這里并不會將方法緩存到當前父類對象f_cls
的cache_t里面。- (8) 如果還沒找到方法,則會通過
f_cls
的superclass
進入更上層的父類對象里面,按照(6)->(7)->(8)
步驟流程重復。如果此時已經到了基類對象NSObject
,仍沒有找到mssage
,則進入步驟(9)
- (9) 接下來將會轉到消息機制的 動態(tài)方法解析 階段
消息發(fā)送流程
至此,OC Runtime里面的消息發(fā)送流程
和方法緩存策略
就分析完畢。
Runtime系列文章
Runtime原理探究(一)—— isa的深入體會(蘋果對isa的優(yōu)化)
Runtime原理探究(二)—— Class結構的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問底消息機制
Runtime原理探究(五)—— super的本質
[Runtime原理探究(六)—— Runtime的應用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime