iOS高级开发 RunTime机制讲解六---应用实例
通过上面的几篇文章,对Runtime的说明已经算是详细了,对runtime的用途感觉还是很局限,接下来通过具体的实例看下runtime具体能做什么,废话不多说,开始吧
同样还是采用解说+代码的形式,完整的实例代码点击下载DEMO:https://github.com/sleepsheep/RunTimeDemo06,你也可以直接看DEMO,里面也有详细的注释
具体的应用场景有哪些呢
- 一、添加私有成员变量
- 二、 添加公有属性
- 三、方法交换
- 四、解归档
- 五、数据模型转化
一、添加私有成员变量
相信大家都是用过分类,在分类中我们只能添加方法而不能添加成员变量,有时候我们真的需要在分类中添加成员变量的时候我们该怎么办呢?这时候我们就可以使用runtime来实现。
首先先了解几个runtime提供的方法
//关联对象
/** 例:A类包含一个name属性
* @param object 被关联的对象 (A类)
* @param key 关联的key 唯一性
* @param value 关联的对象 (name属性)
* @param policy objc_AssociationPolicy 是一个枚举,决定了对象是否被释放的策略
当对象被释放时,会根据这个策略来决定是否释放关联的对象,当策略是RETAIN/COPY时,会释放(release)关联的对象,当是ASSIGN,将不会释放。
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
/**
* @param object 被关联的对象
* @param 关联的key,唯一
*/
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
现在回到上面的问题,如何在一个分类中添加一个属性呢?
下面以给一个button添加回调为例:
创建一个分类CallBack
typedef void(^callBack)(UIButton *);
@interface UIButton (Callback)
- (instancetype)initWithFrame:(CGRect)frame callback:(callBack)callback;
@end
#import "UIButton+Callback.h"
#import <objc/runtime.h>
@interface UIButton()
@property (nonatomic, copy) callBack callback;//私有成员变量
@end
@implementation UIButton (Callback)
- (instancetype)initWithFrame:(CGRect)frame callback:(callBack)callback {
self = [self initWithFrame:frame];
if (self) {
self.callback = callback;
[self addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (callBack)callback {
return objc_getAssociatedObject(self, @selector(callback));
}
- (void)setCallback:(callBack)callback {
objc_setAssociatedObject(self, @selector(callback), callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)buttonClick:(UIButton *)button {
self.callback(button);
}
@end
- 调用参考ViewController
//1、01添加私有成员变量
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(20, 200, 300, 100) callback:^(UIButton *btn) {
NSLog(@"~~~");
}];
button.backgroundColor = [UIColor orangeColor];
[self.view addSubview:button];
二、添加共有属性
上面的例子是我们不需要把在分类中添加的属性暴露在外面,但是假如我们需要把属性暴露在外面呢,当然了 这是废话 直接在属性放到h文件里面就可以了
@class CustomTabBar;
@interface UITabBarController (Custom)
@property (nonatomic, strong) CustomTabBar *customTabBar;
@end
#import "UITabBarController+Custom.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation UITabBarController (Custom)
- (CustomTabBar *)customTabBar {
return objc_getAssociatedObject(self, @selector(customTabBar));
}
- (void)setCustomTabBar:(CustomTabBar *)customTabBar {
objc_setAssociatedObject(self, @selector(customTabBar), customTabBar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//...其他的可以操作customTabBar的方法
@end
这样我们就可以随意的操作自定义的tabbar了
三、方法交换
在讲解方法交换之前,我们首先必须对Method
有一定的了解
看一下Runtime
里面的定义
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; 指向方法实现的指针
}
交换的原理:一个方法包含一个方法名和一个指向方法实现的指针,这两者是一一对应的关系:
Asel---AIMP
Bsel---BIMP
即A的方法名指向的A的方法的实现,B的方法名指向的是B的方法的实现
交换后
Asel---BIMP
Bsel---AIMP
先看代码:
@interface UIImage (Hook)
+ (void)load;
@end
#import "UIImage+Hook.h"
#import <objc/runtime.h>
@implementation UIImage (Hook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class selfClass = object_getClass([self class]);
SEL sel1 = @selector(imageNamed:);
Method method1 = class_getClassMethod(selfClass, sel1);
SEL sel2 = @selector(myImageNamed:);
Method method2 = class_getClassMethod(selfClass, sel2);
BOOL flag = class_addMethod(selfClass, sel1, method_getImplementation(method1), method_getTypeEncoding(method1));
if (flag) {//添加成功说明没有实现,没有实现只能替换
class_replaceMethod(selfClass, sel1, method_getImplementation(method2), method_getTypeEncoding(method2));
} else {
method_exchangeImplementations(method1, method2);
}
});
}
+ (UIImage *)myImageNamed:(NSString *)imageName {
NSString *imageNameNew = [NSString stringWithFormat:@"%@%@", @"new_", imageName];
return [self myImageNamed:imageNameNew];
}
调用参考ViewController
注:引入工程中的图片的名字是new_123.png
//3、03方法交换
[UIImage load];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(20, 200, 400, 400)];
UIImage *image = [UIImage imageNamed:@"123"];
imageView.image = image;
[self.view addSubview:imageView];
说明:
//获取一个类的objc_class
object_getClass(id obj)
//根据方法所在的类和类SEL获取方法(类方法)
Method class_getClassMethod(Class cls, SEL name)
//根据方法所在的类和类SEL获取方法(对象方法)
Method class_getInstanceMethod(Class cls, SEL name)
//根据objc_method获取方法的实现
IMP method_getImplementation(Method m)
//根据objc_method获取一个方法编码类型
const char *method_getTypeEncoding(Method m)
/**
* 给一个类添加一个方法
*
* @param cls The class to which to add a method.
* @param name A selector that specifies the name of the method being added.
* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
* @param types An array of characters that describe the types of the arguments to the method.
*
* @return YES if the method was added successfully, otherwise NO
* (for example, the class already contains a method implementation with that name).
* 如果这个方法添加成功,就返回成功,否则失败,例如:如果这个类已经包含了你要添加的方法名字的实现 就返回失败
*
* @note class_addMethod will add an override of a superclass's implementation,
* but will not replace an existing implementation in this class.
* To change an existing implementation, use method_setImplementation.
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替换一个方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//交换两个方法
void method_exchangeImplementations(Method m1, Method m2)
四、解归档
@interface Person : NSObject
- (id)initWithCoder:(NSCoder *)decoder;
- (void)encodeWithCoder:(NSCoder *)encoder;
@end
#import "Person.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person
/*!
* @author yangL, 16-03-30 18:03:39
*
* @brief 使用runtime进行归档原理:
1、获取待归档的类的所有的成员变量
2、获取成员变量的名称key
3、根据key和decoder获取到value
4、归档返回
*
* @return self
*/
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);//获取到类的所有的成员变量 count代表个数
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
NSString *value = [decoder decodeObjectForKey:key];
// [self setValue:value forKey:key];
[self setValue:value forKeyPath:key];
}
free(ivars);
}
return self;
}
//解归档
/*!
* @author yangL, 16-03-30 18:03:50
*
* @brief 使用runtime进行解归档的原理:
1、获取待归档的类的所有的成员变量
2、获取成员变量的名称key
3、根据key和self获取到value
4、解归档
* @param encoder
*/
- (void)encodeWithCoder:(NSCoder *)encoder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
// NSString *value = [self valueForKey:key];
id value = [self valueForKeyPath:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
@end
五、数据模型转化
我们平常也会用到数据模型转化,但是会有一些弊端,一种情况是根据KVC转化,但是这样可能会有一些值为空的情况不好处理;另外一种情况是一个值一个值的校验然后,然后再赋值,这样当变量比较多的时候也会特别的麻烦;
接下来就用Runtime实现json到model的转化;
@interface NSObject (Extension)
- (void)setDiction:(NSDictionary *)dict;
+ (instancetype)objectWithDic:(NSDictionary *)dict;
@end
#import "NSObject+Extension.h"
#import <objc/runtime.h>
@implementation NSObject (Extension)
/*!
* @author yangL, 16-03-31 10:03:19
*
* @brief 字典转化为模型的方法
*
* @param dict 待转化的字典
*/
- (void)setDiction:(NSDictionary *)dict {
Class selfclass = self.class;
//循环遍历 类 (本类 或 所有父类)
while (selfclass && selfclass != [NSObject class]) {
unsigned int count = 0;
//获取到待转化的模型的所有的变量
Ivar* ivars = class_copyIvarList(selfclass, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
//获取变量的名字 但是注意 这里获取到的变量前面会有一个下划线 '_'
NSString *keyTemp = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
//去掉‘_’ 以便和dict中的key匹配
NSString *key = [keyTemp substringFromIndex:1];
//获取字典中对应的key的value
id value = [dict objectForKey:key];
//如果字典中没有这个值那么value将会是一个空值
if (value == nil) {
continue;
}
/*
这里进行下说明:字符串和类的encodingType都是以@开头、int --> i、float--> f
*/
//如果类中包含另一个类的对象,也将这个类对象的字典转化为模型
NSString *encodeType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
//对象会以 @名字 的形式出现 但是NSString也会以这个形式出现
NSRange range = [encodeType rangeOfString:@"@"];
//说明是字符串或者是对象
if (range.location != NSNotFound) {
//forType是类名或者NSString
NSString *forType = [encodeType substringWithRange:NSMakeRange(2, encodeType.length - 3)];
//不是字符串,说明是类
if (![forType isEqualToString:@"NSString"]) {
//字符串转化为类
Class classS = NSClassFromString(forType);
//解析类中的字段
value = [classS objectWithDic:value];
}
}
[self setValue:value forKey:key];
}
//官方文档说明,使用完后必须释放
free(ivars);
selfclass = [selfclass superclass];
}
}
+ (instancetype)objectWithDic:(NSDictionary *)dict {
NSObject *object = [[self alloc] init];
[object setDiction:dict];
return object;
}
@end
本人也是初涉Runtime,欢迎指正、交流;