Objective-C runtime 的简单理解与使用(二)
简书上的所有内容都可以在我的个人博客上找到(打个广告😅)
在我们知道了 Objective-C 中类的本质,以及它的消息分发机制后,我们就可以来看看那些与 runtime 相关的的函数了。当然,我们只会讲比较常见的那些。
关联对象(Associated Object)
关联对象,顾名思义,就是给某对象关联许多其他的对象。这些对象通过 key 来区分。
与关联对象相关的函数有三个:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void *key);
objc_removeAssociatedObjects(id object);
从函数名我们也可以看出来,这三个函数分别是用来设置,获取和移除关联对象的。这里要解释一下的是他们的参数。
- 第一个参数 id object 显然就是你要设置关联对象的那个对象。
- 第二个参数 const void *key 就是用来区分不同的关联对象的 key,因为想让两个 key 匹配到同一个关联对象就必须是完全相等的指针,所以我们一般用静态全局变量来作为 key。
static const void *AssociatedKey = "AssociatedKey";
- 第三个参数 id value 就是要关联的对象了。
- 第四个参数 objc_AssociationPolicy policy 指的是关联对象的存储策略,它是一个枚举,可以与 property 的 attribute 相对应:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // nonatomic, copy
OBJC_ASSOCIATION_RETAIN = 01401, // retain
OBJC_ASSOCIATION_COPY = 01403 // copy
};
大家知道,在 category 中,我们无法添加 property,因为无法添加实例变量。那么,我们现在就可以通过关联对象来实现在 category 中添加属性的功能了。
我们现在 CYClass 类的拓展中声明了一个属性
@interface CYClass (Property)
@property (nonatomic, copy)NSString *aString;
@end
如果这个时候我们直接在外部访问这个属性, 那个程序是会 crash 的,不信你可以试试😅,编译器会说:
'-[CYClass setAString:]: unrecognized selector sent to instance 0x1001060a0'
所以我们给它加上 setter 和 getter 方法, 并且在这两个方法中给它设置关联对象:
static void *aStringKey = "aStringKey";
@implementation CYClass (Property)
- (void)setAString:(NSString *)newString{
objc_setAssociatedObject(self, aStringKey, newString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)aString{
return objc_getAssociatedObject(self, aStringKey);
}
@end
现在我们再进行读写操作,程序就不会 crash 了。当然,没有必要的情况下,还是不要滥用关联对象, 否则有可能会出现一些难以发现的bug。
方法调配(Method Swizzling)
在前一篇博客中我们知道了每个类中的方法是以 objc_method 结构体的形式放在 methodLists 中的。每一个 selector 对应了一个实现的函数的指针 IMP。而 method swizzling 技术就是通过交换这个函数指针来实现的。
我们最好在 +load 方法中使用 method swizzling,因为 +load 方法对于加入运行期中的每个类及分类都会调用且只调用一次。所以在这里交换方法是最安全的。
我们来看一下苹果为我们提供了哪些API来实现 method swizzling:
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
可以直接替换方法,当需要的方法不存在时,会先调用 class_addMethod 来添加一个新的方法。会返回替换前的实现函数指针 。
Method class_getInstanceMethod(Class cls, SEL name);
根据类和 selector 得到 method,用来作为下面两个方法的参数。
IMP method_setImplementation(Method m, IMP imp);
直接为一个方法设置它的实现,返回之前的实现函数指针
void method_exchangeImplementations(Method m1, Method m2)
交换两个方法的实现,实际上就是调用了两次 method_setImplementation,并且是线程安全的。
我们用 method_exchangeImplementations 来简单的尝试一下 method swizzling,我添加了一个 NSString 的分类,用我自己的方法交换了系统的 lowercaseString 方法:
@implementation NSString (Swizzling)
+ (void)load {
Method originalMethod = class_getInstanceMethod([self class], @selector(lowercaseString));
Method swappedMthod = class_getInstanceMethod([self class], @selector(swizzle_lowercaseString));
method_exchangeImplementations(originalMethod, swappedMthod);
}
- (NSString *)swizzle_lowercaseString {
NSString *lowercase = [self swizzle_lowercaseString];
NSLog(@"FROM: %@ TO: %@", self, lowercase);
return lowercase;
}
@end
可能有人会觉得在自己新写的 swizzle_lowercaseString 方法中又调用 [self swizzle_lowercaseString] 会导致死循环,其实在交换了方法以后我们调用原来的 lowercaseString 方法就会进入这个方法的实现,而这时候调用 swizzle_lowercaseString 其实调用的是系统原来的方法,所以是不会产生死循环的。这里理解起来可能有点奇怪。
我们在看一下调用的结果
2016-03-11 20:01:05.645 Example[4129:101067] FROM: Hello World TO: hello world
当然 method swizzling 是一把双刃剑,我们可以用它来进行黑盒测试,在真正的项目中如果用 method swizzling 一定要格外小心。
消息转发机制(Message Forwarding)
当我们的对象接收到一个无法解读的消息时,就会进入消息转发。消息转发分为两大阶段,第一阶段是动态方法解析,第二阶段是完整的消息转发。
动态方法解析(dynamic method resolution)
要实现动态方法解析只要重写两个方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 处理无法识别的实例方法
+ (BOOL)resolveClassMethod:(SEL)sel; // 处理无法识别的类方法
这两个方法传进来的参数 selector 就是那个无法解析的方法,我们可以根据这个 selector 来动态的为这个类添加方法。比如像下面这样:
void dynamicMethod(id self, SEL _cmd) {
// do something here
}
@implementation CYClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(someSelector)) { // 对selector做一些逻辑判断
class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:"); // 为类添加方法
return YES;
} else {
return NO;
}
}
@end
还要提一下 class_addMethod 函数:
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types);
它的最后一个参数是用来描述这个函数的返回值和参数类型的,称之为 类型编码(Type Encoding)。在前面那个例子里的 "v@:" 中, v 表示返回值为 void, @ 表示第一个参数是 id, : 表示第二个参数类型是 SEL 。更多的类型编码可以看这里
当 resolveInstanceMethod: 返回 NO 时,就会进入消息转发的第二阶段 完整的消息转发机制。
完整的消息转发机制
完整的消息转发主要涉及两个方法:
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
如果在 + resolveInstanceMethod: 方法中返回了 NO 那么就会执行 - forwardingTargetForSelector: 方法。在这个方法内我们可以给对象返回一个备援的接受者来处理这个位置的信息。在 CYClass 的实现中我们这么写:
@implementation CYClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(unrecognizedSel)) {
return [AnotherClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
AnotherClass 的实例就是我们用来作为备援接受者的对象,我们在 AnotherClass 中实现了 unrecognizedSel 方法:
@implementation AnotherClass
- (void)unrecognizedSel {
NSLog(@"forwarding target for unrecognized selector in AnotherClass");
}
@end
然后我们再给 CYClass 的实例发送 unrecognizedSel 的消息就不会 crash 了:
CYClass *c = [CYClass new];
[c performSelector:@selector(unrecognizedSel)];
// 打印结果
2016-03-12 12:46:30.608 example[1577:19943] forwarding target for unrecognized selector in AnotherClass
如果这一步我们也没有提供一个备援的接收者,那么就会进入最后一步 - forwardInvocation: 方法,系统会把所有与那条消息相关的信息全部封装在一个 NSInvocation 对象中,我们可以在直接改变调用的目标, 也可以修改消息的内容后再进行转发。我们把前一个方法去掉,然后重写一下 - forwardInvocation: 方法:
@implementation CYClass
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
if (sel == @selector(unrecognizedSel)) {
[anInvocation invokeWithTarget:[AnotherClass new]];
} else {
[super forwardInvocation: anInvocation];
}
}
@end
需要注意的是我们还要重写- methodSignatureForSelector: 方法,因为生成 NSInvocation 对象会调用到这个方法,否则会抛出异常。关于 forwardInvocation 了解的还不是很多,所以例子比较简单,以后有了更深的理解后会再加上。
消息转发的全过程
总结
到这里对于 runtime 的简单理解与使用就基本结束了。总的来说,理解了 Objective-C 的运行时会让我们的代码更加灵活,当然也会增大维护的难度。不过想要学好 Objective-C 这门语言,runtime 是必不可少的!