面试题 For BearLiniOS模块详解Runtime

Runtime奇技淫巧之class_addMethod以及消息转

2017-05-26  本文已影响845人  穿山甲救蛇精

上回书说道,你和伍丽娟已经不可能了!我们也同时了解,虽然你的硬需求不能扩展,但是你可以努力奋斗,用你残缺的体魄通过不断累积方法走上人生巅峰,这... ...,就是我们今天的主题,但... ...,你还是个单身狗!


我们之前说过过于Method的一些方法,并且充分说明了SELMethodIMP之间是何种关系,今天我们先来重新把有关于它常用的方法做一次梳理:
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的List
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
⚠️:当判断一个实例方法是否实现时,第一个参数要用类对象,也就是[Person class]。
    当判断一个类方法是否实现时,第一个参数要传元类,也就是object_getClass([Person class])。

其他函数根据注释,参数以及返回值应该都能明白,说下class_addMethod这个方法的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用class_replaceMethod或者更深一步使用method_setImplementation,如果你想把一个函数替换为一个并未实现的函数,原对应函数实现保持不变(淡定,不会crash)。大概代码如下:

在ViewController中实现:
-(void)swizzTest{
    NSLog(@"swizzTest_swizz");
}
在Person类中实现:
-(void)eat{
    NSLog(@"eat_person");
}
//在viewDidLoad中
class_replaceMethod([self class], NSSelectorFromString(@"swizzTest"), method_getImplementation(class_getInstanceMethod([Person class], NSSelectorFromString(@"eat"))), "v@:");
或者:
method_setImplementation(class_getInstanceMethod([self class], NSSelectorFromString(@"swizzTest")), method_getImplementation(class_getInstanceMethod([Person class], NSSelectorFromString(@"eat"))));
然后调用:
[self swizzTest];

打印结果:

RuntimeSkill[3701:729966] eat_person

⚠️:把ViewController中的方法替换为Person的方法了?之前写的几篇总有人局限于类和对象的概念里出不来,会觉得只有对本类内进行操作才是可行的,在runtime的概念里,就是一堆C的结构体和函数这些个玩意,对于方法,只要取到函数的指针,还不是你想干嘛就干嘛,为所欲为,勇往无前,不撞南墙不回头!

参数分析

对于这些C函数,我们来剖析一下它的参数:
Class cls:类对象(⚠️:我们可以看到获取方法的函数有两个class_getInstanceMethodclass_getClassMethod,分别获取实例方法和类方法,但是如果我们要添加获或者替换方法就需要注意你操作的是实例方法还是类方法,如果是类方法这个参数一定要传本类的元类)
SEL name:方法的selector
IMP imp:函数对应实现
const char *types:代表函数类型,比如无参数无返回值->”v@:”,int类型返回值,一个参数传入->”i@:@”,如果你知道了对应的Method,你可以直接通过method_getTypeEncoding函数获取。

消息转发

我们都知道调用一个没有实现的方法时,会crash,我们来微笑着,一步步的看它是如何crash的,也许你还能插一手。同时想要深入灵活的了解关于函数方法的东西,我们也需要明白消息转发的机制:

规避崩溃

其实对于class_addMethod等关于方法的函数本人感觉不像之前说到的那些函数功能指向性那么明确,也可以说它可以实现的东西更为灵活,我们从消息转发的途径上来说一下这个东西的用处:
+(BOOL)resolveInstanceMethod:(SEL)sel(BOOL)resolveClassMethod:(SEL)sel方法中进行转发:

首先在`Person`类中
在.h中声明两个方法,但不去实现:
-(void)unKnowSel_obj;
+(void)unKonwSel_class;
在.m中实现这两个方法:
-(void)noObjMethod{
    NSLog(@"未实现这个实例方法");
}
+(void)noClassMethod{
    NSLog(@"未实现这个类方法");
}
并且重写消息转发的方法:
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
//注意:实例方法是存在于当前对象对应的类的方法列表中
+(BOOL)resolveInstanceMethod:(SEL)sel{
    SEL aSel = NSSelectorFromString(@"noObjMethod");
    Method aMethod = class_getInstanceMethod(self, aSel);
    class_addMethod(self, sel, method_getImplementation(aMethod), "v@:");
    return YES;
}
// 当一个类调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
//注意:类方法是存在于类的元类的方法列表中
+(BOOL)resolveClassMethod:(SEL)sel{
    SEL aSel = NSSelectorFromString(@"noClassMethod");
    Method aMethod = class_getClassMethod(self, aSel);
    class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), "v@:");
    return YES;
}

在VC中调用未实现的两个方法:
Person* person = [[Person alloc] init];
[person unKnowSel_obj];
[Person unKonwSel_class];

打印结果:

RuntimeSkill[4503:948902] 未实现这个实例方法
RuntimeSkill[4503:948902] 未实现这个类方法

可见,我们在第一步对调用的方法使用class_addMethod进行实现,可以使消息正确转发,找到指定对应函数实现(IMP)(你去财务要薪资,直接人家就给你了!)。把消息转发第一步的两个方法干掉,我们这样试试:

声明一个`Boss`类,并在.m中实现方法:
@implementation Boss
-(void)unKnowSel_obj{
    NSLog(@"unKnowSel_obj_Boss");
}
@end
在`Person`类中重写方法:
-(id)forwardingTargetForSelector:(SEL)aSelector{
    return [[Boss alloc] init];
}

在VC中调用未实现的两个方法:
Person* person = [[Person alloc] init];
[person unKnowSel_obj];

打印结果:

RuntimeSkill[4540:956249] unKnowSel_obj_Boss

我们制定了相应该方法的对象,同样完成消息转发。(你去财务,财务说老板跑了,但是你找到老板了,正好老板有钱,你的工资到位了!)
我们再把 forwardingTargetForSelector :方法去掉,做如下操作:

在`Person`类中重写方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"unKnowSel_obj"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation invokeWithTarget:[[Boss alloc] init]];
}

在VC中调用未实现的两个方法:
Person* person = [[Person alloc] init];
[person unKnowSel_obj];

打印结果:

RuntimeSkill[5019:1010897] unKnowSel_obj_Boss

我们通过NSInvocation转化为正常的消息转发。(最终如果你去财务和直接找老板都失败了,你还可以通过特殊手段拿到钱,并且不管是不是老板给钱就行。也有可能侦探告诉你的消息是假的,当你反应过来,回去拿货的时候,货已经被转移了,你的讨薪计划失败!)

使用场景

扯了这么多淡,无非就是想让你认清现实,伍丽娟坑你了!!!

吃一堑长一智,我们来看看如何避免吧:

老板开会的时候又会像上次一样想你投来你看这个菜比,这时候你马上登陆统计平台,就像我这么做线上异常分析,然后你发现服务器给了你一个NULL,顿时杀心四起,不是说好不给NULL吗 ?不是说好做彼此的天使吗?于是你看到了我的讲解:因为服务器返回数据中只有数字,字符串, 数组和字典四种类型,所以我们只要在NULL找不到方法实现的时候向能响应这个方法的对象进行转发就可以啦。方法如下:

给`NSNull`创建一个分类,并在.m中实现:

#import "NSNull+safe.h"
@implementation NSNull (safe)
#define pLog
#define JsonObjects @[@"",@0,@{},@[]]
- (id)forwardingTargetForSelector:(SEL)aSelector {
    for (id jsonObj in JsonObjects) {
        if ([jsonObj respondsToSelector:aSelector]) {
#ifdef pLog
            NSLog(@"NULL出现啦!这个对象应该是是_%@",[jsonObj class]);
#endif
            return jsonObj;
        }
    }
    return [super forwardingTargetForSelector:aSelector];
}

然后调用这样调用:

NSDictionary* dict = [[NSNull alloc] init];
[dict objectForKey:@"123"];

结果:

RuntimeSkill[5526:1078091] NULL出现啦!这个对象应该是是___NSDictionary0
如果不实现这个分类则直接异常:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSNull objectForKey:]: unrecognized selector sent to instance 0x10c8de180'

加入这个之后,就再也不怕服务器不做你的天使啦!!!

结语

Runtime就先到这里了,总共7篇,你和伍丽娟的爱情故事也算有头有尾,欢迎大家拍砖。端午节快乐!!!搬砖总地址

上一篇下一篇

猜你喜欢

热点阅读