OC的runtime机制和基本使用场景
Runtime即我们通常叫的运行时,也就是程序在运行的时候做的事情。是 Objective-C底层的一套C语言的API,是 iOS 内部的核心之一,我们平时编写的 Objective-C 代码,底层都是基于它来实现的,Objective-C代码编译后,其实都是Runtime形式的C语言代码。
OC会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
因此,只靠编译器是不够的,我们还需要一个运行时系统来处理编译后的代码。
所以一些用OC不太好实现的功能 我们可以利用runtime来实现 比如:
1:动态交换两个方法的实现(常用于交换系统方法)
2:动态添加对象的成员变量和成员方法
3:获得某个类的所有属性变量属性及方法
4:实现分类Category中可以增加属性
5:拦截系统自带的方法调用,比如拦截viewDidLoad
67891.。。。。。。。。等等等
先来看动态交换两个方法
用到的方法是method_exchangeImplementations
举个例子
[self fuck];
[self dontFuck];
Method method1 = class_getInstanceMethod([self class], @selector(fuck));
Method method2 = class_getInstanceMethod([self class], @selector(dontFuck));
//类方法用class_getClassMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
method_exchangeImplementations(method1, method2);
[self fuck];
[self dontFuck];
- (void)fuck
{
NSLog(@"fuck");
}
- (void)dontFuck
{
NSLog(@"dontFuck");
}
再看系统方法的拦截交换
比如遇到需求 iOS9 以上的版本需要使用另一套图片, 这时候需要在一个个使用的地方判断版本来加载不同的图片吗? 这样会不会太繁琐呢? 有好的解决方法吗?
这时候就可以使用Swizzle, 来拦截UIImage的 imageName这个加载图片的系统方法, 来交换成我们自己的方法
创建一个UIImage的分类:(UIImage+Category);
在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断修改图片名
//自定义方法
+ (UIImage *)new_ImageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 9.0) {
name = [name stringByAppendingString:@"_ios9"];
}
return [UIImage new_ImageNamed:name]; //方法交换后, 调用imageNamed方法, 让有加载图片的功能
}
Category中重写 UIImage 的 load 方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)
+ (void)load {
//获取两个类的类方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(new_ImageNamed:));
//开始交换方法实现
method_exchangeImplementations(m1, m2);
}
在使用中, 如果iOS9以上版本使用另一版本的图片, 就可以交换系统的方法, 直接使用 imageNamed方法, 调用的是new_ImageNamed的实现
再看分类Category中创建属性
一般情况下在 iOS 分类中是无法直接设置属性的,如果在分类的声明中写 @property 只能为其生成 get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash
但是利用runtime可以巧妙的实现
比如给UIView添加一个count和一个viewName 在Category的.h中
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, strong) NSString *viewName;
.m中
- (void)setCount:(NSInteger)count
{
//将name值和对象关联起来, 将name值存储到当前对象中
/*参数:
object: 给哪个对象设置属性;
key: 一个属性对应一个key, 存储后需要通过这个key取出值, key可为double,int等任意类型, 建议用char可节省字节;
value: 给属性设置的值;
policy: 存储策略 (assign, copy, retain);
*/
objc_setAssociatedObject(self, @selector(count), [NSNumber numberWithInteger:count], OBJC_ASSOCIATION_ASSIGN);
}
- (NSInteger)count
{
return [objc_getAssociatedObject(self, @selector(count)) integerValue];
}
- (void)setViewName:(NSString *)viewName
{
objc_setAssociatedObject(self, @selector(viewName), viewName, OBJC_ASSOCIATION_COPY);
}
- (NSString *)viewName
{
return objc_getAssociatedObject(self, @selector(viewName));
}
这样所有的UIView及其子类都带有这两个属性了
UIView *testView = [UIView new];
testView.count = 5;
testView.viewName = @"testView";
NSLog(@"%ld,%@",(long)testView.count,testView.viewName);
这里提一下 如果新增的属性和系统的一样 可能会走系统的方法也可能会走你新增的方法 所以命名时需要注意些
再看获取类的所有属性变量和属性
假如有个类是这样的
@interface Person : NSObject
@property (nonatomic, strong) NSString *father;
@property (nonatomic, strong) NSString *mother;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSString *sister;
@end
你需要获取model的所有属性及值可以用class_copyPropertyList获取
Person *person = [Person new];
person.father = @"父亲的名字";
person.mother = @"母亲的名字";
person.age = 18;
//获取所有成员变量
/*
参数:
1.哪个类
2.接收值的地址, 用于存放属性的个数
3.返回值: 存放所有获取到的属性, 可调出名字和类型
*/
unsigned int count;
objc_property_t *propertyArray = class_copyPropertyList([person class], &count);
for (int i = 0; i < count; i ++) {
objc_property_t property = propertyArray[i];
NSString *key = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
id value = [person valueForKey:key];
NSLog(@"%@ = %@",key,value);
}
打印结果为
输出结果.png
再看字典转模型
基本就是处理
1.字典的key和模型的属性匹配不上;
2.模型中嵌套模型(模型属性是另外一个模型对象);
3.数组中装着模型(模型的属性是一个数组,数组中是一个个对象)
三种情况
第一种 runtime 是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可
第二种 需要利用 runtime 的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归
第三种 需要获取数组里的元素再进去第一步和第二部的判断
再看动态添加方法
如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决
比如Person类现在是没有实现eat方法的
Person *person = [Person new];
[person performSelector:@selector(eat) withObject:nil];
NSLog(@"%@",person.father);
可以在Person.m中#import "objc/message.h"
void effect(id self, SEL _cmd) {
[self setValue:@"父亲" forKey:@"father"];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if(sel ==NSSelectorFromString(@"eat")) {
// class: 给哪个类添加方法
// SEL: 添加哪个方法
// IMP: 方法实现 => 函数 => 函数入口 => 函数名
// type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
class_addMethod(self, sel, (IMP)effect,"v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
打印出的person.father值为
image.png
未完待续·····