用Runtime的手段填充任意NSObject对象的nil属性

前言

好久没有写东西了,忙啊。

前段时间参加了一下我们华科联创的HackDay(本人在读研=。=,目前在阿里实习),作品是一款实时在线对战游戏 - 波波攒,(介绍请看知乎
从iOS游戏客户端(用的SpriteKit)到后台(PHP CI + Node + SocketIO + MySQL)全是自己一个人倒腾出来的,做了一把真正的全栈工程师,爽啊~
后面会完善整个游戏,增加角色、优化啥的,过上一段时间会上线的哈~

回到正文,本文主要介绍了怎么用Runtime的手段遍历任意NSObject对象的所有property,检查其值是否是nil,是的话根据其类型为其填充一个默认值。
Runtime毕竟是个“危险”的技术,本文的代码只是个初步的尝试。

初衷

在做项目的过程中,总是会写一大堆if、else语句去检查对象的Property是否是nil,如从服务器返回的JSON中缺少属性,导致Entity的某些值为空;或者创建的对象没有对所有属性做初始化等等。写多了觉得好烦啊=。=
所以想到本文的方法,嗯,程序员总是懒的。

解决步骤

  1. 遍历一个对象的所有属性(默认不包括父类属性)。
  2. 判断属性是否是nil。
  3. 为nil的属性,获取它的类型。
  4. 根据类型设置初始值(如NSString可以设为空字符串;NSNumber可以设为@0)

Runtime

OC是一门“动态”、“基于消息”的语言,而Runtime就是利用OC的动态特性,在运行时对程序做出“调整”的技术。有关Runtime的官方文档、网上的资料很多,大家自学哈~

本文主要用了如下几个Runtime的函数:

1
2
3
4
5
6
// 获取类的所有Property
1. objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
// 获取一个Property的变量名
2. const char *property_getName(objc_property_t property)
// 获取一个Property的详细类型表达字符串
3. const char *property_getAttributes(objc_property_t property)

示例

不好一块一块拆开说,直接上代码:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 解析Property的Attributed字符串,参考Stackoverflow
*/
static const char *getPropertyType(objc_property_t property) {
const char *attributes = property_getAttributes(property);

NSLog(@"%s", attributes);

char buffer[1 + strlen(attributes)];
strcpy(buffer, attributes);
char *state = buffer, *attribute;
while ((attribute = strsep(&state, ",")) != NULL) {
// 非对象类型
if (attribute[0] == 'T' && attribute[1] != '@') {
// 利用NSData复制一份字符串
return (const char *) [[NSData dataWithBytes:(attribute + 1) length:strlen(attribute) - 1] bytes];
// 纯id类型
} else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
return "id";
// 对象类型
} else if (attribute[0] == 'T' && attribute[1] == '@') {
return (const char *) [[NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4] bytes];
}
}
return "";
}

/**
* 给对象的属性设置默认值
*/
void checkEntity(NSObject *object) {
// 不同类型的字符串表示,目前只是简单检查字符串、数字、数组
static const char *CLASS_NAME_NSSTRING;
static const char *CLASS_NAME_NSNUMBER;
static const char *CLASS_NAME_NSARRAY;

// 初始化类型常量
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// "NSString"
CLASS_NAME_NSSTRING = NSStringFromClass([NSString class]).UTF8String;
// "NSNumber
CLASS_NAME_NSNUMBER = NSStringFromClass([NSNumber class]).UTF8String;
// "NSArray"
CLASS_NAME_NSARRAY = NSStringFromClass([NSArray class]).UTF8String;
});

@try {
unsigned int outCount, i;
// 包含所有Property的数组
objc_property_t *properties = class_copyPropertyList([object class], &outCount);

// 遍历每个Property
for (i = 0; i < outCount; i++) {
// 取出对应Property
objc_property_t property = properties[i];
// 获取Property对应的变量名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
// 获取Property的类型名
const char *propertyTypeName = getPropertyType(property);
// 获取Property的值
id propertyValue = [object valueForKey:propertyName];

// 值为空,才设置默认值
if (!propertyValue) {
// NSString
if (strncmp(CLASS_NAME_NSSTRING, propertyTypeName, strlen(CLASS_NAME_NSSTRING)) == 0) {
[object setValue:@"" forKey:propertyName];
}

// NSNumber
if (strncmp(CLASS_NAME_NSNUMBER, propertyTypeName, strlen(CLASS_NAME_NSNUMBER)) == 0) {
[object setValue:@0 forKey:propertyName];
}

// NSArray
if (strncmp(CLASS_NAME_NSARRAY, propertyTypeName, strlen(CLASS_NAME_NSARRAY)) == 0) {
[object setValue:@[] forKey:propertyName];
}
}
}

// 别忘了释放数组
free(properties);
} @catch (NSException *exception) {
NSLog(@"Check Entity Exception: %@", [exception description]);
}
}

重点 - 解析property_getAttributes函数的结果

在整个处理过程中,property_getAttributes函数是关键,因为我们要首先确定Property的类型,才能根据类型赋初值,但是property_getAttributes函数返回的字符串比较“晦涩难懂”:

如下定义的Property:

1
2
3
4
5
6
@property (copy, nonatomic) NSString *name;
@property (strong, nonatomic) NSNumber *number;
@property (strong, nonatomic) NSArray *array;
@property (assign, nonatomic) NSInteger i;
@property (assign, nonatomic) CGFloat f;
@property (assign, nonatomic) char *cStr;

依次通过property_getAttributes获取的结果是:

1
2
3
4
5
6
T@"NSString",C,N,V_name
T@"NSNumber",&,N,V_number
T@"NSArray",&,N,V_array
Tq,N,V_i
Td,N,V_f
T*,N,V_cStr

参考 Declared Properties of Objective-C Runtime Programming Guide
我们大概可以知道,T表示Type,后面跟着@表示Cocoa对象类型,后面的表示Property的属性,如Copy、strong等,然后就是变量名。
所以getPropertyType函数的工作就是纯粹的解析字符串,获取T@后面的类型名。

效果

例如我们有如下对象:

1
2
3
4
5
@interface UserEntity : NSObject
@property (copy, nonatomic) NSString *name;
@property (strong, nonatomic) NSNumber *number;
@property (strong, nonatomic) NSArray *array;
@end

设置默认值:

1
2
3
4
5
6
7
UserEntity *userEntity = [UserEntity new];
// 检查属性,设置默认值。
checkEntity(userEntity);
// 使用...
NSLog(@"name: %@", userEntity.name);
NSLog(@"number: %@", userEntity.number);
NSLog(@"array: %@", userEntity.array);

输出:

1
2
3
4
2015-07-11 18:17:25.918 Common[6939:270543] name:
2015-07-11 18:17:25.918 Common[6939:270543] number: 0
2015-07-11 18:17:25.918 Common[6939:270543] array: (
)

这样,一个对象的所有Property都有了初值。

总结

上面的例子只是个粗略的版本,只是检查了字符串、数字、数组,其实完全可以扩展出很多功能,如针对不同的类型,根据对象的类型,设置不同的默认初值等,靠读者你了~

Runtime是个好东西,但是别乱用啊=。=

参考

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器