前言
上篇 objc_msgSend慢速查找 中,其中当查找不到 imp
时(不仅是当前类找不到,其父类直到 NSObject
都没找到),会进行 behavior
判断,进而直接返回 resolveMethod_locked
方法的返回值。
其目的是苹果给你一次容错的机会。如果在resolveMethod_locked
方法不处理,那么就会抛出崩溃异常。下面将重点探究 resolveMethod_locked
的原理,即 动态方法决议
。
动态方法决议
// 如果找不到imp,执行一次方法解析
// 执行一次的原因:
// 第一次:behavior = 3,LOOKUP_RESOLVER = 2, 3 & 2 = 2,进入if, behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1
// 第二次:behavior = 1,LOOKUP_RESOLVER = 2, 1 & 2 = 0,不再进入if
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
// 动态方法决议
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
- 此方法只执行
一次
,类似于单利。- 经过运算,最终
behavior = 1
。
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()) {
// 对象方法的动态决议
resolveInstanceMethod(inst, sel, cls);
}
// 元类
else {
// 类方法的动态决议
resolveClassMethod(inst, sel, cls);
// 再次尝试查找,此时 behavior = 1
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
// 如果没找到imp
// 对象方法的动态决议
resolveInstanceMethod(inst, sel, cls);
}
}
// 因为上面在执行动态方法决议时,有可能已经存在缓存,此时再次查找缓存,并使用。此时 behavior = 1
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
- 如果非元类,执行对象方法动态协议
resolveInstanceMethod
- 如果元类,首先执行类方法动态协议
resolveClassMethod
,然后lookUpImpOrNilTryCache
尝试查找imp
,如果没找到imp
,则执行元类对象方法动态协议resolveInstanceMethod
。- 最后再次
lookUpImpOrForwardTryCache
查找缓存。
lookUpImpOrNilTryCache 和 lookUpImpOrForwardTryCache
当元类时,会再次尝试查找 lookUpImpOrNilTryCache
,那么最后会执行 lookUpImpOrForwardTryCache
,那么两者有什么区别?
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
// 此时参数 behavior = 5
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
// 此时参数 behavior = 1
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
复制代码
- 共同调用一个方法
_lookUpImpTryCache
cls
、behavior
参数不同,这也导致了两个本质上的不同,是否进行了消息转发。lookUpImpOrForwardTryCache
进行消息转发,而lookUpImpOrNilTryCache
不进行消息转发。
_lookUpImpTryCache
既然 lookUpImpOrNilTryCache
和 lookUpImpOrForwardTryCache
都会执行 _lookUpImpTryCache
,下面具体分析源码:
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
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:
// 对于慢速查找动态方法决议来说,由于 behavior = 5, 该if不会执行
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
// 返回imp
return imp;
}
复制代码
isInitialized()
基本不会执行,因为在慢速查找流程中,已经对类进行初始化了。cache_getImp
查找缓存。如果能找到,返回imp
;如果找不到,查找共享缓存,如果共享缓存也没找到,那么再次执行lookUpImpOrForward
慢速查找流程。- 再次执行慢速查找流程时,不会执行
resolveMethod_locked
动态方法决议了。
resolveInstanceMethod
和 resolveClassMethod
是在返回 lookUpImpOrForwardTryCache
之前调用的。那么他们到底是什么作用?又该如何使用?
resolveInstanceMethod 对象方法动态决议
当对象进行调用 objc_msgSend
方法时,并且通过快速和慢速查找都没找到时,会执行 resolveInstanceMethod
,其源码分析如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
// 获取类方法 +resolveInstanceMethod
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 先进行元类查找是否实现了`resolveInstanceMethod` 方法,也就是类的类方法。如果没有实现直接返回。
// 如果当前类的元类是NSObject,则不会返回,因为NSObject默认实现了。
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
return;
}
// 系统会发送一个 resolveInstanceMethod 方法,因为消息的接收者是cls,所以 resolveInstanceMethod 是类方法。
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 查找 imp
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
// 日志打印
if (resolved && PrintResolving) { ... }
}
复制代码
- 获取
resolveInstanceMethod
类方法 ,之所以是类方法
是因为在下面的流程中系统自动调用了objc_msgSend
,其消息接收者是cls
,所以是类方法
。- 进行元类
resolveInstanceMethod
的查找,此处不会为空。- 系统给类发送
+resolveInstanceMethod
消息。- 查找
imp
。此时并没有直接返回imp
,而是只进行了判断。resolved && PrintResolving
是日志打印
resolveInstanceMethod 流程分析
- 在执行
resolveInstanceMethod
方法时,并执行lookUpImpOrNilTryCache
进行查找,查找元类是否实现了resolveInstanceMethod
方法。其中cls->ISA()
说明是元类,最终会找到NSObject
中。如果没有实现,则将resolveInstanceMethod
缓存到元类中。( 此流程NSObject
默认实现了)- 接着系统发送
resolveInstanceMethod
消息。- 再次执行
lookUpImpOrNilTryCache
在ZLSubObject
中查找imp
,其目的是将imp
加入ZLSubObject
的缓存中,如果找不到会存入_objc_msgForward_impcache
。- 返回继续执行
lookUpImpOrForwardTryCache
查找,此时消息慢速查找不会执行。因为前面已经写入了对应的缓存。这次会从缓存中获取到imp
为_objc_msgForward_impcache
。直接进行了消息转发。
结论:
lookUpImpOrNilTryCache
只是将方法插入缓存lookUpImpOrForwardTryCache
从缓存中获取imp
- 这就是为什么执行了
lookUpImpOrNilTryCache
查找了imp
,却没有返回,反而又执行了lookUpImpOrForwardTryCache
进行查找imp
,调用两次的原因。
resolveInstanceMethod 案例分析
继续使用上篇 objc_msgSend慢速查找-案例 的案例,其中 ZLObject
改进如下:
@interface ZLObject : NSObject
+ (void)classMethod1;
+ (void)classMethod2;
- (void)instanceMethod1;
- (void)instanceMethod2;
@end
@implementation ZLObject
+ (void)classMethod1 {
NSLog(@"%s",__func__);
}
- (void)instanceMethod1 {
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%@", self, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
复制代码
执行代码如下:
ZLSubObject *subObj = [[ZLSubObject alloc] init];
[subObj subInstanceMethod];
[subObj instanceMethod1];
[subObj instanceMethod2];
复制代码
打印结果如下:
-[ZLSubObject subInstanceMethod]
-[ZLObject instanceMethod1]
resolveInstanceMethod: ZLSubObject-instanceMethod2
resolveInstanceMethod: ZLSubObject-instanceMethod2
-[ZLSubObject instanceMethod2]: unrecognized selector sent to instance 0x10060e470
复制代码
- 虽然还是崩溃,但是会发现多了两条打印:
resolveInstanceMethod: ZLSubObject-instanceMethod2
。- 这说明在类中实现了
+ (BOOL)resolveInstanceMethod
是可以获取到的。
但是为什么会打印两条:resolveInstanceMethod: ZLSubObject-instanceMethod2
?
在 + (BOOL)resolveInstanceMethod
打上断点如下,并执行:
第一次进入堆栈如下:
第二次进入堆栈如下:
结合
resolveInstanceMethod
的流程,分析如下:
- 第一次执行
resolveInstanceMethod
方法时,最终会执行lookUpImpOrForwardTryCache
进行消息转发。当消息转发时,会执行class_getInstanceMethod
方法。class_getInstanceMethod
的源码如下:
Method class_getInstanceMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; // 慢速查找方法,且 behavior = LOOKUP_RESOLVER lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER); // 查找 cls 的 method return _class_getMethod(cls, sel); } 复制代码
- 又执行了一次
lookUpImpOrForward
,并且behavior = LOOKUP_RESOLVER
,所以这次不进行消息转发了,不会造成死循环。这就是第二次打印的原因。
resolveInstanceMethod 案例改进
根据上面分析,如果 imp
实现了,那么就不会进行消息转发,也会不会打印第二次。相关代码修改如下:
- (void)instanceMethod1 {
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%@", self, NSStringFromSelector(sel));
if (sel == @selector(instanceMethod2)) {
IMP imp = class_getMethodImplementation(self, @selector(instanceMethod1));
Method m = class_getInstanceMethod(self, @selector(instanceMethod1));
const char * type = method_getTypeEncoding(m);
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
复制代码
打印结果如下:
resolveInstanceMethod: ZLSubObject-instanceMethod2
-[ZLObject instanceMethod1]
复制代码
当获取到
instanceMethod2
时,通过动态添加方式,让其执行其他已经实现的方法,比如instanceMethod1
方法。
resolveClassMethod 类方法动态决议
类方法动态决议
和 对象方法动态协议
有很多相似的地方。类方法动态决议源码如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// 先查找当前类是否实现了`resolveClassMethod` 方法,如果没有实现直接返回。
// 这里不会返回,因为NSObject默认实现了。
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
return;
}
// 非元类
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);
// 查找 imp
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
// 日志打印
if (resolved && PrintResolving) { ... }
}
复制代码
流程分析:
- 在执行
resolveClassMethod
方法时,并执行lookUpImpOrNilTryCache
进行查找,查找类是否实现了resolveClassMethod
方法。如果没有实现,则将resolveClassMethod
缓存到类中。( 此流程NSObject
默认实现了)- 接着获取非元类
nonmeta
,目的是防止非元类没有实现。其中获取非元类方法getMaybeUnrealizedNonMetaClass
,主要逻辑是:如果是非元类直接返回非元类
;如果是类的ISA
指向自身
,那么返回NSObject
;如果是其他类,循环获取父类,直到找到NSObject
为止;以及其他类的判断逻辑,此处不再赘述。- 系统发送
resolveClassMethod
消息。- 再次执行
lookUpImpOrNilTryCache
在ZLSubObject
中查找imp
,其目的是将imp
加入ZLSubObject
的缓存中,如果找不到会存入_objc_msgForward_impcache
。- 返回继续执行
lookUpImpOrForwardTryCache
查找,此时消息慢速查找不会执行。因为前面已经写入了对应的缓存。这次会从缓存中获取到imp
为_objc_msgForward_impcache
。直接进行了消息转发。resolved && PrintResolving
是日志打印。
结论:
lookUpImpOrNilTryCache
只是将方法插入缓存lookUpImpOrForwardTryCache
从缓存中获取imp
resolveClassMethod 案例分析
ZLObject
中添加代码如下:
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@-%@", self, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
复制代码
执行代码如下:
[ZLSubObject classMethod2];
复制代码
打印结果如下:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-classMethod2
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-classMethod2
+[ZLSubObject classMethod2]: unrecognized selector sent to class 0x100008310
复制代码
- 虽然还是崩溃,但是会发现打印多条:
ZLSubObject-
。其中classMethod2
也是两次打印。 (encodeWithOSLogCoder
暂时不去探究 )- 当调用
lookUpImpOrNilTryCache
没有找到imp
时,就调用resolveInstanceMethod
去查找(此时cls
为元类,因为类方法其本质上是元类的对象方法),没有找到就执lookUpImpOrForwardTryCache
,即消息转发。- 最后在消息转发的时候会再执行一次方法动态决议。
- 此时打印两条的原因和
resolveInstanceMethod
类似,第二次是因为消息转发,执行了元类的class_getInstanceMethod
。
resolveClassMethod 案例改进
相关代码修改如下:
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@-%@", self, NSStringFromSelector(sel));
if (sel == @selector(classMethod2)) {
Class metaClass = objc_getMetaClass("ZLObject");
IMP imp = class_getMethodImplementation(metaClass, @selector(classMethod1));
Method m = class_getClassMethod(self, @selector(classMethod1));
const char * type = method_getTypeEncoding(m);
return class_addMethod(metaClass, sel, imp, type);
}
return [super resolveClassMethod:sel];
}
复制代码
打印结果如下:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-classMethod2
+[ZLObject classMethod1]
复制代码
当获取到
classMethod2
时,通过动态添加方式,让其执行其他已经实现的方法,比如classMethod1
方法。
动态方法决议-分类
由于调用类方法时,执行类方法动态决议,当 lookUpImpOrNilTryCache
没找到时,执行元类的 resolveInstanceMethod
。
// 类方法的动态决议
resolveClassMethod(inst, sel, cls);
// 再次尝试查找,此时 behavior = 1
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
// 如果没找到imp
// 对象方法的动态决议
resolveInstanceMethod(inst, sel, cls);
}
复制代码
所以,不管是
对象方法
动态决议还是类方法
动态决议,都可以在元类resolveInstanceMethod
中对方法进行处理。
根据 isa的走位图,
NSObject
既是根类也是元类,在NSObject
调用+方法
存到NSObject
的元类中,也就是NSObject
自己,通过NSObject
的resolveInstanceMethod
方法就可以实现了。
添加一个 NSObject
的分类,实现方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%@", self, NSStringFromSelector(sel));
if (sel == @selector(instanceMethod2)) {
IMP imp = class_getMethodImplementation(self, @selector(instanceMethod1));
Method m = class_getInstanceMethod(self, @selector(instanceMethod1));
const char * type = method_getTypeEncoding(m);
return class_addMethod(self, sel, imp, type);
} else if (sel == @selector(classMethod2)) {
Class metaClass = objc_getMetaClass("ZLObject");
IMP imp = class_getMethodImplementation(metaClass, @selector(classMethod1));
Method m = class_getClassMethod(self, @selector(classMethod1));
const char * type = method_getTypeEncoding(m);
return class_addMethod(metaClass, sel, imp, type);
}
return NO;
}
复制代码
分别调用对象方法和类方法:
ZLSubObject *subObj = [[ZLSubObject alloc] init];
[subObj instanceMethod2];
[ZLSubObject classMethod2];
复制代码
执行结果:
resolveInstanceMethod: ZLSubObject-instanceMethod2
-[ZLObject instanceMethod1]
resolveInstanceMethod: ZLSubObject-classMethod2
+[ZLObject classMethod1]
复制代码
这样就在 NSObject
的分类中实现 resolveInstanceMethod
,既处理了类方法,也处理了实例方法。两次调用参数不同,一次是类调用,一次是元类调用。
总结
lookUpImpOrNilTryCache
只对方法进行缓存,lookUpImpOrForwardTryCache
,从缓存查找imp
,进行消息转发。resolveInstanceMethod
和resolveClassMethod
打印两条的原因,是因为第二次执行了消息转发,执行了元类的class_getInstanceMethod
。- 当执行类方法动态决议时,如果没有命中,会调用元类的
resolveInstanceMethod
对象方法。这也符合,调用类方法其实是调用元类的对象方法这一原则。 - 在
NSObject
的分类中实现resolveInstanceMethod
,既处理了类方法,也处理了实例方法。 - 动态方法决议的返回值不会影响功能,只是对日志打印有影响。
动态方法决议流程图
aop & oop
动态方法决议的意义在于,当苹果查找 imp
找不到的时候给的一次解决错误的机会。例如上面的案例中,可以在 NSObject
的分类中实现 + resolveInstanceMethod
, 这样找不到的 imp
都会在 resolveInstanceMethod
中监听到。
那么在实际的开发过程中,是不是可以用这种方式处理呢?比如在自己的工程中类名是可以根据前缀、模块、事务、类型等进行区分(例如:ZLLoginCodeController
),当发现 方法
有问题的时候,可以进行容错处理。比如当获取登录验证码时 getLoginCodeEvent
出现问题的时候,进行上报处理。其实是有一定的 弊端
。
这种方式就是切面编程。
oop
oop
是面向对象编程(Object Oriented Programming),是常用的编程方式,其特点是封装性
、继承性
、多态性
。oop
达到了软件工程的三个主要目标:重用性
、灵活性
和 扩展性
。OOP = 对象+类+继承+多态+消息
,其中核心概念是 类和对象
。
一般情况下会提取 公共的类
,但是遵循后会有 弊端
,就是对类有很强的依赖,耦合性很强。但是其实对于开发者来说更关心是的业务,所以处理公共类时,尽量 少侵入
,最好 无侵入
。通过动态方式注入代码,对原始方法没有影响。其中要切入的方法和类就是 切点
。
aop
aop
是面向切面编程(Aspect Oriented Programming),aop
是oop
的延伸。是 函数式编程
的一种衍生范型。利用 aop
可以对业务逻辑的各个部分进行 隔离
,从而使得业务逻辑各部分之间的 耦合度降低
,提高程序的可重用性,同时提高了开发的效率。
但是其缺点也暴露出来了:
- 代码冗余:比如上面案例中的
if-else
判断,如果方法再多,还得写if-else
判断。 - 浪费性能:方法会调用很多次,浪费了性能。如果命中还好,没有命中会走多次,会有
性能消耗
。 - 重复处理:如果其它模块也做了相应处理,重复了这块,不一定会执行到。
动态方法决议是
消息转发机制
的前一个阶段。意味着如果在这里做了容错处理,后面的流程就被切掉了。那么转发流程就没有意义了。所以在后面的流程做aop
更合理。