我在工程裏準備了這麼一個類LCHero,有一個對象方法throwSkill
, 繼承至LCPerson。 LCPerson裏面有一個對象方法attack
, 一個類方法defence
,LCPerson 繼承至NSObject. 我在NSObject的一個分類裏準備了一個測試方法test
代碼以下:c++
#import <Foundation/Foundation.h>
#import "LCHero.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCHero *hero = [LCHero new];
[hero throwSkill];
}
return 0;
}
/******************************************/
@interface LCPerson : NSObject
- (void)attack;
+ (void)defence;
- (void)revival;
@end
@implementation LCPerson
- (void)attack {
NSLog(@"%s --> 開始進攻!",__func__);
}
+ (void)defence {
NSLog(@"%s ||-- 開始防護! ",__func__);
}
@end
/******************************************/
@interface LCHero : LCPerson
- (void)throwSkill;
@end
@implementation LCHero
- (void)throwSkill {
NSLog(@"%s --> 釋放終極技能!",__func__);
}
@end
/******************************************/
@interface NSObject (test)
- (void)test;
@end
@implementation NSObject (test)
- (void)test {
NSLog(@"%s, 測試一下!",__func__);
}
@end
複製代碼
使用的命令以下:程序員
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk main.m
複製代碼
咱們看到的對應的cpp文件中對main文件中咱們寫的內容的c++編譯內容objective-c
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCHero *hero = ((LCHero *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCHero"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)hero, sel_registerName("throwSkill"));
}
return 0;
}
複製代碼
若是把方法的類型以及類型轉換去掉,就是以下的樣式。咱們發現底層調用的是一個objc_msgSend
方法,咱們調用的方法被轉成了SEL。數組
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LCHero *hero = (LCHero *)objc_msgSend(objc_getClass("LCHero"), sel_registerName("new"));
(void *)objc_msgSend(hero, sel_registerName("throwSkill"));
}
return 0;
}
複製代碼
既然咱們知道了底層吊起的是objc_msgSend,那麼咱們在方法調用以前打一個調試斷點,當斷點來了以後咱們按住control
鍵 + step into 一步一步點擊看看它會怎麼走。 緩存
來到這一步以後,咱們想既然方法這些都在類裏面,而類和對象是經過isa聯繫起來的,是否是要找isa呢?而後經過isa找到對應的類,答案是確定的。bash
找到class以後開始查找類中的方法緩存。 多線程
慢速查找
經過上面那個景點咱們看到了_class_lookupMethodAndLoadCache3
方法,源碼中以下:發現它直接調起一個下層方法lookUpImpOrForward
,_class_lookupMethodAndLoadCache3只是起到中間鏈接做用app
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼
這個方法裏面的內容有點多同時也很重要
,我就把判斷、斷言、賦值的代碼都刪掉了,保留一些關鍵的方法和註釋。你們先過個眼隱。咱們再一一分析下。oop
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
if (!cls->isRealized()) { //先判斷類有沒有加載到內存,若是沒有,先加載類
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {//判斷是否實現了initialize,若是有實現,先調用initialize
_class_initialize (_class_getNonMetaClass(cls, inst));
}
retry:
// Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; // Try this class's method lists.
//從類的方法表裏查詢,若是有就返回imp 順便存一份到類的cache裏
// Try superclass caches and method lists.
// Found the method in a superclass. Cache it in this class.
// No implementation found. Try method resolver once.
// No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlock(); return imp; } 複製代碼
cls->isRealized()
判斷類是否加載了,realizeClass(cls)
這是一個遞歸操做,全部繼承鏈上的類都會被加載。父類-->根類-->根元類,直到cls爲空才退出遞歸。methodizeClass(cls)
分類方法加載咱們來看看他們作了什麼,主線流程我在代碼塊中介紹了先看註釋,我英文不太好可是大概意思明白了,把方法、協議、屬性安排好,而後把外面的分類也添加進來 --> 感受看到了美景 ><
/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list. * Attaches any outstanding categories. * Locking: runtimeLock must be held by the caller **********************************************************************/ 咱們把這個方法的主要內容拆開說明一下 1. 把方法、屬性、協議從類的ro 拷貝到 rw中來,爲啥有個1?-->是由於把數組的首地址傳進去組成了一個二維數組 method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } 2. 判斷是不是根元類,若是是根元類須要把方法加載一下,確保在分類替換以前就已經加載好了 --> ? 下面的註釋也有, 就是說若是根類調用了一個它本身沒有的方法,它會往根元類中找。 個人根元類要是有相關方法我要把他添加到個人類的方法表裏面,它才能找的獲得,並且要早於分類方法添加以前。 爲何是在分類替換以前呢?我在這裏只能大膽猜測一下,方法表設計的多是一個相似棧的表, 若是有分類在後面添加以後那麼我就找imp的時候就先找到分類的imp就返回了,就出現了替換了類的方法的這麼個現象 // Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}
3. 添加分類方法
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/); 複製代碼
咱們前面已經找過緩存了,爲何還要找緩存呢?緣由有2:組件化
Method meth = getMethodNoSuper_nolock(cls, sel);//找類的方法表
log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就緩存一份
imp = meth->imp;
goto done
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
imp = cache_getImp(curClass, sel); //遞歸找父類的方法表
log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就緩存一份
imp = meth->imp;
goto done
複製代碼
若是咱們找了類的方法表,同時遞歸找了父類都沒有找到,因爲咱們傳遞的resolver默認是YES
同時triedResolver也沒有進行重新賦值仍是NO,咱們會走下到下一站方法決議_class_resolveMethod
,具體源碼在下面,
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// 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;
}
複製代碼
在經歷了類->父類->元類->根元類->...->NSObject ,分類等一系列的查找以後沒有找到,那而後怎麼辦?咱們的旅程還要繼續啊!蘋果爸爸仍是很心疼咱們的,給你個機會處理一下吧,我不直接讓它崩潰。因而咱們看到了下一站的風景_class_resolveMethod(cls, sel, inst)
咱們來看下這個方法裏有啥東西,具體代碼在下面代碼塊。步驟在下面分析
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
複製代碼
爲何要判斷?--> 對象方法在類裏面,類方法在元類裏面
註釋中寫try [cls resolveInstanceMethod:sel] ?--> 咱們的類裏面沒有怎麼try? 是否是系統幫咱們實現了這個方法,不知道,繼續往下看
先判斷是否沒有實現這個方法,若是沒有就直接返回。我本身寫的類裏面沒有這個方法,若是咱們本身沒有實現的話是否是系統幫咱們實現?
在objc的源碼中找到了這個方法,默認返回的是NO.
如今咱們思考一下,若是我能讓這個方法執行下去勢必要找到一個imp返回回去。若是咱們重寫這個方法而後添加一個imp到類裏面是否是就解決這個問題了呢?咱們去文檔中查一下這個方法,果真驗證了咱們的想法。
無論咱們有沒有處理lookUpImpOrNil
都會調起,而後再回到lookUpImpOrForward
,由於已經retry過了這個結果已經保存了,若是找到imp直接到done流程,若是仍是沒找到就會來到imp = (IMP)_objc_msgForward_impcache
.
咱們搜一下,結果這個傢伙在彙編裏面調用的是__objc_msgForward
。
咱們再看看__objc_msgForward
,它裏面調用的有兩個很像的方法,再搜一下就發如今objc的源碼中有實現,這裏的打印內容咱們好熟悉哦,咱們來試下在咱們開始準備的main裏面調用LCHero沒有實現的對象方法- (void)revival
看看錯誤輸出 --> 沒錯就是沒找到方法的報錯輸出
類方法和對象方法處理有點不太同樣調用的是resolveClassMethod
,添加imp是往元類裏面添加,只是和對象方法相似的處理流程只不過調用的方法不同,只是調用完類方法決議以後竟然還走了對象方法的決議
。咱們就猜測爲何還要走這步。
lookUpImpOrNil
再次查找一下若是仍是沒有就會走一次對象方法決議_class_resolveInstanceMethod
。_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
複製代碼
可是就這樣結束了麼?咱們再來看看奔潰時候的堆棧是否是還有咱們沒了解過的方法,明顯還有。
log_and_fill_cache
方法,除了fill_cache 還有log,不妨一看究竟。發現調用了
logMessageSend
,再往裏面看看發現了一個相似log開關控制的參數
這裏咱們不妨大膽地玩一下,由於根據咱們在log_and_fill_cache的流程中發現它有個打印log的方法,還會寫到一個文件裏,這個文件里根據它的註釋說會給咱們一些方法有關的線索。咱們把這個開關在咱們準備的工程中拓展一下使用範圍在奔潰先後都調用了啥方法?
再運行一下,咱們在/tmp/msgSends 找有沒有相似的文檔記錄log
打開一看,OMG, 啥!! 都打印出來的確有兩個咱們沒跟出來的流程一個是forwardingTargetForSelector
,另外一個是methodSignatureForSelector
。突然發現咱們還想逛的還要不少,咱們都想看看後面兩站都是什麼風景!!
表面意思是傳遞一個對象,什麼意思?原來我方法調用時傳入的對象不要了麼? 咱們來看看源碼是否是NSObject也實現了只是跟上一個站點同樣沒有作處理額?
- (void)revival
@implementation LCEnemy
- (void)revival {
NSLog(@"%s --> 哈哈哈 本魔王又活了!",__func__);
}
@end
複製代碼
forwardingTargetForSelector
,返回一個LCEnemy對象。-(id)forwardingTargetForSelector:(SEL)aSelector {
return [NSClassFromString(@"LCEnemy") alloc];
}
複製代碼
到這個流程以後也就意味着:
那這個時候我是否是要在網上發給求助帖-->看看哪一個好心人能處理。 可是總得知道你這個方法是什麼格式吧,否則別人怎麼知道能不能處理?-->方法調用必須簽名和SEL 相匹配纔會被調用。 咱們來看看官方文檔怎麼解釋的
- (void)forwardInvocation:(NSInvocation *)anInvocation;
那麼咱們來試一下:
// 方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s",__func__);
if (aSelector == @selector(revival)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息轉發 -- 開始祈禱誰來處理一下
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s",__func__);
NSLog(@"%@",anInvocation);
}
複製代碼
若是咱們調用沒有實現的方法,既沒有動態決議、也沒有轉發給其餘對象處理同時也沒有寫求助帖🙏好心人來處理此時蘋果爸爸也幫不了你,只好結束你的方法旅程,給你一張紅色的回程票doesNotRecognizeSelector
--> 熟悉的崩潰。
objc_msgSend彙編流程 -->從類緩存中快速查找imp
_class_lookupMethodAndLoadCache3 --> 開始進入c、c++慢速查找
lookUpImpOrForward --> 繼承鏈上的類的方法表裏遍歷查找,找到了緩存一份而後返回imp
_class_resolveMethod --> 開始查看是否有動態決議,若是有給到imp,從新lookUpImpOrForward
forwardingTargetForSelector --> 本身沒有處理,是否有交給別人代理處理。
methodSignatureForSelector + forwardInvocation --> 若是也沒有代理者,請按照規範寫求助信
doesNotRecognizeSelector --> 若是什麼都不作,你太懶了 蘋果爸爸表示上帝也救不了你。
複製代碼
通過這段方法探索的旅程我領悟了一些東西
void instrumentObjcMessageSends(BOOL flag)
,在合適的時候拓展做用域就能跟蹤一些方法調用的線索。感謝你們的閱讀,若是你以爲寫得還能夠請動動大家的小手給我點個贊。我會更有動力給你們分享一下好東西。下一次計劃更新關於類的加載的文章。有興趣交流學習的能夠加我QQ:578200388