Runtime

作者: Abner_XuanYuan | 来源:发表于2023-02-28 22:30 被阅读0次

感谢大佬分享:
https://www.jianshu.com/p/6ebda3cd8052
http://www.jianshu.com/p/adf0d566c887
http://www.jianshu.com/p/ab966e8a82e2

1 概念

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C编译语言写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Objective-C 是一个动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。了解 Runtime ,要先了解它的核心 - 消息传递
在 Runtime 中,对象可以用 C 语言中的结构体表示,而方法可以用 C 函数来实现,另外再加上了一些额外的特性。这些结构体和函数被 Runtime 函数封装后,让 OC 的面向对象编程变为可能。
Runtime 其实有两个版本: modern 和 legacy。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的 32 位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
Runtime 基本是用 C汇编写的,可见苹果为了动态系统的高效而作出的努力。苹果和 GNU 各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是 OC 并不能直接编译为汇编语言,而是要先转写为纯 C 语言再进行编译和汇编的操作,从 OC 到 C 语言的过渡就是由 runtime 来实现的。然而我们使用 OC 进行面向对象开发,而 C 语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

2 方法与消息

1、SEL

SEL 又叫选择器,是表示一个方法的 selector 的指针。是根据方法名 hash 化了的 KEY 值,能唯一代表一个方法,加快了方法的查询速度。其定义如下:

typedef struct objc_selector *SEL;

1、方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int 类型的地址),这个标识就是 SEL。
2、Objective-C 同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。也就是不支持方法重载。
3、不同的类,只要方法名相同,那么方法的 SEL 就是一样的,每一个方法都对应着一个 SEL。不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP。
4、所有的 SEL 组成一个 Set 集合,SEL 实际上就是根据方法名 hash 化了的一个字符串,查找方法时可以根据方法对应的字符串比较他们的地址就可以了。

获取 SEL 方法
1、sel_registerName 函数;
2、Objective-C 编译器提供的 @selector();
3、NSSelectorFromString() 方法;

2、IMP

IMP 实际上是一个函数指针,指向方法实现的地址。其定义如下:

id (*IMP)(id, SEL,...)


第一个参数:是指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针);
第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。

由于每个方法对应唯一的 SEL,因此我们可以通过 SEL 方便快速准确地获得它所对应的 IMP。取得 IMP 后,就获得了执行这个方法代码的入口点,就可以像调用普通的 C 语言函数一样来使用这个函数指针了。

3、Method

Method 用于表示类定义中的方法,则定义如下:

typedef struct objc_method *Method
struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
}

该结构体中包含一个 SEL 和 IMP,实际上相当于在 SEL 和 IMP 之间作了一个映射。有了 SEL,便可以找到对应的 IMP,从而调用方法的实现代码。

4、方法调用流程

在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver selector] 转化为一个消息函数的调用即 objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示

objc_msgSend(receiver, selector)

如果消息中还有其它参数,则该方法的形式如下所示:

objc_msgSend(receiver, selector, arg1, arg2,...)

这个函数完成了动态绑定的所有事情:
1、首先它找到 selector 对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
2、调用方法实现,并将接收者对象及方法的所有参数传给它。
3、最后,它将实现返回的值作为它自己的返回值。

3 Runtime 消息传递

1、消息传递流程

以 [obj foo] 方法为例
1、编译器转化为消息发送 objc_msgSend(obj, foo)。
2、系统找到接收消息的对象,然后通过该对象的 isa 找到它的类。
3、在它的类中查找 method_list ,是否有 foo 方法,若没有则查找父类的 method_list。
4、如果找到对应的 foo,执行它的 IMP,转发 IMP 的 return 值。
5、如果找不到方法,则会沿着继承树向上一直查找直到继承树根部(通常为 NSObject)。
6、如果还找不到则进行消息转发。
7、如果消息转发也失败了,则会调用 doesNotRecognizeSelector:方法报 unrecognized selector 错误。
注:
1、objc_msgSend 这个是最基本的用于发送消息的函数。其实编译器会根据情况在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有 stret 的函数。
2、objc_msgSend 函数第二个参数类型为 SEL,它是 selector 在 Objc 中的表示类型(Swift 中是 Selector 类)。selector 是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是 SEL :,即 typedef struct objc_selector *SEL;其实它就是个映射到方法的 C 字符串,你可以用 Objc 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器.

2、类与对象

Objc_Object 实例对象

struct objc_object{
     Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

只有一个指向其类的 isa 指针。当我们向一个 Objective-C 对象发送消息时,运行时库会根据实例对象的 isa 指针找到这个实例对象所属的类。Runtime 库会在类的方法列表及父类的方法列表中去寻找与消息对应的 selector 指向的方法,找到后即运行这个方法。

Objc_Class 类对象
Objective-C 类是由 Class 类型来表示的,它实际上是一个指向 objc_class 结构体的指针。

typedef struct object_class *Class

结构如下:
struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父类
     const char *name                         OBJC2_UNAVAILABLE;  // 类名
     long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
     long info                                OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
     long instance_size                       OBJC2_UNAVAILABLE;  // 该类的实例变量大小
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表
     struct objc_method_list *methodLists     OBJC2_UNAVAILABLE;  // 方法定义的链表
     struct objc_cache *cache                 OBJC2_UNAVAILABLE;  // 方法缓存
     struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE;  // 协议链表
#endif
}OBJC2_UNAVAILABLE;
3、元类

Meta-Class 是一个类对象的类。


对象、类与元类关系图

类对象:存储实例方法列表等信息。
元类对象:存储类方法列表等信息。

4、isa 指针

是一个共用体,可以分为两种:指针型的 isa 和非指针型的 isa 。
指针型的 isa:isa 的值代表 Class 地址。
非指针型的 isa:isa 值的一部分代表 Class 地址。isa 指针的一部分就可以找到 Class 的地址,多出来的部分用于存储其他内容,用于节省内存。

4 消息转发

消息转发分为3个过程:动态方法解析,备用接收者,完整消息转发

消息转发过程.png
动态方法解析

首先 OC 运行时会调用 + resolveInstanceMethod 或者 + resolveClassMethod,让我们有机会提供一个函数实现,如果实现了方法且返回为 YES,则运行时系统会重新启动一次消息发送过程。


动态方法解析示例
备用接收者

如果目标对象实现了 -forwardingTargetForSelector:,Runtime 会调用这个方法,给我们把这个方法转发给其他对象的机会。


备用接收者示例
完整消息转发

首先会发送 -methodSignatureForSelector: 消息获得的参数和返回值类型.若 -methodSignatureForSelector:返回为 nil, Runtime 会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了.若返回一个函数签名, Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。


完整消息转发示例
完整消息转发示例

5 Runtime 应用

关联对象,给分类增加属性;
方法的增加,替换(黑魔法)和 KVO 的实现;
方法转发(热更新)解决 BUG(JSPatch);
实现 NSCoding 的自动归档和解档;
实现字典和模型的转换(MJExtension);

关联对象,给分类增加属性

原理:给一个类添加属性,本质是给这个类添加关联。
场景:在分类中即使使用 @property 也仅仅只会自动生成 set 和 get 方法的声明,并不会生成带下划线的属性和方法的实现。


关联对象方法
关联对象方法参数
给分类添加属性

总结:给属性赋值的本质是让对象和该属性产生关联。

关联对象策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0,             // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401,         // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403            // 表示关联对象copy,不是线程安全的
};
动态添加方法

场景:使用的时候再动态将方法添加到内存。


动态添加方法
黑魔法(方法交换)
黑魔法图示

应用场景:系统/第三方的功能不能满足我们的需求时,额外添加的功能。
实现步骤:
1、给要修改的方法添加分类;
2、自己实现一个扩展功能的方法;
3、交换方法;


交换方法1
交换方法2

注:
1、swizzling 要在加载时比较靠前的方法中实现,如 +load 或 +initialize 方法。
2、swizzling 应只在 dispatch_once方法中。

消息转发(热更新)解决Bug(JSPatch)

原理和上面类似,实现方法的替换。

实现 NSCoding 的自动归档和自动解档

原理:利用 runtime 提供的函数遍历 model 自身所有的属性,并对属性进行 encode 和 decode操作。
实现:在 model 的基类中重写方法。


自动归档和解档
实现字典和模型的自动转换(MJExtension)

原理:利用 runtime 提供的函数遍历 model 自身的所有属性,如果属性在 json 中有对应的值,则将其赋值。
实现:在 NSObject 分类中实现方法。


字典和模型的转换

测试


Runtime1.png
Runtime2.png
Runtime3.png
动态变量控制
动态变量控制

5、Runtime 常用方法

获取属性列表
获取方法列表
获取成员变量列表
获取协议列表
获得类方法
获得实例方法
添加方法:
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));

替换方法:
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

交换方法:
method_exchangeImplementations(oriMethod, cusMethod);

获取类的所有方法:
Method allMethods = class_copyMethodList([Person class], &count);

获取所有成员变量:
Ivar allVariables = class_copyIvarList([Person class], &count);

获取所有属性:
objc_property_t properties = class_copyPropertyList([Person class], &count);

根据名字得到类变量的 Ivar 指针,但是这个在 OC 中好像毫无意义
Ivar oneCVIvar = class_getClassVariable([Person class], name); 

根据名字得到实例变量的 Ivar 指针:
Ivar oneIVIvar = class_getInstanceVariable([Person class], name);

找到后可以直接对私有变量赋值:
object_setIvar(_per, oneIVIvar, @"Mike");

强制修改 name 属性/动态添加方法:   
第一个参数表示 Class cls 类型;   
第二个参数表示待调用的方法名称;  
第三个参数 (IMP)myAddingFunction,IMP 是一个函数指针,这里表示指定具体实现方法 myAddingFunction;   
第四个参数表方法的参数,0代表没有参数;
class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);  

交换两个方法  :
method_exchangeImplementations(method1, method2);

关联两个对象:
objc_setAssociatedObject(id object, const void key, id value, objc_AssociationPolicy policy)
id object:表示关联者,是一个对象,变量名理所当然也是object
const void key:获取被关联者的索引key 
id value:被关联者,这里是一个block 
objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC

获得某个类的类方法:
Method class_getClassMethod(Class cls , SEL name)

获得某个类的实例对象方法:
Method class_getInstanceMethod(Class cls , SEL name)

交换两个方法的实现:
void method_exchangeImplementations(Method m1 , Method m2)

将某个值跟某个对象关联起来,将某个值存储到某个对象中:
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)

利用参数 key 将对象 object 中存储的对应值取出来:
id objc_getAssociatedObject(id object , const void *key)

获得某个类的所有成员变量(outCount 会返回成员变量的总数)
Ivar class_copyIvarList(Class cls , unsigned int outCount)

获得成员变量的名字:
const char *ivar_getName(Ivar v)

获得成员变量的类型:
const char *ivar_getTypeEndcoding(Ivar v)

获取类里面所有方法:
class_copyMethodList(__unsafe_unretained Class cls, unsigned int *outCount)

获取类里面属性:
class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount)

相关文章

网友评论

      本文标题:Runtime

      本文链接:https://www.haomeiwen.com/subject/jpyxyftx.html