Runtime底層原理--IMP查找流程、動態(tài)方法解析鸠踪、消息轉(zhuǎn)發(fā)源碼分析

Runtime底層原理

了解了Runtime函數(shù)含義丙者,我們就可以直接使用Runtime的API了,那接下來繼續(xù)探究Runtime的源碼营密,經(jīng)過源碼分析來更加深刻的了解Runtime原理械媒。

開發(fā)應(yīng)用

都知道Runtime很重要,但是有很多小伙伴根本不了解,或者只是知道可以替換方法啊纷捞、可以交換兩個方法的調(diào)用痢虹,項目中也用不到,
從進入iOS開始主儡,寫了無數(shù)個[[objc alloc] init]奖唯,這個到底在干嘛?初始化和init糜值?alloc和init到底做了什么丰捷?

通過匯編查看方法調(diào)用
        Person *person = [Person alloc];
        Person *person1 = [person init];
        Person *person2 = [person init];
        NSLog(@"%p-----%p------%p", person, person1, person2);

這里會輸出什么呢?

0x10102e1a0-----0x10102e1a0------0x10102e1a0

來寂汇,讓我們斷點看下病往,allocinit是怎么調(diào)用的

objc_msgSend

我們看到調(diào)用allocinit都調(diào)起了objc_msgSend,接下來跟著符號斷點走

libobjc
callAlloc

進入libobjc庫的dylib之后走+[NSObject alloc]方法健无,指針調(diào)起_objc_rootAlloc荣恐,進入_objc_rootAlloc方法,繼續(xù)調(diào)起callAlloc累贤,通過寄存器内斯,可以看到alloc已經(jīng)通過類創(chuàng)建實例對象

類對象

init按照同樣方法 依然可以通過匯編看出方法調(diào)用順序蟹倾,可以用真機進行測試并打印

通過編譯C++

當新的對象被創(chuàng)建時,其內(nèi)存同時被分配,實例變量也同時被初始化残拐。對象的第一個實例變量是一個指向該對象的類結(jié)構(gòu)的指針,叫做 isa错沽。通過該指針按灶,對象可以訪問它對應(yīng)的類以及相應(yīng)的父類。在 Objective-C 運行時系統(tǒng)中對象需要有 isa 指針始鱼,我們一般創(chuàng)建的從 NSObject 或者 NSProxy 繼承的對象都自動包括 isa 變量仔掸。接下來看下對象被創(chuàng)建的過程
首先,我們通過clang命令

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.c++

也可以用clang -rewrite-objc main.m -o test.c++命令医清,只不過會有很多警告起暮、代碼會更長(大概9萬多行)。
編譯main函數(shù)中的OC代碼為C++代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        [p run];
  
    }
    return 0;
}

編譯后多一個testMain.c++文件会烙,打開后在代碼最后面會發(fā)現(xiàn)我們的main函數(shù)

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));

    }
    return 0;
}

可以看出负懦,我們的方法調(diào)用會編譯成objc_msgSend,

person對象

由此還會發(fā)現(xiàn)對象的本質(zhì)其實就是一個結(jié)構(gòu)體

下層通訊(通過源碼查看objc_msgSend內(nèi)部實現(xiàn))

首先我們到蘋果open source官網(wǎng)下載最新源碼

源碼

方法調(diào)用的時候柏腻,會發(fā)送objc_msgSend消息纸厉,objc_msgSend會根據(jù)sel找到函數(shù)實現(xiàn)的指針imp,進而執(zhí)行函數(shù)五嫂,那sel是如何找到imp的呢颗品?
objc_msgSend在發(fā)送消息時候根據(jù)sel查找imp有兩種方式

  • 快速(通過匯編的緩存快速查找)
  • 慢速(C配合C++、匯編一起查找)
    先看下objc_class
objc_class

bits中包含各種數(shù)據(jù),cache(每個類都有一個)用來存儲方法select和imp抛猫,select和imp會以哈希表形式存在
objc_msgSend在快速查找的時候蟆盹,就是通過匯編查找objc_class中的cache,如果找到則直接返回闺金,否則通過C的lookup逾滥,找到后再存入cache

匯編部分快速查找

首先調(diào)用objc_msgSend會走到ENTRY

ENTRY

先判斷p0檢查是否為空和tagged pointer(特殊類型)判斷,調(diào)用LNilOrTagged進行isa處理败匹,通過isa找到相應(yīng)類class寨昙,最后調(diào)用LGetIsaDone來執(zhí)行CacheLookup在緩存中查找imp,如果查找到直接調(diào)起imp否則調(diào)起objc_msgSend_uncached掀亩,objc_msgSend_uncached有兩種情況

CacheLookup

首先舔哪,第一個是CacheHit,直接調(diào)起imp槽棍,第二個是CheckMiss捉蚤,之后調(diào)用objc_msgSend_uncached,第三個就是add炼七,下面是CacheHit和CheckMiss的宏

CacheLookup macro

那如果在緩存中沒有查找到imp缆巧,調(diào)起objc_msgSend_uncached,在方法列表中找到imp之后再TailCallFunctionPointer調(diào)起imp

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup      // 方法列表中找到imp
    TailCallFunctionPointer x17

重點:MethodTableLookup是怎么操作的

小知識點:通過method list查找method豌拙,下面是method_t的結(jié)構(gòu)陕悬,method其實是一個哈希表,sel和imp是鍵值對

struct method_t {
    SEL name;
    const char *types;       // 參數(shù)類型
    MethodListIMP imp;
    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

進入MethodTableLookup之后按傅,調(diào)起了__class_lookupMethodAndLoadCache3捉超,如下圖

MethodTableLookup

__class_lookupMethodAndLoadCache3是C方法,再次進入_class_lookupMethodAndLoadCache3方法唯绍,注意拼岳,因為這里由匯編跳轉(zhuǎn)到C,所以要全局搜索_class_lookupMethodAndLoadCache3况芒,要刪去一個"_",下面是_class_lookupMethodAndLoadCache3函數(shù)

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
C/C++部分查找

調(diào)起lookUpImpOrForward裂问,因為當前cls對象已經(jīng)經(jīng)過匯編編譯到結(jié)構(gòu),有了isa牛柒,并且在cache中沒有找到,所以這里的initialize為YES痊乾,cache為NO皮壁,resolver為YES

image.png

進入lookUpImpOrForward,這里再次判斷是否存在cache哪审,如果有則直接快速查找蛾魄,但是這里是NO,所以不會走。接下來走checkIsKnownClass判斷是否是已經(jīng)聲明的類滴须,如果沒有則報錯"Attempt to use unknown class %p."舌狗,之后走realizeClass判斷是否已經(jīng)實現(xiàn),如果就相應(yīng)賦值data扔水。

realizeClass

data賦值后走_class_initialize初始化cls痛侍,接下來開始retry操作。
前方高能
再次進行cache_getImp魔市,why主届?并發(fā)啊,還有重映射(在初始化init的時候有個remap(class)第一次通過匯編找不到待德,但是在加載類的時候?qū)Ξ斍邦愡M行重映射)

cache_getImp

接下來開始先在自己的class_rw_t的methods中根據(jù)sel查找方法返回method_t

method_t

如果拿到Method后保存到緩存中君丁,保證以后調(diào)用可以直接走匯編的CacheHit快速查找,如果拿不到則繼續(xù)從父類開始查找将宪,直到找到NSObject(因為NSObject的父類為nil)绘闷,如果找到imp則一樣保存在緩存中,如果到最后還是沒有查找到较坛,則進入動態(tài)方法解析印蔗。


父類查找方法
動態(tài)方法解析

如果前面一系列操作還是沒有找到方法,那么就會進行動態(tài)方法解析燎潮,動態(tài)方法解析只執(zhí)行一次

動態(tài)方法解析

首先執(zhí)行_class_resolveMethod喻鳄,這里會執(zhí)行+resolveClassMethod 或者 +resolveInstanceMethod

class resolveMethod

先判斷當前cls是否為元類确封,如果是元類則執(zhí)行_class_resolveClassMethod除呵,再執(zhí)行_class_resolveInstanceMethod,如果不是元類則直接執(zhí)行_class_resolveInstanceMethod爪喘,_class_resolveInstanceMethod內(nèi)部調(diào)用objc_msgSend實現(xiàn)消息發(fā)送颜曾,對cls發(fā)送了SEL_resolveInstanceMethod類型的消息,所以在方法中會走到resolveInstanceMethod方法秉剑。

class resolveInstanceMethod

為什么元類最后也執(zhí)行了_class_resolveInstanceMethod方法呢泛豪?因為類方法以實例對象的形態(tài)存在元類里面,比如類方法中沒有找到方法侦鹏,會去元類中查找诡曙,元類中沒有再繼續(xù)去根元類中查找,最后會查到NSObject略水。

代碼示例:

.h實現(xiàn)

- (void)run;
+ (void)eat;

.m實現(xiàn)(沒有實現(xiàn)-run方法和+eat方法)

- (void)walk {
    NSLog(@"%s",__func__);
}
+ (void)drink {
    NSLog(@"%s",__func__);
}

// .m沒有實現(xiàn),并且父類也沒有,那么我們就開啟動態(tài)方法解析
//- (void)walk{
//    NSLog(@"%s",__func__);
//}
//+ (void)drink{
//    NSLog(@"%s",__func__);
//}


#pragma mark - 動態(tài)方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        // 我們動態(tài)解析我們的 對象方法
        NSLog(@"對象方法解析走這里");
        SEL walkSEL = @selector(walk);
        Method readM= class_getInstanceMethod(self, walkSEL);
        IMP readImp = method_getImplementation(readM);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(self, sel, readImp, type);
    }
    return [super resolveInstanceMethod:sel];
}


+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(eat)) {
        // 我們動態(tài)解析我們的 對象方法
        NSLog(@"類方法解析走這里");
        SEL drinkSEL = @selector(drink);
        // 類方法就存在我們的元類的方法列表
        // 類 類犯法
        // 元類 對象實例方法
        //        Method hellowordM1= class_getClassMethod(self, hellowordSEL);
        Method drinkM= class_getInstanceMethod(object_getClass(self), drinkSEL);
        IMP drinkImp = method_getImplementation(drinkM);
        const char *type = method_getTypeEncoding(drinkM);
        NSLog(@"%s",type);
        return class_addMethod(object_getClass(self), sel, drinkImp, type);
    }
    return [super resolveClassMethod:sel];
}
消息轉(zhuǎn)發(fā)

經(jīng)歷了動態(tài)方法決議還沒有找到价卤,會進入蘋果尚未開源的消息轉(zhuǎn)發(fā),繼續(xù)查找方法渊涝,_objc_msgForward_impcache再次跨域到匯編慎璧。

消息轉(zhuǎn)發(fā)

走到__objc_msgForward_impcache后執(zhí)行__objc_msgForward

__objc_msgForward_impcache

沒有了源碼實現(xiàn)床嫌,但是我們可以通過instrumentObjcMessageSends函數(shù)來打印調(diào)用堆棧信息⌒厮剑可以進入instrumentObjcMessageSends內(nèi)部看下具體實現(xiàn)厌处。

instrumentObjcMessageSends

先判斷了是否可以寫入日志信息等,接下來同步日志文件

logMessageSend

所以我們每次運行會在/private/tmp文件下多一個msgSends-xxx文件岁疼,里面是所有調(diào)用過程

堆棧調(diào)用信息

如果還沒有找到的話最后會報錯調(diào)用__objc_forward_handler

__objc_forward_handler

這也是我們在方法報錯的時候會報unrecognized selector sent to instance %p " "(no message forward handler is installed)"錯誤的原因阔涉,會提示出元類信息,+或者-方法五续,方法的名字還有SEL方法編號

代碼示例:
#pragma mark - 實例對象消息轉(zhuǎn)發(fā)

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    //    if (aSelector == @selector(run)) {
    //        // 轉(zhuǎn)發(fā)給Student對象
    //        return [Student new];
    //    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(run)) {
        // forwardingTargetForSelector 沒有實現(xiàn)洒敏,就只能方法簽名了
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"------%@-----",anInvocation);
    anInvocation.selector = @selector(walk);
    [anInvocation invoke];
}

#pragma mark - 類消息轉(zhuǎn)發(fā)

+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    return [super forwardingTargetForSelector:aSelector];
}
//

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(walk)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    
    NSString *sto = @"奔跑吧";
    anInvocation.target = [Student class];
    [anInvocation setArgument:&sto atIndex:2];
    NSLog(@"%@",anInvocation.methodSignature);
    anInvocation.selector = @selector(run:);
    [anInvocation invoke];
}

現(xiàn)在我們應(yīng)該也知道了為什么objc_msgSend的源碼用的匯編,因為匯編可以通過寄存器x0-x31來保留未知參數(shù)來跳轉(zhuǎn)到任意的指針疙驾,還有匯編更高效一點凶伙,而C滿足不了。

言而總之它碎,總而言之

Runtime就是C函荣、C++、匯編實現(xiàn)的一套API扳肛,給OC增加的一個運行時功能傻挂,也就是我們平時所說的運行時。
在運行工程時工程會被裝載到內(nèi)存挖息,來提供運行時功能金拒。

該文章為記錄本人的學(xué)習(xí)路程,希望能夠幫助大家套腹,也歡迎大家點贊留言交流P髋住!电禀!文章地址:http://www.reibang.com/p/1ddd15e47343

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末幢码,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子尖飞,更是在濱河造成了極大的恐慌症副,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件政基,死亡現(xiàn)場離奇詭異贞铣,居然都是意外死亡,警方通過查閱死者的電腦和手機沮明,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門咕娄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人珊擂,你說我怎么就攤上這事圣勒。” “怎么了摧扇?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵圣贸,是天一觀的道長。 經(jīng)常有香客問我扛稽,道長吁峻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任在张,我火速辦了婚禮用含,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘帮匾。我一直安慰自己啄骇,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布瘟斜。 她就那樣靜靜地躺著缸夹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪螺句。 梳的紋絲不亂的頭發(fā)上虽惭,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音蛇尚,去河邊找鬼芽唇。 笑死,一個胖子當著我的面吹牛取劫,可吹牛的內(nèi)容都是我干的匆笤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼勇凭,長吁一口氣:“原來是場噩夢啊……” “哼疚膊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起虾标,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤寓盗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后璧函,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體傀蚌,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年蘸吓,在試婚紗的時候發(fā)現(xiàn)自己被綠了善炫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡库继,死狀恐怖箩艺,靈堂內(nèi)的尸體忽然破棺而出窜醉,到底是詐尸還是另有隱情,我是刑警寧澤艺谆,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布榨惰,位于F島的核電站,受9級特大地震影響静汤,放射性物質(zhì)發(fā)生泄漏琅催。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一虫给、第九天 我趴在偏房一處隱蔽的房頂上張望藤抡。 院中可真熱鬧,春花似錦抹估、人聲如沸缠黍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嫁佳。三九已至,卻和暖如春谷暮,著一層夾襖步出監(jiān)牢的瞬間蒿往,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工湿弦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓤漏,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓颊埃,卻偏偏與公主長得像蔬充,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子班利,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345