关于消息转发已经是个烂话题,可以搜索下别人的文章
如果没有搜索,这里也会先简单介绍下消息转发概念和过程
简单介绍消息转发过程
oc的方法调用过程是一次消息发送的过程
当类的方法表里没有消息中的选择子时,会有一个消息转发的阶段
第一阶段:可以在该阶段动态添加方法
类方法需要覆盖resolveClassMethod
实例方法需要覆盖resolveInstanceMethod
这两个方法返回前可以为未找到的选择子动态加入方法
+ (BOOL)resolveClassMethod:(SEL)sel
{
//dynamic add method then return YES
//class_addMethod(Class cls, SEL name, IMP imp, const char *types);
//return YES;
return NO;
}
- (BOOL)resolveInstanceMethod:(SEL)sel
{
//dynamic add method then return YES
return NO;
}
第二阶段:交给其他对象处理
- (id)forwardingTargetForSelector:(SEL)aSelector
{
//返回一个可以处理选择子的对象
// return someObjCanHanleThisSelector;
return [super forwardingTargetForSelector:aSelector];
}
当然这只是字面上的意思,依然可以动态添加方法后返回自己
第三阶段:拥有完整消息的转发处理
NSInvocation进入头文件可以查看到,这个对象包含并且可以设置一次消息发送的所有要素
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//anInvocation can set anything
[anInvocation invoke];
}
还需要配合方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
如果你是新手还不知道如何动态加入方法,一个小提示class_addMethod(Class cls, SEL name, IMP imp, const char *types);xcode进入头文件会有一堆类似的方法
科普时间:
Selector
简单点说是一个方法的名字,或者说是在某个对象寻找方法实现时的索引
IMP
一个接受调用者、选择子、方法入参并返回id的函数指针,看下代码:
typedef id (*IMP)(id, SEL, ...);
可以理解为一个方法真正的实现
NSMethodSignature
名字这么玄乎,实际是对方法参数的一个包装,例如可以调用
[NSMethodSignature signatureWithObjCTypes:SomeTypeEncoding]
NSInvocation
这是一个消息调用的封装,拥有一次消息发送的所有信息,具体可进入头文件
那么问题来了,如果不希望再看到unrecognized selector...报错怎么办
思路就是让这个消息转发能安全走完(废话- -)
兜住实例方法
这里有一片很仔细的文章,介绍了如何兜住一个实例的方法调用
简单概括下,就是重写某一转发方法,用第三者对象去动态添加来保证完整的转发。
或者load时替换掉某一转发方法再去动态添加。
在上面很仔细的文章里也介绍了消息转让前的方法的寻找过程,这个过程就是下面代码往里走,如果需要验证这个过程,这里简单贴个入口(具体实现有点长,可以进这篇文章底部去下载源码):
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
那么如何添加类方法?
实验环境
如果需要考虑可行性,可以先看看这里
为了完成这个实验,先写了篇oc里对象、类、元类的简单介绍。
简单贴一下实验环境:
- (void)startTest { [[self class]performSelector:@selector(goFindNonexistedClassMethod) withObject:nil afterDelay:0]; }
+ (void)nonexistedClassMethodSaver { NSLog(@"recognized selector bla bla"); }
为什么别的转发方法不贴了?因为resolveClassMethod返回NO就崩溃了+ (BOOL)resolveClassMethod:(SEL)sel { if ([NSStringFromSelector(sel) isEqualToString:@"goFindNonexistedClassMethod"]) { //do some thing return YES; } return YES; }
思路就是需要在注有"do some thing"的地方加入方法
class_addMethod
博文上面有提到这个方法class_addMethod(Class cls, SEL name, IMP imp, const char *types),它可以为这个类的实例添加方法,实例(至少翻译过来是这样)
那么但需要为类自己添加方法时可不可以用这个方法呢?(这么问肯定是可以,怎么知道的?崩溃多几次)
在多次尝试后,找到了答案,有一个启发来源于巧大大的一篇文章(博客上没找到,去公众号找到了)
里面提到了关于NSObject哪些方法定义由类定义,哪些方法由元类定义。文中提到NSObject类中定义了实例的init等方法,元类中定义了+(id)alloc,+(void)load灯方法
那么大胆尝试下 给这个函数来一一填入参数
给class传入对象的元类
找到IMP [self methodForselector:@selector(nonexistedClassMethodSave)]
找IMP时遇过的小坑错误的调用了class_getMethodImplemention(cls,SEL),这个函数会找实例方法里有没有对应的IMP,当然没有,然后进入-resolveInstanceMethod:,套用NSObject里方法层级的定义,这里传入metaclass试试,结果可行(当然,‘套用’俩字的背后是代码实现,去metaclass的方法列表找到了类方法)
types method_getTypeEncoding(class_getClassMethod(self)) (为什么传self,这个函数会先找metaclass然后搜索)
最后把上面的参数一一填入,结果成功。
P.S.
欢迎讨论!
最近上架了一个app,可以点击这里查看,实现起来并不难,在体验后如果有需要可以在以后的博文里介绍下实现的心路历程,欢迎反馈