Objective-C:Runtime

Page content

苹果绝学,斗转星移--Runtime

前言

Runtime,即运行时系统,是Objective-C语言的核心,它决定了OC的动态特性。 我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,如上文所说的[obj doSomething];会被转化成objc_msgSend(obj, @selector(doSomething)); 虽然现在Swift推行如火如荼,但短时间内难以完全替代OC,尤其是大厂的热更新,更离不开Runtime的支撑,不熟悉Runtime,是很难领略OC的精髓的。

原理

  1. RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制;
  2. 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性;
  3. OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错);
  4. 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。

对于官方针对Runtime不同版本的解释:

  1. In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
  2. In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.In addition, the modern runtime supports instance variable synthesis for declared properties (see Declared Properties in The Objective-C Programming Language).

在Objective-C编程全解一书中,荻原刚志对Runtime相关章节的命名中,有一章的命名为“全新的运行时系统”,现在的Runtime更为合理与完善,使用灵活且功能强大。

应用

纵观各个种类的SDK与应用场景,Runtime的身影随处可见,从AFN到SDWebImage,从MjExtension到MjRefresh,再到针对Button的连续点击做出限定的封装,以及部分崩溃日志收集SDK的灵活实现与处理,都离不开Runtime。主要应用有如下体现:

  1. 动态的添加对象的成员变量和方法;
  2. 动态交换两个方法的实现;
  3. 实现分类添加属性;
  4. 实现NSCoding的自动归档和解档;
  5. 实现字典转模型的自动转换;
  6. 还可以优雅的解决Button频繁点击的限定问题等等。

获取变量信息

~~~objective-c
unsigned int count;
// 获取成员变量列表(包括私有)
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) 
{
    Ivar var = ivarList[i];
    const char *ivarName = ivar_getName(var);
    NSLog(@"IvarName---->%@", [NSString stringWithUTF8String:ivarName]);
    
    // 动态变量控制
    if ([propertyName isEqualToString:@"_name"]) 
    {
        object_setIvar(self.aclass, var, @"shen");
        break;
    }
}
// 获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) 
{
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"propertyName---->%@", [NSString stringWithUTF8String:propertyName]);
    
    // 属性信息
    const char * property_attributes = property_getAttributes(property);
    NSLog(@"%@", [NSString stringWithUTF8String:property_attributes]);
    // 从属性信息中提取属性类型
    char * property_type = property_copyAttributeValue(property, "T");
    NSLog(@"%@", [NSString stringWithUTF8String:property_type]);
}
// 获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) 
{
    Method method = methodList[i];
    NSLog(@"methodName---->%@", NSStringFromSelector(method_getName(method)));
}
// 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) 
{
    Protocol *myProtocal = protocolList[i];
    const char *protocolName = protocol_getName(myProtocal);
    NSLog(@"protocolName---->%@", [NSString stringWithUTF8String:protocolName]);
}
~~~

动态关联属性

~~~objective-c
// 全局变量,其地址作为关联的key
static char associatedObjectKey;
/*
* 设置关联对象及属性
* id object        要关联的对象
* const void *key  关联属性所需的唯一Key
* id value         关联的值
* objc_AssociationPolicy 关联策略
*/
objc_setAssociatedObject(target, &associatedObjectKey, @"添加的字符串属性", OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
/*
* 获取关联对象的属性
* id object        关联的对象
* const void *key  关联所需的唯一Key
*/
NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
NSLog(@"AssociatedObject = %@", string);
~~~
~~~objective-c
// 关联策略
enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403 
};
~~~

这种关联的应用最常见的就是给Catergory添加属性:

(编译Category的.h文件声明的@Property通过,但如果没有Runtime处理,进行赋值取值,运行后则报错。)

~~~objective-c
@interface Person (Name)
@property (copy , nonatomic) NSString * name;
@end
#import <objc/runtime.h>
@implementation Person (Name)
static char key_name;
- (void)setName:(NSString *)name  {
    objc_setAssociatedObject(self, &key_name, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name  {
    return objc_getAssociatedObject(self, &key_name);
}
@end
~~~

动态添加方法

~~~objective-c
/*
* 动态添加方法
* Class cls     需要动态添加方法的类
* SEL name      动态添加的方法的方法名
* IMP imp       方法的实现,C方法的方法实现可以直接获得;
*                         OC方法可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现
* "v@:"         "v@:" 含义: v代表无返回值void,如果是i则代表int;
*                           @代表 id sel;
*                           :代表 SEL _cmd;      
*               “v@:@@” :两个参数的没有返回值。
*/
- (IBAction)runtime_methods_add:(id)sender {
    class_addMethod([self.aclass class], @selector(canDo), (IMP)candoSomething, "v@:@");
}
/*
* 动态添加的C方法
*/
void candoSomething(id self , SEL _cmd, NSString * str) {
    NSLog(@"i can do %@", str);
}
/*
* 隐式调用添加的方法
*/
- (void)reactDoAction {
    if ([self.aclass respondsToSelector:@selector(canDo)]) {
        [self.aclass performSelector:@selector(canDo) withObject:@"somethingStr"];
    } else {
        NSLog(@"sorry,i don't have this method");
    }
}
~~~

动态添加方法,也可以对于之前消息发送的文章所述消息发送过程中的拦截调用进行处理:

~~~objective-c
// 在外部隐式调用不存在的方法,进入拦截
- (void)handle  {
    [target performSelector:@selector(someAction:) withObject:@"test"];
}
// 在target对象内部重写拦截调用的方法,动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 给本类动态添加一个方法
    if ([NSStringFromSelector(sel) isEqualToString:@"someAction:"])  {
        class_addMethod(self, sel, (IMP)runAddMethod, "v@:@");
    }
    return YES;
}
void runAddMethod(id self, SEL _cmd, NSString *str) 
{
    // 该方法即@selector(someAction:)的实现,成功搞定原本的闪退问题。
    // 后续逻辑处理(或者是本该调用的方法的实现)
}
~~~

动态方法交换 Method Swizzling

方法交换,本质是两个方法的实现互换,以下为简单的实现代码:

~~~objective-c
// 测试调用
- (void)test  {
    [self runtime_methods_exchange:nil];
    [self firstAction];
    [self secondAction];
}
// 动态交换方法
- (IBAction)runtime_methods_Exchange:(id)sender {
    // 获取两个方法的实现
    Method method1 = class_getInstanceMethod([self class], @selector(firstAction));
    Method method2 = class_getInstanceMethod([self class], @selector(secondAction));
    // 交换两个方法的实现
    method_exchangeImplementations(method1, method2);
}
- (void)firstAction  {
    NSLog(@"this is first.");
}
- (void)secondAction  {
    NSLog(@"this is second.");
}
~~~

黑魔法 - Method Swizzling

对于Method Swizzling使用规范,以及另一种使用方法,附上Mattt前辈在NSHipster上的代码:

~~~objective-c
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)
// load方法会在类第一次加载的时候被调用
// 调用的时间比较靠前,适合在这个方法里做方法交换
+ (void)load {
    //方法交换应该被保证,在程序中只会执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 获得viewController的生命周期方法的selector
        SEL systemSel = @selector(viewWillAppear:);
        // 自己实现的将要被交换的方法的selector
        SEL swizzSel = @selector(swiz_viewWillAppear:);
        // 两个方法的Method
        Method systemMethod = class_getInstanceMethod([self class], systemSel);
        Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
        // 首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
        BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
        if (isAdd) {
            // 如果成功,说明类中不存在这个方法的实现
            // 将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
        } else {
            //否则,交换两个方法的实现
            method_exchangeImplementations(systemMethod, swizzMethod);
        }
      });
}
- (void)swiz_viewWillAppear:(BOOL)animated {
    //这时候调用自己,看起来像是死循环
    //但是其实自己的实现已经被替换了
    [self swiz_viewWillAppear:animated];
    NSLog(@"swizzle");
}
@end
  
// 在一个自己定义的viewController中重写viewWillAppear:
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear");
}
~~~

方法交换是实现AOP面向切面编程思想的最佳技术。既然是切面,交换完再调回交换。也有其他的用法,尽量保证只交换一次的情况,这就需要我们酌情处理。

method swizzling需要注意的一些问题:

Swizzling应该总是在+load中执行 在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行 与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

选择器、方法与实现 在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。 以下是Objective-C Runtime Reference中的对这几个术语一些描述:

Selector(typedef struct objc_selector * SEL): 用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。

Method(typedef struct objc_method * Method): 在类定义中表示方法的类型

Implementation(typedef id ( *IMP)(id, SEL, …)): 这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。每一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。 理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。 为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

Method Swizzling 通常被称作“黑魔法”,操作不当,很容易产生不可预知的行为和无法预见的后果,我们对它的使用应该谨慎并且注意规范。

Runtime与KVO

KVO我们都使用过,这是经典的观察者模式,举个简单的使用例子:

Person.h

~~~objective-c
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, assign) double money;
@end
~~~

Person.m

~~~objective-c
#import "Person.h"
@implementation Person
- (instancetype)init {
    if (self = [super init]) {
        _money = 1000;
    }
    return self;
}
@end
~~~

Bank.h

~~~objective-c
#import <Foundation/Foundation.h>
@interface Bank : NSObject
@end
~~~

Bank.m

~~~objective-c
#import "Bank.h"
@implementation Bank
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"keyPath : %@", keyPath);
    NSLog(@"object : %@", object);
    NSLog(@"change : %@", change);
    NSLog(@"context : %@", context);
}
@end
~~~

开启KVO观察

~~~objective-c
@interface ViewController ()
@property (nonatomic, strong) Person * person;
@property (nonatomic, strong) Bank * bank;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    self.bank = [[Bank alloc] init];
    [self.person addObserver:self.bank forKeyPath:@"money" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"PERSON_MONEY_OBSERVER"];
}
- (void)dealloc { 
    [self.person removeObserver:self.bank forKeyPath:@"money"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    double payMoney = 124.f;
    self.person.money -= payMoney;
}
~~~

剖析

给Person对象添加观察者Bank对象,当Person对象的money属性改变后,通知Bank对象。我们在touchesBegan中添加断点,lldb中执行命令:po object_getClassName(self.person),可以看到结果:

~~~objective-c
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"
2017-03-17 14:15:14.876 RuntimeTest[1141:527611] XPC connection interrupted
2017-03-17 14:15:14.875 RuntimeTest[1141:527571] keyPath : money
2017-03-17 14:15:14.877 RuntimeTest[1141:527571] object : <Person: 0x1561e2f0>
2017-03-17 14:15:14.879 RuntimeTest[1141:527571] change : {
    kind = 1;
    new = 876;
    old = 1000;
}
2017-03-17 14:15:14.879 RuntimeTest[1141:527571] context : PERSON_MONEY_OBSERVER
~~~

很明显,self.person的实际类型是NSKVONotifying_Person,这一变化是在运行时进行的(将person对象的类型替换)。

NSKVONotifying_XXX是一个派生类,Runtime会创建一个NSKVONotifying_Person类,NSKVONotifying_Person继承于Person类,并重写了被监听属性的setter方法。

在setter方法中调用[self willChangeValueForKey:@""];[self didChangeValueForKey:@""];前者被调用后,向观察者发送旧的值Old;后者调用后,向观察者发送新的值New。

附Apple官方文档中的说明:

~~~objective-c
- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}
~~~