如果不装X,跟咸鱼又有什么区别了。
听了一节关于runtime相关的课程,这里第一时间做个笔记,方便自己过后的复习。
一、 OC 消息运行时机制
场景1:创建一个继承于NSObject的Person类,添加一个对象方法-(void)eat,当每次调用这个对象方法时打印一行文字。如下图:
// .h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat;
@end
// .m文件
#import "Person.h"
@implementation Person
- (void)eat
{
NSLog(@"我要食大柴饭");
}
@end
正常是导入Person的头文件,创建对象,直接调用对象方法.
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc]init];
[p eat];
}
@end
上面的方法,会OC的人都不是什么问题,接下来慢慢的揭开消息机制的神秘面纱。
调用对象方法
/*
方式一:
*/
[p eat];
/*
方式二:
*/
[p performSelector:@selector(eat)];
/*
方式三:
消息发送(注意要先导入 <objc/message.h> 头文件)
从Xcode 5.0 开始苹果就不建议使用低层方式
*/
objc_msgSend(p, @selector(eat));
补充使用message.h头文件需要以下操作:
前面的只是前菜,接下来是要剥析Person对象的实例另一种方法
//未剥析前
Person *p = [[Person alloc]init];
//1.0
Person *p = [Person alloc];
p = [p init];
//2.0 特别注明一点
//objc_msgSend(<#id self#>, <#SEL op, ...#>)
//第一个参数是id类型,如果是对象就直接传对象就可以,
//如果是类,则传类(好吧!忽略我说的)
Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p, @selector(init));
//3.0 这里还有一点不足就是还是引用了Person类
// 不急我们接下来会一步步的解决
//首先获取"类"的方法有如下三种
//第一种:[Person class]
//第二种:NSClassFromString(@"Person")
//第三种:objc_getRequiredClass("Person")
//3.1
Person *p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));
//3.2 根据面向对象多态的特性:Person类继承于NSObject类
NSObject *p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));
//4.0 上面已经转变完成了,都是通过调用函数,没有用到OC语法
// 再将上面的两行代码写在一块了,这个给人的等级就是上天了
NSObject *p = objc_msgSend( objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc)), @selector(init));
换个写法,是不是有点feel了,接下来验证我们的结论
创建一个新工程(只有main函数的工程)
二、 runtime
2.1 runtime简历
runtime 是苹果提供的一套低层的API
runtime的作用有下面三个:
a、动态的创建一个类
b、动态的修改一个类的属性/方法
c、遍历一个类的所有的成员变量
runtime使用前提
导入同文件<objc/message.h>或者<objc/runtime.h>
备注:<objc/message.h>头文件已经包括了<objc/runtime.h>
两个概念
Method : 代表成员方法
Ivar : 成员变量
2.2 runtime使用
runtime的作用:
1、获取类方法或对象方法
2、交互方法的实现
场景2: 通过URLWithString:方法创建一个NSURL对象,如"www.baidu.com",一般情况是无BUG的,但是如果此时将url改成"www.baidu.com/好好学习",则创建的NSURL为nil,就会导致以后的代码出现BUG,甚至崩溃,问题是就算是做了一个全局的断点也找不到这个BUG。
所以我们的需求是:NSURL 这个类的URLWithString:方法添加一个功能!在创建URL的同时也能判断是否为空
为一个类添加新的方法或者是修改原有的方法,就有创建类别和继承两种方式。
继承
现在这种场景,如果是使用继承创建一个新的NSURL子类的话是能修改这个隐藏的BUG,不过就要将项目中的NSURL替换成继承的子类,在一个已经持续开发几个月的项目,这种方案并不是最可行的。
类别
类别,就是一个补丁,为一个原有类的基础增加新功能。但是类别也是有缺点的
第一:不能添加属性(意思是添加的属性不会自动生成setter方法和getter方法);
第二:不建议重写类的方法(原因请看下面的例子)。
当然这两个问题还是有办法解决的,使用我们今天的主角"runtime",runtime先生有两个特殊的技能:1、动态添加成员变量(属性);2、交换方法的实现
//重写类方法1:会造成死循环
+ (instancetype)URLWithString:(NSString *)URLString
{
NSURL *url = [NSURL URLWithString:URLString];
}
//重写类方法2:不能实现
+ (instancetype)URLWithString:(NSString *)URLString
{
//因为NSURL的父类是NSObject,并没有URLWithString:方法
NSURL *url = [super URLWithString:URLString];
}
现在的解决思路:创建NSURL分类(NSURL+url),并添加一个新的实现方法,使用runtime与原来的URLWithString:交互实现方法,详情看代码
//NSURL(url).h 文件
#import <Foundation/Foundation.h>
@interface NSURL (url)
+ (instancetype)MZ_URLWithString:(NSString *)URLString;
@end
//NSURL(url).m 文件
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)
+ (instancetype)MZ_URLWithString:(NSString *)URLString
{
NSURL *url = [NSURL URLWithString:URLString];
if (url == nil) {
NSLog(@"URL 为空");
}
return url;
}
@end
上面是创建的NSURL类别,新增的一个MZ_URLWithString:方法,但是离我们目标还有一步:交互方法实现
//NSURL(url).m 文件
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)
+ (void)load
{
NSLog(@"NSURL(url).m文件加载了");
//交换方法实现
//第一步:获取这两个方法
//class_getClassMethod 获取类方法
//class_getInstanceMethod 获取对象方法
Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
Method MRURLWithStr = class_getClassMethod(self, @selector(MZ_URLWithString:));
//交换方法
method_exchangeImplementations(URLWithStr, MRURLWithStr);
}
+ (instancetype)MZ_URLWithString:(NSString *)URLString
{
//注意:这里不能调用原来的URLWithString:方法,否则会造成死循环崩溃的
NSURL *url = [NSURL MZ_URLWithString:URLString];
if (url == nil) {
NSLog(@"URL 为空");
}
return url;
}
@end
到这里就已经是完成了,在load方法中交互方法的实现有个好处,就是不用在是使用这个补丁的类中添加"NSURL+url.h"头文件,会直接生效。(备注:利跟弊都是相对的。既然能在不导入头文件的情况就能全局的替换原有方法的实现,容易产生混淆,并且不像继承那样可以直接跳转代码查看实现,所以推荐,不对,是必须添加注释,添加注释,添加注释)
你一定会疑惑为什么在load方法中交互方法的实现,就会全局有效?
小编也是一知半解的,如果想深入了解可以参考下面两篇文章:
TerryZhang:iOS的load方法与initialize方法
Draveness:你真的了解load方法么?
关于+(void)load方法至少要记住:
只要类的实现文件引入项目中(如下图),在程序启动时就会执行,并且是执行一次。
runtime的作用 2
动态创建属性
场景3:
runtime的作用 3
动态创建方法
场景4:Person类声明了一个对象如-(void)eat,但该方法无实现方法。使用runtime为Person类动态创建一个对象方法的实现
viewController.m 文件
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic,strong) Person *p;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.p = [[Person alloc]init];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//调用不带参数的方法
[self.p performSelector:@selector(eat)];
}
Person.m 文件
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
//函数的名字就是函数的指针
//每一个函数内都默认有这两个参数,这是隐式参数,这是系统传过来的
void eat(id self,SEL _cmd){
NSLog(@"调用了%@的%@方法",self,NSStringFromSelector(_cmd));
}
/*
当调用没有实现的对象方法,会先调用此方法
// 当调用没实现的类方法,会调用此方法 + (BOOL)resolveClassMethod:(SEL)sel
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//动态添加方法
if (sel == @selector(eat)) {
/**
1.类类型
2.方法编号 (即方法名)
3.方法实现,函数指针
4.返回值类型(c语言字符串)
*/
class_addMethod([Person class], sel,(IMP)eat, nil);
}
return [super resolveInstanceMethod:sel];
}
@end
到这里动态创建一个方法是已经完成了,这个时候再调用Person类的eat方法就不会出现崩溃的情况了。
这里有3个知识点:
1、当调用没有实现的方法(对象方法或类方法),系统崩溃前会先调用+(BOOL)resolveInstanceMethod:或+(BOOL)resolveClassMethod:方法
2、函数的名字就是函数的指针,虽然这是C语言的基础,但也要提一下(小编自己也是忘了)。并且每个函数都有两个默认的隐式参数(id self,SEL _cmd),"self"代表当前对象,"_cmd"代表方法名。提醒:这两个参数是系统提供的值,系统提供的值,系统提供的值,重要的事说三遍。
3、runtime动态创建『方法』的方法:class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
,这个方法有四个参数,前三个就不提了,代码块上都有标注,重点是第四个参数(c语言字符串),是关于这个方法返回值和入参类型的一个编码字符串。含义如下:
以void eat(id self,SEL _cmd)函数为例子,第四个参数字符串的值应该是"v@:"。
当然这里我们列举的是无参的案列,如果是有带参数该怎么办了?
比如吃东西这个方法一个食物的名称
//带参数
void eatOBJ(id self,SEL _cmd,id obj){
NSLog(@"我吃了%@",obj);
}
/*
当调用没有实现的方法,会先调用此方法
//+ (BOOL)resolveClassMethod:(SEL)sel
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//动态添加方法
if (sel == @selector(eat)) {
/**
1.类类型
2.方法编号 (即方法名)
3.方法实现,函数指针
4.返回值类型(c语言字符串)
*/
class_addMethod([Person class], sel,(IMP)eat, nil);
} else if (sel == @selector(eat:)){
class_addMethod([Person class], sel, (IMP)eatOBJ, "v@:@");
}
return [super resolveInstanceMethod:sel];
}
//调用带参数的方法
[self.p performSelector:@selector(eatOBJ:) withObject:@"红烧肉"];
这里就不解释了