作为一种动态编程语言,Objective-C 拥有一个运行时系统来支持动态创建类,添加方法、进行消息传递和转发。利用 Objective-C 的 Runtime 可以实现一些很棒的功能。本篇文章会简单介绍一下消动态方法解析,并使用它实现一个容易扩展和序列化的实体类。 本文仅简单介绍相关概念,更详尽的说明请参考苹果官方文档Objective-C Runtime Programming Guide。
消息传递(Messaging)
在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就已经确定了。而在 Objective-C 中,执行 [object foo] 语句并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。
事实上,在编译时你写的 Objective-C 函数调用的语法都会被翻译成一个 C 的函数调用 - objc_msgSend()。比如,下面两行代码就是等价的:
[object foo];
objc_msgSend(object, @selector(foo));
消息传递过程: 首先,找到 object 的 class; 通过 class 找到 foo 对应的方法实现; 如果 class 中没到 foo,继续往它的 superclass 中找; 一旦找到 foo 这个函数,就去执行它的实现.
假如,最终没找到 foo 的方法实现,会发生什么呢?让我们看一个类:
@interface SomeClass : NSObject
- (void)foo;
- (void)crash;
@end
@implementation SomeClass
-(void)foo {
NSLog(@"method foo was called on %@", [self class]);
}
@end
SomeClass 这个类声明了一个方法 foo,和一个方法 crash, 我们实现了 foo 方法,但是没有实现 crash 方法。现在分别调用这两个方法,会发生什么?
SomeClass *someClass = [[SomeClass alloc] init];
[someClass foo];
[someClass crash];
运行这段代码,可以看到下面的输出: ```[removed] method foo was called on SomeClass : -[SomeClass crash]: unrecognized selector sent to instance 0x7ff67ac377f0 : Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SomeClass crash]: unrecognized selector sent to instance 0x7ff67ac377f0' First throw call stack: ( 0 CoreFoundation 0x0000000101380e65 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x0000000100a70deb objc_exception_throw + 48 2 CoreFoundation 0x000000010138948d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205 ...
程序执行了 foo 方法,并打印出日志。然后程序崩溃了,在执行 crash 方法时就抛出了一个异常,因为 crash 方法没有对应的实现。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:
- Method resolution
- Fast forwarding
- Normal forwarding
#Method Resolution
首先,Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。还是以 foo 为例,你可以这么实现:
```javascript
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if(aSEL == @selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}
Core Data
有用到这个方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在运行时动态添加的。 如果 resolveInstanceMethod: 方法返回 NO,运行时就会进行下一步:消息转发(Message Forwarding)。
实现一个容易扩展和序列化的实体类
这里,就使用上述的 Normal forwarding 来创建一个容易扩展和序列化的类。 通常我们会这样定义一个实体类:在类中定义许多属性,然后通过属性的 setter 和 getter 方法来存取值。
@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
@property (nonatomic, strong) NSString *prop2;
// ...
@end
现在我们需要把上面的实体类对象导出成一个 JSON,可能就需要下面 toDictionary: 这样的方法:
@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
@property (nonatomic, strong) NSString *prop2;
// ...
- (NSDictionary *)toDictionary;
@end
@implementation MyModel
- (NSDictionary *)toDictionary {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
if (self.prop1) dict[@"prop1"] = self.prop1;
if (self.prop2) dict[@"prop2"] = self.prop2;
return [dict copy];
}
@end
假如 MyModel 有很多个属性,这样写就比较繁琐。那么,既然要导出为 JSON 对象,中间肯定需要构建一个字典对象,能不能再保存值的时候就直接保存到一个字典中呢?于是,对上面的类改造一下:
@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
// ...
@property (nonatomic, strong) NSMutableDictionary *dictionary;
@end
@implementation MyModel
- (NSMutableDictionary *)dictionary {
if (!_dictionary) {
_dictionary = [NSMutableDictionary dictionary];
}
return _dictionary;
}
- (void)setProp1:(NSString *)prop1 {
if (prop1) {
self.dictionary[@"prop1"] = prop1;
} else {
[self.dictionary removeObjectForKey:@"prop1"];
}
}
- (NSString *)prop1 {
return self.dictionary[@"prop1"];
}
@end
我们在 MyModel 中加了一个属性 dictionary,在保存值的时候直接保存到这个字典里面,导出 JSON 的时候就简单许多。但是要对每一个属性写一个 setter 一个 getter,这样也不合适。
通过观察这些 setter 和 getter,我发现他们非常相似,而且通过这些方法名可以解析出属性名。那么,我们能不能在运行时再决定把值存在那个 key 下面呢?结合动态方法解析,然后就有了下面这个雏形:
@implementation MyModel
- (NSMutableDictionary *)dictionary {
if (!_dictionary) {
_dictionary = [NSMutableDictionary dictionary];
}
return _dictionary;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (isGetter) {
// 如果 sel 是一个 Getter,动态添加一个 Getter 实现
// Getter 的实现需要从 dictionary 中取出对应的值
return YES;
}
if (isSetter) {
// 如果 sel 是一个 Setter,就动态添加一个 Setter 实现
// Setter 实现中需要把值保存到 dictionary 中
return YES;
}
return NO;
}
@end
- (void)setProp1:(NSString *)prop1 {
if (prop1) {
self.dictionary[@"prop1"] = prop1;
} else {
[self.dictionary removeObjectForKey:@"prop1"];
}
}
- (NSString *)prop1 {
return self.dictionary[@"prop1"];
}
@end
为了实现上面的功能,要做下面几个事情:
- 需要让 setter 和 getter 在运行时决定
- 运行时要判断需要解析的 selector 是不是 setter 或者 getter。
- 实现一个通用的 setter 和 getter
编译器默认会为每个属性创建 setter 和 getter 方法,可以使用 @dynamic 关键词告诉编译器不要为某个属性创建 setter 和 getter 方法。
@implementation MyModel
// 编译器不再自动实现 setProp1: 和 prop1 方法
// 在运行时就可以为 prop1 属性动态添加 setter 和 getter
@dynamic prop1;
@end
最终实现的 MyModel 类如下:
@interface MyModel : NSObject
@property (nonatomic, strong) NSString *prop1;
@property (nonatomic, strong) NSString *prop2;
// ...
@property (nonatomic, strong) NSMutableDictionary *dictionary;
+ (objc_property_t)parseSelector:(SEL)selector isGetter:(BOOL *)isGetter isSetter:(BOOL *)isSetter;
@end
// 针对 id 类型属性 getter 的实现
void dynamicSetter(MyModel *obj, SEL sel, id value) {
objc_property_t prop = [[obj class] parseSelector:sel isGetter:NULL isSetter:NULL];
NSString *propName = [NSString stringWithFormat:@"%s", property_getName(prop)];
if (value) {
obj.dictionary[propName] = value;
} else {
[obj.dictionary removeObjectForKey:propName];
}
}
// 针对 id 类型属性 setter 的实现
id dynamicGetter(MyModel *obj, SEL sel) {
objc_property_t prop = [[obj class] parseSelector:sel isGetter:NULL isSetter:NULL];
NSString *propName = [NSString stringWithFormat:@"%s", property_getName(prop)];
return obj.dictionary[propName];
}
@implementation MyModel
// 声明这两个属性的 setter 和 getter 是动态创建的
@dynamic prop1, prop2;
- (NSMutableDictionary *)dictionary {
if (!_dictionary) {
_dictionary = [NSMutableDictionary dictionary];
}
return _dictionary;
}
// 判断是否是 setter 或 getter,返回属性名
+ (objc_property_t)parseSelector:(SEL)selector isGetter:(BOOL *)isGetter isSetter:(BOOL *)isSetter {
NSString *selStr = NSStringFromSelector(selector);
// 首先根据 setter 和 getter 的特点推断出属性名
char propName[selStr.length +1];
memset(propName, 0, selStr.length +1);
if ([selStr hasPrefix:@"set"]) {
strncpy(propName, selStr.UTF8String +3, selStr.length -4); // drop 'set' and ':'
propName[0] += ('a' - 'A'); // lowercase first letter
if (isSetter!=NULL) *isSetter = YES;
} else {
strncpy(propName, selStr.UTF8String, selStr.length);
if (isGetter!=NULL) *isGetter = YES;
}
// 然后使用推断出的属性名反查属性,如果没找到,说明这个 selector 既不是某个属性的 setter 也不是 getter
objc_property_t prop = class_getProperty([self class], propName);
if (!prop) {
if (isSetter!=NULL) *isSetter = NO;
if (isGetter!=NULL) *isGetter = NO;
}
return prop;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
BOOL isGetter, isSetter;
objc_property_t prop = [self parseSelector:sel isGetter:&isGetter isSetter:&isSetter];
const char *typeEncoding = property_copyAttributeValue(prop, "T");
if (typeEncoding != NULL) {
if (typeEncoding[0] == '@') {
if (isGetter) {
class_addMethod([self class], sel, (IMP)dynamicGetter, "@@:");
return YES;
}
if (isSetter) {
class_addMethod([self class], sel, (IMP)dynamicSetter, "v@:@");
return YES;
}
} else {
// 这里可以添加一些 setter 和 getter 实现以支持 int, float 等基本类型的属性
}
}
return NO;
}
@end
有关上面提到的属性类型 typeEncoding 可以查看苹果文档
注意:上面的实现仅支持 OC 对象类型的属性,对于 int, float 和结构体等类型的属性,需要实现特别的 setter 和 getter。
现在,可以为 MyModel 添加许多属性,而不用在写 toDictionary 或者手动实现从 dictionary 中存取值的方法了。也可以继承 MyModel,添加许多属性:
@interface MyModelSub : MyModel
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickname;
@end
// 类实现中不需要添加许多代码
@implementation MyModelSub
@dynamic name, nickname;
@end
MyModel 和它子类的对象可以快速转化成一个 NSDictionary:
MyModelSub *model = [[MyModelSub alloc] init];
model.prop1 = @"pro1value";
model.prop2 = @"pro2value";
model.name = @"Alex";
model.nickname = @"alex";
NSLog(@"model.dictionary = %@, \n model.prop1=%@", model.dictionary, model.prop1);
执行后,可以看到下面的输出:
model.dictionary = {
name = Alex;
nickname = alex;
prop1 = pro1value;
prop2 = pro2value;
},
model.prop1=pro1value
我们可以很方便的把 NSDictionary 转化成一个 MyModel 对象:
MyModelSub *model = [[MyModelSub alloc] init];
model.dictionary = [@{@"name":@"Alex", @"nickname":@"alex"} mutableCopy];
执行后,可以看到下面的输出:
model.name = Alex,
model.nickname = alex
利用 Objective-C 的 runtime 特性,我们可以自己来对语言进行扩展,解决项目开发中的一些设计和技术问题。后续文章里,我会介绍消息转发以及使用消息转发实现 MyModel 这样一个类。