iOS--Runtime

概念

Runtime 是一套底层全部用C语言代码构成的API,封装了很多动态性相关的函数。Objective-C 是一门动态语言,允许很多操作推迟到程序运行时再进行。OC的动态特性就是由 Runtime 来支撑和实现的,平时编写的OC代码,底层都是转换成了Runtime API 进行调用。

区别于C语言这样的静态语言

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

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

Runtime无非就是去解决如何在运行时期找到调用方法这样的问题

对于实例变量寻找有如下的思路:

instance -> class -> method -> SEL -> IMP -> 实现函数

实例对象中存放 isa 指针以及实例变量,isa 指针可以找到该实例对象所属的类对象,类中存放着实例方法列表,在这个方法中 SEL 作为 key,IMP 作为 value,在编译时期,根据方法名字会生成一个唯一的 Int 标识,这个标识就是 SEL ,IMP 其实就是函数指针指向了最终的函数实现。

整个 Runtime 的核心就是 objc_msgSend 函数,通过给类发送 SEL 以传递消息,找到匹配的 IMP 再获取最终的实现。

相关的数据结构

isa指针

isa 指针用来维护对象和类之间的关系,并确保对象和类能够通过 isa 指针找到对应的方法、实例变量、属性、协议等;

在arm64架构之前,isa 就是一个普通的指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址;

从 arm64 架构开始,对 isa 进行了优化,变成了一个共用体union结构,不仅仅承担指针的作用,还使用位域来存储更多的信息,例如引用计数

类 Class

当一个对象的实例方法被调用时,会通过isa指针找到这个类,然后在该类中方法列表中查找。

复制代码
//Class定义为指向 objc_class 类型的指针
typedef struct objc_class *Class;

//runtime对 objc_class 结构体的定义
struct objc_class {
    Class _Nonnull isa;                                          // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父类的指针
    const char * _Nonnull name;                                  // 类的名字
    long version;                                                // 类的版本信息,默认为 0
    long info;                                                   // 类的信息,供运行期使用的一些位标识
    long instance_size;                                          // 该类的实例变量大小;
    struct objc_ivar_list * _Nullable ivars;                     // 该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
    struct objc_cache * _Nonnull cache;                          // 方法缓存
    struct objc_protocol_list * _Nullable protocols;             // 遵守的协议列表
#endif
};

对象 Object

平时使用的所有类型都是 id 类型,id 类型对象对应到 Runtime中,就是objc_object结构体。

实例对象 Object 是我们对类对象 alloc 或者 new 操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但不拷贝类定义的方法

调用实例方法时,系统会根据实例的 isa 指针去类的方法列表及父类的方法列表中寻找与消息对应的 selector 指向的方法。

复制代码
typedef struct objc_object *id;

// 对象结构体
struct objc_object {
    Class _Nonnull isa;     OBJC_ISA_AVAILABILITY;
};

由此,我们得出了一个结论,类对象和实例对象的查找机制是一样的:

  • 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
  • 类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

类class与元类meta-class

元类就是类对象的 isa 指向的类,也可以说是类对象的类

在面向对象中,一切都是对象。实例是对象,类本身也是对象。既然类是对象,它就需要一个"类"来描述自己------那就是元类

  • 对象的模版,定义实例该有什么属性和方法
  • 元类的模版,定义类本身该有什么方法,例如类方法


classmeta-class底层结构都是objc_class结构体,objc_class继承自objc_object,所以它也有isa指针,所以它也是对象;

  • class中存储着实例方法、成员变量、属性、协议等信息
  • meta-class中存储着类方法等信息

isa指针和superclass指针的指向(如上图);

由此可见:

  • 实例对象isa指向类对象类对象isa指向元类对象;
  • 元类对象isa指向根元类对象根元类对象isa指向自己
  • 类对象元类对象superclass指向各自的父类,特别注意的一点是根元类对象superclass指向根类对象

基类的meta-classsuperclass指向基类的class,决定了一个性质:

当我们调用一个类方法,会通过classisa指针找到meta-class,在meta-class中查找有无该类方法,如果没有,再通过meta-classsuperclass指针逐级查找父meta-class,一直找到基类的meta-class如果还没找到该类方法的话,就会去找基类的class中同名的实例方法的实现。

方法 Method

Method就是我们平时所说的函数,它表示的是能够独立完成一个功能的一段代码,Method通过 SEL 和 IMP 两个属性,实现了快速查询方法及实现。

复制代码
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name; 			//方法名
    char *method_types;			//方法类型
    IMP method_imp;				//方法实现
};

SEL

SEL是方法选择器, 可以将其理解为方法的ID,是方法名字符串的唯一标识

复制代码
typedef struct objc_selector *SEL;

struct objc_selector {
    char *name; 
    char *types;
};

获取SEL方法:

复制代码
1.OC中,使用@selector("方法名字符串")
2.OC中,使用NSSelectorFromString("方法名字符串")
3.Runtime方法,使用sel_registerName("方法名字符串")
  • 一个类的方法列表中不能存在两个相同的SEL
    • selector是不包含参数类型的,方法名相同,则SEL相同,编译器会报错
  • 不同类中可以有存在两个相同的SEL
    • 它们的SEL相同,但是IMP不同
    • Runtime 动态派发可以实现"同名方法、不同对象 → 调用不同实现"。

这也是Objective-C 不支持函数重载的原因

IMP

IMP 可以理解为函数指针,指向了最终的实现

复制代码
// 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

消息发送

Objectove-C中所有方法的调用 / 类的生成都在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现。

当一个对象的sender调用代码[receiver message],实际上就是 Runtime 在进行消息发送,这个方法会被编译器转化成:

复制代码
// 第一个参数类型是发送者, 第二个参数类型是SEL
id objc_msgSend ( id _Nullable self, SEL op, ... );

在运行阶段 ,调用objc_msgSend函数,消息接受者receiver 去寻找对应的selector

objc_msgSend函数的调用流程:

  1. 检查这个selector是否应该被忽略
  2. 检查发送的 target 是否为nil,如果是则忽略该消息
    • 如果没有处理 nil 的函数,就自动清理并返回。
    • 这一点就是为何在 Objective-C 中给 nil 发送消息不会崩溃的原因
  3. 通过 recevierisa 指针找到 recevier 的类 Class
  4. 在类 Class 的方法缓存 cache 散列表中寻找对应的IMP(快查)
    • 在 cache 中没有找到对应的 IMP ,就继续在类 Class 的方法列表(慢查)methodLists中寻找对应的 SEL ,如果找到,填充到 cache 中,返回 SEL
    • 如果在类 Class 中没有找到这个 SEl,那就在它的superClass父类中寻找
    • 一旦找到对应的 SEL,直接执行 SEL 方法实现的 IMP
  5. 如果还没找到则进入消息转发过程,消息被转发或者临时向 recevier 添加这个 selector 对应的实现方法,否则就会发生崩溃

消息转发

首先会通过resolveInstanceMethod 得知方法是否为动态添加。

  • YES 则通过 class_addMethod动态添加方法,处理消息;NO则进行下一步

  • dynamic属性就与这个过程有关,当一个属性声明为dynamic时,就是告诉编译器,开发者一定会在运行时动态添加setter/getter的实现,编译时不用自动生成。

    //OC方法:
    //类方法未找到时调起,可于此添加类方法实现

    • (BOOL)resolveClassMethod:(SEL)sel

    //实例方法未找到时调起,可于此添加实例方法实现

    • (BOOL)resolveInstanceMethod:(SEL)sel

    //Runtime方法:
    /**
    运行时方法:向指定类中添加特定方法实现的操作
    @param cls 被添加方法的类
    @param name selector方法名
    @param imp 指向实现方法的函数指针
    @param types imp函数实现的返回值与参数类型
    @return 添加方法是否成功
    */
    BOOL class_addMethod(Class _Nullable cls,
    SEL _Nonnull name,
    IMP _Nonnull imp,
    const char * _Nullable types)

消息接受者重定向

如果上一步中 +resolveInstanceMethod:或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向

如果当前对象实现了 -forwardingTargetForSelector:,Runtime 就会调用这个方法,允许将消息的接受者转发给其他对象,其主要方法如下:

复制代码
//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector

//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector

通过forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向

消息重定向

如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 方法获取函数的参数和返回值类型。

其过程:

  • 如果-methodSignatureForSelector: 返回了一个NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个NSInvocation对象

    • 并通过-forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
  • 如果-methodSignatureForSelector: 返回了一个 nil ,则 Runtime 系统会发出-doesNotRecognizeSelector: 消息,程序也就崩溃了。

    // 消息重定向

    • (void)forwardInvocation:(NSInvocation *)anInvocation;

    // 获取函数的参数和返回值类型,返回签名

    • (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;

获取方法签名是为了能让 runtime 正确构造NSInvocation。只有构造出正确 invocation,才能安全、正确地把消息转发给其他对象。

-forwardingTargetForSelector-forwardInvocation: 两个方法都可以转发消息,区别就在于:

  • -forwardingTargetForSelector: 只能将消息转发给一个对象
  • -forwardInvocation: 可以将消息转发给多个对象

消息转发示意图如下所示

Runtime的应用

  • 动态方法交换 Method Swizzling
    • 原理是:通过 Runtime 获取到方法实现的地址,进而动态交换两个方法的功能
  • 类目扩展类能力
    • Category 本质就是 runtime 在加载时向类的方法列表追加方法
  • KVO / KVC
  • 反射
    • 获取类详细属性:属性列表、所有成员变量、所有方法、遵循的协议

Swift中的Runtime

Swift默认是静态、安全的语言,想使用OC的动态特性,可以用以下三个关键词:

  • @objc
    • 让Swift的类、方法、属性暴露给OC Runtime
    • 方法生成selector
    • 类的方法会进入OC得methodLists
    • 允许参与OC Runtime消息转发
  • dynamic
    • 强制某方法使用动态派发,走objc_msgSend
  • @objc dynamic
    • 开启完整的OC Runtimer能力
    • 可以进行方法交换、被动态添加方法、参与消息转发机制、被KVO动态子类化等等,几乎等同于OC方法

Swift标准库中的反射机制Mirror

Mirror 是 Swift 提供的一套轻量级反射系统,用于在运行时查看对象的类型结构,包括:

  • 对象的类型
  • 属性名和属性值
  • 子级元素
  • 父类信息
  • 自身的 displayStyle(结构体 / 类 / 元组 / 枚举等)

Mirror 只支持读取 (只读反射),不支持动态修改类或方法;它适用于调试、打印、序列化等场景;Mirror 的设计目标是安全、轻量,而不是动态强大。

复制代码
struct Person {
    var name: String
    var age: Int
}

let p = Person(name: "Tom", age: 20)
let mirror = Mirror(reflecting: p)

for child in mirror.children {
    print(child.label!, child.value)
}

//输出
name Tom
age 20

https://juejin.cn/post/7188344966580535333

https://juejin.cn/post/7063376892178464799

https://hit-alibaba.github.io/interview/iOS/ObjC-Basic/Runtime.html

https://blog.csdn.net/yueliangmua/article/details/134939537?spm=1001.2014.3001.5502

https://blog.csdn.net/ScheenDuan/article/details/142762110?spm=1001.2014.3001.5502

相关推荐
Antonio9153 小时前
【Swift】UIKit:UISegmentedControl、UISlider、UIStepper、UITableView和UICollectionView
开发语言·ios·swift
在下历飞雨3 小时前
Kuikly基础之音频播放与资源管理:青蛙叫声实现
android·ios·harmonyos
T***16073 小时前
JavaGraphQLAPI
爬虫·objective-c·rizomuv
1***81534 小时前
Swift在服务端开发的可能性探索
开发语言·ios·swift
S***H2834 小时前
Swift在系统级应用中的开发
开发语言·ios·swift
kk哥88995 小时前
iOS进阶1-combine
ios
吴Wu涛涛涛涛涛Tao6 小时前
用 Flutter + BLoC 写一个顺手的涂鸦画板(支持撤销 / 重做 / 橡皮擦 / 保存相册)
android·flutter·ios
MaoJiu7 小时前
Flutter iOS 项目 UIScene 迁移指南
flutter·ios
Antonio9158 小时前
【Swift】 UIKit:UIGestureRecognizer和UIView Animation
开发语言·ios·swift