前言:

Runtime 是 Objective-C 区别于 C 语言这样的静态语言的一个非常重要的特性。

C 语言是一门静态语言,也就是说,在编译时,编译器就已经完全决定了函数的调用地址(也就是哪个函数会被执行)。编译器通过代码中的函数名,直接将它与内存中的函数地址关联起来。函数调用是非常确定的,也就是所谓的“静态绑定”。

相比 C 语言,Objective-C 是动态语言,这意味着方法的调用不是在编译时决定的,而是在运行时决定的。在 Objective-C 中,函数调用不是像 C 语言那样直接绑定到具体函数地址,而是通过一种叫做消息传递的机制来完成。编译时并不知道具体调用的是哪个方法,直到程序运行时,才通过 Runtime 系统查找要调用的方法。

小结:

  • 静态语言(如 C 语言):函数调用在编译时已经确定好了,编译器知道要调用哪个函数,地址是固定的,因此运行时不需要查找。
  • 动态语言(如 Objective-C):方法调用是通过消息传递完成的,直到程序运行时,才通过 Runtime 系统查找要调用的方法。这种动态查找机制使得 Objective-C 更加灵活,允许程序在运行时动态地改变行为(例如替换方法、消息转发等)。

Runtime的优点:

  • 灵活性:你可以在运行时动态地修改类或对象的行为,比如替换方法(Method Swizzling)、动态添加属性和方法等。
  • 动态特性支持:像 KVO(Key-Value Observing)、消息转发、反射等功能都依赖于 Runtime 系统。

Runtime在Swift中的作用:

Swift 本身是静态类型的编程语言,大多数函数和方法的调用都是在编译期确定的。但是,Swift 与 Objective-C 有很强的互操作性,当涉及到 @objc 修饰符或者与 NSObject 交互时,就会依赖于 Objective-C 的 Runtime 系统。

如下代码就用到了Runtime:

import UIKit

let button = UIButton()
//@selector 是 Runtime 的一部分 && addTarget(_:action:for:) 使用了消息传递机制
button.addTarget(self, action: #selector(add), for: .touchUpInside)

func add(){
    print("ss")
}

1.动态派发:

在 Swift 中,大部分方法调用是静态派发的,即编译时已经确定了调用的具体方法。但如果某些类或方法使用了 @objc 修饰符,就会通过 动态派发 实现。动态派发依赖于 Objective-C Runtime,在运行时查找并调用方法。这让程序在运行过程中能够根据对象的类型动态决定要调用的具体实现。

2.反射:

通过 Swift 的 Mirror,你可以在运行时获取对象的类型信息、属性和值,类似于动态语言中的反射功能。反射允许在运行时检查和操作对象,而不是在编译时固定所有行为。

demo

struct Person {
    var name: String
    var age: Int
}

let p = Person(name: "Alice", age: 30)
let mirror = Mirror(reflecting: p)

for child in mirror.children {
    print("\(child.label!): \(child.value)")
}

3.KVO

KVO 是通过 Runtime 实现的,允许对象在某些属性发生变化时通知其他对象。Swift 中使用 @objc dynamic 来启用 KVO。详见KVO(键值观察)-CSDN博客

4.Method Swizzling (方法交换)

Method Swizzling 是 Runtime 提供的一个功能,允许你在运行时交换方法实现。这在某些框架中用于拦截系统方法或增加新功能。其常用于扩展系统类行为,比如在 UIViewController 中自动打印 viewDidLoad 的日志。

5.消息传递 (Message Sending)

在 Objective-C 中,方法调用是通过向对象发送消息完成的,objc_msgSend 函数会在运行时查找方法的实现。在 Swift 中,带有 @objc 修饰符的方法仍然会通过这种机制进行调用。

消息机制的基本原理:

Objective-C 语言 中,对象方法调用都是类似 [receiver selector]; 的形式,其本质就是让对象在运行时发送消息的过程。

我们来看看方法调用 [receiver selector]; 在『编译阶段』和『运行阶段』分别做了什么?

1.编译阶段:[receiver selector]; 方法被编译器转换为:

  1. objc_msgSend(receiver,selector) (不带参数)
  2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)

• receiver 是消息的接收者,也就是哪个对象接收消息。

• selector 是方法的标识符,即你要调用的具体方法。

tips:编译时并不直接调用具体的函数,它只生成一个“消息发送”的代码,这意味着方法调用并没有在编译阶段被确定下来。

2.运行时阶段:消息接受者 recever 寻找对应的 selector(方法的标识符,本质上是一个字符串

  • 通过 recevierisa 指针 找到 recevierClass(类)

每个 对象 都有一个 isa 指针,指向它的 类(Class)。isa 指针可以理解为对象的”身份证”,它告诉 Runtime 该对象属于哪个类。

  • Class(类)cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现)

方法缓存 是为了提高方法查找的效率。当你第一次调用某个方法时,系统会把方法实现缓存起来。

• 于是,Runtime 先在类的缓存里查找对应的 IMP,即方法实现的地址

• 如果找到了,就直接执行这个方法,这样可以避免重复查找,提高性能。

  • 如果在 cache(方法缓存) 中没有找到对应的 IMP(方法实现) 的话,就继续在 Class(类)method list(方法列表) 中找对应的 selector,如果找到,填充到 cache(方法缓存) 中,并返回 selector
  • 如果在 Class(类) 中没有找到这个 selector,就继续在它的 superClass(父类)中寻找;
  • 一旦找到对应的 selector,直接执行 recever 对应 selector 方法实现的 IMP(方法实现)
  • 若找不到对应的 selector,消息被转发或者临时向 recever 添加这个 selector 对应的实现方法,否则就会发生崩溃。

例子:

@interface Person : NSObject
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Hello, World!");
}
@end

Person *person = [[Person alloc] init];
[person sayHello];

在这段代码中,当调用 [person sayHello] 时,以下事情发生:

1. 编译阶段

• [person sayHello] 被编译为 objc_msgSend(person, @selector(sayHello))。

2. 运行阶段

• 通过 person 的 isa 指针找到 Person 类。

• Runtime 在 Person 类的缓存中查找 sayHello 方法的 IMP。

• 如果缓存中没有找到,Runtime 在 Person 类的方法列表中查找 sayHello 的实现。

• 找到后,Runtime 将 IMP 缓存起来并执行该方法。

• 方法 sayHello 被执行,输出 Hello, World!。

参考:

https://juejin.cn/post/6844903878794706957

Objective-C Runtime · 笔试面试知识整理

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部