Runtime的总结(第二篇)

利用Runtime实现方法交换

分析场景     在我们的项目开发中,会遇到这样的问题:如果字符串链接里面带有文字,那它返回的url是空的, “null” ,后面我们再进行网络请求一个空的url结果..... 。这也就说明NSURL 在创建出来后,系统不会检测是否为nil。怎么解决?

开始的想法:我可以为NSURL扩展一个方法,在方法实现里判断 返回的url是否为空,再做一些其他处理,最后再把方法名替换下就行啦。   扩展的方法如下;

+(instancetype)HZ_urlWtihString:(NSString *)str{

NSURL *url = [NSURL URLWithString:str];

if (url == nil) {

NSLog(@"url 空了");

}

return url;

}

要是方法调用次数少,简单替换下方法名 。但是如果类似于这种情况 并且替换的地方很多呢?就要一个一个找替换 很麻烦。  

这时有一个一劳永逸的方法:HOOK(面向切面编程)钩子思想!! 勾住某个方法的调用,改变方法调用的顺序。

继续说,我们可以不改变系统方法的调用,还是调用URLWithString,但是调用后让它走到我们自定义方法的实现里。 方法的调用就是发送消息 , 那么调用URLWithString方法 被编译后C++代码 就是这样:   objc_msgSend(<#id self#>, @selector(URLWithString)) ,只要他发送这消息,我就拦截到  ,接下来就需要我们做些改变 !! Runtime 提供了改变方法调用顺序的功能  ,那改变的是什么?  @selector(URLWithString)这是被编译后的我们无法改变(改变相当于改变源码),在@selector之后还有一个机会改变方法的调用 ,改变的是方法的IMP

提示方法的组成:SEL方法编号+IMP方法实现(实质:函数指针,它指向一块内存区域,内存里面的二进制指令)),SEL和IMP是一一对应的。 也可以这样理解:SEL(书的目录)--IMP(书的页码)--  代码(书里的内容);我们发送消息SEL 它就会找到IMP(IMP不是代码 它是函数指针)


Runtime的总结(第二篇)_第1张图片
发送消息就是通过目录找页码,通过页码找内容

Runtime可以这么做,它可以改变目录上面的对应关系,它可以做交换


Runtime的总结(第二篇)_第2张图片

那什么时候交换?要在一切调用该方法之前做好,main函数里一般不写,我们可以在分类的.m源文件里,源文件再加载的时候有个+(void)load方法,

//App装载进内存的时候 就会执行这个指令;(app安装在手机里都是二进制,点开App第一件事就是将硬盘里的二进制装进内存里,就会读load里面的内容,也就是先执行load里面的指令,再启动APP 执行UIApplicationmain, load比main函数靠前)

+(void)load{

//下钩子

}

我们在load里面“下钩子”。我们会用到这个方法来交换:

method_exchangeImplementations(<#Method m1#>, <#Method m2#>)//改变方法实现

我们通过函数获取m1 m2

+(void)load{

//下钩子

//    class_getInstanceMethod;//获取实例方法

//获取类方法,返回Method结构体

Method urlWithString = class_getClassMethod(self, @selector(URLWithString:));

Method HZ_UrlWithString = class_getClassMethod(self, @selector(HZ_urlWtihString:));

//交换方法的实现(相当于交换:“书的页码”)

method_exchangeImplementations(urlWithString, HZ_UrlWithString);

}

到现在,方法交换就实现啦,原来的代码不用动。但是还有一地方没改,否则会造成循环调用 ,栈溢出:URLWithString 改为HZ_urlWtihString

//HZ_urlWtihString 与 urlWithString 的imp已经交换

+(instancetype)HZ_urlWtihString:(NSString *)str{

NSURL *url = [NSURL HZ_urlWtihString:str];

if (url == nil) {

NSLog(@"url 空了");

}

return url;

}

所谓HOOK,是一种思想,是专门的一种钩子,他会在内存中找到某一个方法的调用(一块儿内存区域),直到调用这个方法时,就勾住他,对它的方法行为进行改变,所以Runtime 的方法交换 某种意义上说也是一种钩子。

动态添加方法

当一个类被调用了没有被实现的方法,那么他会走这两种方法的一种

//当类被调用没有实现的类方法时,调用该方法

+(BOOL)resolveClassMethod:(SEL)sel{

}

//当这个类被调用没有实现的对象方法时,调用该方法

+(BOOL)resolveInstanceMethod:(SEL)sel{

}

比如在控制器里,一个类Person被调用了一个没有实现的无参数方法[p performSelector:@selector(eat)];,会走到resolveInstanceMethod:里来,那么我们就可以在该方法中给Person类动态添加方法。使用Runtime添加方法,就会用到这个函数class_addMethod,代码实例

//当这个类被调用没有实现的对象方法时,调用该方法

+(BOOL)resolveInstanceMethod:(SEL)sel{

NSLog(@"%@",NSStringFromSelector(sel));

//动态添加方法(方法组成:SEL (方法编号) + IMP(函数指针) + 函数体)

/*

1.cls 目标类

2.sel 方法编号

3.imp 方法实现(函数指针)

4.返回值类型 (无返回值无参的函数 为 "v@:")//v代表viod,@代表一个id对象,:代表方法编号

*/

class_addMethod(self, sel, (IMP)haha, "v@:");

//不知道返回什么,父类返回什么 我这就返回什么

return [super resolveInstanceMethod:sel];

}

//写一个函数,haha 就是函数指针,这里需要加(IMP)来强制转化,不然会报警告

void haha(){

NSLog(@"来了!!");

}

以上时添加的无返回值无参数的实例方法,如果调用了一个没有实现的带参数的方法,

[p performSelector:@selector(eat) withObject:@"哈哈"];

这就要改变class_addMethod的第四个参数-返回值类型,和haha函数体里的参数。返回值类型改为“v@:”,更改函数体里的参数,有两个 方法调用的隐式参数 id self,SEL _cmd,

/*

1.方法的调用者

2.方法的编号

(OC 里 每个方法调用,它的函数体里有俩个隐式参数,是默认的)

*/

void eat(id self,SEL _cmd,NSString *str){

NSLog(@"%@",str);

}

为什么会有隐式参数,可能隐约感觉的到,方法调用就是消息发送:

// [p performSelector:@selector(eat) withObject:@"哈哈"];

//调用OC方法的时候,会给IMP传入两个参数:方法的调用者  id self 、方法编号SEL _cmd (行参名字可以改)

    objc_msgSend(p, @selector(eat:),@"哈哈");

这里面的p 和 @selector(eat:) 就传到 void eat(id self,SEL _cmd,NSString *str)这里来啦。

*另外swift调用方法 和oc  调用方法的底层是不一样的。

你可能感兴趣的:(Runtime的总结(第二篇))