Objective-C的运行时库
文章目录
【注意】最后更新于 September 25, 2017,文中内容可能已过时,请谨慎使用。
Objective-C 运行时
Objective-C 是一门基于运行时的编程语言,这意味着所有方法、变量、类之间的链接,都会推迟到应用实际运行的最后一刻才会建立。这将给开发人员极高的灵活性,因为我们可以修改这些链接。而不同的是,Swift 绝大多数时候是一门面向编译时的语言。因此在 Swift 当中,灵活性受到了限制,不过您会因此得到更多的安全性。
runtime.h开源库
Objective-C 的运行时本质上是一个库。它负责了 “Objective” 这个部分,因此您所知、所爱的面向对象编程,都是在这里实现的。如果您想要访问里面的函数的话,只需要导入这个库即可:
|
|
runtime.h
开源库主要由 C 和汇编编写而成,其实现了诸如类、对象、方法调度、协议等面向对象编程这个部分。
成员结构体
在运行时中对象
和类
本质上是一个非常简单的结构体,在运行时环境下,我们就可以创建,读取,修改这些属性方法等,例如:使用allocateClassPair
函数创建类。
- 对象结构体
对象结构体中仅提供一个**
isa
**属性,是关联类引用
的指针。这也就是 Objective-C 当中的所有对象都需要实现的。 在runtime.h
当中对象的定义:
|
|
- 类结构体
|
|
isa属性:建立自身与 super_class
这个值进行关联。
super_class:除了 NSObject 这个类之外,super_class 的值永远不会为 nil,因为 Objective-C 当中的其余类都是以某种方式继承自 NSObject 的。
ivars:变量列表,methodLists:方法列表,protocols:协议列表,其他属性:name
、version
、info
之类的值。
- 变量结构体 包含了变量类型和变量名称。偏移量 (offset) 则是内存管理方面的内容。
|
|
- 方法结构体
|
|
method_name: 方法名,使用Selector
来表示方法编号,对应在 performSelector
当中所匹配的内容。
method_types:方法类型,使用char
字符来表示。
method_imp:方法的实现,IMP
是一个函数指针,方法实现的一种特定的表示方式,是方法混淆特性的根本所在。
成员函数
运行时库提供一系列运行时函数,实现在运行时动态的对成员结构体(类/对象)进行创建,修改等相关操作,例如:创建类,在类别
中添加存储属性
动态创建运行时类
在制作库框架会大量运用使用到运行时函数。如果您无法知道用户将会创建什么样的数据,那么您就需要在运行时进行类的创建了。Core Data 就使用了这个功能。此外,如果您愿意的话,它还可以用在 JSON 解析当中。
类的创建要用的 Objective-C 两个运行时函数:allocateClassPair
和objc_registerClassPair
|
|
[NSObject class]:就是类结构体
的属性isa要关联的类引用
“MyClass”:指定类结构体
的name属性值
额外字节的定义:通常我们都直接赋值 0 即可
2. 添加变量、方法以及协议
3. registerClassPair
注册这个 ClassPair,注册之后,我们就无法修改变量列表了,不过其余的内容仍然可以修改。
为类别中新增存储属性
类别可以在既有的类中添加函数、计算属性,无法添加存储属性。但是在运行时环境下,可以借助 setAssociatedObject
和 getAssociatedObject
实现向既有的类当中添加存储属性。
例如在NSObject
新增一个Name
存储属性:
|
|
内省机制
内省机制是用来判别这个类是否实现具备某项功能。当我们使用了一个带有可选方法的协议时,为了避免崩溃发生,可以借助这个内省机制来判断这个对象是否可以调用此可选方法。
内省机制提供了两个运行时函数
isMemberOfClass
: 对比两者的 isa 是否相同。
respondsToSelector
:则封装了一个运行时函数:class_respondsToSelector
,两个参数类
和 Selector
|
|
使用运行时实现单元测试
当我们在编写 XCTestCase
的时候,需要完成 setUp
和 tearDown
的设定,随后才能编写相关的 test
函数。当测试运行的时候,系统会自行遍历所有的测试函数,并自动运行。
|
|
单元测试的原理就是借助了运行时函数class_copyMethodList
获取到方法名,然后将其转换为字符串,检查其是否包含有 “test”,如果有便可以运行。
运行时方法调度
动态的向对象当中添加方法并调用新增的方法。方法转发,方法混淆:替换或交换
- 动态的为类新增方法
了解到运行时的方法的结构体组成:方法名,SEL和
IMP
实现,需要三个运行时函数来新建一个运行时方法
|
|
class_getInstanceMethod
:获取方法的SEL
method_getImplementation
:方法的实现IMP
method_getTypeEncoding
: 获取方法的类型,char字符表示
class_addMethod
: 向对象当中添加方法的运行时函数。它所需的参数,即上述方法结构体当中的那三个值:Selector、方法实现和方法类型。
- 调用运行时方法
我们可以使用
[self doStuff]
或者[self performSelector:@selector(doStuff)]
来进行调用。 实际上在运行时级别,它们都是借助objc_msgSend
向对象发送了一个消息:
|
|
但是如果调用方法所在的对象为 nil 的时候,我们就会得到一个异常,应用便会崩溃。但事实证明,在崩溃之前会预留几个步骤,从而允许我们对某个不存在的函数进行一些操作:方法转发/替换等。
- 方法转发 当桥接两个不同的框架的时候,可以将方法转发给其它目标,或者,当我们调用某个未实现的方法时,运行时有如下处理步骤:
|
|
3.1. 首先调用两个类方法:一个名为 resolveInstanceMethod
/resolveClassMethod
类方法,这时候我们便有机会来添加方法了,如果我们返回了 YES,就意味着原始方法将会再次被调用。
3.2. forwardingTargetForSelector
:当不要添加新方法时,可以直接返回需要调用方法的目标对象,之后这个对象就会调用 Selector。
3.3. forwardInvocation
:实现目标对象调用 Selector,所有的调用过程都被封装到 NSInvocation
对象当中。需要 通过methodSignatureForSelector
函数创建。
- 动态特性方法混淆:替换或交换
方法混淆是通过
class_replaceMethod
或者method_exchangeImplementations
实现方法的替换。常用于日志记录和 Mock 测试。 当类加载之后,会调用一个名为load
的类函数。由于我们只打算混淆一次,因此我们需要使用dispatch_once
。接着我们便可以得到该方法,然后使用class_replaceMethod
或者method_exchangeImplementations
来替换方法。
|
|
文章作者 iTBoyer
上次更新 2017-09-25