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

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


我们之前说过过于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篇,你和伍丽娟的爱情故事也算有头有尾,欢迎大家拍砖。端午节快乐!!!搬砖总地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,633评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,468评论 33 467
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,109评论 0 7
  • 消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 ...
    lylaut阅读 1,744评论 2 3
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,091评论 0 9