上一篇我们了解了消息转发机制的全貌,传送门在这里:理解消息转发机制。但有时候一些抽象的理念得用实际例子才能帮助理解,尤其是runtime,我们很多时候只知道它的一些基础用法。所以,先从消息转发机制的第一阶段:动态方法解析,举一个完整的例子。
先回顾一下动态方法解析的大概定义:先征询当前接收者所属的类,是否能动态添加方法并处理这个未知的selector,如果接收者没有动态添加方法或者动态添加的方法依然不能处理这个未知的selector,则当前接收者自己就没有办法通过动态新增方法的手段来响应这个selector了,之后就进入消息转发的第二阶段。换句话说,就是没有具体方法实现的时候,通过runtime的征询,动态插入方法的实现,前文提到,CoreData属性的动态绑定,就是在这个阶段完成的。但是CoreData现在并不常用,所以这个例子也没什么代入感。
本文的例子KVC容器感觉也是个很生的名词,但实际上OC中的CAAnimation和CALayer就是常用的KVC容器,下面摘录苹果官方文档的解释:
The CAAnimation and CALayer classes are key-value coding compliant container classes, which means that you can set values for arbitrary keys. Even if the key someKey is not a declared property of the CALayer class, you can still set a value for it as follows:
[theLayer setValue:[NSNumber numberWithInteger:50] forKey:@"someKey"];
You can also retrieve the value for arbitrary keys like you would retrieve the value for other key paths. For example, to retrieve the value of the someKey path set previously, you would use the following code:
someKeyValue=[theLayer valueForKey:@"someKey"];
换成人话就是CAAnimation和CALayer都是KVC容器类,他们可以很方便的通过键值对的形式设置和获取值,哪怕key并没有通过属性声明过。这使得自定义子类的时候能够更方便的声明属性和使用属性,也给了API更多的设计空间。
下面是本次例子的主角:KVCContainerDictionary。他同样是一个KVC容器,本身及其子类只需要将属性设置为@dynamic,属性就可以通过点语法和键值对形式访问和赋值,除了属性,还可以存入任意自己所需要的数据。下面来看看它的实现。
///KVCContainerDictionary.h
#import <Foundation/Foundation.h>
@interface KVCContainerDictionary : NSObject
@property(nonatomic, strong)NSArray *list;
- (void)setObject:(id)object forKey:(NSString*)key;
- (id)objectForKey:(NSString*)key;
@end
头文件中声明了一个NSArray类型的属性list,还有键值对访问和赋值的方法。
将实现文件的关键部分拆分,一步步来说明。
#import "KVCContainerDictionary.h"
#import <objc/runtime.h>
@interface KVCContainerDictionary()
///声明一个存储用的字典storeDic,用于存储属性值
@property(nonatomic, strong)NSMutableDictionary *storeDic;
@end
@implementation KVCContainerDictionary
///用@dynamic修饰属性,代表不自动生成setter和getter
@dynamic list;
声明一个存储用的字典storeDic,用于存储属性值,并用@dynamic修饰属性,代表不自动生成setter和getter,这个很重要。
///初始化和动态方法解析
- (instancetype)init {
self = [super init];
if (self) {
///初始化存储用的字典
_storeDic = [NSMutableDictionary dictionary];
}
return self;
}
///本例的关键,在这里进行动态方法解析,并插入新的方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selString = NSStringFromSelector(sel);
if ([selString hasPrefix:@"set"]) {
class_addMethod(self,sel,(IMP)dicSetter,"v@:@");
}else {
class_addMethod(self, sel, (IMP)dicGetter, "@@:");
}
return YES;
}
由于没有自动生成setter和getter,所以在属性存取的时候selector并没有实现,所以在objc_msgSend失败后,进入动态方法解析阶段,这个时候会调用resolveInstanceMethod这一个类方法,尝试进行方法实现的动态插入,这里检查selector是否为set开头判断是否是setter。之后通过class_addMethod方法动态插入新的setter和getter的实现。这里有大家发现,class_addMethod方法最后一个参数很奇怪,完全不知所云,其实很简单,就是个字符对照,用来表示新的方法实现的入参和返回参数的类型。下面列出一张对照表,就可以很清晰的知道"v@:@"和"@@:"的含义了。
| Code | Meaning |
|---|---|
c |
A char |
i |
An int |
s |
A short |
l |
A long``l is treated as a 32-bit quantity on 64-bit programs. |
q |
A long long |
C |
An unsigned char |
I |
An unsigned int |
S |
An unsigned short |
L |
An unsigned long |
Q |
An unsigned long long |
f |
A float |
d |
A double |
B |
A C++ bool or a C99 _Bool |
v |
A void |
* |
A character string (char *) |
@ |
An object (whether statically typed or typed id) |
# |
A class object (Class) |
: |
A method selector (SEL) |
| [array type] | An array |
| {name=type...} | A structure |
| (name=type...) | A union |
bnum |
A bit field of num bits |
^type |
A pointer to type |
? |
An unknown type (among other things, this code is used for function pointers) |
///getter
id dicGetter(id self, SEL sel) {
KVCContainerDictionary *kvcDic = (KVCContainerDictionary*)self;
NSString *key = NSStringFromSelector(sel);
return [kvcDic.storeDic objectForKey:key];
}
///setter
void dicSetter(id self, SEL sel,id value) {
KVCContainerDictionary *kvcDic = (KVCContainerDictionary*)self;
NSString *selString = NSStringFromSelector(sel);
NSMutableString *key = [selString mutableCopy];
//去除尾部":"
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
//去除"set"
[key deleteCharactersInRange:NSMakeRange(0, 3)];
//更改驼峰大小写
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:[[key substringToIndex:1] lowercaseString]];
if (value) {
[kvcDic.storeDic setObject:value forKey:key];
}else {
[kvcDic.storeDic removeObjectForKey:key];
}
}
这两个方法就没啥好说的了,就是新的setter和getter的实现,set时将值写入storeDic,get时通过key将值从storeDic中取出来。
之后补全键值对所对应的存取方法:
///详细的异常处理我就省略了。。。
- (void)setObject:(id)object forKey:(NSString*)key {
if (object && key && key.length >0) {
[self.storeDic setObject:object forKey:key];
}else {
[self.storeDic removeObjectForKey:key];
}
}
- (id)objectForKey:(NSString*)key {
if (key && key.length > 0) {
return [self.storeDic objectForKey:key];
}else {
return nil;
}
}
做的更细致一些的话,可以模仿系统KVC的机制,实现类似- (id)valueForUndefinedKey:(NSString *)key和- (void)setValue:(id)value forUndefinedKey:(NSString *)key。使得key的异常能够更合理的被处理。
之后使用一下看看。
KVCContainerDictionary *dic = [KVCContainerDictionary new];
///点语法设置属性
dic.list = @[@"hello",@"world"];
///键值对取出属性
NSLog(@"%@",[dic objectForKey:@"list"]);///输出(hello,world)
///键值对设置任意值
[dic setObject:@"goodbye,world" forKey:@"bye"];
///键值对取出对应的值
NSLog(@"%@",[dic objectForKey:@"bye"]);///输出goodbye,world
之后创建一个子类CYCustomDictionary。
///.h
@interface CYCustomDictionary : KVCContainerDictionary
///声明一个新的属性
@property(nonatomic, strong)NSDate *currentDate;
@end
///.m
@implementation CYCustomDictionary
///不自动生成setter和getter
@dynamic currentDate;
同样我们使用一下
CYCustomDictionary *customDic = [CYCustomDictionary new];
///点语法赋值
customDic.currentDate = [NSDate date];
///键值对取出
NSLog(@"%@",[customDic objectForKey:@"currentDate"]);
从以上的例子可以看出,KVC容器类的实现其实就是利用的动态方法解析,将setter和getter的过程重新实现并在obj_msgSend后动态注入,将属性和值通过键值对形式存储起来。这样自定义的子类的属性的存取其实都有基类负责,子类可以很方便的任意添加属性,也可以更方便的利用属性读取和添加键值对的形式配合使用,让接口设计更加统一。