Runtime是运用 C 和汇编完成的运行时代码库,Objective-C 中有许多言语特性都是经过它来完成。了解 Runtime 开发能够帮助咱们更灵敏的运用 Objective-C 这门言语,咱们能够将程序功用推迟到运行时再去决议怎么做,还能够利用 Runtime 来处理项目开发中的一些规划和技能问题,使开发进程愈加具有灵敏性。

一些关键字

  • self:类的躲藏参数变量,指向当时调用办法的目标
  • super:是编译器的标示符,经过 super 调用办法会被翻译成 objc_msgSendSuper(self, _cmd,…)
  • SEL:以办法名为内容的 C 字符串
  • IMP:指向办法完成的函数指针
  • id:指向类目标或实例目标的指针
  • isa:为 id 目标所属类型 (objc_class),Objc 中的承继就是经过 isa 指针找到 objc_class,然后再经过 super_class 去找对应的父类
  • metaclass:在 Objc 中,类本身也是目标,实例目标的 isa 指向它所属的类,而类目标的 isa 指向元类 (metaclass),元类的 isa 直接指向根元类,根元类的isa指向它自己,它们之间的联系如下图所示。

Objective-C Runtime 开发

音讯传递 (Messaging)

Objective-C 对于调用目标的某个办法这种行为叫做给目标发送音讯,实践上就是沿着它的 isa 指针去查找真正的函数地址。下面咱们来了解一下这个进程:

咱们写一个给目标发送音讯的代码

[array insertObject:obj atIndex:5];

编译器首先会将上面代码翻译成这种样子

objc_msgSend(array, @selector(insertObject:atIndex:), obj, 5);

体系在运行时会经过 array 目标的 isa 指针找到对应的 class(假如是给类发音讯,则找到的是metaclass),然后在 class 的 cache 办法列表顶用 SEL 去找对应 method,假如找不到便去 class 的办法列表中去找,假如在办法列表中也找不对对应 method 时,便沿着承继体系继续向上查找,找到后将 method 放入 cache,以便下次能快速定位,然后再去履行 method 的 IMP,找不到时体系便报错:unrecognized selector sent to insertObject:atIndex:

Runtime 提供了三种办法防止因为找不到办法而溃散

当找不到办法完成时,Runtime 会先发送 +resolveInstanceMethod: 或 +resolveClassMethod: 音讯,咱们能够重写它然后为目标指定一个处理办法。

void dynamicXXXMethod(id obj, SEL _cmd) {
    NSLog(@"ok...");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if(aSEL == @selector(xxx:)) {
        class_addMethod([self class], aSEL, (IMP)dynamicXXXMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

class_addMethod 办法的最后一个参数用来指定所增加办法的参数及回来值,叫Type Encodings。

假如 resolve 办法回来 NO,Runtime 会发送 -forwardingTargetForSelector: 音讯,允许咱们将音讯转发给能处理它的其它目标。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

当 -forwardingTargetForSelector: 回来 nil 时,Runtime 会发送 -methodSignatureForSelector: 和 -forwardInvocation: 音讯。咱们能够挑选疏忽音讯、抛出反常、将音讯转由当时目标或其它目标的任意音讯来处理。

//根据 SEL 生成 NSInvocation 目标,然后再由 -forwardInvocation: 办法进行转发。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature = [otherObject instanceMethodSignatureForSelector:aSelector];
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if([otherObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:otherObject]; // 转发音讯
    } 
    else {
        [self doesNotRecognizeSelector:sel]; // 抛出反常
    }
}

KVO

当咱们为目标增加观察者后,Runtime 会在运行时创建这个目标所在类的子类,而且将该目标的 isa 指针指向这个子类,然后重写监听特点的 set 办法并在办法中调用 -willChangeValueForKey: 和 -didChangeValueForKey: 来通知观察者,所以假如直接修正实例变量便不会触发监听办法。当移除观察者后,Runtime 便会将这个子类删去。

所以 isa 指针并不总是指向实例目标所属的类,也有可能指向一个中间类,所以不能依托它来确定类型,而是应该用 class 办法来确定实例目标的类。

相关目标 (Associated Objects)

在 Category 中能够为类增加实例办法或类办法,可是不支持增加实例变量,所以即使咱们在 Category 中为类增加了 property,也不能直接运用它,Runtime 能够处理这个问题,咱们只需求界说一个指针,然后经过 objc_setAssociatedObject 办法将指针与目标进行相关并指定内存管理方式,数据以 KeyValue 的形式存储在一个 HashMap 里。

Objc 中的类和目标都是结构体,Category 也是这样,界说的办法和特点在结构体中的存储,并在运行时按倒序增加到主类中(增加的办法会放在办法列表的上面),所以假如增加的办法与原类中的一样,那么在调用此办法时,优先找到的就是咱们增加的这个办法。假如有多个 Category 增加相同称号的办法,那么这些办法在办法列表中的次序取决于他们的编译次序,也就是这些 Category 文件在 Compile Sources 中的次序。

@interface NSObject (JC)
@property (nonatomic, copy) NSString *ID;
@end
@implementation NSObject (JC)
static const void *IDKey;
- (NSString *)ID {
    return objc_getAssociatedObject(self, &IDKey);
}
- (void)setID:(NSString *)ID {
    objc_setAssociatedObject(self, &IDKey, ID, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end  

AOP(Method Swizzling)

咱们能够经过承继、Category、AOP 方式来扩展类的功用。

  • 承继比较适合在规划底层代码架构时运用,不适当的运用会让代码看起来很烦琐,而且增加维护难度。
  • Category 适合为现有类增加办法。
  • 当需求修正现有类的办法而且拿不到源码时,承继和 AOP 都能处理问题,可是用 AOP 来处理代码耦合度更低。其实就算能拿到源码,往往直接去改源码也不是个好办法。

在 Objective-C 中,能够经过 Method Swizzling 技能来完成 AOP,下面咱们经过交流两个办法的完成代码来向已存在的办法中增加其它功用。

#import <objc/runtime.h>
@implementation UIViewController (Tracking) 
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class aClass = [self class]; 
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(swizzled_viewWillAppear:); 
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
        // 假如要对类办法进行交流,运用下面注释的代码
        // Class aClass = object_getClass((id)self);
        // 
        // Method originalMethod = class_getClassMethod(aClass, originalSelector);
        // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
 		// 交流两个办法的完成
 		// 防止 aClass 不存在 originalSelector,所以增加一下试试,但指向地址为新办法地址
        BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); 
        if (didAddMethod) { 
        	// 增加成功,说明 aClass 不存在 originalSelector,所以替换 swizzledSelector 的 IMP 为 originalMethod,实质上它们都指向 swizzledMethod
            class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 
        } 
        else { 
         	// 增加失败,说明 aClass 存在 originalSelector,直接交流
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
#pragma mark - Method Swizzling 
// 因为办法完成现已被交流,所以体系在调用 viewWillAppear: 时,实践上会调用 swizzled_viewWillAppear:
- (void)swizzled_viewWillAppear:(BOOL)animated { 
	// 下面代码表面上看起来会引起递归调用,因为函数完成现已被交流,实践上会调用 viewWillAppear:
   [self swizzled_viewWillAppear:animated]; 
	// 在原有基础上增加其它功用(写日志等)
} 
@end

运用 Method Swizzling 需求注意下面几个问题

  • 需求在 +load 办法中履行 Method Swizzling,+initialize 办法有可能不会被调用
  • 防止父类与子类同时 hook 父类的某办法,防止不了时至少要保证不在 +load 办法中履行 super.load(),否则父类中的 +load 办法会被履行两次
  • 需求在 dispatch_once 中履行,防止因多线程等问题倒致的偶数次交流后失效的问题
  • 假如你用了 swizzled_viewWillAppear 作为办法名,那么假如你引证的第三方 SDK 中也用了这个办法名来做办法交流,那会形成办法的递归调用,所以你最好换一个不太会被重复运用的办法名,例如 mx_swizzled_viewWillAppear
  • 即使运用 mx_swizzled_viewWillAppear 尽量防止了与第三方库或自己项目中其他当地对 viewWillAppear 交流倒致的递归调用问题,仍然会存在调用次序问题,处理办法就是在 Build Phases 中调整类文件的次序

其它

咱们能够经过 Runtime 特性来获得类的所有特点称号和类型,然后再经过 KVC 将 JSON 中的值填充给该类的目标。还能够在程序运行时为类增加办法或替换办法从而使目标能够更灵敏的根据需求来挑选完成办法。总归 Runtime 库就象一堆积木,只需发挥想象力便能完成各式各样的功用,但前提是你需求了解它。