runtime消息傳遞與轉(zhuǎn)發(fā)

官方文檔及資源地址
Documentation Archive
Apple Open Source
查看runtime開源文件 arm64 位
objc-msg-arm64.s - ARM64 code to support objc messaging
將Object-C 語言轉(zhuǎn)換為C++:

xcrun -sdk iphonesimulator clang -rewrite-objc main.m

以上指令 會生成.cpp文件 查看cpp文件代碼 與官方源碼 得知
objc_msgSend 是用匯編寫的卫病。
C語言不能通過寫一個函數(shù),去跳轉(zhuǎn)到任意的指針,匯編可以利用寄存器實現(xiàn)侈百,C語言使用“靜態(tài)綁定”胖喳,也就是說孝常,在編譯時就能決定運行時所應調(diào)用的函數(shù)七冲,如果待調(diào)用的函數(shù)地址無法硬編碼在指令之中背镇,那就要在運行期讀取出來锨用,使用“動態(tài)綁定”丰刊,我們都知道c語言是面向過程,由編譯器進行處理增拥,顯然無法實現(xiàn)這樣的需求啄巧。而runtime是運行時寻歧,在運行的時候會進行特殊操作訪問不同的內(nèi)存空間,因此oc具備動態(tài)特性秩仆。

1.對象及方法本質(zhì)
@autoreleasepool {
        YJPerson * P = [YJPerson new];
        [P run];    
}
//編譯后 (環(huán)境依賴部分代碼暫不考慮 此處沒有粘貼出來)

#pragma clang assume_nonnull begin
#ifndef _REWRITER_typedef_YJPerson
#define _REWRITER_typedef_YJPerson
typedef struct objc_object YJPerson;
typedef struct {} _objc_exc_YJPerson;
#endif

struct YJPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        YJPerson * P = ((YJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YJPerson"), sel_registerName("new"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)P, sel_registerName("run"));
}
void runImp (id self ,SEL _cmd){
}

將.m文件轉(zhuǎn)換為C++文件 即可得出

  • 對象的本質(zhì):結(jié)構(gòu)體码泛, 占用內(nèi)存大小
  • 方法的本質(zhì)就是 _objc_msgSend 發(fā)消息
    接下來 看下 消息傳遞與轉(zhuǎn)發(fā)。

2.消息傳遞

消息發(fā)送 _objc_msgSend

void objc_msgSend(id self, SEL cmd, ...)
//接受兩個或兩個以上的參數(shù),第一個參數(shù)代表接收者澄耍,第二個參數(shù)代表SEL(SEL是選擇子的類型)噪珊,后續(xù)參數(shù)就是消息中的那些參數(shù).編譯器會進行轉(zhuǎn)換
id returnValue =  objc_msgSend(someObject,@selector(messageName:), parameter);

以下幾個概念需要搞清

  1. objc_class
    重要成員(也都是結(jié)構(gòu)體 建議看下源碼 很有意思的)
  • objc_method_list($)
    方法列表
  • objc_cache($)
    緩存列表 method_name:method_imp 。 key:value的形式
  • 結(jié)構(gòu)體里保存了指向父類的指針齐莲、類的名字痢站、版本、實例大小选酗、實例變量列表阵难、方法列表、緩存芒填、遵守的協(xié)議列表等

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete;
    int method_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
};

struct objc_method {
    SEL method_name;
    char *method_types;    /* a string representing argument/return types */
    IMP method_imp;
};

  1. 源碼部分分析
調(diào)用 objc_msgSend 后 系統(tǒng)會進行一系列的復雜操作
- 首先呜叫,通過 obj 的 isa 指針找到它的 class ;
- 在 class 的 method list 找 對應的 func ;
- 如果 class 中沒到 func,繼續(xù)往它的 superclass 中找 ;
- 一旦找到 func 這個函數(shù)氢烘,就去執(zhí)行它的實現(xiàn)IMP .
由于每個消息都需要遍歷一次怀偷,效率會比較低。
objc_class 中另一個重要成員 objc_cache把經(jīng)常被調(diào)用的函數(shù)緩存下來播玖,大大提高函數(shù)查詢的效率椎工。把 func 的 method_name 作為 key ,method_imp 作為 value 給存起來蜀踏。
當再次收到 func 消息的時候维蒙,直接在 cache 里找到,避免去遍歷 objc_method_list

接下來看下詳細的具體流程

  • ENTRY _objc_msgSend 入口
    判斷接收者recevier是否為空果覆,為空則返回颅痊,不為空,就處理isa局待。
    Objective-C 是一門面向?qū)ο蟮恼Z言斑响,對象又分為實例對象、類對象钳榨、元類對象以及根元類對象舰罚。它們是通過一個叫 isa 的指針來關(guān)聯(lián)起來,具體關(guān)系如下圖:
    isa superclass

    案例點擊可查看
// _objc_msgSend 入口
    ENTRY _objc_msgSend
// 窗口
    UNWIND _objc_msgSend, NoFrame
// tagged pointer 特殊 數(shù)據(jù)類型 數(shù)據(jù)非常小
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

建議仔細的看下這個完整的文件 相信自己能看懂 就行薛耻。官方代碼邏輯非常的清晰营罢。

首先進行緩存檢查和類型判斷
LNilOrTagged

taggedPoint:存儲小值類型,地址中包含值和類型數(shù)據(jù)饼齿,能進行快速訪問數(shù)據(jù)饲漾,提高性能

LGetIsaDone

通過匯編指令b LGetIsaDone跳轉(zhuǎn)到CacheLookup蝙搔,對緩存進行快速的查找,如果有緩存就直接返回考传,由于這一步是通過匯編執(zhí)行吃型,所以是快速查找,效率很高(這里存在查找的過程)


image.png
CacheLookup 分為三種
  • CacheHit
    找到了伙菊,則調(diào)用CacheHit進行call or return imp
  • CheckMiss
    找不到 __objc_msgSend_uncached
  • add
    別的地方找到了這imp就進行add操作败玉,方便下一次快速的查找敌土。
MethodTableLookup

如果來到這里 說明在緩存里面不存在
先找自己镜硕,如果自己沒有IMP,然后找父類的緩存,如果沒有返干,循環(huán)遞歸查找父類的IMP兴枯,一直找到NSObject,如果還是沒有矩欠,接下來就開始動態(tài)方法解析财剖,如果動態(tài)方法解析沒有實現(xiàn),接下來再調(diào)用消息轉(zhuǎn)發(fā)癌淮,流程如下,核心方法----- lookUpImpOrForward


image.png

底層源碼如下

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 緩存中有IMP躺坟,直接返回
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    //runtimeLock 加鎖 保證線程安全(數(shù)據(jù)安全) 保證 舊數(shù)據(jù)不再重新填充
    // 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();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // 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.assertLocked();

    // Try this class's cache. 緩存中有IMP,直接返回

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists. 1 找自己的IMP,找到加入方法緩存
    {
        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. 2 找父類 這一步大致與上一步相同 只是找的是 父類 上一步是自己
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.內(nèi)存溢出
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            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;
                }
                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;
            }
        }
    }

    // No implementation found. Try method resolver once.
   // 沒有IMP乳蓄,調(diào)用一次resolver動態(tài)方法解析,通過triedResolver變量來控制該方法只走一次

    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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.消息轉(zhuǎn)發(fā)

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

消息傳遞:底層的確很復雜咪橙,涉及到寄存器位運算,下面是流程圖
流程圖
func沒有找到虚倒,通常情況下美侦,程序會在運行時掛掉并拋出 unrecognized selector sent to … 的異常。但在異常拋出前魂奥,Objective-C 的運行時會有三次拯救程序的機會菠剩。繼續(xù)看消息轉(zhuǎn)發(fā)的過程

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

對象在收到消息無法處理,將調(diào)用resolver解析

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);
        }
    }
}

1.+ (BOOL)resolveInstanceMethod:(SEL)selector;
對象在收到無法解讀的消息后調(diào)用此函數(shù),參數(shù)就是那個未知的SEL(字符編碼)耻煤,其返回值為Boolean類型具壮,表示這個類是否能新增一個處理此SEL的方法。讓你有機會提供一個函數(shù)實現(xiàn)哈蝇。
如果你添加了函數(shù)并返回 YES棺妓, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程。此方法常用來實現(xiàn)@dynamic屬性买鸽、訪問CoreData框架中NSManagedObjects對象

如果 resolve 方法返回 NO 涧郊,運行時就會移到下一步:消息轉(zhuǎn)發(fā)

+(BOOL)resolveInstanceMethod:(SEL)sel{
    //方法名
    NSString *selStr = NSStringFromSelector(sel);
    if ([selStr isEqualToString:@"name"]) {
        //增加name方法的實現(xiàn)
        class_addMethod(self, sel, (IMP)nameGetter, "@@:");
        return YES;
    }
    if ([selStr isEqualToString:@"setName:"]) {
        class_addMethod(self, sel, (IMP)nameSetter, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
// 或者 runtime api 
IMP fooIMP = imp_implementationWithBlock(^(id _self) {
    NSLog(@"Doing foo");
}); 
class_addMethod([self class], aSEL, fooIMP, "v@:");

2.- (id)forwardingTargetForSelector:(SEL)aSelector
接收者有第二次機會處理未知的SEL,就是把這條消息轉(zhuǎn)給其他接收者來處理眼五,這一步無法操作轉(zhuǎn)發(fā)的消息妆艘。如要修改或者處理的話就需要觸發(fā)完整的消息轉(zhuǎn)發(fā)機制

-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSString *selStr = NSStringFromSelector(aSelector);
    //companyName彤灶,則處理轉(zhuǎn)發(fā)
    if ([selStr isEqualToString:@"companyName"]) {
        //返回處理這個轉(zhuǎn)發(fā)的對象
        return self.companyModel;
    }else{
        return [super forwardingTargetForSelector:aSelector];
    }
}

3.- (void)forwardInvocation:(NSInvocation *)anInvocation
這一步是 Runtime 最后一次給你挽救的機會,啟用完整的消息轉(zhuǎn)發(fā)機制批旺,創(chuàng)建NSInvocation對象:(SEL幌陕、目標及參數(shù)).
首先它會發(fā)送 -methodSignatureForSelector: 消息獲得函數(shù)的參數(shù)和返回值類型如果返回了一個函數(shù)簽名,Runtime 就會創(chuàng)建一個 NSInvocation 對象并發(fā)送 -forwardInvocation: 消息給目標對象汽煮,
觸發(fā)NSInvocation對象時搏熄,“消息派發(fā)系統(tǒng)”將親自出馬,把消息指派給目標對象.

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSMethodSignature *sig = nil;
    NSString *selStr = NSStringFromSelector(aSelector);
    //判斷你要轉(zhuǎn)發(fā)的SEL
    if ([selStr isEqualToString:@"deptName"]) {
        //此處返回的sig是方法forwardInvocation的參數(shù)anInvocation中的methodSignature
        //為你的轉(zhuǎn)發(fā)方法手動生成簽名
        sig = [self.companyModel methodSignatureForSelector:@selector(deptName:)];
    }else{
        sig = [super methodSignatureForSelector:aSelector];
    }
    return sig;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    NSString *selStr = NSStringFromSelector(anInvocation.selector);
    if ([selStr isEqualToString:@"deptName"]) {
        //設置處理轉(zhuǎn)發(fā)的對象
        [anInvocation setTarget:self.companyModel];
        //設置轉(zhuǎn)發(fā)對象要用的方法
        [anInvocation setSelector:@selector(deptName:)];
        BOOL hasCompanyName = YES;
        //第一個和第二個參數(shù)是target和sel
        [anInvocation setArgument:&hasCompanyName atIndex:2];
        [anInvocation retainArguments];
        [anInvocation invoke];
    }else{
        [super forwardInvocation:anInvocation];
    }
}

細節(jié):
resolveInstanceMethod 此函數(shù)會調(diào)用倆次暇赤。
第一次:先走方法 _objc_msgSend_uncached心例,然后走方法 lookUpImpOrForward,再走方法 _class_resolveInstanceMethod鞋囊,從這個大致的流程可以知道止后,這個流程,就是上面所分析的流程溜腐,尋找 imp的過程译株,沒有找到就動態(tài)解析
第二次:消息轉(zhuǎn)發(fā)流程


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挺益,隨后出現(xiàn)的幾起案子歉糜,更是在濱河造成了極大的恐慌,老刑警劉巖望众,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匪补,死亡現(xiàn)場離奇詭異,居然都是意外死亡黍檩,警方通過查閱死者的電腦和手機叉袍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刽酱,“玉大人喳逛,你說我怎么就攤上這事】美铮” “怎么了润文?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長殿怜。 經(jīng)常有香客問我典蝌,道長,這世上最難降的妖魔是什么头谜? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任骏掀,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘截驮。我一直安慰自己笑陈,他們只是感情好,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布葵袭。 她就那樣靜靜地躺著涵妥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坡锡。 梳的紋絲不亂的頭發(fā)上蓬网,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音鹉勒,去河邊找鬼帆锋。 笑死,一個胖子當著我的面吹牛贸弥,可吹牛的內(nèi)容都是我干的窟坐。 我是一名探鬼主播海渊,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼绵疲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了臣疑?” 一聲冷哼從身側(cè)響起盔憨,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讯沈,沒想到半個月后郁岩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡缺狠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年问慎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挤茄。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡如叼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出穷劈,到底是詐尸還是另有隱情笼恰,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布歇终,位于F島的核電站社证,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏评凝。R本人自食惡果不足惜追葡,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宜肉,春花似錦疾渣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至朱浴,卻和暖如春吊圾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翰蠢。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工项乒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梁沧。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓檀何,卻偏偏與公主長得像,于是被迫代替她去往敵國和親廷支。 傳聞我的和親對象是個殘疾皇子频鉴,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容