Runtime核心點(diǎn)解析及萬(wàn)能跳轉(zhuǎn)

Qinz
關(guān)于Runtime的概念網(wǎng)上很多資料可供參考,本文不做介紹。文章將重點(diǎn)放在核心源碼的分析及梳理消息轉(zhuǎn)發(fā)機(jī)制,所以本文需要一定的Runtime基礎(chǔ)闪朱。
一、 對(duì)象及方法本質(zhì)
1. 首先我們用最簡(jiǎn)單的對(duì)象調(diào)用方法來(lái)一步一步深入解析:
#import <Foundation/Foundation.h>
#include <objc/runtime.h>
#import "Person.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        Person*p = [[Person alloc]init];
        [p run];
        return 0;
    }
}
2. 通過(guò)下面的命令將.m文件轉(zhuǎn)換為C++文件:
clang -rewrite-objc main.m -o test.c++
3. 編譯的文件有98906行钻洒,最終main的核心文件如下:
int main(int argc, 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;
    }
}
  • 3.1 由此可得奋姿,對(duì)象調(diào)用方法的本質(zhì)是發(fā)送消息:
  ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
//(void *)objc_msgSend)((id)p 消息接受者
// sel_registerName("run")  方法編號(hào)
  • 3.2 對(duì)象的本質(zhì)是結(jié)構(gòu)體:
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};
二、Runtime核心源碼解析
1. 通過(guò)在objc源碼中搜索arm64架構(gòu)下的_objc_msgSend,在ENTRY _objc_msgSend,首先會(huì)進(jìn)行緩存檢查和類型判斷航唆,LNilOrTagged此處會(huì)判斷是否為nil或taggedPoint類型胀蛮,如果是,則直接返回糯钙。taggedPoint是用來(lái)存儲(chǔ)小值類型粪狼,其地址中包含值和類型數(shù)據(jù),可以進(jìn)行快速的訪問(wèn)數(shù)據(jù)任岸,提高性能再榄。
LGetIsaDone:
    CacheLookup NORMAL

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

2. 如果不為nil,通過(guò)匯編指令b LGetIsaDone跳轉(zhuǎn)到CacheLookup,來(lái)對(duì)緩存進(jìn)行快速的查找享潜,如果有緩存就直接返回困鸥,由于這一步是通過(guò)匯編執(zhí)行,所以是快速查找剑按,效率很高疾就,查找過(guò)程如下圖:
快速查找
3. 如果緩存沒(méi)有,將會(huì)進(jìn)入慢速查找過(guò)程艺蝴,核心方法為MethodTableLookup
    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
       //方法列表
    MethodTableLookup
    br  x17

    END_ENTRY __objc_msgSend_uncached
4. 通過(guò)MethodTableLookup方法的調(diào)用猬腰,會(huì)從匯編跳轉(zhuǎn)到C/C++,所以說(shuō)runtime是由匯編猜敢,C/C++組成的一套API
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
5. 接下來(lái)看下lookUpImpOrForward核心代碼:
 retry:    
    runtimeLock.assertReading();
    // 如果緩存中有IMP姑荷,直接返回
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    // 如果沒(méi)有,先找自己的IMP,找到加入方法緩存
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }
    // 自己沒(méi)有缩擂,找父類鼠冕,一直找到NSObject,因?yàn)镹SObject的父類為nil,下面的條件是curClass != nil
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            //  內(nèi)存溢出相關(guān)
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            // 先從父類緩存中找IMP
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父類中找到IMP,加入緩存
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    // 沒(méi)有IMP,調(diào)用一次resolver動(dòng)態(tài)方法解析,通過(guò)triedResolver變量來(lái)控制該方法只走一次
   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;
    }
    // 沒(méi)有IMP胯盯,也沒(méi)有resolver,調(diào)用forwarding進(jìn)行消息轉(zhuǎn)發(fā)
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
  • 5.1 過(guò)程就是先找自己懈费,如果自己沒(méi)有IMP,然后找父類的緩存,如果沒(méi)有博脑,循環(huán)查找父類的IMP憎乙,一直找到NSObject薄坏,如果還是沒(méi)有,接下來(lái)就開始動(dòng)態(tài)方法解析寨闹,如果動(dòng)態(tài)方法解析沒(méi)有實(shí)現(xiàn),接下來(lái)再調(diào)用消息轉(zhuǎn)發(fā)君账,流程如下圖:
    IMP查找流程
6. 動(dòng)態(tài)方法解析調(diào)用如下
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        NSLog(@"=====   對(duì)象方法解析 ========");
        SEL myRunSEL = @selector(myRun);
        Method myRunSELM= class_getInstanceMethod(self, myRunSEL);
        IMP myRunImp = method_getImplementation(myRunSELM);
        const char *type = method_getTypeEncoding(myRunSELM);
        return class_addMethod(self, sel, myRunImp, type);
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(run)) {
        NSLog(@" ======  類方法解析 =======");
        SEL myRunSEL = @selector(myRun);
       //打印hellowordM1和hellowordM會(huì)發(fā)現(xiàn)地址一樣
       //說(shuō)明類方法在元類中是以實(shí)例方法的形式存在的
       // Method hellowordM1= class_getClassMethod(self, hellowordSEL);
        Method myRunM= class_getInstanceMethod(object_getClass(self), myRunSEL);
        IMP myRunImp = method_getImplementation(myRunM);
        const char *type = method_getTypeEncoding(myRunM);
        NSLog(@"%s",type);
        return class_addMethod(object_getClass(self), sel, myRunImp, type);
    }
    return [super resolveClassMethod:sel];
}

7. 消息轉(zhuǎn)發(fā)調(diào)用如下:
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
//    if (aSelector == @selector(run)) {
//        return [Person new];
//    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(run)) {
        // forwardingTargetForSelector 沒(méi)有實(shí)現(xiàn) 就只能方法簽名了
        Method method    = class_getInstanceMethod(object_getClass(self), @selector(readBook));
        const char *type = method_getTypeEncoding(method);
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"------%@-----",anInvocation);
    anInvocation.selector = @selector(readBook);
    [anInvocation invoke];
}
8. 消息轉(zhuǎn)發(fā)流程圖如下:
三繁堡、使用runtime實(shí)現(xiàn)萬(wàn)能跳轉(zhuǎn)
1. 首先模擬簡(jiǎn)單的后臺(tái)返回?cái)?shù)據(jù),下面的QinzVC為項(xiàng)目中不存在的控制器:
self.dataArr = @[
                    @{@"class":@"SecondVC",
                    @"data":@{@"name":@"小明"}},
                     
                    @{@"class":@"QinzVC",
                    @"data":@{@"name":@"我是動(dòng)態(tài)創(chuàng)建的控制器"}},
                    ];
2. 針對(duì)存在的控制器和不存在的控制器進(jìn)行跳轉(zhuǎn)乡数,下面代碼中有詳細(xì)注釋:

- (void)pushToAnyVCWithData:(NSDictionary *)dataDict{
    
    //1.獲取類名
    const char *clsName = [dataDict[@"class"] UTF8String];
    //2.通過(guò)類型獲取類
    Class cls = objc_getClass(clsName);
    //3. 如果不存在椭蹄,使用runtime動(dòng)態(tài)創(chuàng)建類
    if (!cls) {
        Class superClass = [UIViewController class];
        cls  = objc_allocateClassPair(superClass, clsName, 0);
        //添加屬性
        class_addIvar(cls, "name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
        class_addIvar(cls, "showLB", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
        //注冊(cè)類
        objc_registerClassPair(cls);
        
        //添加方法
        Method method = class_getInstanceMethod([self class], @selector(myInstancemethod));
        IMP methodIMP = method_getImplementation(method);
        const char *types = method_getTypeEncoding(method);
        BOOL result = class_addMethod(cls, @selector(viewDidLoad), methodIMP, types);
        if (result) {
            NSLog(@"===  方法添加成功 =====");
        }
    }
    
    // 實(shí)例化對(duì)象
    id instance = nil;
    @try {
        //先嘗試從SB中加載
        UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
        instance = [sb instantiateViewControllerWithIdentifier:dataDict[@"class"]];
    } @catch (NSException *exception) {
        //SB中沒(méi)有直接初始化
        instance = [[cls alloc] init];
        
    } @finally {
        NSLog(@"控制器實(shí)例化完成");
    }
    //獲取后臺(tái)返回的數(shù)據(jù),給屬性賦值
    NSDictionary *dict = dataDict[@"data"];
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        // 檢測(cè)是否存在key的屬性
        if (class_getProperty(cls, [key UTF8String])) {
            [instance setValue:obj forKey:key];
        }
        // 檢測(cè)是否存在key的變量
        else if (class_getInstanceVariable(cls, [key UTF8String])){
            [instance setValue:obj forKey:key];
        }
    }];
    
    
    [self.navigationController pushViewController:instance animated:YES];
}


- (void)myInstancemethod {
    [super viewDidLoad];
    
    [self setValue:[UIColor orangeColor] forKeyPath:@"view.backgroundColor"];
    [self setValue:[[UILabel alloc] initWithFrame:CGRectMake(100, 300, 200, 44)] forKey:@"showLB"];
    UILabel *showTextLB = [self valueForKey:@"showLB"];
    [[self valueForKey:@"view"] addSubview:showTextLB];
    
    //設(shè)置屬性
    showTextLB.text = [self valueForKey:@"name"];
    showTextLB.font = [UIFont systemFontOfSize:14];
    showTextLB.textColor = [UIColor blackColor];
    showTextLB.textAlignment = NSTextAlignmentCenter;
    showTextLB.backgroundColor = [UIColor whiteColor];
}
3. 實(shí)現(xiàn)萬(wàn)能界面跳轉(zhuǎn)的演示如下:
萬(wàn)能界面跳轉(zhuǎn)

總結(jié):runtime是由匯編净赴,C/C++組成的一套API绳矩,蘋果在發(fā)送消息做了很多優(yōu)化處理,目的是使消息的發(fā)送更加有效率玖翅,同時(shí)runtime的應(yīng)用也很廣泛翼馆,如實(shí)現(xiàn)上面的萬(wàn)能跳轉(zhuǎn),當(dāng)然還可以使用runtime實(shí)現(xiàn)頁(yè)面統(tǒng)計(jì)金度,全局改變字體大小应媚,逆向中進(jìn)行Hook等。

我是Qinz,希望我的文章對(duì)你有幫助猜极。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末中姜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子跟伏,更是在濱河造成了極大的恐慌丢胚,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件受扳,死亡現(xiàn)場(chǎng)離奇詭異携龟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)辞色,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門骨宠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人相满,你說(shuō)我怎么就攤上這事层亿。” “怎么了立美?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵匿又,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我建蹄,道長(zhǎng)碌更,這世上最難降的妖魔是什么裕偿? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮痛单,結(jié)果婚禮上嘿棘,老公的妹妹穿的比我還像新娘。我一直安慰自己旭绒,他們只是感情好鸟妙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挥吵,像睡著了一般重父。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忽匈,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天房午,我揣著相機(jī)與錄音,去河邊找鬼丹允。 笑死郭厌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雕蔽。 我是一名探鬼主播沪曙,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼萎羔!你這毒婦竟也來(lái)了液走?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贾陷,失蹤者是張志新(化名)和其女友劉穎缘眶,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體髓废,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巷懈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了慌洪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顶燕。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖冈爹,靈堂內(nèi)的尸體忽然破棺而出涌攻,到底是詐尸還是另有隱情,我是刑警寧澤频伤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布恳谎,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏因痛。R本人自食惡果不足惜婚苹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸵膏。 院中可真熱鬧膊升,春花似錦、人聲如沸谭企。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赞咙。三九已至,卻和暖如春糟港,著一層夾襖步出監(jiān)牢的瞬間攀操,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工秸抚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留速和,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓剥汤,卻偏偏與公主長(zhǎng)得像颠放,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吭敢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344