iOS底層學習 - Runtime之方法消息的前世此生(二)

通過上一章的學習,咱們瞭解到了方法是如何經歷快速和慢速的流程進行查找的,若是通過方法的查找,能夠找到對應IMP的話,則會直接返回,若是沒有找到,就要進入本章的動態方法解析和消息轉發流程了緩存

經過上一章,咱們基本瞭解了方法查找的3個階段以下:bash

  • 消息發送階段:從類及父類的方法緩存列表及方法列表查找方法;post

    傳送門☞iOS底層學習 - Runtime之方法消息的前世此生(一)學習

  • 動態解析階段:若是消息發送階段沒有找到方法,則會進入動態解析階段,負責動態的添加方法實現;ui

  • 消息轉發階段:若是也沒有實現動態解析方法,則會進行消息轉發階段,將消息轉發給能夠處理消息的接受者來處理;編碼

經過閱讀lookUpImpOrForward源碼,咱們知道動態方法解析,主要在_class_resolveMethod中,消息轉發,主要在_objc_msgForward_impcache中,自此,咱們來逐個進行研究spa

動態方法解析

lookUpImpOrForward方法中,通過對類,父類,元類的緩存和方法列表的查詢後,仍舊沒有找到方法,則會進入動態方法解析階段,源碼以下,咱們能夠看到,在通過 _class_resolveMethod後,會進行一遍retry操做,從新進行一遍方法的查找流程,而且只有一次動態方法解析的機會 3d

動態解析主要方法爲 _class_resolveMethod,源碼以下,主要是對元類的判斷。由於類方法是儲存在元類之中的,處理方式略有不一樣

  • 若是不是元類,則說明是對儲存在類中的實例方法進行處理
  • 若是是元類,則說明是對元類中的類方法進行處理,可是元類中的方法是在根元類中以實例方法的形式存儲的,因此最終會查找根源類的實例方法,調用實例方法解析查找
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);
        }
    }
}
複製代碼

_class_resolveInstanceMethod

該方法是進行實例方法動態解析的主要實現方法,咱們經過源碼來逐行分析日誌

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    ️✅// 判斷系統是否實現SEL_resolveInstanceMethod方法,即+(BOOL)resolveInstanceMethod:(SEL)sel
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        ️✅// 若是沒有找到(通常是不繼承子NSObject的類),則直接返回
        return;
    }

    ️✅// 若是找到,則經過objc_msgSend調用一下+(BOOL)resolveInstanceMethod:(SEL)sel方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver does not fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    ️✅// 同時再次尋找方法的IMP
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); } } } 複製代碼

經過閱讀源碼,咱們知道,若是要進行方法的動態解析的話,則須要再系統方法code

+ (BOOL)resolveInstanceMethod:(SEL)sel中進行處理,咱們須要經過對未實現的方法指定一個已經實現的方法的IMP,並添加到類中,實現方法動態解析,這樣咱們就能夠對一個未實現的方法進行動態解析了

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
     ️✅// 獲取到須要動態解析的方法名
    if (sel == @selector(saySomething)) {
        NSLog(@"說話了");
         ️✅// 獲取到須要動態解析到的方法sayHello的IMP和Method
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayHType = method_getTypeEncoding(sayHMethod);
         ️✅// 經過API添加方法
        return class_addMethod(self, sel, sayHIMP, sayHType);
    }
    
    return [super resolveInstanceMethod:sel];
}
複製代碼

_class_resolveClassMethod

若是是元類,則相關類方法的處理在_class_resolveClassMethod方法中處理,該方法的實現步驟和實例方法的實現步驟相似,只不過是消息發送的時候獲取的是元類

當咱們要進行類方法的動態解析時,須要添加 + (BOOL)resolveClassMethod:(SEL)sel進行動態方法解析

+ (BOOL)resolveClassMethod:(SEL)sel{
    
     if (sel == @selector(sayLove)) {
          ️✅// 獲取到元類中存儲的類方法sayObjc
         IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         Method sayHMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         const char *sayHType = method_getTypeEncoding(sayHMethod);
          ️✅// 將類方法實現添加在元類之中
         return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayHIMP, sayHType);
     }
     return [super resolveClassMethod:sel];
}
複製代碼

小結

  • 在方法通過緩存查找,方法列表查找後,後進入動態方法解析階段
  • 動態方法解析分爲實例方法和類方法的動態解析
  • 實例方法解析須要實現resolveInstanceMethod方法,並添加方法在類中
  • 類方法解析須要實現resolveClassMethod方法,並添加方法在元類中
  • 類方法存儲在元類之中,若是沒有實現相關類方法的動態解析,由於元類的方法以實例方法存儲在根元類中,因爲元類和根源類由系統建立,沒法修改,因此能夠再根元類的父類NSObject中,添加對應的實例方法resolveInstanceMethod進行動態解析
  • 因爲動態方法解析依賴方法名等,統一處理起來耦合性較大,判斷也比較多,因此在平時的運用較少

消息轉發

lookUpImpOrForward方法中,通過對接受對象緩存,方法列表查找和動態方法解析後,若是以上步驟都沒有進行處理,那麼就會進入,消息處理的最後一步,即消息轉發流程,這是補救方法崩潰最終的一步了,若是不想方法崩潰,那此時必定要處理了。

imp = (IMP)_objc_msgForward_impcache;
 cache_fill(cls, sel, imp, inst);
複製代碼

_objc_msgForward_impcache也是一段彙編的代碼,經過代碼咱們能夠知道,彙編通過了一段__objc_msgForward方法,發現裏面只有一段崩潰的實現,可是根據崩潰信息,咱們能夠發現中間還通過了___forwarding____CF_forwarding_prep_0等方法,可是是在CoreFoundation庫中的,因此消息轉發的處理在此時進行的

咱們經過查看源碼,發如今獲取到IMP以後,系統會調用 log_and_fill_cache,說明系統會對緩存的方法加日誌,咱們能夠經過系統的日誌在查看方法調用的狀況。經過方法咱們能夠看到,日誌會記錄在 /tmp/msgSends目錄下,且經過 objcMsgLogEnabled變量來控制是否存儲日誌

咱們找到 objcMsgLogEnabled的賦值是在 instrumentObjcMessageSends之中,因此咱們能夠暴露這個方法,來達到外部打日誌的操做
經過在代碼中暴露 instrumentObjcMessageSends方法,並定位在要崩潰的方法中,能夠打上日誌,來查看調用

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGStudent *student = [LGStudent alloc] ;
        
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);

    }
    return 0;
}
複製代碼

經過尋找/tmp/msgSends文件,以下圖所示,咱們發現通過了resolveInstanceMethod,forwardingTargetForSelector,methodSignatureForSelector,doesNotRecognizeSelector,這就是咱們要尋找的處理方法。

其中resolveInstanceMethod爲方法動態解析,doesNotRecognizeSelector爲通過上述的最後崩潰調用

因此,最終消息的轉發就再forwardingTargetForSelectormethodSignatureForSelector中了,這也是一個快速慢速兩種流程

快速流程 forwardingTargetForSelector

經過查看- (id)forwardingTargetForSelector:(SEL)aSelector方法的文檔,咱們可得

  • 該方法的返回對象是執行sel的新對象,也就是本身處理不了會將消息轉發給別人的對象進行相關方法的處理,可是不能返回self,不然會一直找不到
  • 該方法的效率較高,若是不實現或者nl,會走到forwardInvocation:方法進行處理
  • 底層會調用objc_msgSend(forwardingTarget, sel, ...);來實現消息的發送
  • 被轉發消息的接受者參數和返回值等須要和原方法相同

咱們能夠經過一下相似代碼,處理相關方法,表示 saySomething方法的處理被轉發到 LGTeacher的相關類中實現。至此消息轉發的快速流程結束,不會崩潰

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

慢速流程methodSignatureForSelector

若是沒有通過上述的消息轉發的快速流程,那麼會進入一個消息轉發的慢速流程之中,實現慢速流程。

首先必須實現methodSignatureForSelector方法,經過下面文檔的能夠得出

  • 該方法是讓咱們根據方法選擇器SEL生成一個NSMethodSignature方法簽名並返回,這個方法簽名裏面其實就是封裝了返回值類型,參數類型等信息。

而後實現methodSignatureForSelector後,還必須實現- (void)forwardInvocation:(NSInvocation *)anInvocation;方法進行處理,經過文檔,咱們可得

  • forwardInvocationmethodSignatureForSelector必須是同時存在的,底層會經過方法簽名,生成一個NSInvocation,將其做爲參數傳遞調用
  • 查找能夠響應 InInvocation中編碼的消息的對象。對於全部消息,此對象沒必要相同。
  • 使用 anInvocation將消息發送到該對象。anInvocation將保存結果,運行時系統將提取結果並將其傳遞給原始發送者。

咱們能夠看到NSInvocation源碼

  • 封裝了anInvocation.target -- 方法調用者
  • anInvocation.selector -- 方法名
  • - (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;-- 方法參數,

所以在此方法裏面,咱們能夠決定將消息轉發給誰(target),甚至還能夠修改消息的參數,因爲anInvocation會存儲消息selector裏面帶來的參數,而且能夠根據消息所對應的方法簽名肯定消息參數的個數,因此咱們經過- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;能夠對參數進行修改。總之你能夠按照你的意願,配置好anInvocation,而後執行[anInvocation invoke];便可完成消息的轉發調用

咱們能夠經過以下相似的代碼來實現消息的慢速轉發

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

//
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);
   SEL aSelector = [anInvocation selector];

   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}
複製代碼

小結

  • forwardingTargetForSelector實現消息轉發的快速流程,直接轉發到能處理相關方法的對象中,而且方法要保持一致
  • methodSignatureForSelector提供一個方法簽名,用來生成NSInvocation參數進行後續的使用
  • forwardInvocation經過對NSInvocation來實現消息的最終轉發
  • 若是沒有重寫上述的方法,則進入NSObjectdoesNotRecognizeSelector方法,致使方法尋找不到,程序崩潰

消息源碼

MJ大神提供的相關消息轉發C語言實現源碼

int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 調用 forwardingTargetForSelector:

    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
            NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}
複製代碼

流程圖

相關文章
相關標籤/搜索