runtime - 初探·三种交互方式与术语名词解释
您将了解到:
- 什么是runtime
- 与runtime系统发生交互的三种方式
- 一些runtime的术语的数据结构
什么是runtime
我们都知道高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime
来实现的。
Objective-C是基于C的,它为C添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了runtime
运行时来处理,可以说runtime
是我们Objective-C幕后工作者。
-
runtime
(简称运行时),是一套纯C(C和汇编写的)的API。而OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。 -
对于C语言,方法的调用会在编译的时候就决定好调用哪个方法。
-
OC的方法调用成为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个方法,只有在真正运行的时候才会根据方法的名称找到对应的方法实现来调用。
-
事实证明:在编译阶段,OC可以调用任何方法,即使这个方法并未实现,只要声明过就不会报错,只有运行的时候才会报错,这是因为OC是运行时动态调用的。而C语言调用未实现的方法就会报错。
与runtime系统发生交互的三种方式
runtime
是一个共享动态库,由一系列的C函数和结构体构成。和runtime
系统发生交互的方式有三种,一般都是用前两种:
-
通过Objective-C源代码
- 多数情况我们只需要编写OC代码即可,runtime系统自动在幕后搞定一切。
-
通过Foundation框架的NSObject类定义的方法
Cocoa 程序中绝大部分类都是NSObject类的子类,所以都继承了NSObject的行为。(NSProxy
类时个例外,它是个抽象超类)一些情况下,NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如
-description
方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。还有一些NSObject的方法可以从runtime系统中获取信息,允许对象进行自我检查。例如:
-
-class
方法返回对象的类; -
-isKindOfClass:
和-isMemberOfClass:
方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量); -
-respondsToSelector:
检查对象能否响应指定的消息; -
-conformsToProtocol:
检查对象是否实现了指定协议类的方法; -
-methodForSelector:
返回指定方法实现的地址;
-
-
通过对runtime库函数的直接调用
使用时引入#import <objc/runtime.h>
和#import <objc/message.h>
头文件,一些基础方法都定义在这两个文件中。
一些runtime的术语的数据结构
SEL
它是selector在Objc中表示(swift中是Selector类)。selector是方法选择器,selector对方法名进行包装,以便找到对应的方法实现。它的数据结构是:
typedef struct objc_selector *SEL;
注意:
不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。
id
id是一个参数类型,它是指向某个类的实例的指针。定义如下:
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
以上定义,看到objc_object
结构体包含一个isa
指针,根据isa
指针就可以找到对象所属的类。
注意:
isa
指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的-class
方法。
PS: KVO 的实现机理就是将被观察对象的 isa
指针指向一个中间类而不是真实类型,详见:
Class
typedef struct objc_class *Class;
Class
其实是指向objc_class
结构体的指针。objc_class
的数据结构如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
#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; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
从objc_class
可以看到,一个运行时类中关联了它的父类
、类名
、成员变量
、方法
、缓存
以及附属的协议
。其中objc_ivar_list
和objc_method_list
分别是成员变量列表和方法列表:
// 成员变量列表
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
// 方法列表
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
由此可见,我们可以动态修改*methodList
的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。这里可以参考下美团技术团队的文章:美团技术团队--深入理解Objective-C:Category
objc_ivar_list
结构体用来存储成员变量的列表,而objc_ivar
则是存储了单个成员变量的信息;同理,objc_method_list
结构体存储着方法数组的列表,而单个方法的信息则由objc_method
结构体存储。
值得注意的是,objc_class
中也有一个isa
指针,这说明Objc类
本身也是一个对象。为了处理类和对象的关系,runtime
库创建了一种叫做Meta Class(元类)
的东西,类对象所属的类就叫做元类
。Meta Class
表述了类对象本身所具备的元数据。
我们所熟悉的类方法,就源自于Meta Class
。我们可以立即为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。
当你发出一个类似[NSObject alloc](类方法)
的消息时,实际上,这个消息被发送给了一个类对象(Class Object)
,这个类对象必须是一个元类的实例,而这个元类同时也是也是一个根元类(Root Meta Class)
的实例。所有元类的isa
指针最终都指向根元类
。
所以当[NSObject alloc]
这条消息发送给类对象的时候,运行时代码objc_msgSend()
会去它的元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。
上图实线是super_class
指针,虚线是isa
指针。而根元类的父类是NSObject,isa
指向了自己。而NSObject没有父类。最后objc_class
中还有一个objc_cache
,缓存,它的左右很重要,后面会提到。
Method
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
objc_method
存储了方法名,方法类型和方法实线:
- 方法名类型为
SEL
- 方法类型
method_types
是个char
指针,存储方法的参数类型和返回值类型 -
method_imp
指向了方法的实现,本质是一个函数指针
Ivar
Ivar
是表示成员变量的类型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
其中ivar_offset
是基地址偏移字节
IMP
IMP
在objc.h
中的定义是:
typedef void (*IMP)(void /* id, SEL, ... */ );
它就是一个函数指针,这是由编译器生成的。当你发起一个ObjC
消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP
这个函数指针就是指向了这个方法的实现。
如果得到了执行某个实例某个方法的入口,我们可以绕开消息传递阶段,直接执行方法,这在下面的Cache
中会提到。
你会发现IMP
指向的方法与objc_msgSend
函数类型相同,参数都包含id
和SEL
类型。每个方法名都对应一个SEL
类型的方法选择器,而每个实例对象中的SEL
对应方法实线总是唯一的,通过一组id
和SEL
参数就能确定唯一的方法实线地址。
而一个确定的方法也只有唯一的一组id
和SEL
参数。
Cache
Cache定义如下:
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在isa指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在Cache中查找。
runtime系统会把被调用的方法存到Cache中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中CPU绕过主存先访问Cache一样。
Property
typedef struct objc_property *objc_property_t;
可以通过class_copyPropertyList
和protocol_copyPropertyList
方法获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount);
注意:返回的是属性列表,列表中每个元素都是一个
objc_property_t
指针
#import <Foundation/Foundation.h>
@interface Person : NSObject
/** 姓名 */
@property (strong, nonatomic) NSString *name;
/** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
@end
以上是一个Person类,有三个属性。让我们用上述方法获取类的运行时属性。
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
for (NSInteger i = 0; i < outCount; ++i) {
NSString *name = @(property_getName(properties[i]));
NSString *attributes = @(property_getAttributes(properties[i]));
NSLog(@"%@ ------- %@", name, attributes);
}
打印结果:
name ------- T@"NSString",C,N,V_name
age ------- TQ,N,V_age
weight ------- Td,N,V_weight
property_getName
用来查找属性的名称,返回c字符串。property_getAttributes
函数挖掘属性的真实名称和@encode类型,返回c字符串。
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
class_getProperty
和protocol_getProperty
通过给出属性名在类和协议中获得属性的引用。
总结
一些runtime术语讲完了,我们队结构体struct objc_class
有了了解,下一篇,将介绍runtime的api。