iOS运行时(Runtime) 常用总结
runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的OC代码,底层都是基于runtime实现的。它和runloop组合在一起使OC具有了面向对象功能。
本文主要介绍runtime的一些常用用法。将所用的的方法封装到一个工具里
本文主要介绍以下几个方面:
1.动态添加一个类
2.交换方法实现
3.获取类的属性列表
4.给分类动态添加属性
5.字典转模型
6.实现NSCoding的自动归档和解档
7.获取类的实例方法列表 getter,setter,对象方法等
8.获取协议列表
1.动态添加一个类
调用的方法是
class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
class_addMethod方法有个参数,第一个参数是要添加方法的类,第二个参数是方法的SEL 可以理解为方法编号,一个SEL对应一个IMP,第三个参数则是提供方法实现的IMP,也就是implementation,是一个指针,指向方法的具体实现。第四个参数是方法的类型,一般写的是"V@:@",V表示返回值是Void,@表示是OC对象,:表示调用@selector方法。
方法实现如下:
/**
往类上添加相应的方法与其实现
@param fromclass 方法实现的类
@param class 绑定方法的类
@param methodSel 方法名
@param methodselImp 对应方法实现的方法名
*/
+ (void)addMethodWithClass:(Class)class fromClass:(Class)fromclass method:(SEL)methodSel methodImp:(SEL)methodselImp
{
Method method = class_getInstanceMethod(fromclass, methodSel);
IMP methodIMP = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(class, methodSel, methodIMP, types);
}
2.交换方法实现
SEL 和IMP的关系是一一对应的,如图
sel.png
方法交换其实也就是交换了他们的IMP,如图:
exchangeImp.png
实现代码为:
/**
方法交换
@param selone 方法1
@param selTwo 方法2
*/
+ (void)exchangeMethodWithClsas:(Class)class Sel:(SEL)selone selTwo:(SEL)selTwo;
{
Method firstMethod = class_getInstanceMethod(class, selone);
Method secondMethod = class_getInstanceMethod(class, selTwo);
method_exchangeImplementations(firstMethod, secondMethod);
}
3.获取类的属性列表
使用了class_copyPropertyList(Class,&count)来获取的属性列表,然后通过for循环通过property_getName()来获取每个属性的名字。当然使用property_getName()获取到的名字依然是C语言的char类型的指针,所以我们还需要将其转换成NSString类型,然后放到数组中一并返回。
代码如下:
/**
获取类的属性列表 包括使用属性和公有属性 以及定义在延展中的属性
@param class 类
@return 数组 里面是字符串
*/
+ (NSArray *)getPropertyWithClass:(Class)class
{
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; i++)
{
const char *propertyName = property_getName(propertyList[i]);
[mutableList addObject:[NSString stringWithUTF8String:propertyName]];
}
free(propertyList);
return [NSArray arrayWithArray:mutableList];
}
4.给分类动态添加属性
系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
新建一个测试的分类UIImage+Test
代码如下:
.h
#import <UIKit/UIKit.h>
@interface UIImage (Test)
@property (nonatomic,strong)NSString *urlImage;
@end
.m
#import "UIImage+Test.h"
#import <objc/runtime.h>
@implementation UIImage (Test)
- (NSString *)urlImage
{
return objc_getAssociatedObject(self, (const void*)@"urlImage");
}
- (void)setUrlImage:(NSString *)urlImage
{
// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(self, (const void*)@"urlImage", urlImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
5.字典转模型
转载至: https://juejin.im/post/593f77085c497d006ba389f0
字典转模型的方式:
一个一个的给模型属性赋值(初学者)。
字典转模型KVC实现
KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key 一一对应。
如果不一致,就会调用[ setValue:forUndefinedKey:] 报key找不到的错。
分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。
字典转模型 Runtime 实现
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。
考虑情况:
1.当字典的key和模型的属性匹配不上。
2.模型中嵌套模型(模型属性是另外一个模型对象)。
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解;
步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
MJExtension 字典转模型实现
底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已_.)。
这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,可以参考 实用「KVC编码 & KVO监听
字典转模型 Runtime 方式实现:
说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解
Runtime 字典转模型
5-1、runtime 字典转模型-->字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:
// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
代码为:
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
/**
class_copyIvarList: 获取类中的所有成员变量
Ivar:成员变量
第一个参数:表示获取哪个类中的成员变量
第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
count: 成员变量个数
*/
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
// 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
// 而报错 (could not set nil as the value for the key age.)
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
注:
这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。
原因:
Ivar:成员变量,以下划线开头,Property 属性
获取类里面属性 class_copyPropertyList
获取类中的所有成员变量 class_copyIvarList
{
int _a; // 成员变量
}
@property (nonatomic, assign) NSInteger attitudes_count; // 属性
这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和``getter方法的。
5-2.runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:
/**
字典转模型 模型中包括模型
@param dict 字典
@return 结果
*/
+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替换: @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
//--------------------------- 我是分割线 ------------------------------//
//
// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
// 判断下value是否是字典,并且是自定义对象才需要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型 userDict => User模型, 转换成哪个模型
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(ivarType);
if (modelClass) { // 有对应的模型才需要转
// 把字典转模型
value = [modelClass modelWithDict2:value];
}
}
// 给模型中属性赋值
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
5-3.runtime 字典转模型-->数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种情况处理如下:
Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
//--------------------------- 我是分割线 ------------------------------//
//
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
// arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict3:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
runtime字典转模型-->数组中装着模型 打印输出
总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
6.实现NSCoding的自动归档和解档
在开发项目的过程中,我们经常会用到需要对一个模型进行本地存储,这样就好对对模型惊喜解档和归档,也就是序列化。一般我们是这样写的
eg:
.h
#import <Foundation/Foundation.h>
@class TestModel;
@protocol PersonDelegate <NSObject>
- (void)test;
@end
@interface Person : NSObject
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)NSInteger age;
@property (nonatomic,assign)BOOL sex;
@property (nonatomic,strong)TestModel *model;
@end
.m
#import "Person.h"
@interface Person()<NSCoding>
@end
@implementation Person
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeInteger:_age forKey:@"age"];
[aCoder encodeBool:_sex forKey:@"sex"];
[aCoder encodeObject:_model forKey:@"model"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self)
{
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
self.sex = [aDecoder decodeBoolForKey:@"sex"];
self.model = [aDecoder decodeObjectForKey:@"model"];
}
return self;
}
@end
如果这里有100个属性,或者更多,那么再手写就太麻烦了。
如果使用runtime,可以很好的解决这个问题。
代码如下:
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++)
{
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init])
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count;i++)
{
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 查看成员变量
const char *name = ivar_getName(ivar);
// 归档
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
也可定义两个宏 这样就可以在各处调用了
eg:
#define encodeRuntime(A) \
- (void)encodeWithCoder:(NSCoder *)aCoder\
{\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([self class], &count);\
for (int i = 0; i<count; i++)\
{\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
}
#define initCoderRuntime(A) \
- (id)initWithCoder:(NSCoder *)aDecoder\
{\
if (self = [super init]){\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count;i++)\
{\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
}
在.m方法里直接调用就可以了
#import "Person.h"
#import <objc/runtime.h>
@interface Person()<NSCoding>
@end
@implementation Person
encodeRuntime(self)
initCoderRuntime(self)
@end
7.获取类的实例方法列表 getter,setter,对象方法等,不能获取类方法
+ (NSArray *)getInstaceMethodListWithClass:(Class)class
{
unsigned int count = 0;
Method *methodList = class_copyMethodList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i<count; i++)
{
Method method = methodList[i];
SEL methodName = method_getName(method);
[mutableList addObject:NSStringFromSelector(methodName)];
}
free(methodList);
return [NSArray arrayWithArray:mutableList];
}
8.获取协议列表
+ (NSArray *)getProtocolListWithClass:(Class)class
{
unsigned int count = 0;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i<count; i++)
{
Protocol *protocol = protocolList[i];
const char *protocolName = protocol_getName(protocol);
[mutableList addObject:[NSString stringWithUTF8String:protocolName]];
}
return [NSArray arrayWithArray:mutableList];
return nil;
}