文章

Runtime相关

Runtime 是什么?

Runtime 又叫运行时,是一套由 C、C++ 会和编写的一套 API,为OC加入了面向对象与运行时机制。

Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

KVO 实现原理

简单理解

  1. KVO是基于runtime机制实现的
  2. 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。派生类在被重写的 setter 方法内实现真正的通知机制
  3. 如果原类为Person,那么生成的派生类名为 NSKVONotifying_Person
  4. 每个类对象中都有一个 isa 指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的 setter 方法
  5. 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

深入理解

  1. Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
  2. NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
  3. 所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
  4. (isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制.
  5. 子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

消息发送原理

如下这个方法:

1
[lisi read:huozhe];

会被编译成

1
objc_sgSend(lisi, @selector(read:), huozhe);

objc_msgSend的具体流程如下:

  1. 通过isa指针找到所属类
  2. 查找类的cache列表, 如果没有则下一步
  3. 查找类的”方法列表”
  4. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
  5. 找不到, 就沿着继承体系继续向上查找
  6. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
  7. 找不到, 执行”消息转发”.

消息转发机制原理

消息转发机制基本分为三个步骤:

  1. 动态方法解析
  2. 备用接受者
  3. 完整转发

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveInstanceMethod:(实例方法)或者 +resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法”“。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过 class_addMethod 函数动态添加到类里面就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface HiClass: NSObject
- (void)hello;
@end
@implementation HiClass
void functionForMethod(id self, SEL _cmd) {
    NSLog(@"hello!");
}
// selector : 那个未知的选择子
// 返回YES则结束消息转发
// 返回NO则进入备用接受者
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod");
    NSString *selString = NSStringFromSelector(sel);
    if ([selString isEqualToString:@"hello"]) {
        class_addMethod(self, @selector(hello), (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

备用接受者

动态方法解析无法处理消息,则会走备用接受者。这个备用接受者只能是一个新的对象,不能是self本身,否则就会出现无限循环。如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@interface RuntimeMethodHelper: NSObject
@end
@implementation RuntimeMethodHelper
- (void)hello {
    NSLog(@"%@, %p -> hello", self, _cmd);
}
@end

@interface HiClass: NSObject
{
    RuntimeMethodHelper *_helper;
}
- (void)hello;
@end
@implementation HiClass
// selector : 那个未知的消息
// 返回一个能响应该未知选择子的备用对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    NSString *selectorString = NSStringFromSelector(aSelector);
    // 将消息交给_helper来处理
    if ([selectorString isEqualToString:@"hello"]) {
        return _helper;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

完整消息转发

如果动态方法解析和备用接受者都没有处理这个消息,那么就会走完整消息转发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface RuntimeMethodHelper: NSObject
@end
@implementation RuntimeMethodHelper
- (void)hello {
    NSLog(@"%@, %p -> hello", self, _cmd);
}
@end

@interface HiClass: NSObject
{
    RuntimeMethodHelper *_helper;
}
- (void)hello;
@end
@implementation HiClass
// invocation : 封装了与那条尚未处理的消息相关的所有细节的对象
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
    if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:_helper];
    }
}

// 必须重新这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象
// NSMethodSignature : 该selector对应的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"methodSignatureForSelector");
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([RuntimeMethodHelper instancesRespondToSelector:aSelector]) {
            signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}
@end

weak实现原理

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

  1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
  2. 添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
  3. 释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

附加:

  1. 实现weak后,为什么对象释放后会自动为nil? runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为 0 的时候会 dealloc,假如 weak 指向的对象内存地址是 a ,那么就会以 a 为键, 在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil 。
  2. 当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?
    1. 调用objc_release
    2. 因为对象的引用计数为0,所以执行dealloc
    3. 在dealloc中,调用了_objc_rootDealloc函数
    4. 在_objc_rootDealloc中,调用了object_dispose函数
    5. 调用objc_destructInstance
    6. 最后调用objc_clear_deallocating,详细过程如下: a. 从weak表中获取废弃对象的地址为键值的记录 b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为 nil c. 将weak表中该记录删除 d. 从引用计数表中删除废弃对象的地址为键值的记录
本文由作者按照 CC BY 4.0 进行授权