Runtime详解(上)
这篇关于Runtime讲解参考https://juejin.im/post/593f77085c497d006ba389f0以及https://www.jianshu.com/p/6ebda3cd8052
针对iOS开发人员,相信每个人都对Runtime有所了解。面试官更是对Runtime非常钟爱,可能5分钟不到就决定offer属不属于你或者要更高的薪资,而面试官一般问了解Runtime嘛,使用过他嘛,应用场景有哪些以及底层如何实现的。文章读下来大约10-20分钟,建议先收藏起来。大神可绕过!
Runtime介绍
Objective-C是基于C的,它为C添加了面向对象的特性,它将很多静态语言在编译和链接时期做的事放到了runtime运行时来处理。Runtime(简称运行时)是一套纯C(C和汇编编写的)API,而OC就是运行时机制,也就是在运行时的一些机制,其中最主要的就是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪一个函数。OC的函数调用成为消息发送,属于动态调用过程,在编译的时候并不能确定调用哪一个函数,只有在真正运行的时候,才会根据函数的名称找到对应的函数来调用。事实证明:在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有在运行的时候,才会报错,这就是因为是运行时动态调用的,而C语言调用未使用的函数就会报错。
Runtime消息传递
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
一个对象的方法[obj foo],编译器转为消息发送objc_msgSend(obj,foo),Runtime时执行的流程如下:
(1)通过obj的isa指针找到它的class
(2)在class的method list找foo
(3)如果class中没找到foo,继续往他的superclass中找
(4)一旦找到foo这个函数,就去执行它的实现IMP
换句而言:
(1)OC在向一个对象发送消息时,Runtime库会根据对象的isa指针找到该对象的类或其父类中查找方法。
(2)注册方法编号(可以快速查找)
(3)根据方法编号去查找对应方法
(4)找到只是最终函数实现地址,根据地址去方法区调用对应函数。
补充:每一个对象内部都有一个isa指针,这个指针指向它的真实类型,根据这个指针就能知道将来调用哪一个类的方法。
但这其中有一个问题,效率低。并不可能都需要遍历一次objc_method_list。如果把经常被调用的函数缓存下来,那可以大大提高效率。这就是objc_class 中另一个重要成员objc_cache做的事情,-再找到foo之后,把foo的method_name作为key,method_imp作为value给存起来,当再次收到foo消息的时候,可以直接在cache里找到,避免去遍历objc_method_list。
Runtime用到的概念
类对象(objc_class)
Objective-C类是由Class类型来表示的,它实际上指的是一个指向objc_class结构体的指针。
typedef struct object_class *Class
查看objc_class结构体的定义如下:
struct objc_class 结构体定义了很多变量,通过命名不难发现:
结构体保存了指向父类的指针,类的名字,版本,实例大小,方法列表,缓存以及遵守的协议列表等。类对象就是一个结构体,这个结构体存放的数据称为元数据。
实例(objc_object)
/// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id;
类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?
就是从isa指针指向的结构体创建,类对象isa指针指向的我们称为元类(metaclass)
元类(meta class)
通过上图我们可以看出整个体系构成了一个自闭环,struct_objc_object结构体实例他的isa指针指向的类对象。
类对象的isa指针指向了元类,super_class 指针指向了父类的类对象。
而元类的super_class 指针指向了父类的元类,那元类的isa指针又指向了自己。而基类的meta-class
的isa
指针是指向它自己。
如下图:
Method(objc_method)
先看下定义:就是表示能够独立完成一个功能的一段代码
runtime.h /// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型 typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;
SEL method_name: 方法名
Char *method_types:方法类型
IMP method_imp:方法实现
说明SEL和IMP其实都是Method的属性。
SEL(objc_selector)
Objc.h /// An opaque type that represents a method selector.代表一个方法的不透明类型 typedef struct objc_selector *SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型,selector是方法选择器,可以理解为区分方法的ID,这个ID的数据结构为SEL:
@property SEL selector;
可以看到selector
是SEL
的一个实例。
其实selector就是个映射到方法的c字符串,你可以用Objective-C编译器命令@selector或者Runtime系统的sel_registerName函数获得一个SEL类型的方法选择器。
命名规则:
同一个类:selector不能重复
不同的类,selector可以重复
这也带来一个弊端,我们在写C
代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C
中是行不通的,因为selector
只记了method
的name
,没有参数,所以没法区分不同的method
。
- (void)caculate(NSInteger)num; - (void)caculate(CGFloat)num;
这样会被报错的。
我们可以这样:
- (void)caculateWithInt(NSInteger)num; - (void)caculateWithFloat(CGFloat)num;
在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。
IMP
看下IMP的定义:就是指向最终实现程序的内存地址的指针。
在iOS的Runtime Method通过selector和IMP两个属性,实现了快速查询方法和实现,相对提高了性能,又保持了灵活性。
类缓存(objc_cache)
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache
,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime
系统实际上非常快,接近直接执行内存地址的程序速度。
category(objc_category)
Category是表示一个指向分类的结构体的指针,其定义如下:
struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; };
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。
下面将讲解Runtime(下)的应用