文章分享至我的个人技术博客: https://cainluo.github.io/15069332898903.html
上一章我们耍了一些RunTime的应用, 但并没有完全讲完, 现在继续接着说, 如果没有看到上一篇文章的朋友可以去玩转iOS开发:装逼技术RunTime的应用(一)看看.
转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.
给Category添加属性
在此之前, 我们了解到了一个类里面对应有的是isa指针, 但实际上这个isa指针是一个Class的结构体, 里面有这个类各式各样的信息, 其中有一个methodLists里面存储着实例方法列表.
而我们给对应的类添加Category就意味着是再给methodLists添加方法, 也就是因为这样子, 所以一般我们是不能在Category里面添加属性的, 虽然我们用了@property声明, 但也只是仅仅声明了get和set的方法, 并没有去实现.
而在这里, 我们就是要利用RunTime去实现这个get和set:
@interface NSObject (CLObject)
@property (nonatomic, copy) NSString *categoryName;
@end
#import "NSObject+CLObject.h"
#import <objc/runtime.h>
@implementation NSObject (CLObject)
- (void)setCategoryName:(NSString *)categoryName {
objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY);
}
- (NSString *)categoryName {
return objc_getAssociatedObject(self, @"categoryName");
}
@end
#import "RunTimeCategoryController.h"
#import "NSObject+CLObject.h"
@interface RunTimeCategoryController ()
@end
@implementation RunTimeCategoryController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = NSStringFromClass([self class]);
self.view.backgroundColor = [UIColor whiteColor];
NSObject *objc = [[NSObject alloc] init];
objc.categoryName = @"NSObject+CLObject";
NSLog(@"%@", objc.categoryName);
}
@end
最终效果:
2017-10-04 11:41:57.159145+0800 RunTimeExample[1431:87605] -[RunTimeCategoryController viewDidLoad] 第27行
NSObject+CLObject
这里解释一下:
在Category这里添加属性, 其实并不是真的添加, 只是关联上而已.
这里的objc_setAssociatedObject有四个参数:
- id _Nonnull object: 给哪个对象关联属性
- const void * _Nonnull key: 属性的名称, 这里我们可以用
OC字符串, 也可以用静态的void. - id _Nullable value: 属性的数值
- objc_AssociationPolicy policy: 保存的策略, 这里共有五种保存的策略:
- OBJC_ASSOCIATION_ASSIGN
- OBJC_ASSOCIATION_RETAIN_NONATOMIC
- OBJC_ASSOCIATION_COPY_NONATOMIC
- OBJC_ASSOCIATION_RETAIN
- OBJC_ASSOCIATION_COPY
光看字面就知道什么意思了, 这里就不多作解释.
利用RunTime将字典转成模型
我们在开发中, 都会用到字典转模型, 方式有:
- 手动一一对应的给模型赋值
- 利用
KVC将字典转成模型, 但这里有一些不太好的地方- 必须保证字典中的
属性和模型中的属性一一对应. - 如果不一样的话, 就会调用
setValue:forUndefinedKey:报Key找不到. - 解决办法:
- 重写
setValue:forUndefinedKey:方法, 把系统的方法覆盖了, 就能继续使用KVC字典转模型了.
- 重写
- 必须保证字典中的
- 利用
RunTime实现字典转模型
主要思路: 利用RunTime遍历模型中所有的属性, 根据模型的属性名去字典中找对应的Key, 然后取出对应的值给模型的属性赋值.
考虑的问题:
- 当字典的
Key和模型的属性对应不上, 这里有两种情况.- 当字典的
Key数量大于模型的属性数量时, 这个时候我们不用去管, 因为RunTime是会先遍历模型中的所有属性, 然后再根据字典里的Key一一赋值, 多出来的就不会再看了. - 当
模型里的属性数量大于字典的Key的数量时, 这时候由于属性没有对应的值就会被赋值为nil, 就会导致Crash, 这个时候我们只要加个判断就好了.
- 当字典的
- 模型中嵌套另一个模型(模型里的某个属性是模型).
- 数组里的对象是装着模型(模型的属性是数组, 而数组里装的对象都是模型).
这里我们写一个工具类, 针对上面三种情况用RunTime来转换模型:
字典转模型(模型的属性数量大于字典的Key数量)
给NSObject添加分类方法:
+ (instancetype)cl_modelToDictionary:(NSDictionary *)dictionary {
id object = [[self alloc] init];
unsigned int count = 0;
// 获取成员变量列表
Ivar *ivarList = class_copyIvarList(self, &count);
for (NSInteger i = 0; i < count; i++) {
// 根据角标取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取对应的属性名
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 从下标1开始取对应的key, 不然的话, 就会取到带"_"的key
NSString *key = [ivarName substringFromIndex:1];
// 根据属性名在字典中查找对应的value
id value = dictionary[key];
// 判断value是否为nil, 如果不是, 就给属性赋值
// 当属性的数量大于字典Key的数量时的判断
if (value) {
[object setValue:value
forKey:key];
}
}
return object;
}
查看一下转换后的结果:

这里我们是采用class_copyIvarList的方式获取所有成员变量的, 而成员变量都是使用"_"符号进行命名的, 所以我们要处理一下, 当然也可以使用class_copyPropertyList, 命名就不用处理, 其他都差不多.
为什么使用class_copyIvarList呢? 在前面我们都看到过给分类添加属性的时候, 只是简单的声明了set和get, 并没有实现
如果我们使用class_copyPropertyList, 可能就会漏掉了成员变量或者是没有实现set和get方法的属性, 而使用class_copyIvarList就不会有这个问题.
字典转模型(模型中嵌套模型)
其实模型中嵌套模型这样子的例子并不少见, 几乎都是这样子的, 借助刚刚的例子, 再加以改进一下, 我们就可以实现解析模型嵌套模型的场景:
+ (instancetype)cl_modelToDictionary2:(NSDictionary *)dictionary {
id object = [[self alloc] init];
unsigned int count = 0;
// 获取成员变量列表
Ivar *ivarList = class_copyIvarList(self, &count);
for (NSInteger i = 0; i < count; i++) {
// 根据角标取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取对应的属性名
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量的类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 从下标1开始取对应的key, 不然的话, 就会取到带"_"的key
NSString *key = [ivarName substringFromIndex:1];
// 根据属性名在字典中查找对应的value
id value = dictionary[key];
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 替换成员变量的类型
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
Class modelClass = NSClassFromString(ivarType);
// 有对应的模型才需要转
if (modelClass) {
// 把字典转模型
value = [modelClass cl_modelToDictionary2:value];
}
}
// 判断value是否为nil, 如果不是, 就给属性赋值
// 当属性的数量大于字典Key的数量时的判断
if (value) {
[object setValue:value
forKey:key];
}
}
return object;
}
查看一下转换后的结果:

数组中嵌套字典
在上面, 我们解决了前面两个问题, 最后一个问题就是在数组里嵌套模型, 这里我们需要先声明一个代理:
@protocol ModelDelegate <NSObject>
@optional
// 提供一些用来转换模型的协议, 只要遵守了这个协议, 就可以把数组中的字典转成模型
+ (NSDictionary *)cl_arrayToModelClass;
@end
+ (instancetype)cl_modelToDictionary3:(NSDictionary *)dictionary {
id object = [[self alloc] init];
unsigned int count = 0;
// 获取成员变量列表
Ivar *ivarList = class_copyIvarList(self, &count);
for (NSInteger i = 0; i < count; i++) {
// 根据角标取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取对应的属性名
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 从下标1开始取对应的key, 不然的话, 就会取到带"_"的key
NSString *key = [ivarName substringFromIndex:1];
// 根据属性名在字典中查找对应的value
id value = dictionary[key];
// 判断是否是数组类型
if ([value isKindOfClass:[NSArray class]]) {
// 判断能否响应代理方法
if ([self respondsToSelector:@selector(cl_arrayToModelClass)]) {
// 转换一下self
id allSelf = self;
// 获取数组中字典对应的模型
NSString *classType = [allSelf cl_arrayToModelClass][key];
// 生成对应的模型
Class classModel = NSClassFromString(classType);
NSMutableArray *modelArray = [NSMutableArray array];
// 遍历字典里的数组
for (NSDictionary *dictionary in value) {
// 字典转模型
id model = [classModel cl_modelToDictionary3:dictionary];
[modelArray addObject:model];
}
value = modelArray;
}
}
// 判断value是否为nil, 如果不是, 就给属性赋值
// 当属性的数量大于字典Key的数量时的判断
if (value) {
[object setValue:value
forKey:key];
}
}
return object;
}
然后在对应的模型解析时, 我们需要实现代理方法:
+ (NSDictionary *)cl_arrayToModelClass {
// 我这里对应的类型是RunTimeDataList
return @{@"data" : @"RunTimeDataList"};
}
查看一下转换后的结果:

工程地址
项目地址: https://github.com/CainRun/iOS-Project-Example/tree/master/RunTime/Five
最后
码字很费脑, 看官赏点饭钱可好
