背景
之前一篇文章《iOS关联对象》详细介绍了如何通过关联对象添加属性,本篇文章将介绍如何通过runtime的class_addProperty
或class_addIvar
动态添加属性,并且带领大家看看这两个方法底层是如何实现的。
class_addProperty
添加属性
对于已经存在的类我们用class_addProperty
方法来添加属性,而对于动态创建的类我们通过class_addIvar
添加属性,
它会改变一个已有类的内存布局,一般是通过objc_allocateClassPair
动态创建一个class,才能调用class_addIvar
创建Ivar,最后通过objc_registerClassPair
注册class。
对于已经存在的类,
class_addIvar
是不能够添加属性的
首先我们声明了一个Person
类1
2
3@interface Person : NSObject
@property (nonatomic,copy)NSString *name;
@end
然后我们通过class_addProperty
动态添加属性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
42id getter(id object,SEL _cmd1){
NSString *key = NSStringFromSelector(_cmd1);
return objc_getAssociatedObject(object, (__bridge const void * _Nonnull)(key));
}
void setter(id object,SEL _cmd1,id newValue){
NSString *key = NSStringFromSelector(_cmd1);
key = [[key substringWithRange:NSMakeRange(3, key.length-4)] lowercaseString];
objc_setAssociatedObject(object, (__bridge const void * _Nonnull)(key), newValue, OBJC_ASSOCIATION_RETAIN);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
objc_property_attribute_t backingivar = { "V", [[NSString stringWithFormat:@"_%@", @"sex"] UTF8String] }; //variable name
objc_property_attribute_t attrs[] = { type, ownership0, ownership,backingivar};//这个数组一定要按照此顺序才行
BOOL add = class_addProperty([Person class], "sex", attrs, 4);
if (add) {
NSLog(@"添加成功\n");
}else{
NSLog(@"添加失败\n");
}
class_addMethod([Person class], NSSelectorFromString(@"sex"), (IMP)getter, "@@:");
class_addMethod([Person class], NSSelectorFromString(@"setSex:"), (IMP)setter, "v@:@");
unsigned int count;
objc_property_t *properties =class_copyPropertyList([Person class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSLog(@"名字:%s--属性:%s\n",property_getName(property),property_getAttributes(property));
}
Person *person = [Person new];
person.name = @"FlyOceanFish";
[person setValue:@"男" forKey:@"sex"];
NSLog(@"name:%@",person.name);
NSLog(@"sex:%@",[person valueForKey:@"sex"]);
}
return 0;
}
这里要注意几点:
- attrs属性设置的数组一定是此顺序
此属性设置是参照原有属性的打印格式进行设置的,name
是原先有的,sex
是后来动态添加的,如下:2019-01-02 14:39:39.370405+0800 MyProperty[1354:159207] 名字:sex–属性:T@”NSString”,C,N,V_sex
2019-01-02 14:39:39.370425+0800 MyProperty[1354:159207] 名字:name–属性:T@”NSString”,C,N,V_name
- 添加完属性我们要添加上对应的set、get方法,因为我们是通过kvo的方式设值和取值的,它会调用set、get方法,如果没有的话,会报错。
底层代码实现
上边代码演示了如何动态添加属性,接下来让我们看看苹果底层是如何实现的。
class_addProperty
1 | BOOL |
1 | static bool |
通过代码我们可以看到如果添加一个已经存在的属性是添加不成功的;
添加一个新属性,是实例化了一个property_list_t
对象,最终调用了cls的data方法,返回了class_rw_t
指针,最终添加在属性properties
的一个数组中。
还有一个结构体的名字是
class_ro_t
,与class_rw_t
是配合使用的,大家有兴趣可以自行去研究。ro即read only;rw即read write。看到这里应该能猜个八九不离十
class
对象结构体
1 | struct objc_class : objc_object { |
class_rw_t
结构体1
2
3
4
5
6
7
8
9
10
11
12struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
...
}
class_addIvar
1 | BOOL |
通过以上代码我们可以看到通过此方法添加的属性是实例化了ivar_t对象,并且存储在了class_ro_t
对象中了。所以跟我们上边说的改变了类的内存布局一致。
总结
我们通过一个demo实现了动态添加属性,通过底层源码解析让大家彻底认识了class_addProperty
和class_addIvar
两个方法。runtime让oc成为了一门动态语言,只有我们想不到的,没有runtime做不到的。