认识Runtime运行时机制
OC方法的本质
首先了解OC方法的本质到底是什么:
OC方法由两个部分组成:
SEL: 方法编号(一本书的目录编号)
IMP: 方法实现,是函数指针,指向函数(一本书的目录页码,页码指向对应页的内容)
动态绑定
简单举个例子:一个Person类的.m文件中不实现-(void)eat:(NSString *)
通过运行时来动态实现这个eat方法,这个过程叫做 动态绑定 :
#import <objc/message.h>
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 给类添加eat方法,IMP==eat
if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
//self:方法调用者,sel:方法编号,eat:(IMP函数指针)方法实现
class_addMethod(self, sel, eat, “”);
}
return [super resolveInstanceMethod:sel];
}
// 实现c函数eat()
void eat(id self, SEL _cmd, NSString *objc){
NSLog(@”我来了%@”, objc);
}
OC方法调用 会传递两个 隐式参数 self, _cmd,self 是方法的调用者,_cmd 是方法编号;
OC的方法调用其实是 消息发送 (通过终端clang –rewrite-objc main.m,生成一个.cpp文件,可以看到.m文件的底层实现。)
Person *p = [[Person alloc]init];
//[P eat:@”汉堡”]; 的底层就是objc_msgSend函数
objc_msgSend(P, @selector(eat:), @”汉堡”);
消息转发
重定向
当对象的方法签名在头文件中暴漏出来,而在.m文件中忘记实现,一般程序会报运行时错误不识别的选择器,通过消息转发可以改变这行为。
消息转发: 当对象接收到与其 方法集 不匹配的消息时,通过消息转发机制可以使对象执行用户预先定义的逻辑,如:将消息发送给能够做出响应的其他接收器(对象),或者将所有无法识别的消息都发送给同一个
接收器 再或者 默默的吞下消息(既不执行处理过程也不使程序抛出运行时错误)。
还是上面的例子,在Dog类中实现了eat方法,在Person类中可以通过 消息转发 让Dog去相应eat(我吃不了,dog你帮我吃吧)
//消息重定向
-(id)forwardingTargetForSelector:(SEL)aSelector{
if ([_dog respondsToSelector:aSelector]) {
return _dog; // 相当于 [_dog performSelector:aSelector];
}
// 给nil发消息
return nil;
}
方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if(aSelector == @selector(eat:)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; // v@:@ (type encoding)
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(eat)) {
Dog *dog = [[Dog alloc] init];
[anInvocation invokeWithTarget:dog];
}
}
重写methodSignatureForSelector:和forwardInvocation:方法,将方法签名,转发给真正实现了该方法的目标对象,让其去调用已实现的方法。
methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名,必须实现,并且返回不为空的methodSignature,否则会crash
forwardInvocation:将选择器转发给一个真正实现了该消息的对象。
1.forwardingTargetForSelector同为消息转发,但在实践层面上有什么区别?何时可以考虑把消息下放到forwardInvocation阶段转发?
forwardingTargetForSelector 仅支持一个对象的返回,也就是说消息只能被转发给一个对象。比如转发给一个专门用于处理未识别的方法的处理类。
forwardInvocation 可以将消息同时转发给任意多个对象,如果你想执行其他逻辑(如记录日志并吞下该消息),可以考虑用 forwardInvocation
• 关于 signatureWithObjCTypes: 中的objcTypes,是OC的类型编码 Type Encodings【相关文档链接】
• 语法参照图:
前面提到OC方法调用 会传递两个 隐式参数 self, _cmd,self 是方法的调用者,_cmd 是方法编号,指向方法本身。
例如:-(void)eat:(NSString *)food;实际上有三个参数:self, _cmd和food。
将 eat: 转ObjcTypes为:
"返回值类型 第一参数 第二参数 [第三参数...]"
如果没有返回值用v,如果有用@代替id类型,
第一二参数是必须存在的,即 id 类型的 self,和 SEL 类型的 _cmd,第三参数是用户自定义的参数,可有可无例如: "v@:@" : void id类型的self SEL类型的_cmd 自定义参数;
"@@:" :id类型的返回值 id类型的self SEL类型的_cmd
因此我们可以调用[anInvocation getArgument: atIndex:] 获取指定的参数值
Runtime应用场景—HOOK(钩子)
HOOK,方法欺骗
直接上例子:
/*当url中含有中文时(需要转码),request还是能创建,但是此时
request中的url为空,request的创建方法没有检测url为空的情况,
很容易出现难以定位的bug(Swift中有可选类型,可以避免这问题)。
*/
NSURL *url = [NSURL URLWithString:@”http://www.baidu.com/中文”];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
解决:①用 category 创建分类 NSURL+HOOK 及XW_URLWithString 方法,将用到 URLWithString 的地方替换成我们自己的方法 XW_URLWithString , XW_URLWithString 保留了 URLWithString 原本内部创建 url 的方式,添加 url 是否为空的判断。
不足:每次都要去导入头文件,每次都要去替换项目中原本的urlWithString方法,比较麻烦;
②利用runtime运行时,改变方法调用的顺序。
OC发送 URLWithString 消息会对应的的去找这个方法的实现,用运行时可以去改变这种一一对应的关系,
只要HOOK住 URLWithString 这个方法的调用,当发送 URLWithString(SEL)消息时,让它去找 XW_URLWithString(IMP)这个实现。
NSURL+HOOK.m 文件中在 +(void)load 方法中下钩子HOOK,以交换方法的IMP实现。
#import<objc/runtime.h>
+(void)load {
//获取method
Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
Method XWURLWithStr = class_getClassMethod(self, @selector(XW_URLWithString:));
//交换方法的IMP
method_exchangeImplementations(URLWithStr, XWURLWithStr);
}
外界不需要导入 NSURL+HOOK.h ,也不需要修改 URLWithString 为 XW_URLWithString 就能直接把 URLWithString 方法实现替换。
XW_URLWithString 实现如下:
+(void) XW_URLWithString:(NSString*)URLString{
// 保留系统原本的实现,实现交换后这里不能用URLWithString,否则会递归
NSURL *url = [NSURL XW_URLWithString:URLString];
if(url == nil){
NSLog(@"空了");
}
return url;
}