我们一般用runtime做以下这些事情:
1.动态交换两个方法的实现(同时也可以替换系统的方法)
2.获得某个类的所有成员方法、所有成员变量
3.动态的改变属性值、增加一个属性
4.动态的增加一个方法
5.实现NSCoding的自动归档和解档
6.实现字典转模型的自动转换
一、使用runtime如何交换两个方法的实现,拦截系统自带的方法调用功能。
1.交换两个方法的实现:
首先我们创建一个工程,使用市面上最常用的一个例子,创建一个Person类,写两个类方法、两个实例方法。
.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
+ (void)eat;
+ (void)sleep;
- (void)study;
- (void)playGame;
@end
.m
#import "Person.h"
@implementation Person
+ (void)eat{
NSLog(@"哥么 吃了");
}
+ (void)sleep{
NSLog(@"哥么 睡了");
}
- (void)study{
NSLog(@"小伙儿 在学习");
}
- (void)playGame{
NSLog(@"小伙儿 在打游戏");
}
@end
预备东西已经准备完全,现在开始交换两个方法。
在用runtime交换两个方法的时候,首先需要倒入头文件#import <objc/runtime.h>。之后你需要了解下面三个方法是干什么用的。
//获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name)
//获得某个类的实例对象方法
Method class_getInstanceMethod(Class cls , SEL name)
//交换两个方法的实现
void method_exchangeImplementations(Method m1 , Method m2)
了解了上面的知识点后,就进入我们的主题了:
案例1:交换两个类方法
//交换类方法
Method method1 = class_getClassMethod([Person class], @selector(eat));
Method method2 = class_getClassMethod([Person class], @selector(sleep));
method_exchangeImplementations(method1, method2);
[Person eat];
[Person sleep];
案例2:交换实例方法
//交换实例方法
Method method3 = class_getInstanceMethod([Person class], @selector(study));
Method method4 = class_getInstanceMethod([Person class], @selector(playGame));
method_exchangeImplementations(method3, method4);
Person *person = [[Person alloc] init];
[person study];
[person playGame];
2.拦截系统方法:
例如在iOS7出来之后,设计风格偏向于扁平化 扁平化设计风格介绍 ,有些公司为了适应趋势,会做出图片的大批量更改,如果不想一张一张替换原先的图片,就可以使用runtime截获imageNamed方法,作出相应的处理:
我们创建一个UIImage的分类,在.m文件里里重写一个类方法
+ (UIImage *)LF_imageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) {
name = [name stringByAppendingString:@"_change"];
}
return [UIImage LF_imageNamed:name];
}
我们可以在load方法中将重写的方法跟imageNamed方法互换:
+ (void)load {
// 获取两个类的类方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(LF_imageNamed:));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
}
二、使用runtime获得某个类的所有成员方法、所有成员变量
class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
上面的方法是获取属性需要用的方法
class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
上面的方法是获取类中所有方法的方法
首先创建一个继承NSObject的Student类,在.h文件中
#import <Foundation/Foundation.h>
@interface Student : NSObject
@property (assign, nonatomic) int age;
- (void)study;
- (void)sleep;
@end
.m
#import "Student.h"
@interface Student()
{
NSString *name;
}
@end
@implementation Student
//初始化person属性
-(instancetype)init{
self = [super init];
if(self) {
name = @"Tom";
self.age = 12;
}
return self;
}
- (void)study{
NSLog(@"学生要学习");
}
- (void)sleep{
NSLog(@"学生要睡觉");
}
//输出person对象时的方法:
-(NSString *)description{
return [NSString stringWithFormat:@"name:%@ age:%d",name,self.age];
}
@end
注意在.m文件中我们声明了私有实例变量,这个我们也是可以通过runtime获取到的。
我们在ViewController中进行获取全部属性 和全部方法的实现代码
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Student class], &outCount);
// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
}
// 注意释放内存!
free(ivars);
上面是获取所有成员变量的方法,获取的结果如下:
2017-05-27 14:09:53.786 runtime-归档解档[4081:960384] 成员变量名:name 成员变量类型:@"NSString"
2017-05-27 14:09:53.787 runtime-归档解档[4081:960384] 成员变量名:_age 成员变量类型:i
其中的i代表的是int类型。
这里经常在其他博主文章那里会看到有人问" "和没有下划线的问题。这里就不得不提到另一个方法copyPropertyList。copyPropertyList和copyIvarList的区别就在于后者能够获取到"{}"中的成员变量,而前者只能获取到“@property”声明的属性变量。而copyIvarList获取到的一般都会主动在成员变量名称前面加上“”,当然如果是“{}”中的成员变量copyIvarList也不会主动加“”。
三、动态的改变属性值、增加一个属性
//改变属性值
- (IBAction)changeVariable:(UIButton *)sender {
NSLog(@"打印当前对象 -- %@",student);
unsigned int count = 0;
Ivar *variLists = class_copyIvarList([Student class], &count);
Ivar ivar = variLists[0];//这里我们知道第一个参数是name,所以就直接取第一个元素
const char *str = ivar_getName(ivar);
NSLog(@"得到的Ivar是 -- %s",str);
object_setIvar(student, ivar, @"Mars");
NSLog(@"改变之后的student:%@",student);
}
怎样获取属性,在第二块已经说过了,这里我们获取到属性之后,使用
object_setIvar(<#id obj#>, <#Ivar ivar#>, <#id value#>)
这个方法,进行设置属性的值。
2017-05-27 14:41:24.113 runtime-归档解档[4081:960384] 打印当前对象 -- name:Tom age:12
2017-05-27 14:41:24.114 runtime-归档解档[4081:960384] 得到的Ivar是 -- name
2017-05-27 14:41:24.114 runtime-归档解档[4081:960384] 改变之后的student:name:Mars age:12
从输出的log中,我们可以看出,初始值name是Tom,在用object_setIvar这个方法进行了设置属性之后,name变成了Mars,这样就做到动态的修改属性值的效果了。
动态的添加一个属性,我们往往是创建某个类的分类,之后在这个分类里面做处理,譬如这里,我们给Student类添加一个属性,我们在Student的分类.h里面生命一个属性
@property (nonatomic,assign)float height; //新属性
在分类的.m文件里面
#import "Student+category.h"
#import <objc/runtime.h>
const char * str = "myKey"; //做为key,字符常量 必须是C语言字符串;
@implementation Student (category)
-(void)setHeight:(float)height{
NSNumber *num = [NSNumber numberWithFloat:height];
/*
第一个参数是需要添加属性的对象;
第二个参数是属性的key;
第三个参数是属性的值,类型必须为id,所以此处height先转为NSNumber类型;
第四个参数是使用策略,是一个枚举值,类似@property属性创建时设置的关键字,可从命名看出各枚举的意义;
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
*/
objc_setAssociatedObject(self, str, num, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//提取属性的值:
-(float)height{
NSNumber *number = objc_getAssociatedObject(self, str);
return [number floatValue];
}
@end
这里主要的是objc_setAssociatedObject 和 objc_getAssociatedObject两个方法的 调用。
在ViewController中对新添加的属性进行赋值,之后再用Student类的实例对象调用这个属性的get方法。
//增加属性
- (IBAction)addVariable:(UIButton *)sender {
student.height = 12; //给新属性height赋值
NSLog(@"%f",[student height]); //访问新属性值
}
打印的结果如下:
动态增加属性.png四、动态的增加一个方法
//添加一个新的方法
- (IBAction)addNewMethod:(UIButton *)sender {
/* 动态添加方法:
第一个参数表示Class cls 类型;
第二个参数表示待调用的方法名称;
第三个参数(IMP)myAddingFunction,IMP一个函数指针,这里表示指定具体实现方法myAddingFunction;
第四个参数表方法的参数,0代表没有参数;
*/
class_addMethod([student class], @selector(addNewMethod), (IMP)myAddingFunction, 0);
//调用方法
[student performSelector:@selector(addNewMethod)];
}
//具体的实现(方法的内部都默认包含两个参数Class类和SEL方法,被称为隐式参数。)
int myAddingFunction(id self, SEL _cmd){
NSLog(@"新增的addNewMethod方法已经加入");
return 1;
}
- (void)addNewMethod{
NSLog(@"啦啦啦");
}
动态的添加一个方法,主要的是对runtime中class_addMethod这个方法的使用。addNewMethod是给Student对象调用的方法名,实际的调用的是函数myAddingFunction,写功能性代码也是写在这里的。
上面二三四点的Demo地址
五、使用runtime实现NSCoding的自动归档和解档
这里只是处理一些常用的类型,像const void *这样类似的类型是不支持kvc的,没做处理。如果想了解推荐去看下 标哥的技术博客 。下面开始说怎样利用runtime实现NSCoding的自动归档和解档。
新建一个项目,添加一个继承NSObject的LFUserInfo类,遵守了NSCoding协议之后,我们就可以在实现文件中实现-encodeWithCoder:方法来归档和-initWithCoder:解档。
.h文件:
#import <Foundation/Foundation.h>
@interface LFUserInfo : NSObject<NSCoding>
@property (copy , nonatomic) NSString *username;
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double height;
@property (strong, nonatomic) NSNumber *phoneNumber;
@end
.m文件
#import "LFUserInfo.h"
#import <objc/runtime.h>
@implementation LFUserInfo
- (NSArray *)ignoredNames {
return @[@"_height",@"_phoneNumber"];
}
//解档数据
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivarLists = class_copyIvarList([self class], &count);
for (int i = 0 ; i<count; i++) {
Ivar ivar = ivarLists[i];
const char *key = ivar_getName(ivar);
NSString *keyStr = [NSString stringWithUTF8String:key];
//忽略不需要归档的元素
if ([[self ignoredNames] containsObject:keyStr]) {
continue;
}
//进行解档取值
id value = [aDecoder decodeObjectForKey:keyStr];
//利用KVC对属性赋值
[self setValue:value forKey:keyStr];
}
free(ivarLists);
}
return self;
}
//归档数据
-(void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;
Ivar *ivarLIst = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivarLIst[i];
const char *key = ivar_getName(ivar);
NSString *keyStr = [NSString stringWithUTF8String:key];
// 忽略不需要解档的属性
if ([[self ignoredNames] containsObject:keyStr]) {
continue;
}
//利用KVC取值
id value = [self valueForKey:keyStr];
[aCoder encodeObject:value forKey:keyStr];
}
free(ivarLIst);
}
@end
之后我们在ViewController中进行归档解档操作
归档解档后.png从打印的结果可以看到,我们忽视的参数,在归档解档后被忽略,其他的属性都可以归档解档操作。关于runtime实现归档解档其实不难,主要就是通过runtime动态的获取到相关的属性之后,进行操作就行了。关于归档解档的Demo在这里
六、实现字典转模型的自动转换
这里只是最简单的一层字典直接转模型的处理,多层数据结构的字典转模型不再本Demo范畴。
首先我们创建几个比较正常的属性列表,譬如此Demo中,我们创建一个LFStudentmodel的model类,声明三种类型属性变量:
@property (strong,nonatomic) NSString *name;
@property (strong,nonatomic) NSString *schoolName;
@property (strong,nonatomic) NSString *unitState;
第二步,我们创建一个继承于NSObject的分类NSObject (Category),在.h文件中创建一个类方法
+ (instancetype)modelWithDict:(NSDictionary *)dict;
在.m文件中实现这个方法:
#import "NSObject+Category.h"
#import <objc/runtime.h>
@implementation NSObject (Category)
+ (NSArray *)propertList
{
unsigned int count = 0;
//获取模型属性, 返回值是所有属性的数组 objc_property_t
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
NSMutableArray *arr = [NSMutableArray array];
//便利数组
for (int i = 0; i< count; i++) {
//获取属性
objc_property_t property = propertyList[i];
//获取属性名称
const char *cName = property_getName(property);
NSString *name = [[NSString alloc]initWithUTF8String:cName];
//添加到数组中
[arr addObject:name];
}
//释放属性组
free(propertyList);
return arr.copy;
}
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
id obj = [self new];
// 遍历属性数组
for (NSString *property in [self propertList]) {
// 判断字典中是否包含这个key
if (dict[property]) {
// 使用 KVC 赋值
[obj setValue:dict[property] forKey:property];
}
}
return obj;
}
@end
之后将我们这个分类的头文件导入到LFStudentmodel模型类中就可以实现,字典转模型的功效了。
之后我们看一下字典转模型的实操
创建一个数据管理类
#import "LFDataManager.h"
#import "LFStudentmodel.h"
@implementation LFDataManager
- (NSArray *)getSourceDataArray
{
NSArray *sourceArray = @[@{@"name":@"Tom",@"schoolName":@"aaa",@"unitState":@"111"},@{@"name":@"Sum",@"schoolName":@"bbb",@"unitState":@"222"},@{@"name":@"Amy",@"schoolName":@"ccc",@"unitState":@"333"},@{@"name":@"Evy",@"schoolName":@"ddd",@"unitState":@"444"},@{@"name":@"Any",@"schoolName":@"eee",@"unitState":@"555"}];
NSMutableArray *mArray = [NSMutableArray new];
for (int i=0; i<sourceArray.count; i++) {
LFStudentmodel *stuModel = [LFStudentmodel modelWithDict:sourceArray[i]];
[mArray addObject:stuModel];
}
return mArray;
}
最后我们在ViewController中调用展示下数据
- (void)viewDidLoad {
[super viewDidLoad];
LFDataManager *manager = [LFDataManager new];
NSArray *dataArray = [manager getSourceDataArray];
for (int i =0; i<dataArray.count; i++) {
LFStudentmodel *model = dataArray [i];
NSLog(@"dataArray --- name= %@ schoolName= %@ unitState = %@",model.name,model.schoolName,model.unitState);
}
}
结果如下:
现在市面上很多主流解析模型的三方MJExtension、YYModel等思想也是通过runtime进行封装解析,最主要的还是setValue -- forkey这个大招来操作的。有兴趣的可以去深入了解下解析模型的三方底层实现方式。
Demo地址
喜欢的朋友请不吝你的👍,GitHub上点个star啥的。大神觉得写的有毛病的地方,还请指教,谢谢!
网友评论