iOS-OC底層09:動態方法決議 & 消息轉發

前沿

我們在oc層面調用對象方法實質是向某對象發送消息也就是objc_msgSend,objc_msgSend需要找到對應方法的實現也就是函數指針IMP,查找IMP首先在緩存中查找也就是快速查找,然后慢速查找也就是在類的方法類表中查找,如果這兩種方法都找不到IMP,則在源碼中有lookUpImpOrForward,可以看到會走resolveMethod_locked函數也就是動態方法決議

動態方法決議

在NSObject頭文件中有兩個方法去實現動態方法決議

//類方法動態決議
+ (BOOL)resolveClassMethod:(SEL)sel ;
//對象方法動態決議
+ (BOOL)resolveInstanceMethod:(SEL)sel;

我們看一下在OC底層是怎么調用動態決議方法的

對象方法動態決議resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // lookup resolveInstanceMethod
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

}

1.先慢速查詢resolveInstanceMethod的IMP,如果查到進行下一步,如果查不到能返回
2.調用resolveInstanceMethod方法

  1. resolveInstanceMethod之后重新查詢方法的IMP,并返回

類方法動態決議resolveClassMethod

我們需要注意一點,在調用resolveClassMethod代碼

   resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {  // 為什么要有這行代碼
            resolveInstanceMethod(inst, sel, cls);
        }

通過繼承樹我們知道類方法的最后繼承自NSObject,會最終調用NSObject的實例方法(對象方法)

動態方法決議使用

我們可以在resolveInstanceMethod或resolveClassMethod方法中添加對應方法的實現,在慢速查詢方法中,調用resolveInstanceMethod方法之后查詢sel的IMP,如果我們在resolveInstanceMethod方法中添加方法,所以在查詢IMP時就能找到,程序就能正常運行。

動態方法決議項目中的運行

我們可以在NSObject 的resolveInstanceMethod收集信息,如果類方法和對象方法都過繼承樹都要走到NSObject的resolveInstanceMethod,我們可以添加NSObject的Category在來實現resolveInstanceMethod。我們可以在resolveInstanceMethod中收集崩潰信息,我們也可以添加一個靜態IMP,來防止程序崩潰。
如果動態方法決議不能解決方法未找到的情況就要走消息轉發了
示例如下

 LGPerson *person = [LGPerson alloc];
   [person sayNB];
//sayNB只聲明不實現打印日志如下

-[LGPerson sayNB]: unrecognized selector sent to instance 0x1018560a0
//實現resolveInstanceMethod
void sayNB(){
    NSLog(@"12345");
    
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayNB)) {
       return  class_addMethod([self class], sel, sayNB, nil);
    }
    return [super resolveInstanceMethod:sel];
}
//打印結果如下
//2020-09-25 17:15:38.507991+0800 KCObjc[61134:553937] 12345

消息轉發

消息轉發分快速轉發和慢速轉發

快速轉發forwardingTargetForSelector:

如果動態方法決議中沒有給對應方法加IMP,則會走快速轉發流程
快速轉發就是讓這個消息讓其他對象去接收

-(id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayNB)) {
        
        return [LGTeacher new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
//LGTeacher實現sayNB方法
//打印日志
//我是老師,我當然牛逼

如果返回的對象時nil則[LGPerson sayNB]: unrecognized selector sent to instance

慢速轉發

當在快速轉返回nil時,則進入最后的一次挽救機會,重寫methodSignatureForSelector,

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    
    return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
}
打印結果
 ---+[LGPerson resolveInstanceMethod:]--sayNB
-[LGPerson methodSignatureForSelector:]
---+[LGPerson resolveInstanceMethod:]--_forwardStackInvocation:
-[LGPerson forwardInvocation:]

我們在forwardInvocation中只是簡單的做一個NSLog,程序就不會崩潰。
我們也可以對invocation事務進行處理,修改invocation的target為[LGStudent alloc],調用 [anInvocation invoke] 觸發 即LGPerson類的say666實例方法的調用會調用LGStudent的say666方法。
動態方法決議和消息轉發的流程圖


消息轉發.png

從日志出發查看方法動態方法決議和消息轉發的先后順序

我們在lookUpImpOrForward發現一個函數log_and_fill_cache,我們知道fillCache是怎么回事,那log呢?我走進函數,看到打印日志的標記是objcMsgLogEnabled,而objcMsgLogEnabled是可以我們設置的通過instrumentObjcMessageSends

extern void instrumentObjcMessageSends(BOOL flag); //聲明
     LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];//只聲明未實現
        instrumentObjcMessageSends(NO);

在logMessageSend中我們看到日志是寫到/tmp文件下,運行之后出現文件msgSends-38368

+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject class
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:

我們在源碼中看到resolveInstanceMethod之后就沒有forwardingTargetForSelector,methodSignatureForSelector方法的調用,bt打印堆棧信息,看到關于CoreFoundation,我們猜想動態方法決議之后CoreFoundation接下來處理,查看CoreFoundation之后沒看到關于堆棧信息的forwarding_prep_0_ forwarding

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x00007fff2fdbb6e6 CoreFoundation`CFStringGetLength + 6
    frame #1: 0x00007fff2fde5816 CoreFoundation`CFStringAppend + 229
    frame #2: 0x00007fff2fe25e4b CoreFoundation`-[NSArray componentsJoinedByString:] + 309
    frame #3: 0x00007fff2ff2b95f CoreFoundation`__handleUncaughtException + 761
    frame #4: 0x00007fff68cfb5a3 libobjc.A.dylib`_objc_terminate() + 90
    frame #5: 0x00007fff671ce887 libc++abi.dylib`std::__terminate(void (*)()) + 8
    frame #6: 0x00007fff671d11a2 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
    frame #7: 0x00007fff671d1169 libc++abi.dylib`__cxa_throw + 113
    frame #8: 0x00007fff68cf96ed libobjc.A.dylib`objc_exception_throw + 350
    frame #9: 0x00007fff2ff31be7 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    frame #10: 0x00007fff2fe173bb CoreFoundation`___forwarding___ + 1427
    frame #11: 0x00007fff2fe16d98 CoreFoundation`__forwarding_prep_0___ + 120
  * frame #12: 0x0000000100000e80 002-instrumentObjcMessageSends輔助分析`main(argc=1, argv=0x00007ffeefbff260) at main.m:19:9
    frame #13: 0x00007fff69ea1cc9 libdyld.dylib`start + 1

反編譯查看CoreFoundation image list查看工程中使用的庫

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 

前往該目錄

image.png

使用反匯編工具 hopper Disassembler搜索forwarding_prep_0_ 或者forwarding查看消息轉發流程
image.png

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。