初次接触RunTime,记录下自己的学习心得,为后来者铺平道路,提供一个学习的切入点。
首先简单的介绍下RunTime,它就是传说中的运行时,为什么是传说中的呢?因为我们首次听到运行时大多都是从他人口中,只知道很底层的东西,非常难以理解。好了,上面都是废话,我们先要了解苹果的Object-C是面向运行时的语言,是动态的,它把编译和链接期所做的事推迟到运行时才进行处理,给了我们很大的灵活性,可以进行消息的转发,替换方法等。
理论上的东西说的再多,不会用都是瞎掰,我相信用的多了,慢慢自己也会深究其理,自然也就懂了。那么RunTime可以用来做什么呢?
一、动态添加方法的实现
先简单了解一下方法的调用机制,以Person类为例:
以对象方法为例当调用eat方法时[p eat],底层会调用[p performSelector:@selector(eat)]方法,编译器再将代码转化为objc_msgSend(p, @selector(eat));
当使用类方法调用时,eat为类方法,[Person eat]会转化为[[Person class] performSelector:@selector(eat)]方法,然后编译器再将代码转化为objc_msgSend(p, @selector(eat));
那么当我们只是对方法进行了声明,而没有实现时,编译时会有警告而不会报错。我们都知道在运行时系统找不到相应的方法实现,程序就会崩溃。但系统给了我们机会来防止崩溃,在崩溃之前系统会来到拦截调用。拦截调用就是系统在找不到调用的方法,程序崩溃之前调用的方法。
当调用了没有实现的对象方法时,就会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法。当调用了没有实现的类方法时,就会调用+(BOOL)resolveClassMethod:(SEL)sel方法。通过这两个方法就可以知道哪些方法没有实现,从而动态添加方法。参数sel即表示没有实现的方法。
一个objective - C方法最终都是一个C函数,默认任何一个方法都有两个参数。self : 方法调用者 _cmd : 调用方法编号。我们可以使用函数class_addMethod为类添加一个方法以及实现。
代码实现:需要在Person类的实现方法中调用
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 动态添加eat方法
// 首先判断sel是不是eat方法 也可以转化成字符串进行比较。
if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
/**
来看一下class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types) 的参数:
第一个参数: cls:给哪个类添加方法
第二个参数: SEL name:添加方法的编号
第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
第四个参数: types :方法类型,需要用特定符号,参考API
示例:v -> void 表示无返回值, @ -> object 表示id参数,: -> method selector 表示SEL
*/
class_addMethod(self, sel, (IMP)eat , "v@:");
// 处理完返回YES
return YES;
}
return [super resolveInstanceMethod:sel];
}
void eat (id self ,SEL _cmd){// 实现内容
NSLog(@"%@的%@方法动态实现了",self,NSStringFromSelector(_cmd));
}
二、动态为分类添加属性
RunTime提供了动态添加属性和获得属性的方法:
设置关联属性的值:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); 参数介绍:参数1 为哪个对象关联属性
参数2 可以自定义key值,用来获取被关联属性的值
参数3 关联的值,也就是set方法传入的值给属性去保存。
参数4 属性的保存方式,常用OBJC_ASSOCIATION_RETAIN_NONATOMIC
获取关联属性的值:
objc_getAssociatedObject(id object, const void *key);
参数介绍:参数1 为哪个对象关联属性
参数2 通过自定义的key值获取被关联属性的值
示例:为UIImageview的分类添加属性@property(copy,nonatomic)NSString *imageName;
// 给分类添加属性
- (void)setImageName:(NSString *)imageName {
//使用关联对象设置值
objc_setAssociatedObject(self, @"name", imageName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)imageName {
//获取关联对象设置的值
NSString *imgName = objc_getAssociatedObject(self, @"name");
return imgName;
}
三、动态获取一个类中的成员变量、属性、方法、协议
需要创建NSObject的分类,这样就可以获取所有类的员变量、属性、方法、协议,并且与KVC配合可以修改系统的类
1)获取属性列表
const void * kAssociateObjectKey = @"kAssociateObjectKey";// key值用来做标识的
+ (NSArray *)yk_propertyArray
{//关联属性
NSArray *prolist = objc_getAssociatedObject(self, kAssociateObjectKey);
if (prolist != nil) {
return prolist;
}
unsigned int count ;
objc_property_t *protertylist = class_copyPropertyList([self class], &count);
//临时可变数组
NSMutableArray *arrM = [NSMutableArray array];
for (NSInteger i = 0; i < count ; i ++ ){
//取出属性
objc_property_t proterty = protertylist[i];
//获取属性名
const char *name = property_getName(proterty);
NSString *nameStr = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
[arrM addObject:nameStr];
}
// 设置关联属性
objc_setAssociatedObject(self, kAssociateObjectKey, arrM.copy,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//释放内存
free(protertylist);
return arrM.copy;
}
2)获取成员变量列表
// 获取成员变量
+(NSArray *)yk_ivarArray
{
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);
NSMutableArray *arr = [NSMutableArray array];
for (NSInteger i = 0; i < count ; i ++ ) {
// 取出成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名
const char *name = ivar_getName(ivar);
// 将C语言字符串转化为OC字符串
NSString *ivarStr = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
NSLog(@"ivar = %@",ivarStr);
[arr addObject:ivarStr];
}
free(ivarList);
// NSLog(@"%zd",count);
return arr.copy;
}
3)获取方法列表
//获取方法名
+ (NSArray *)yk_methodList{
unsigned int count;
Method *list = class_copyMethodList([self class], &count);
NSMutableArray *arr = [NSMutableArray array];
for (NSInteger i = 0; i < count ; i++ ){
// 获取方法结构体
Method m = list[i];
// 获取方法名
SEL selector = method_getName(m);
// 转化为字符串
NSString *name = NSStringFromSelector(selector);
[arr addObject:name];
}
free(list);
return arr.copy;
}
4) 获取协议列表
// 获取协议名
+ (NSArray *)yk_protocolList{
unsigned int count;
// 返回值需要标记
__unsafe_unretained Protocol ** proList = class_copyProtocolList([self class], &count);
NSMutableArray *arr = [NSMutableArray array];
//遍历求协议名
for (NSInteger i = 0; i < count ; i ++ ){
Protocol *pro = proList[i];
const char *name = protocol_getName(pro);
NSString *nameStr = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
[arr addObject:nameStr];
}
free(proList);
return arr.copy;
}
四、在获取属性列表的基础上实现简单的字典转模型
// 字典数组转模型 以字典转模型为基础的
+ (NSArray *)modelWithDictArray:(NSArray *)array
{
if (array.count == 0) {
NSLog(@"数组为空!");
return nil;
}
NSAssert([array[0] isKindOfClass:[NSDictionary class]], @"数组内容必须为字典");
NSMutableArray *arr = [NSMutableArray array];
for (NSDictionary *dict in array) {
// 创建模型对象
id instance = [self modelWithDict:dict];
[arr addObject:instance];
}
return arr.copy;
}
// 字典转模型
+(instancetype)modelWithDict:(NSDictionary *)dict
{
id instance = [[self alloc] init];
//获取属性列表 不能每次都用调用获取属性列表的方法 效率太低
NSArray *prolist = [self yk_propertyArray];
//遍历字典
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 判断字典属性是否在属性列表中
if ([prolist containsObject:key]) {
[instance setValue:obj forKey:key];
}
}];
return instance;
}
提供GitHub上demo的下载地址:链接