一、方法实现缺失报错
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayHelloPlus;
- (void)sayPerson;
+ (void)sayPersonPlus;
@end
@implementation LGPerson
- (void)sayPerson{
NSLog(@"%s",__func__);
}
+ (void)sayPersonPlus{
NSLog(@"%s",__func__);
}
@interface LGTeacher : LGPerson
- (void)sayTeacher;
+(void)sayTeacherPlus;
@end
- (void)sayTeacher {
NSLog(@"%s",__func__);
}
+(void)sayTeacherPlus {
NSLog(@"%s",__func__);
}
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayTeacher];
[teacher sayPerson];
[teacher sayHello];
打印输出:
-[LGTeacher sayTeacher]
-[LGPerson sayPerson]
-[LGTeacher sayHello]: unrecognized selector sent to instance 0x1006479d0
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[LGTeacher sayHello]: unrecognized selector sent to instance 0x1006479d0'
可以看到LGTeacher中sayTeacher、LGPerson中sayPerson都正常输出,LGPerson里的sayHello因为没有实现,报错了。
试一下调用类方法呢?
LGTeacher *teacher = [LGTeacher alloc];
[LGTeacher sayTeacherPlus];
[LGTeacher sayPersonPlus];
[LGTeacher sayHelloPlus];
打印输出:
+[LGTeacher sayTeacherPlus]
+[LGPerson sayPersonPlus]
[LGTeacher sayHelloPlus]: unrecognized selector sent to class 0x100008388
同样 子类,父类实现了方法的都正常打印,父类未实现的方法报错。
来探索下为什么会报这个错误,报错流程是怎么走的。
二、报错流程探索
调用方法相当于是找方法的实例,也就是调用class_getInstanceMethod这个方法。
lookUpImpOrForward方法就是我们上篇文章分析的慢速查找流程,但是我们没有分析最终未查找到的处理。
这里break掉了死循环,然后最终返回imp,看下这里的forward_imp。
全局搜索_objc_msgForward_impcache,又找到了汇编里。
TailCallFunctionPointer我们探索过,就是返回$0,也就是x17。
x17指向的是__objc_forward_handler方法,全局搜索
最终找到了这里,看到了报错的元凶,而且里面连 '+' ' -'号都写好了。~
三、动态方法决议
上边最终找不到方法会报错,那么报错之前有没有进行什么补救措施,或者进行一些其他处理呢?来探索一下~
lookUpImpOrForward这个方法中这段代码我们没研究过,来看下:
先来看判断条件:LOOKUP_RESOLVER = 2;
behavior = 1 | 2, 0001 | 0010 = 0011 = 3
所以判断条件为 3 & 2 (0011 & 0010 = 0010) = 2;
behavior ^= LOOKUP_RESOLVER;(2 ^2);behavior = 0,那么下次进行判断的话,判断条件就成了0,相当于一个单例,只走一次。
接下来看返回方法:resolveMethod_locked(inst, sel, cls, behavior);
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
//实例方法,看这里
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
//这里为什么又查找一遍呢,因为上边if else又做了一些补救措施查找imp,确保程序不会crash
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
既然找不到以后系统会发送消息给resolveInstanceMethod方法,那么我们是不是可以重写下该方法,来赋值imp,以防crash呢。试一试。
因为消息是发送给cls,所以我们重写要写类方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod:%@--%@", self, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
然后我们在main方法中调动sayHello;
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
输出结果:
因为还是没找到,还是报错,但是报错之前打印了我们重写方法的NSLog,证明我们拦截成功了~ 至于为什么输出2次,这个我们后续再分析。
那么是不是可以在她寻找不到imp的时候动态的给sel插入一个相应的imp呢,来解决这个crash。
- (void)saveU {
NSLog(@"666-%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if(sel == @selector(sayHello)) {
//补救 - 动态添加一个方法
IMP imp = class_getMethodImplementation(self, @selector(saveU));
Method m = class_getInstanceMethod(self, @selector(saveU));
const char *type = method_getTypeEncoding(m);
return class_addMethod(self, sel, imp, type);
}
NSLog(@"resolveInstanceMethod:%@--%@", self, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
来输出一下:
完美解决~原本调用sayHello,未找到让我们修改为了saveU。
同样的,如果是类方法imp缺失呢?
存在类方法里,那么元类里的对象方法不就是类里面的类方法吗,那么我们同样可以在LGTeacher类里通过实现resolveClassMethod:方法来规避crash。
+ (void)saveUPlus {
NSLog(@"888-%s",__func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(sayHelloPlus)){
IMP imp = class_getMethodImplementation(objc_getMetaClass(class_getName(self)), @selector(saveUPlus));
Method m = class_getInstanceMethod(objc_getMetaClass(class_getName(self)), @selector(saveUPlus));
const char *type = method_getTypeEncoding(m);
return class_addMethod(objc_getMetaClass(class_getName(self)), sel, imp, type);
}
NSLog(@"resolveClassMethod:%@--%@", self, NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
打印输出:
可以LGTeacher里实现resolveClassMethod方法一样可以拦截错误。
四、疑问
上边看的resolveMethod_locked(inst, sel, cls, behavior);方法中的代码里,下图这个地方为什么又要查找一次实例方法呢?
这相当于是遵循了isa流程图的两条线。
如果是非元类的,走红线的流程查找,如果是元类的,走路线查找流程。
五、imp查找失败统一拦截
上边实例方法和类方法的imp查找失败拦截我们用2个方法负责拦截的,那么这两个方法可不可以统一到一起呢?
答案是阔以的~ 通过上边我们分析if else的流程,结合isa走位图发现最终都会查找到NSObject里,放到NSObject的category里就好了~ 因为lookUpImpOrForward方法里的死循环中
这段代码就是不断的去父类里查找imp,找不到的话最终都会找到NSObject里,所以放到NSObject里总会找到。
那么我们统一一下,在NSObject里就不分类方法了,通通是实例方法,所以都会走到
+(BOOL)resolveInstanceMethod:方法里。
@interface NSObject (SaveCrash)
@end
@implementation NSObject (SaveCrash)
- (void)saveU {
NSLog(@"666-%s",__func__);
}
+ (void)saveUPlus {
NSLog(@"888-%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if(sel == @selector(sayHello)) {
//补救 - 动态添加一个方法
IMP imp = class_getMethodImplementation(self, @selector(saveU));
Method m = class_getInstanceMethod(self, @selector(saveU));
const char *type = method_getTypeEncoding(m);
return class_addMethod(self, sel, imp, type);
} else if (sel == @selector(sayHelloPlus)){
IMP imp = class_getMethodImplementation(objc_getMetaClass(class_getName(self)), @selector(saveUPlus));
Method m = class_getInstanceMethod(objc_getMetaClass(class_getName(self)), @selector(saveUPlus));
const char *type = method_getTypeEncoding(m);
return class_addMethod(self, sel, imp, type);
}
NSLog(@"resolveInstanceMethod:%@--%@", self, NSStringFromSelector(sel));
return NO;
}
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
[LGTeacher sayHelloPlus];
正常打印我们自己写的imp方法,成功 !
六、AOP & OOP
AOP是OOP的一个延伸,什么是OOP呢?
OOP即面向对象程序设计(Object Oriented Programming),是一种以对象为基础的编程思想。主要关注“谁来做”,即完成任务的对象。
程序中我们的对象是分工非常明确的,那么其中可能有一些冗余代码,对象A、B、C都要执行x方法,我们可以将A,B,C中的x方法提取出来放到新类M中,A,B,C都引用M来调用x方法即可,但这种方法A,B,C都必须引入M类,属于强依赖强耦合。
AOP即面向切面编程(Aspect Oriented Programming),基于OOP延伸出来的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
这里AOP就像我们创建的NSObject(SaveCrash),无侵入,不需要额外引入,低耦合。
但是AOP也有缺点
像输出放在if前边,我们可以看到打印结果,除了打印了666,888我们拦截的两个方法,还打印了其他的一些系统方法,浪费了一些性能。那么系统最终做了什么处理呢,我们下一篇来探索一下苹果的另一个方式‘消息转发’~