Runtime总结

什么是Runtime

Objective-C语言是一门动态语言,就是尽可能地把决定从编译器推迟到运行期, 就是尽可能地做到动态. 只是在运行的时候才会去确定对象的类型和方法的. 因此利用Runtime机制可以在程序运行时动态地修改类和对象中的所有属性和方法.
Runtime是OC底层的一套C语言的Api,编译器最终都会将OC代码转化为运行时的代码。通过终端命令编译.m文件:$clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)

例如创建一个对象[[NSObject alloc]init],最终被转换为几万行代码,截取关键的一句可以看到底层是如何通过runtime创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)
((NSObject *(*)(id, SEL))(void *)objc_msgSend)
((id)objc_getClass("NSObject"),
sel_registerName("alloc")),
sel_registerName("init"));
}
return 0;
}

删除一些强制转换的语句,可以看到调用方法本质就是发消息,[[NSObject allock]init]语句发送了两次消息,第一次发了alloc消息,第二次发送init消息。

1
2
id obj = objc_msgSend(objc_getClass("NSObject"),sel_registerName("alloc"))
objc_msgSend(obj,sel_registerName("init"))

另外利用runtime可以做一些OC不容易实现的功能

  • 动态交换两个方法的实现(特别是交换系统自带的方法)
  • 动态添加对象的成员变量和成员方法
  • 获得某个类的所有成员方法、所有成员变量

如何应用运行时?

  1. 将某些OC代码转化为运行时代码,探究底层,比如block的实现原理;
  2. 拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed、viewDidload、alloc;
  3. 实现分类添加属性;
  4. 实现NSCoding的自动归档和自动解档;
  5. 实现字典和模型的自动转换;

实例

一、交换两个方法的实现,拦截系统自带方法的调用

需要用到的库 <objc/runtime.h>

  • 获得某个类的类方法
    1
    Method class_getClassMethod(Class cls , SEL name)
  • 获得某个类的实例对象方法
    1
    Method class_getInstanceMethod(Class cls , SEL name)
  • 交换两个方法的实现
    1
    void method_exchangeImplementations(Method m1 , Method m2)

案例1:方法简单的交换

创建一个Person类,类中实现以下两个类方法,并在.h文件中声明

1
2
3
4
5
6
7
+ (void)run {
NSLog(@"跑");
}

+ (void)study {
NSLog(@"学习");
}

运行

1
2
3
4
5
6
[Person run];
[Person study];
//
2017-12-11 16:10:08.747473+0800 runtime[1216:568709] 跑
2017-12-11 16:10:08.747623+0800 runtime[1216:568709] 学习

下面通过runtime实现方法的交换,类方法class_getClassMethod对象方法用class_getInstanceMethod

1
2
3
4
5
6
7
8
9
10
11
// 获取两个类的类方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
// 交换后,先打印学习,再打印跑!
[Person run];
[Person study];
//
2017-12-11 16:10:08.747473+0800 runtime[1216:568709] 学习
2017-12-11 16:10:08.747623+0800 runtime[1216:568709] 跑

案例2:拦截系统方法

需求:所有对象创建时进行打印,如何不自定义初始化方法实现打印呢?

步骤:

1、 为NSObject创建一个分类(NSObject+Category)
2、 在分类中实现一个自定义方法,方法中打印对象创建的语句

1
2
3
4
- (instancetype)hk_init {
NSLog(@"创建一个类%@",[self description]);
return [self zh_init];
}

3、分类中重写NSObjct的load方法,实现方法的交换

1
2
3
4
5
//获取类的实例方法
Method m1 = class_getInstanceMethod([NSObject class], @selector(init));
Method m2 = class_getInstanceMethod([NSObject class], @selector(zh_init));
//交换方法的实现
method_exchangeImplementations(m1, m2);

运行

1
2
3
4
5
6
7
 Person *p = [[Person alloc] init];

NSLog(@"%@", p);
//
2017-12-11 16:54:17.028029+0800 runtime[1408:711381] 创建一个类<Person: 0x1007007d0>
2017-12-11 16:54:17.028069+0800 runtime[1408:711381] <Person: 0x1007007d0>

二、在分类中设置属性,给任何一个对象设置属性

众所周知,分类中是无法添加属性的。但是如果确实有这个需求就需要用到runtime为分类添加属性

需要用到的库<objc/runtime.h>

  • set方法
1
2
3
4
5
6
7
8
9
10
/**
将值value 跟对象object 关联起来(将值value 存储到对象object 中)

@param object 给哪个对象设置属性
@param key 一个属性对应一个Key,将来可以通过key取出这个存储的值,
key 可以是任何类型:double、int 等,建议用char 可以节省字节
@param value 给属性设置的值
@param policy 存储策略 (assign 、copy 、 retain就是strong)
*/
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
  • get方法
1
2
3
4
5
6
7
8
/**
利用参数key 将对象object中存储的对应值取出来

@param object 对哪个对象取值
@param key 存储时绑定的key
@return 存储的值
*/
id objc_getAssociatedObject(id object , const void *key)

步骤:
1、创建一个分类,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)
2、先在.h@property声明get和set方法,方便点语法调用

1
@property(nonatomic,copy)NSString *name;

3、在.m中重写set和get方法,内部利用runtime给属性赋值和取值

1
2
3
4
5
6
7
8
9
10
char nameKey;

- (void)setName:(NSString *)name {
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}

运行

1
2
3
4
5
Person *p = [[Person alloc] init];
[p setName:@"Person"];
NSLog(@"%@", p.name);
//
2017-12-11 17:35:39.727320+0800 runtime[1487:808106] Person

三、获得一个类的所有成员变量

最典型的用法就是一个对象在归档和解档的encodeWithCoderinitWithCoder:方法中需要该对象所有的属性进行decodeObjectForKey:encodeObject:,通过runtime我们声明中无论写多少个属性,都不需要再修改实现中的代码了。

需要用到的库<objc/runtime.h>

  • 获得某个类的所有成员变量(outCount会返回成员变量的总数)
    参数

    1. cls:那个类
    2. outCount:放一个接收值的地址,用来存放属性的个数
    3. ivar:存放所有获取到的属性
1
Ivar *ivars = class_copyIvarList(Class cls , unsigned int *outCount)
  • 获取成员变量的名字
1
const char *ivar_getName(Ivar v)
  • 获得成员变量的类型
1
const char *ivar_getTypeEndcoding(Ivar v)

案例1:获取Person类中所有成员变量的名字和类型

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person 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);

案例2:利用runtime获取所有属性来重写归档解档方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);
for (int i=0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id n = [self valueForKey:key];
[aCoder encodeObject:n forKey:key];
}
free(ivars);
}
//解档
- (instancetype)initWithCoder:(nonnull NSCoder *)aDecoder {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);
for (int i=0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id n = [aDecoder decodeObjectForKey:key];
[self setValue:n forKey:key];
}
free(ivars);
return self;
}

同理用以上方法可以直接对NSObject做一个分类,让所有对象都具有归档能力

案例3:利用runtime获取所有属性来进行字典转模型

字典转模型我们需要考虑三种特殊情况:
1.当字典的key和模型的属性匹配不上
2.模型中嵌套模型
3.数组包含模型

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
{
"name":"Father",
"age":12,
"money":1000, //多余的属性
"dog" : { //模型嵌套模型
"name":"doge",
"age":12
},
"books":[{ //数组模型
"name":"iOS developer",
"price":24.23
},{
"name":"runtime",
"price":34.5
}]
}
}

NSObject+JSONExtention.h

1
2
// 返回数组中都是什么类型的模型对象
- (NSString *)arrayObjectClass ;

此方法需对应的模型实现,返回数组模型的类名

NSObject+JSONExtention.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

-(void)setDic:(NSDictionary *)dic {

Class c = self.class;
while (c &&c != [NSObject class]) {

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

if ([key hasPrefix:@"_"]) {
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
}
// 取出字典的值
id value = dic[key];

// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;

// 获得成员变量的类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

// 如果属性是对象类型
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取对象的名字(比如@"Dog",截取为Dog)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系统的对象类型
if (![type hasPrefix:@"NS"]) {
// 将对象名转换为对象的类型,将新的对象字典转模型(递归)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];

}else if ([type isEqualToString:@"NSArray"]) {

// 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
NSArray *array = (NSArray *)value;
NSMutableArray *mArray = [NSMutableArray array];

// 获取到每个模型的类型
id class ;
if ([self respondsToSelector:@selector(arrayObjectClass)]) {

NSString *classStr = [self arrayObjectClass];
class = NSClassFromString(classStr);
}
// 将数组中的所有模型进行字典转模型
for (int i = 0; i < array.count; i++) {
[mArray addObject:[class objectWithDict:value[i]]];
}

value = mArray;
}
}

// 将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}


+ (instancetype )objectWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc]init];
[obj setDic:dict];
return obj;
}

Demo地址