目錄
前言
方法的本質
向不同對象發送消息
發送實例方法
發送類方法
對象調用方法 實際執行是父類
向父類發送類方法
消息查找流程
開始查找
快速查找流程
慢速查找流程
動態方法決議
應用場景
優化方案
消息轉發機制
快速轉發流程
應用場景
慢速轉發流程
應用場景
前言
在OC底層中,方法的調用實質上是通過消息的發送實現的,這篇文章我們來看一看消息的發送是怎么樣的
方法的本質
方法的本質就是通過objc_msgSend
發送消息,有兩個參數,第一個是id類型,表示消息接受者,第二個,表示方法編號。
向不同對象發送消息
發送實例方法
消息接收者是實例對象
發送類方法
本質上是向類對象發送消息
objc_getClass得到的是類對象
對象調用方法 實際執行是父類
Runtime中提供了一個接口處理這種情況:父類中實現了該方法,而子類沒有實現該方法,子類對象調用方法,會執行父類中實現(符合繼承的特性)
這個接口是objc_msgSendSuper
,使用時還需要用到objc_super
結構體,并給結構體賦值(receiver、super_class)
該結構體中receiver表示接收消息的實例對象,super_class表示父類類對象,根據這個賦值
可以看到這兩種方式都是執行父類的實現,因此可以推斷:方法調用首先在類中查找,如果找不到就到父類中查找
向父類發送類方法
上面向父類發送實例方法時,receiver表示實例對象,super_class表示父類類對象。而如果向父類發送類方法,reciever表示類對象,super_class表示父類元類對象
消息查找流程
消息查找的流程就是通過上層的sel發送消息objc_msgSend找到底層具體imp的實現的過程,objc_msgSend是用匯編寫的而不是用C語言
開始查找
在開始objc_msgSend之后
-
首先會判斷消息接受者是否為空,為空就直接返回
-
然后會判斷是否為小對象,也就是是否為tagged_pointers
-
之后取對象中的isa存到寄存器p13中,根據isa進行mask地址偏移來得到對應的上級對象(類、元類)
取得了上級對象之后,就可以開始快速查找流程了,也就是在緩存中找imp的過程
快速查找流程
-
首先通過類的首地址偏移16字節找到
cache
的地址(cache離首地址16字節,isa占8字節,superclass占8字節),cache
高16位存mask
,低48位存buckets
-
然后從cache中分別取出buckets和mask,根據mask通過哈希算法算出哈希下標,根據哈希下標和bukets首地址來得到對應的bucket,bucket中存放著imp和sel
-
那么怎么確定找到的imp和sel就是要找的那個呢?主要是通過兩層循環:
-
第一層循環:比較bucket中的sel和objc_msgSend中第二個參數_cmd是否相等:如果相等,就直接跳轉到CacheHit,即緩存命中,返回imp;如果不相等,有三種情況:
-
一種是一直找不到,就直接跳轉到
CheckMiss
,因為參數$0是normal,會跳轉到__objc_msgSend_uncached,看英文就能明白意思就是沒找到,這時就會進入慢速查找流程 -
第二種是如果獲取到的bucket是第一個元素,那么就手動把它設置為最后一個元素,然后進行第二層循環
-
如果當前bucket不是第一個元素,那就繼續當前的循環
-
-
第二層循環:和第一層循環基本相同,只是如果bucket還是等于buckets中第一個元素,就直接跳轉到
JumpMiss
,此時也會跳轉到沒找到__objc_msgSend_uncached,進入慢速查找
-
慢速查找流程
慢速查找的過程分為匯編和C兩個部分,這里我們不糾結匯編部分,匯編最后調用的是lookUpImpOrForward,這是一個C實現的函數
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{// 定義的消息轉發const IMP forward_imp = (IMP)_objc_msgForward_impcache;IMP imp = nil;Class curClass;
?runtimeLock.assertUnlocked();
?if (slowpath(!cls->isInitialized())) {// The first message sent to a class is often +new or +alloc, or +self// which goes through objc_opt_* or various optimized entry points.//// However, the class isn't realized/initialized yet at this point,// and the optimized entry points fall down through objc_msgSend,// which ends up here.//// We really want to avoid caching these, as it can cause IMP caches// to be made with a single entry forever.//// Note that this check is racy as several threads might try to// message a given class for the first time at the same time,// in which case we might cache anyway.behavior |= LOOKUP_NOCACHE;}
?// 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.lock();
?// We don't want people to be able to craft a binary blob that looks like// a class but really isn't one and do a CFI attack.//// To make these harder we want to make sure this is a class that was// either built into the binary or legitimately registered through// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.//判斷是否是一個已知的類:判斷當前類是否是已經被認可的類,即已經加載的類checkIsKnownClass(cls);
?//判斷類是否實現,如果沒有,需要先實現,此時的目的是為了確定父類鏈,方法后續的循環cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);// runtimeLock may have been dropped but is now locked againruntimeLock.assertLocked();curClass = cls;
?// The code used to lookup the class's cache again right after// we take the lock but for the vast majority of the cases// evidence shows this is a miss most of the time, hence a time loss.//// The only codepath calling into this without having performed some// kind of cache lookup is class_getInstanceMethod().//----查找類的緩存// unreasonableClassCount -- 表示類的迭代的上限//(猜測這里遞歸的原因是attempts在第一次循環時作了減一操作,然后再次循環時,仍在上限的范圍內,所以可以繼續遞歸)for (unsigned attempts = unreasonableClassCount();;) {if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHESimp = cache_getImp(curClass, sel);if (imp) goto done_unlock;curClass = curClass->cache.preoptFallbackClass();
#endif} else {// curClass method list.//---當前類方法列表(采用二分查找算法),如果找到,則返回,將方法緩存到cache中Method meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {imp = meth->imp(false);goto done;}//當前類 = 當前類的父類,并判斷父類是否為nilif (slowpath((curClass = curClass->getSuperclass()) == nil)) {// No implementation found, and method resolver didn't help.// Use forwarding.//--未找到方法實現,方法解析器也不行,使用轉發imp = forward_imp;break;}}
?// Halt if there is a cycle in the superclass chain.// 如果父類鏈中存在循環,則停止if (slowpath(--attempts == 0)) {_objc_fatal("Memory corruption in class list.");}
?// Superclass cache.// --父類緩存imp = cache_getImp(curClass, sel);if (slowpath(imp == forward_imp)) {// Found a forward:: entry in a superclass.// Stop searching, but don't cache yet; call method// resolver for this class first.// 如果在父類中找到了forward,則停止查找,且不緩存,首先調用此類的方法解析器break;}if (fastpath(imp)) {// Found the method in a superclass. Cache it in this class.//如果在父類中,找到了此方法,將其存儲到cache中goto done;}}
?// No implementation found. Try method resolver once.//沒有找到方法實現,嘗試一次方法解析
?if (slowpath(behavior & LOOKUP_RESOLVER)) {//動態方法決議的控制條件,表示流程只走一次behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}
?done:if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHESwhile (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endif//存儲到緩存log_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock://解鎖runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
}
上面是慢速查找的源碼,用自然語言來表述就是:
-
首先進行一次快速查找,也就是在cache緩存中查找,找到就直接返回imp,沒找到就繼續
-
先判斷cls是否是已知類,如果不是就報錯;再判斷類是否實現,如果沒實現需要先實現,這個時候實現的目的是為了確定它的父類鏈,ro以及rw等,方便之后數據讀取和查找;還要判斷是否初始化,沒有就初始化
-
接下來進入for循環,沿著類或元類的繼承鏈進行查找:
-
對于當前cls,在方法列表中使用二分查找進行查找,如果找到就進入cache寫入流程并返回imp,如果沒找到就返回nil
-
當前cls賦值為父類,如果父類為nil,
imp = 消息轉發
,并終止遞歸,開始判斷是否執行過動態方法解析 -
如果父類鏈中存在循環就報錯
-
在父類中查找時,會先在父類緩存中查找,再在方法列表中查找
-
-
判斷是否執行過動態方法解析,如果沒有就執行動態方法解析,執行過一次的話就走消息轉發流程
在二分查找過程中,如果找到的與key的value值相等,需要先排除分類方法
在進行完快速查找和慢速查找的流程之后,會進入動態方法決議和消息轉發流程
動態方法決議
在查找流程沒找到方法時,有一次機會補救就是動態方法決議,以實例方法為例,程序會走到resolveInstanceMethod方法:
用自然語言描述如下:
-
在發送resolveInstanceMethod消息前,先查找cls中有沒有這個方法的實現,也就是通過lookUpImpOrNil方法進入lookUpImpOrForward慢速查找流程找這個方法:
-
如果沒找到就直接返回
-
如果找到了就發送resolveInstanceMethod消息
-
-
再慢速查找實例方法的實現,又進行一次慢速查找
應用場景
使用動態方法決議可以解決一些方法未實現的報錯,重寫resolveInstanceMethod類方法并在其中將其指向其他方法的實現,比如有一個say666沒實現,但是實現了sayMaster方法
類方法同理,將方法名改為resolveClassMethod即可
優化方案
在上面的場景中,我們需要對每一個類的方法進行重寫,并且我們又知道慢速方法查找路徑最后都會走到根類,因此我們可以為NSObjct添加分類來統一處理
消息轉發機制
如果前面的過程都沒找到該方法,那我也是沒招了(bushi),那就會進行消息轉發流程,消息轉發流程分為快速轉發和慢速轉發,如果方法沒有實現而崩潰報錯,在崩潰之前會調用兩遍動態方法決議,兩遍快速轉發,兩遍慢速轉發
快速轉發流程
forwardingTargetForSelector在源碼中只有聲明,但是我們可以從幫助文檔中看到有關于它的解釋:
-
該方法的返回對象是執行
sel
的新對象,也就是自己處理不了會將消息轉發給別的對象進行相關方法的處理,但是不能返回self
,否則會一直找不到 -
該方法的效率較高,如果不實現,會走到
forwardInvocation:
方法進行處理 -
底層會調用
objc_msgSend(forwardingTarget, sel, ...);
來實現消息的發送 -
被轉發消息的接受者參數、返回值等應和原方法相同
應用場景
比如TCJPerson沒實現的方法,轉發給實現了的TCJStudent
也可以直接調用父類的該方法,如果沒找到的話會直接報錯
慢速轉發流程
methodSignatureForSelector慢速查找流程同樣在幫助文檔中尋找,可以發現forwardInvocation
和methodSignatureForSelector
必須同時存在
底層會通過方法簽名生成一個NSInvocation,作為參數傳遞使用,接著查找可以響應NSInvocation中編碼的消息的對象,找到后使用anInvocation將消息發送給該對象,并且anInvocation保存結果,運行時系統將提取結果并傳遞給原始發送者
應用場景
慢速轉發的流程就是methodSignatureForSelector提供一個方法簽名,然后forwardInvocation通過NSInvocation來實現消息的轉發
無論在forwardInvocation方法中是否處理invocation事務,程序都不會崩潰
方法和消息的流程就到這里了,在上面的過程中你有沒有注意到動態方法決議進行了兩遍這個問題?它為什么會執行兩遍呢?
其實第二次動態方法決議是在methodSignatureForSelector
和 forwardInvocation
方法之間,是開始進行慢速消息轉發之前再給的一次機會