Runtime应用
Runtime的能力很强大,有很多优秀的第三方库基于Runtime实现了不可思议的功能。Apple也有原生框架使用Runtime使得奇妙的技术得以被自然合理地使用。这里介绍4种:Associated Object、Method Swizzling(Aspect Oriented Programming)、KVO和NSZombie。
Associated Objects
对象关联(或称为关联引用)是Runtime2.0(起始于OSX10.5和iOS4)的一个特性,它允许你在运行时向对象动态赋值关联、获取关联和删除关联:
- objc_setAssociatedObject
- objc_getAssociatedObject
- objc_removeAssociatedObjects
这意味着可以对已经存在的类在Category中添加自定义属性,这几乎弥补了Objc最大的缺点。对于第2个参数key而言,通常应该是常量、唯一的、在适用范围内用getter和setter访问到的:
static char kAssociatedObjectKey;
objc_getAssociatedObject(self, &kAssociatedObjectKey);
但是,因为SEL被编译器确保了其是常量又是唯一的,所以更简单的方式是用SEL实现:
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
@end
而对象的关联策略由类型objc_AssociationPolicy指定:
- OBJC_ASSOCIATION_ASSIGN = @property(assign)或者@property(unsafe_unretained)
- OBJC_ASSOCIATION_RETAIN_NONATOMIC = @property (nonatomic,strong)
- OBJC_ASSOCIATION_COPY_NONATOMIC = @property (nonatomic, copy)
- OBJC_ASSOCIATION_RETAIN = @property (atomic, strong)
- OBJC_ASSOCIATION_COPY = @property (atomic, copy)
删除关联时,根据文档描述不要使用objc_removeAssociatedObjects,因为这样是删除所有关联,也许别的关联使用者添加过你不知道的关联过早被一起删除了。正确的方法是使用objc_setAssociatedObject将对应的关联设置为nil。
无论是帮助内部实现的私有属性,还是帮助外部调用的公有属性,在已有类需要属性时能实现这种需求。但是关联不是万能药,再有适合需求更好的实现时(比如Delegate,NSNotification,KVO或者Subclass),关联应该被视为最后的选择。
Method Swizzling(Aspect Oriented Programming)
类(Class)维护一张调度表(dispatch table)在运行时解析消息;表中每项(entry)都是一个方法(Method),其key是一个唯一的名字选择器(SEL)-字符串,对应着一个方法实现(IMP)-指向标准C函数的指针。Method swizzling通过Runtime,改变类调度表里选择器与方法实现之间的映射关系,从而让消息解析时从一个选择器对应到另外一个的实现:
- class_addMethod
- class_replaceMethod
- method_exchangeImplementations
举个例子:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
因为Method Swizzling影响全局,所以减少冒险情况就很重要。+load能够保证类初始化时就被调用,这为改变系统行为提供了统一性。而+initialize并不能保证在何时被调用直到第一次调用类的方法,这使得不同类之间没有统一性。所以Swizzling应该在+load方法中实现。同时,为了确保Swizzling即使在多线程时也只被执行一次,Swizzling应该在dispatch_once中实现。
乱用Swizzling容易导致不可预料的行为和结果,在没有足够的沟通的前提下,很容易造成看似理所当然实则莫名其妙的问题。除了显示说明和沟通以外,应用时始终调用原始实现能够保证至少正常API功能,同时给替换方法添加前缀避免和其他地方的代码产生冲突。
An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches). —Wikipedia
AOP是对OOP的一个补充,利用Runtime和AOP,可以把琐碎事务逻辑从主逻辑中分离出来,作为单独模块。有很多方式可以实现AOP,Method Swizzling就是其中之一。Aspects就是一个不错的AOP库。
KVO
KVO是观察者模式在Objc的实现,是MVC中解耦MC通信的方法,是实现Cocoa Bindings的基础。
KVO的优美在于,使用时的自然合理是建立在Runtime底层一系列复杂处理换来的。简单来说,当某个对象第一次被观察时,Runtime会创建一个新的原类子类,重写所有被观察key的setter方法,并将该对象isa指向子类变成子类的实例。被重写的方法实现了如何通知观察者。有趣的是,Apple甚至也重写了-class方法,使得子类依旧返回原类,让使用者看起来什么都没发生一样。举个例子:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface TestClass : NSObject
@property int x;
@property int y;
@property int z;
@end
@implementation TestClass
@end
static NSArray *ClassMethodNames(Class c) {
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
for(unsigned int i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
static void PrintDescription(NSString *name, id obj) {
NSString *str = [NSString stringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
int main(int argc, char **argv) {
[NSAutoreleasePool new];
TestClass *x = [[TestClass alloc] init];
TestClass *y = [[TestClass alloc] init];
TestClass *xy = [[TestClass alloc] init];
TestClass *control = [[TestClass alloc] init];
[x addObserver:x forKeyPath:@"x" options:0 context:NULL];
[xy addObserver:xy forKeyPath:@"x" options:0 context:NULL];
[y addObserver:y forKeyPath:@"y" options:0 context:NULL];
[xy addObserver:xy forKeyPath:@"y" options:0 context:NULL];
PrintDescription(@"control", control);
PrintDescription(@"x", x);
PrintDescription(@"y", y);
PrintDescription(@"xy", xy);
SEL setX = @selector(setX:);
printf("Using NSObject methods, normal setX: is %p, overridden setX: is %p\n",
[control methodForSelector:setX], [x methodForSelector:setX]);
printf("Using libobjc functions, normal setX: is %p, overridden setX: is %p\n",
method_getImplementation(class_getInstanceMethod(object_getClass(control), setX)),
method_getImplementation(class_getInstanceMethod(object_getClass(x), setX)));
return 0;
}
执行结果为(测试结果在撰写原文和本文时不一样,本文的methodForSelector:结果没有隐藏修改,但是为了阐述KVO知识,我还是引用原文的结果):
control: <TestClass: 0x104b20>
NSObject class TestClass
libobjc class TestClass
implements methods <setX:, x, setY:, y, setZ:, z>
x: <TestClass: 0x103280>
NSObject class TestClass
libobjc class NSKVONotifying_TestClass
implements methods <setY:, setX:, class, dealloc, _isKVOA>
y: <TestClass: 0x104b00>
NSObject class TestClass
libobjc class NSKVONotifying_TestClass
implements methods <setY:, setX:, class, dealloc, _isKVOA>
xy: <TestClass: 0x104b10>
NSObject class TestClass
libobjc class NSKVONotifying_TestClass
implements methods <setY:, setX:, class, dealloc, _isKVOA>
Using NSObject methods, normal setX: is 0x195e, overridden setX: is 0x195e
Using libobjc functions, normal setX: is 0x195e, overridden setX: is 0x96a1a550
可以看出,没有被Observer过的对象control它的信息没有改变,无论是类型还是方法。被观察过的对象,无论被观察的property有多少,类型都会被动态地修改为另一个动态创建的子类NSKVONotifying_TestClass。这个子类重写了所有被观察property的setter方法,其内部除了调用父类实现以外,还通知观察者。
(lldb)p (IMP)0x96a1a550
$1 = 0x96a1a550 (Foundation`_NSSetIntValueAndNotify)
所以,子类内部应该会有一份维护property映射观察者们的数据结构(根据文档,观察者和被观察者都没有内存retain,只是简单的映射关系。比如add不remove且观察者已经release,再有property变化时就会crash),也需要重写dealloc方法至少清理该数据结构。而class的重写是为了封装变化,让使用者感到自然。_isKVOA看起来像是一个私有方法。NSObject的addObserver和removeObserver内部除了动态创建NSKVONotifying_Class子类以外,应该还有添加和删除观察者到子类中。
另外,Foundation中有一系列这样的函数:
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSetObjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
__NSSetUnsignedCharValueAndNotify
__NSSetUnsignedIntValueAndNotify
__NSSetUnsignedLongLongValueAndNotify
__NSSetUnsignedLongValueAndNotify
__NSSetUnsignedShortValueAndNotify
Apple为每一种primitive type都写了对应的实现。object会用到的其实只有__NSSetObjectValueAndNotify,但看起来也没有实现完全,比如long dobule或通用指针类型(generic pointer type)提供方法。所以,不在这个方法列表里的属性其实是不支持KVO的。
NSZombie
僵尸对象(NSZombie)用于调试内存管理问题的时相当有用,具体是当发送消息到已经释放的对象时,僵尸对象就能检测到。一个比较常见的就是”use after free”错误。
通常情况下,对象释放后,其内存内容或者被重写或者不变。无论是哪种结果,都是无效会引发冲突。如果说这段内存恰好重写成一个新对象,那么消息可能就会发送给与原来风马牛不相及的对象,而且往往会由于无法识别选择器而抛出异常,假如这个错误的对象恰好也有该方法的话,那么就会发生更奇怪的问题。如果对象没有被重写尚未改变,处于将被销毁的状态,那么这会引起更匪夷所思的错误,比如多次调用释放占有的资源。
而僵尸对象会与对象的dealloc挂钩,最后一步不是真正释放所占内存,而是将其动态改为一个僵尸类对象,并拦截所有发送给该对象的消息。这样,再有任何消息发给该对象时都会检测到,而不是像正常调试下那样出现五花八门的状况。
如何拦截信息?
一个空的对象什么方法都不实现,对它发送消息后,Runtime会启动消息转发机制,所以重写forwardInvocation:是拦截消息的最佳位置。而与其对应的,需要实现methodSignatureForSelector:。
如何保留类型?
通过动态创建Zombie类型,指明其类名和原类产生映射,比如NSZombie_Class,这样保持原始大小,Zombie类不用额外的空间保留原类型引用。
如何改变类型?
Zombie主要是检测错误向已释放对象发送消息的问题,所以在原类型对象的dealloc处最适合动态修改对象类型。而重写所有对象的dealloc,需要用到上面的Method Swizzling。
总而言之,对于类型系统中原本没有的Zombie类型,通过Swizzling修改NSObject的dealloc方法,使得在运行期间任意类型对象dealloc时,动态创建和注册包含该类名信息的Zombie子类,动态添加methodSignatureForSelector:和forwardInvocation:的实现,再将原本要释放的对象的类型设置为Zombie子类。这样如果还有消息发送给本已无效的僵尸对象时,都会根据Runtime的消息转发机制最终找不到实现而产生异常(可观察释放对象信息)不至于直接crash。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
void EmptyIMP(id obj, SEL _cmd) {}
NSMethodSignature *ZombieMethodSignatureForSelector(id obj, SEL _cmd, SEL selector) {
Class class = object_getClass(obj);
NSString *className = NSStringFromClass(class);
className = [className substringFromIndex: [@"MAZombie_" length]];
NSLog(@"Selector %@ sent to deallocated instance %p of class %@", NSStringFromSelector(selector), obj, className);
abort();
}
Class ZombifyClass(Class class) {
NSString *className = NSStringFromClass(class);
NSString *zombieClassName = [@"MAZombie_" stringByAppendingString: className];
Class zombieClass = NSClassFromString(zombieClassName);
if(zombieClass) return zombieClass;
zombieClass = objc_allocateClassPair(nil, [zombieClassName UTF8String], 0);
class_addMethod(zombieClass, @selector(methodSignatureForSelector:), (IMP)ZombieMethodSignatureForSelector, "@@::");
class_addMethod(object_getClass(zombieClass), @selector(initialize), (IMP)EmptyIMP, "v@:");
objc_registerClassPair(zombieClass);
return zombieClass;
}
void ZombieDealloc(id obj, SEL _cmd) {
Class c = ZombifyClass(object_getClass(obj));
object_setClass(obj, c);
}
void EnableZombies(void) {
Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
method_setImplementation(m, (IMP)ZombieDealloc);
}
int main(int argc, const char * argv[]) {
EnableZombies();
NSObject *test = [[NSObject alloc] init];
[test release];
[test description];
return 0;
}
每个类及其子类在被第一次发送消息前都会先调用+initialize,从而使其能够初始化本身。如果Runtime发现该类没有实现+initialize方法,就会将消息转发。如果在此让消息转发的话,Zombie类也就没什么用了。所以需要添加一个空的+initialize方法来避免这个问题。