前言
上篇 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);
}
- 共同调用一个方法
_lookUpImpTryCachecls、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更合理。