原文出处:用Runtime的手段填充任意NSObject对象的nil属性
原文写的没有一句废话,无法再次总结升华,拿来主义。。。
初衷
在做项目的过程中,总是会写一大堆if、else语句去检查对象的Property是否是nil,如从服务器返回的JSON中缺少属性,导致Entity的某些值为空;或者创建的对象没有对所有属性做初始化等等。写多了觉得好烦啊=。=
所以想到本文的方法,嗯,程序员总是懒的。
解决步骤
- 遍历一个对象的所有属性(默认不包括父类属性)。
- 判断属性是否是nil。
- 为nil的属性,获取它的类型。
- 根据类型设置初始值(如NSString可以设为空字符串;NSNumber可以设为@0)
Runtime
OC是一门“动态”、“基于消息”的语言,而Runtime就是利用OC的动态特性,在运行时对程序做出“调整”的技术。有关Runtime的官方文档、网上的资料很多,大家自学哈~
本文主要用了如下几个Runtime的函数:
// 获取类的所有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)
示例
不好一块一块拆开说,直接上代码:
/**
* 解析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:
@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获取的结果是:
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@后面的类型名。
效果
例如我们有如下对象:
@interface UserEntity : NSObject
@property (copy, nonatomic) NSString *name;
@property (strong, nonatomic) NSNumber *number;
@property (strong, nonatomic) NSArray *array;
@end
设置默认值:
UserEntity *userEntity = [UserEntity new];
// 检查属性,设置默认值。
checkEntity(userEntity);
// 使用...
NSLog(@"name: %@", userEntity.name);
NSLog(@"number: %@", userEntity.number);
NSLog(@"array: %@", userEntity.array);
输出:
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都有了初值。
总结
上面的例子只是个粗略的版本,只是检查了字符串、数字、数组,其实完全可以扩展出很多功能,如针对不同的类型,根据对象的类型,设置不同的默认初值等。