在前一篇文章objc_msgSend慢速办法查找中,探究了音讯慢速查找,即音讯发送objc_msgSend
从快速查找进入到慢速查找,并盯梢源码学习了办法慢速查找的流程。本篇重视假如快速查找和慢速查找都没有找到办法怎么办?便是上一篇遗留下来的动态办法抉择与音讯转发。
一、慢速查找遗留的两个问题
-
在慢速办法查找的
c++
函数lookUpImpOrForward
中,无论是在当时类class
仍是父类superclass
的缓存cache
中仍是类办法列表methods
中只要找对应imp
就会直接回来成果;可是都找不到就会依据情况先进入resolveMethod_locked
,再履行forward_imp
。 -
forward_imp
是什么?与resolveMethod_locked
是什么?什么情况触发与作业流程?
forward_imp是什么?
在函数lookUpImpOrForward
中,假如办法imp
未找到,即superclass
一路找到了nil
;从当时类到父类再到NSObject
最终到NSObject
的空父类仍未找到,则imp
默许会被设置为forward_imp
。那么forward_imp
是什么呢?
- 在慢速查找流程的
lookUpImpOrForward
函数的榜首行代码,即对forward_imp
进行了赋值:
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
...
...
}
- 此部分代码是经过汇编完结的,大局查找
__objc_msgForward_impcache
,在objc_msg_arm64.s
中查找到:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
-
汇编完结中查找
__objc_forward_handler
,并没有找到,在源码中去掉一个下划线_进行大局查找_objc_forward_handler
;在objc-runtime.mm
中找到,该办法本质是调用的objc_defaultForwardHandler
办法:
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
// 办法未找到,办法报错,打印内容
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
- 定论:
看上去很熟悉,没错便是咱们日常开发中遇到的常见过错:函数未完结,运转程序溃散时报的过错描绘信息。 所以forward_imp
负责打印未找到该办法的内容。
resolveMethod_locked
是什么?
当superclass = nil
,跳出循环,紧接着会再给一次时机,即动态办法抉择
,从头界说你的办法完结。
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
// No implementation found. Try method resolver once.
/**
* 假如遍历查找的进程找到了,会跳过此步骤,取到done分支,进行后续操作
* 假如找不到,会进行下面这个算法,终究进入动态办法抉择resolveMethod_locked函数
* 此算法真实达到的意图为单例,确保一个lookUpImpOrForward
* 只履行一次resolveMethod_locked
**/
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
...
}
- 定论:
slowpath(behavior & LOOKUP_RESOLVER)
能够理解为一个开关阀,确保动态办法抉择只会履行一次!直到behavior
被从头赋值!进入resolveMethod_locked
函数便是动态办法抉择,能够详细了解动态办法抉择的流程。
二、动态办法抉择resolveMethod_locked
可是假如当时类与父类的cache
缓存与methods
办法列表都没有时,当superclass = nil
,跳出循环,紧接着会再给一次时机进入resolveMethod_locked
,即动态办法抉择,从头界说你的办法完结。
resolveMethod_locked
办法
当你调用了一个办法的时分,榜首进入音讯快速查找流程 -> 然后进入音讯慢速查找流程,当底层源码现已给你办法查找了2
遍之后仍然找不到你完结的当地;此刻imp=nil
,理论上来讲程序应该溃散,可是在开发者的视点上来讲,此做法会令这个结构不稳定,或者说这个体系很不友善。
所以此结构抉择再给你一次时机,为你供给了一个自界说的imp
回来的时机,resolveMethod_locked
此函数便是动态音讯转发的进口。
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 区分类和元类:实例办法在类中,类办法在元类中
if (! cls->isMetaClass()) {
//imp为实例办法
resolveInstanceMethod(inst, sel, cls);
}
else {
//imp为类办法
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// 经过resolveInstanceMethod函数很有可能现已对sel对应imp完结了动态增加
// 所以再一次尝试查找
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
- 定论:
- 此函数里边有三个关键的函数:
- ①
resolveInstanceMethod
:实例办法动态增加imp
- ②
resolveClassMethod
:类办法动态增加imp
- ③
lookUpImpOrForwardTryCache
:当完结增加之后,回到之前的慢速查找流程再来一遍。
- ①
resolveInstanceMethod
办法
目标办法动态办法抉择会调用resolveInstanceMethod
办法,处理的是目标的实例办法。
- 实例办法动态增加:
/*********************************************************
* 解析实例办法
* 调用+resolveInstanceMethod,寻觅要增加到类cls的办法。
* cls 可能是元类或非元类。
* 不查看该办法是否现已存在。
*********************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//当你为完结resolveInstanceMethod的时分,此处也不会进入return
//因为体系给resolveInstanceMethod函数默许回来NO,即默许完结了
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
return;
}
//体系会在此处为你发送一个音讯resolve_sel
//当你的这个类检测了这个音讯,而且做了处理
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
//那么此刻体系会从头查找,此函数终究会触发LookUpImpOrForward
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
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));
}
}
}
resolveClassMethod
办法
类办法动态办法抉择会调用resolveClassMethod
办法,处理的是元类目标的办法。
- 类办法动态增加:
/*********************************************************
* 解析类办法
* 调用+resolveClass 办法,寻觅要增加到类cls 的办法。
* cls 应该是一个元类。
* 不查看该办法是否现已存在。
*********************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//当你为完结resolveClassMethod的时分,此处也不会进入return
//因为体系给resolveClassMethod函数默许回来NO,即默许完结了
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
//nonmeta容错处理
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
//体系会在此处为你发送一个音讯resolveClassMethod
//当你的这个类检测了这个音讯,而且做了处理
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
//那么此刻体系会从头查找,此函数终究会触发LookUpImpOrForward
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
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 resolveClassMethod:%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));
}
}
}
resolveClassMethod & resolveInstanceMethod
NSObject
现已在NSObject.mm
默许完结这两个类办法。一般都是自界说复写这两个办法,来动态增加办法。
//默许回来NO,当用户不完结这个办法的时分,程序也不会return
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
//默许回来NO,当用户不完结这个办法的时分,程序也不会return
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
lookUpImpOrForwardTryCache过度函数
这个是动态办法抉择后,从头查找办法情况。
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
_lookUpImpTryCache
经过动态增加办法之后,再次尝试查找sel
对应的最新增加的imp
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
//当类未初始化的时分,进入lookUpImpOrForward
//在里边处理不缓存任何办法
if (slowpath(!cls->isInitialized())) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//经过汇编查找
IMP imp = cache_getImp(cls, sel);
if (imp != NULL) goto done;
//同享缓存查找
#if CONFIG_USE_PREOPT_CACHES
if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
}
#endif
//假如仍然找不到,证明办法真的不存在,也便是说,只能依托办法的动态增加了
//那么此刻再次进入办法的慢速查找流程
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
//此判别是当时imp现已存在了,而且这个imp是默许赋值的forward_imp
//此刻回来imp为nil;
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
}
- 定论:
那这些函数有什么用途?resolveMethod_locked
与resolveInstanceMethod
函数都会履行lookUpImpOrNilTryCache
,为什么要履行2遍呢?那接下来用复写类办法resolveInstanceMethod
探究流程。
实例办法的动态抉择
依照源码复原,经过源码已得知会给开发者一次修正的时机,经过resolveInstanceMethod
这个办法,这里在自界说类CJPerson
内复写一下这个办法。
- 测试代码:
@interface CJPerson : NSObject
- (void)sayHello;
- (void)sayHello1;
+ (void)say666;
+ (void)say6661;
@end
@implementation CJPerson
- (void)sayHello1 {
NSLog(@"sayHello1 %s", __func__);
}
+ (void)say6661 {
NSLog(@"say6661 %s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"给你一次时机...");
// 什么也没做
return NO;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
CJPerson *p = [[CJPerson alloc]init];
[p sayHello];
}
return 0;
}
- 控制台打印成果:
2023-01-08 15:54:27.966872+0800 KCObjcBuild[59458:17979158] 给你一次时机...
warning: KCObjcBuild was compiled with optimization - stepping may behave oddly; variables may not be available.
2023-01-08 15:54:37.901855+0800 KCObjcBuild[59458:17979158] 给你一次时机...
2023-01-08 15:55:05.431298+0800 KCObjcBuild[59458:17979158] -[CJPerson sayHello]: unrecognized selector sent to instance 0x600003004000
2023-01-08 15:55:08.244166+0800 KCObjcBuild[59458:17979158] Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CJPerson sayHello]: unrecognized selector sent to instance 0x600003004000'
*** First throw call stack:
(
0 CoreFoundation 0x00007ff8100d543b __exceptionPreprocess + 242
1 libobjc.A.dylib 0x000000010070fb8a objc_exception_throw + 42
2 CoreFoundation 0x00007ff81016c56b -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007ff81003f69b ___forwarding___ + 1324
4 CoreFoundation 0x00007ff81003f0d8 _CF_forwarding_prep_0 + 120
5 KCObjcBuild 0x00000001000035d0 main + 64
6 dyld 0x00007ff80fc51310 start + 2432
)
libc++abi: terminating with uncaught exception of type NSException
warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CJPerson sayHello]: unrecognized selector sent to instance 0x600003004000'
terminating with uncaught exception of type NSException
-
进程:
仍然报错是因为此刻办法
sayHello
还并未完结,可是在溃散之前打印了手动介入的信息,也便是说,在溃散之前有弥补的办法。可是进程中resolveInstanceMethod
履行了两次。经过bt
指令在终端打印内存情况!
- 定论:
-
榜首次动态抉择:榜首次,和咱们分析的是共同的,是在查找
sayHello
办法时没有找到,会进入动态办法抉择,发送resolveInstanceMethod
音讯。 -
第2次动态抉择:第2次,是在调用了
CoreFoundation
结构中的NSObject(NSObject) methodSignatureForSelector:
后,会再次进入动态抉择。
探究resolveInstanceMethod
动态办法抉择流程
尽管重写了动态办法抉择办法resolveInstanceMethod
,可是仍然报错,而且该办法还被调用了两次
。下面进行代码盯梢调试。
- 再次运转上面的事例,过滤出咱们需求研究的内容,即
CJPerson
目标调用sayHello
办法,榜首次进入动态办法抉择办法resolveInstanceMethod
。
- 图:
- 判别
cls
,也便是CJPerson
,是否完结了resolveInstanceMethod
类办法。
- 图:
- 在元类中进行办法查找(即查找类办法),是否完结了类办法
resolveInstanceMethod
,途径为:lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
- 图:
- 在办法列表
methods
中,成功找到resolveInstanceMethod
办法,并刺进缓存。
- 图:
- 假如没有找到,此处会直接回来,即没有运用这次时机,直接回来!而假如找到则发送一条
resolveInstanceMethod
音讯,即履行resolveInstanceMethod
办法。
- 图:
- 完结音讯发送后,会再进行
sayHello
办法的查找,可是仍然找不到!因为CJPerson
尽管完结了resolveInstanceMethod
,可是里边什么也没有做!
- 图:
-
resolveInstanceMethod
履行完结后,回到resolveMethod_locked
流程中,调用lookUpImpOrForwardTryCache
再次进行办法查找。
- 图:
- 在
lookUpImpOrForwardTryCache
中,仍然没有查找到sayHello
,此刻会从缓存中回来forward_imp
。
- 图:
- 最终动态办法抉择流程完毕,只是此事例中,尽管完结了
动态办法抉择
,可是里边什么也没有做,进入音讯转发
,终究运转成果报错!
- 图:
- 定论:
经过上面的事例,理清楚了动态办法抉择的流程。可是实践开发进程中,咱们肯定会捉住这次处理过错的时机。下面咱们对事例进行修正,将sayHello
办法指向其他办法。
运用resolveInstanceMethod
动态办法抉择
仍然是上面的事例,假如咱们向类中增加一个办法,办法的sel
仍然是sayHello
,可是其对应的办法完结imp
为sayHello1
的完结。
// 动态办法抉择 - 给一次时机, 需求进行办法完结
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"给你一次时机...");
if (sel == @selector(sayHello)) {
IMP imp = class_getMethodImplementation(self, @selector(sayHello1));
Method method = class_getInstanceMethod(self, @selector(sayHello1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- 留意点:
在objc4.886
会报错!因为这个版本的源码将addMethod
函数改成了只适配arm64e
的bigSigned()
;因为调试源码是macOS
环境,将bigSigned()
改回big()
;假如是iOS真机不需求更改。
运用动态抉择流程探究
运转复写类办法resolveInstanceMethod办法
的代码,这次有什么不同呢?咱们仍然把重视点放到resolveInstanceMethod办法
中,盯梢①、②、③三个当地别离做了什么?
调用CJPerson类
的实例办法sayHello
,别离进行快速查找和慢速查找,均找不到该办法。终究会进入源码动态办法抉择的resolveMethod_locked
-> resolveInstanceMethod
流程中:
-
运转到①、会查找类是否完结了
resolveInstanceMethod
?假如完结了,则会将该办法刺进缓存,以便下次进行快速办法查找;假如没有完结,直接回来。此处流程和初探时的流程是共同的! -
运转到②,发送
msg
,即履行CJPerson 类
中的resolveInstanceMethod
办法。因为将sayHello
办法指向了sayHello1
,则此处class_addMethod
会将办法刺进class_rw_ext_t
,也便是刺进CJPerson
的办法列表中。 -
运转到③,再次查找
sayHello
,此流程会在办法列表中查找到办法完结sayHello1
,并以sel=sayHello
,imp=sayHello1
完结的形式刺进办法缓存。- ③调用途径:
lookUpImpOrNilTryCache -> _lookUpImpTryCache -> lookUpImpOrForward
- 进入
lookUpImpOrForward
,查找sayHello
办法 - 终究在办法列表中找到了,并将其刺进缓存中。
- 继续运转代码,回到
resolveMethod_locked
,并再次调用lookUpImpOrForwardTryCache
办法,进行办法查找。 - 此刻,经过
cache_getImp
找到了办法完结,办法完结为sayHello1
,回来imp
。
- ③调用途径:
类办法的动态抉择
对于类办法的动态抉择,与实例办法类似;同样能够经过重写resolveClassMethod
类办法来解决前文的溃散问题,即在CJPerson类
中重写该办法,并将say666
类办法的完结指向类办法say6661
。
+(BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"给你一次时机...+++");
if (sel == @selector(say666))
{
Class meteCls = objc_getMetaClass("CJPerson");
IMP imp = class_getMethodImplementation(meteCls, @selector(say6661));
Method method = class_getInstanceMethod(meteCls, @selector(say6661));
return class_addMethod(meteCls, sel, imp, method_getTypeEncoding(method));
}
return [super resolveClassMethod:sel];
}
动态办法抉择运用优化
上面的这种完结办法便是是单独在每个类中重写resolveInstanceMethod
/resolveClassMethod
,太麻烦了也欠好管理。其实经过办法慢速查找流程能够发现其查找途径有两条:
-
实例办法
:类 -- 父类 -- 根类 -- nil
-
类办法
:元类 -- 根元类 -- 根类 -- nil
它们的共同点是假如前面没找到,都会来到根类即NSObject
中查找,所以咱们能够将上述的两个办法共同整合在一起,能够经过NSObject
增加分类的办法来完结共同处理,而且因为类办法的查找,在其承继链,查找的也是实例办法,所以能够将实例办法和类办法的共同处理放在resolveInstanceMethod
办法中,如下所示:
// NSObject分类
#import "NSObject+CJ.h"
#import <objc/message.h>
@implementation NSObject (CJ)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(sayHello)) {
NSLog(@"%@ 给你一次时机...", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayHello1));
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello1));
const char *type = method_getTypeEncoding(sayHello1);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(say666)) {
NSLog(@"%@ 给你一次时机+++", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("CJPerson"), @selector(say6661));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("CJPerson"), @selector(say6661));
const char *type = method_getTypeEncoding(say6661);
return class_addMethod(objc_getMetaClass("CJPerson"), sel, imp, type);
}
return NO;
}
@end
- 定论:
这种完结办法与源码中针对类办法的处理逻辑是共同的,即完美阐述为什么调用了类办法动态办法抉择,还要调用目标办法动态办法抉择,其根本原因仍是类办法是元类中的实例办法。
当然,上面这种写法仍是会有其他的问题:比如体系办法也会被更改。针对这一点是能够优化的,即咱们能够针对自界说类中办法共同办法名的前缀,依据前缀来判别是否是自界说办法,然后共同处理自界说办法。例如能够在溃散前pop
到首页,主要是用于app
线上防溃散的处理,提高用户的体会。
三、音讯转发
经过instrumentObjcMessageSends
办法打印发送音讯的日志。在慢速办法查找的刺进缓存流程中:log_and_fill_cache -> logMessageSend
,找到了instrumentObjcMessageSends
源码完结。假如要打印音讯发送运转日志,首要需求控制objcMsgLogEnabled
为true
,同时能够在发送音讯的当地调用instrumentObjcMessageSends
办法才行。
- 依据以上两点,完结下面的事例:
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface CJPerson: NSObject
- (void)sayHello;
@end
@implementation CJPerson
// 屏蔽复写resolveInstanceMethod办法与resolveClassMethod办法
// 否则打印里边会多其他目标如NSString类型的办法流程
// 就这样子简洁运用
@end
extern bool instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleseaPool {
CJPerson * person = [CJPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
}
return 0;
}
日志打印在哪里呢?在logMessageSend 源码完结
中,现已告诉咱们了。 snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
运转以上代码,然后Finder
中前往/tmp/msgSends/
,就能够找到运转日志。
- 图:
- 打开日志发现,在溃散前调用了如下办法:
- 定论:
打印了慢速查找后的整个流程是先动态办法抉择,再快速音讯转发与慢速音讯转发后边第2次动态抉择,最终报错。
- 动态办法抉择:
resolveInstanceMethod
- 快速音讯转发:
forwardingTargetForSelector
- 慢速音讯转发:
methodSignatureForSelector
快速音讯转发forwardingTargetForSelector
经过日志咱们了解到forwardingTargetForSelector
目标办法实践调用者是CJPerson
目标,所以在CJPerson
类中增加目标办法forwardingTargetForSelector
的完结,仍然调用CJPerson
的目标办法sayHello
。
// 动态办法抉择 - 给一次时机, 需求进行办法完结
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"给你榜首次时机...");
return [super resolveInstanceMethod:sel];
}
// 快速音讯转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次时机...%@", NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- 定论:
- 运转仍然溃散。因为仍然没有找到办法完结。可是在快速音讯转发中的打印了:
给你二次时机...sayHello
。也便是说在错过榜首次弥补时机动态办法抉择后,快速音讯转发forwardingTargetForSelector
会给咱们第2次的弥补时机。
运用快速音讯转发
这次时机咱们能够理解为甩锅,简单理解为:我处理不了了,你让别人帮我处理吧!
- 现在对上面的事例进行一些修正,增加一个
CJAnimal
类,声明并完结sayHello
办法。
@interface CJAnimal : NSObject
- (void)sayHello;
@end
@implementation CJAnimal
- (void)sayHello {
NSLog(@"sayHello %s", __func__ );
}
@end
-
CJPerson
类的快速音讯转发中,将办法甩锅给CJAnimal
目标。也便是捉住这次时机,从头设置一个办法接受者。
// 快速办法转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"给你二次时机...%@", NSStringFromSelector(aSelector));
return [CJAnimal alloc];
}
-
定论:
①. 运转后不再溃散,并成功调用了
CJAnimal
中的sayHello
办法。说明这次甩锅起到了作用,将办法的接受者变成了CJAnimal
目标。②. 需求说明的是,在
CJAnimal
中寻觅sayHello
办法时,仍然会走快速查找、慢速查找、动态办法抉择等流程。
慢速音讯转发methodSignatureForSelector
依据上面的流程咱们知道,慢速音讯转发流程调用了methodSignatureForSelector
办法。
- 在苹果官方文档中查找
methodSignatureForSelector
办法的运用说明,发现需求配合invocation
运用,即需求完结forwardInvocation
办法。见下图:
继续上面的事例,不在快速音讯转发forwardTargetForSelector
办法中做任何处理。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"给你第三次时机...%@", NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
}
成功走到了methodSignatureForSelector
办法中,可是仍然溃散。下面做一些修正,在该办法中回来一个办法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"给你第三次时机...%@", NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
打印发现forwardInvocation
办法中即便不对invocation
事务进行处理,也不会溃散报错了。
2023-01-12 17:41:58.909171+0800 testOD1[87290:19787932] 给你一次时机...
2023-01-12 17:42:00.759684+0800 testOD1[87290:19787932] 给你二次时机...sayHello
2023-01-12 17:42:01.438143+0800 testOD1[87290:19787932] 给你第三次时机...sayHello
2023-01-12 17:42:01.439236+0800 testOD1[87290:19787932] 给你一次时机...
Program ended with exit code: 0
程序运转到此处,能够理解为:爱谁处理,谁处理,横竖我是不处理了。在快速音讯转发中,只可修正办法的接受者;而在慢速音讯转发中能够从头设置办法、接受者等,更加灵敏,权限更大。
运用慢速音讯转发
在办法forwardInvocation
办法中做一些修正,从头设置事务的target = CJAnimal、selector = sayHello
。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"给你第三次时机...%@", NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation %s", __func__ );
CJAnimal *animal = [CJAnimal alloc]; // self;
anInvocation.target = animal;
anInvocation.selector = @selector(sayHello); // @selector(sayHello1);
[anInvocation invoke];
}
- 定论:
成果如下。不溃散,而且从一开始的调用CJPerson
目标的sayHello
,到成功调用CJAnimal
目标的sayHello
办法。
2023-01-12 17:49:53.173226+0800 testOD1[87576:19797552] 给你一次时机...
2023-01-12 17:49:54.700033+0800 testOD1[87576:19797552] 给你二次时机...sayHello
2023-01-12 17:49:55.300064+0800 testOD1[87576:19797552] 给你第三次时机...sayHello
2023-01-12 17:49:55.300729+0800 testOD1[87576:19797552] 给你一次时机...
2023-01-12 17:49:56.781131+0800 testOD1[87576:19797552] forwardInvocation -[CJPerson forwardInvocation:]
2023-01-12 17:49:56.781496+0800 testOD1[87576:19797552] sayHello -[CJAnimal sayHello]
Program ended with exit code: 0
- 弥补:
在打印成果中发现,第2次动态办法抉择在methodSignatureForSelector
和forwardInvocation
办法之间。
四、总结
objc_msgSend
音讯慢速查找后的动态办法抉择与音讯转发流程就现已验证完了:先进行榜首次动态办法抉择,没有进行音讯快速转发,再没有进行音讯慢速转发,在音讯慢速转发中进行第2次动态办法抉择。
- 基本流程就如下图:
五、弥补点:
objc_msgSend
发送音讯的流程
到目前为止,objc_msgSend
发送音讯的流程就分析完结了,在这里简单总结下:
-
快速查找流程:在类的缓存
cache
中查找指定办法的完结。 -
慢速查找流程:假如缓存中没有找到,则在类的办法列表
methods
中查找,假如还是没找到,则去父类链的缓存和办法列表中查找。 -
动态办法抉择:假如慢速查找仍是没有找届时,榜首次弥补时机便是尝试一次动态办法抉择,即实例办法重写
resolveInstanceMethod
/类办法重写resolveClassMethod
办法。 -
音讯转发:假如动态办法抉择仍是没有找到,则进行音讯转发,音讯转发中有两次弥补时机:快速转发
forwardingTargetForSelector
、慢速转发methodSignatureForSelector
。 -
报错溃散:假如音讯转发之后也没有,则程序直接报错溃散
unrecognized selector sent to instance
。
oop
与aop
oop
:面向目标编程,什么人做什么什么事情,分工十分清晰。
- 好处:耦合度很低
- 痛点:有许多冗余代码,常规解决办法是提取,那么会有一个公共的类,所有人对公共的类进行集成,那么所有人对公共类进行强依赖,也就代表着呈现了强耦合
aop
:面向切面编程,是oop
的延伸
- 切点:要切入的办法和切入的类,比如上述的比如中的sayHello和CJPerson
- 优点:对事务无侵入,经过动态办法将某些办法进行注入
- 缺点:做了一些判别,履行了许多无关代码,包括体系办法,形成功能消耗。会打断apple的办法动态转发流程。