Objective-C 动态方法决议
一、动态方法决议
当imp
没有找到的时候的时候会赋值libobjc.A.dylib
_objc_msgForward_impcache`,首先会进入如下代码逻辑:
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
//要查找的对象,方法,类,1
return resolveMethod_locked(inst, sel, cls, behavior);
}
- 这其实可以理解为一个单类,相同流程只会进入一次。
behavior
上篇文章已经分析,值中有LOOKUP_INITIALIZE|LOOKUP_RESOLVER
进入后异或LOOKUP_INITIALIZE|LOOKUP_RESOLVER^ LOOKUP_RESOLVER
=LOOKUP_INITIALIZE
,相当于清空了LOOKUP_RESOLVER
。resolveMethod_locked
参数最后一个是LOOKUP_INITIALIZE
。
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()) {
//这里的cls是类
resolveInstanceMethod(inst, sel, cls);
}
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
//这里的cls是元类
resolveInstanceMethod(inst, sel, cls);
}
}
//又会去查找一次,既然这里又会去查找一次,那么肯定有什么地方会加入之前查找不存在的方法。
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
- 当快速和慢速消息查找都没有找到的时候进入了
resolveMethod_locked
。 - 查找的是实例方法则进行对象方法动态决议
resolveInstanceMethod
。 - 查找的是类方法则先进行类方法动态决议
resolveClassMethod
,再执行resolveInstanceMethod
(这里resolveInstanceMethod
调用与实例方法的resolveInstanceMethod
参数不同。)。 - 最后会调用
lookUpImpOrForwardTryCache
查找。
核心问题是最后要返回imp
,那么先看下lookUpImpOrForwardTryCache
进行的操作:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
只是一个简单的调用,继续排查:
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
//是否初始化,正常情况下是已经初始化了。
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
//这就是慢速消息查找流程,与之前的区别是 behavior = LOOKUP_INITIALIZE,没有动态方法决议参数了。
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//缓存查找
IMP imp = cache_getImp(cls, sel);
//找到直接跳转done
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
//imp不存在继续慢速消息查找流程
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
//是否消息转发
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
//返回imp
return imp;
}
isInitialized
正常情况是不会进入的。- 先去缓存查找对应的
imp
,找到直接返回。 - 没有找到会去动态共享缓存查找(如果支持)。
- 仍然没有会进行
lookUpImpOrForward
也就是再进行一次慢速消息查找。
既然这个函数也是进行快速和慢速消息查找的,那么就说明resolveInstanceMethod
与resolveClassMethod
可以在某个时机将方法加入类中。这样后面方法的调用才有意义。
二、对象方法动态决议 resolveInstanceMethod
通过源码分析发现在进行了快速与慢速消息查找后如果找不到imp
,苹果仍然给了机会进行resolveInstanceMethod
处理,那么核心肯定是要给类中添加imp
,源码如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//先进行元类查找是否实现了`resolveInstanceMethod`实例方法,也就是类的类方法。没有实现直接返回,这里不会返回,因为NSobject默认实现了。
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//系统自动发送了`resolveInstanceMethod`消息,由于消息的接受者是类,所以是+方法。
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 = 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));
}
}
}
- 先进行元类
resolveInstanceMethod
的查找和缓存。 - 系统自动给类发送了
resolveInstanceMethod
消息。既然是类调用的,那么就是+
方法。 - 接着进行了快速慢速方法查找
imp
,但是没有返回imp
(为什么不返回?这里只是缓存,如果有的话)。 lookUpImpOrNilTryCache
与lookUpImpOrForwardTryCache
唯一的区别是是否进行动态转发。这里不进行动态转发。- 可以看到返回的
resolved
只是进行了日志打印。也就是resolved
返回YES/NO
对功能没有影响。
那么就有个问题?
既然查找了imp
为什么不进行返回操作?而resolveInstanceMethod
调用结束后还查了一次?
2.1 + (BOOL)resolveInstanceMethod 调试分析
resolveInstanceMethod
源码跟踪流程如下:
cls->ISA
元类也就是HPObject
元类中查找有没有实现resolveInstanceMethod-imp
,最终会找到NSObject
元类然后将resolveInstanceMethod-imp
缓存写入HPObject
元类的缓存。(NSObject
默认实现了)。- 给类发送
resolveInstanceMethod
消息。 HPObject
查找instanceMethod
有没有实现,没有实现会将instanceMethod-_objc_msgForward_impcache(IMP)
写入HPObject
缓存。这个时候由于LOOKUP_NIL
的存在返回的是`nil。- 如果
lookUpImpOrNilTryCache
没有找到imp
会返回继续执行lookUpImpOrForwardTryCache
继续进行缓存->消息慢速查找
流程(消息慢速查找不会执行)。因为前面已经写入了对应缓存。这次会从缓存中获取到imp
为_objc_msgForward_impcache
的imp
。不会进入消息慢速查找流程,直接进行了消息转发。
这就说明resolveInstanceMethod
中首先元类查找resolveInstanceMethod
,目的是将resolveInstanceMethod
写入缓存。然后类发送resolveInstanceMethod
消息。接着lookUpImpOrNilTryCache
调用是将的imp
加入缓存中(无论是否找到,找不到会存入_objc_msgForward_impcache
)。返回后lookUpImpOrForwardTryCache
从缓存中找方法返回。
结论:resolveInstanceMethod中lookUpImpOrNilTryCache只是将方法插入缓存,返回后lookUpImpOrForwardTryCache从缓存中获取imp 这也是调用两次的原因。
2.2 + (BOOL)resolveInstanceMethod 实现
既然系统已经给了+ (BOOL)resolveInstanceMethod:(SEL)sel
进行容错处理,那么就实现下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
调用后发现这个方法调用了两次:
resolveInstanceMethod: HPObject-instanceMethod
resolveInstanceMethod: HPObject-instanceMethod
- 在
HPObject
的元类中能找到resolveInstanceMethod
方法,缓存的直接是自己的imp
了。 - 仍然是没有命中进行了消息转发。
消息转发会进入class_getInstanceMethod
:
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
又进行了一次lookUpImpOrForward
所以这也是调用了两次的原因。但是这次不进行消息转发了,所以不会造成死循环。
总结:第一次没有命中后,再进行消息转发后又会进行一次lookUpImpOrForward
消息慢速查找流程,所以resolveInstanceMethod
会执行两次。
那么如果实现中添加了imp
就肯定只调用一次了。
修改代码如下:
- (void)instanceMethod1 {
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"HPObject resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(instanceMethod)) {
IMP instanceMethod1 = class_getMethodImplementation(self, @selector(instanceMethod1));
Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, instanceMethod1, type);
}
return NO;
}
按照源码理解在lookUpImpOrNilTryCache
调用中只是增加到了缓存中,后面lookUpImpOrForwardTryCache
会从缓存中查找,找到imp
然后执行。
+ (BOOL)resolveInstanceMethod:(SEL)sel
返回NO/YES
根据源码来看只是打印日志相关的内容,应该是没有影响的。经过调试验证确实没有影响。
结论:
- resolveInstanceMethod 调用中只是对方法的缓存,lookUpImpOrNilTryCache 从缓存中再次查找方法。这也是为什么会查找两次的原因。
- resolveInstanceMethod 执行两次的原因是,在方法没有命中的时候消息转发过程中会再次进行lookUpImpOrForward(消息慢速查找),这就是执行两次的原因。
- + (BOOL)resolveInstanceMethod:(SEL)sel 返回值不会影响功能,只是对日志打印有影响,并且默认情况下是不打印日志的。
三、类方法动态决议resolveClassMethod
在上面最开始分析的时候类方法动态决议会先调用resolveClassMethod
,如果没有命中那么就会调用resolveInstanceMethod
:
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
resolveClassMethod
的实现如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//不会进入这里,先查找元类是否实现`resolveClassMethod`
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
//类方法存在元类中,操作元类防止没有实现。
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//非元类调用,也就是类方法
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {//......}
}
- 第一个
lookUpImpOrNilTryCache
查找元类是否实现,先将resolveClassMethod
插入HPObject
元类的缓存中。resolveClassMethod
。NSObject
默认实现了。 - 操作元类,防止元类没有实现。
- 元类中是以对象方法存在,所以在类中实现类方法就可以了。系统主动给类方法发送
+ resolveClassMethod
消息。这里细节的一点是通过nonmeta
来发送消息。 lookUpImpOrNilTryCache
查找目标imp
,先缓存后慢速。查找到后将imp
插入缓存,没有找到则将_objc_msgForward_impcache
插入缓存。
实现如下:
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
调用后发现打印了8
次:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
- 其中
encodeWithOSLogCoder
与我们无关,classMethod
出现两次符合预期(另外一次消息转发过程中调用)。 - 当调用
resolveClassMethod
没有实现的时候,就调用resolveInstanceMethod
去查找(这里的cls
参数是元类,与查找实例方法不同),仍然没有找到就执行lookUpImpOrForwardTryCache
。 - 最后在消息转发的时候会再执行一次方法动态决议。
修改实现:
+ (void)classMethod1 {
NSLog(@"%s",__func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(classMethod)) {
IMP classMethod1 = class_getMethodImplementation(objc_getMetaClass("HPObject"), @selector(classMethod1));
Method method = class_getClassMethod(self, @selector(classMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("HPObject"), sel, classMethod1, type);
}
return [super resolveClassMethod:sel];
}
这个时候就调用一次了。
既然resolveClassMethod
找不到的时候会执行一次resolveInstanceMethod
,那意味者可以在resolveInstanceMethod
中对类方法进行处理。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"HPObject resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
return NO;
}
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"HPObject resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
这个时候调试发现resolveInstanceMethod
并没有执行。为什么?因为这里是HPObject
元类调用resolveInstanceMethod
。
根据isa
的走位图,NSObject
同时也是元类,那么元类调用+
方法就要存到元类的元类中也就是存在根元类的元类,那么就是NSObject
自己,通过NSObject
的resolveInstanceMethod
方法就可以实现了。
添加一个NSObject
的分类,实现方法:
- (void)instanceMethod1 {
NSLog(@"%s",__func__);
}
+ (void)classMethod1 {
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%p-%@",self,self,NSStringFromSelector(sel));
if (sel == @selector(instanceMethod)) {
IMP instanceMethod1 = class_getMethodImplementation(self, @selector(instanceMethod1));
Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, instanceMethod1, type);
} else if (sel == @selector(classMethod)) {
IMP classMethod1 = class_getMethodImplementation(objc_getMetaClass("HPObject"), @selector(classMethod1));
Method method = class_getInstanceMethod(objc_getMetaClass("HPObject"), @selector(classMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("HPObject"), sel, classMethod1, type);
}
return NO;
}
分别调用instanceMethod
和classMethod
输出如下:
HPObject:0x1000082c8, HPMetaObject:0x1000082a0, NSObject:0x100358140, NSMetaObject:0x1003580f0
resolveInstanceMethod: HPObject-0x1000082c8-instanceMethod //类
-[NSObject(Additions) instanceMethod1]
resolveInstanceMethod: HPObject-0x1000082a0-classMethod //元类
HPObjcTest[59242:11857560] +[NSObject(Additions) classMethod1]
这样就在NSObject
的resolveInstanceMethod
中即处理了类方法也处理了实例方法。两次调用参数不同,一次是类调用,一次是元类调用。
⚠️如果两个都实现在
HPObject
类中,则都是类调用。
总结:
- resolveClassMethod 调用中只是对方法的缓存,lookUpImpOrNilTryCache会从缓存中再次查找方法,这也是为什么会查找两次的原因。
- resolveClassMethod 执行两次的原因是在方法没有命中的时候消息转发过程中会再次进行lookUpImpOrForward(消息慢速查找),再次走这个流程。这就是执行两次的原因。(
LOOKUP_NIL
有值,所以不会再次消息转发,不会造成死循环。) - resolveClassMethod 没有命中的时候会先调用resolveInstanceMethod(这里的
cls
是元类),再次调用时因为NSObject
是元类的父类。 - 这里resolveInstanceMethod由于是元类调用,所以只能实现在NSObject的分类中。(根元类的元类是自己,它的父类是
NSObject
) - + (BOOL)resolveClassMethod:(SEL)sel 返回值不会影响功能,只是对日志打印有影响。
三、aop & oop
那么动态方法决议的意义在哪里呢?
这是苹果在sel
查找imp
找不到的时候给的一次解决错误的机会。有什么意义呢?在NSObject
的分类中,所有找不到的OC
方法都能在resolveInstanceMethod
中监听到。
那么在自己的工程中可以根据类名前缀、模块以及事物进行区分prefix_ module_traffic
。当发现有问题的时候可以进行容错处理并且上报错误信息。 比如HP_Setting_didClickLogin
出现问题的时候进行上报,当超过阈值时进行报警。
这种方式就是aop
切面编程。我们比较习惯的方式是oop
。
oopoop
分工非常明确,耦合度小,冗余代码。一般情况下会提取公共的类,但是遵循后会对它有强依赖,强耦合。
这些其实不是我们关心的,我们更关心业务的内容,所以公共类尽量少侵入,最好无侵入。通过动态方式注入代码,对原始方法没有影响。这就相当于整个切面切入了,要切入的方法和类就是切点。aop
是oop
的延伸。
aopaop
的缺点在上面的例子中是if-else
过多冗余。正如上面看到的那样,方法会调用很多次浪费了相应的性能。如果命中还好,没有命中会走多次,会有性能消耗。它是消息转发机制的前一个阶段。意味着如果在这里做了容错处理,后面的流程就被切掉了。苹果写转发流程就没有意义了。
如果其它模块也做了相应处理,重复了这块不一定会执行到。所以在后面的流程做aop
更合理。
作者:HotPotCat
链接:https://www.jianshu.com/p/7daa33b95cd3