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方法来避免这个问题。


延伸阅读


Runtime应用
https://hllovesgithub.github.io/2015/11/07/2015-11-07-Runtime应用/
作者
Hu Liang
发布于
2015年11月7日
许可协议