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

2字数 2272阅读 4886

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


我们之前说过过于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的,也许你还能插一手。同时想要深入灵活的了解关于函数方法的东西,我们也需要明白消息转发的机制:

  • 消息转发第一步:+(BOOL)resolveInstanceMethod:(SEL)sel+(BOOL)resolveClassMethod:(SEL)sel->讨薪
    当向调用一个方法,但没有实现时,消息会通过上面两个方法寻找是否能找到实现?如果没有则返回NO,进入下一步。(虽然伍丽娟和你不可能了,但是她给你介绍了个工作,去浙江温州伍氏皮革厂工作。但是老板到了开工资的日期,却没有发工资,你抓紧去问问到底还能不能发工资)。

  • - (id)forwardingTargetForSelector:(SEL)aSelector -> 寻找温州皮革厂老板(能给钱的人)
    第一步如果返回NO会通过- (id)forwardingTargetForSelector:(SEL)aSelector方法再次寻找,不过这次找的是一个能响应该方法的对象。如果返回一个能响应该消息的对象,那么消息会转发到该对象那里, 如果返回nil则进行下一步,如果返回的对象不能相应此消息,直接返回异常。(你发现老板根本就不给发工资,老板带着资金跑路了!于是你开始找老板,找到老板就能拿回工资,找不到老板你就只能出此下策进行下一步了)。

  • - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector->私家侦探找线索
    forwardingTargetForSelector :返回nil时,会进行这一步,生成方法签名,如果方法签名为nil直接调用doesNotRecognizeSelector:返回异常,如果正常生成方法签名,则进行最后一步。(找私家侦探帮你找线索,如果没找到直接上口号老板带着小姨子跑了,我们没有办法,只能拿着钱包抵工资,原价二百多,三百多的钱包统统二十元... ...!如果侦探有线索你将踏上寻找老板并说服他发工资的漫漫长路。)

  • - (void)forwardInvocation:(NSInvocation *)anInvocation->私家侦探有线索,你踏上讨薪的长征路!
    到这这一步,其实我们还可以通过NSInvocation来力挽狂澜(我们在前面说过这个东西,也是很神奇的存在,不过有点麻烦),如果在这一步也不处理,只要你实现forwardInvocation :方法就不会抛出异常,消息被过滤掉,也就是并不会走doesNotRecognizeSelector:方法。(财务没帮你找到老板,私家侦探来帮你找到了线索,如果你能通过线索找到老板并说服给你发工资,讨薪完成,如果找不到,破釜沉舟,弃场拿货,统统二十元... ...,也有可能在你找他的这段时间,他还把货转移了!!!)

规避崩溃

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

使用场景

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

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

  • 对于class_addMethod这个方法之前与JSPatch结合较多,但是现在苹果大有势不两立只势,如果风声能过过去我们再说。(估计是过不去了,但是我们仍可以绕一些弯弯来做热修复)。
  • 我们进行数据解析时,经常碰到服务器会给我们返回NULL,导致crash,然后你就会为你的容错机制不健全感到羞愧。

老板开会的时候又会像上次一样想你投来你看这个菜比,这时候你马上登陆统计平台,就像我这么做线上异常分析,然后你发现服务器给了你一个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篇,你和伍丽娟的爱情故事也算有头有尾,欢迎大家拍砖。端午节快乐!!!搬砖总地址

推荐阅读更多精彩内容