Runtime是什么
Runtime是什么 ———— Runtime又叫运行时,是一套底层的C语言的API,其为iOS内部核心之一,我们平时编写的OC代码,底层都是基于它来实现的。比如:
[receiver message];
//底层运行时会被编译器转化为:
objc_msgSend(receiver, selector)
//如果其还有参数比如:
[receiver message:(id)arg...];
//底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
为什么需要Runtime ———— Objective-C是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定代码运行。
因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。
Runtime基本是用C和汇编写的,由此可见苹果为了动态系统的高效而做出的努力。苹果和GNU各自维护一个开源的Runtime版本,这两个版本之间都在努力保持一致。
Runtime的作用
OC在三种层面上与Runtime系统进行交互:
1.通过Objective-C源代码
只需要编写OC代码,Runtime系统自动在幕后处理一切,调用方法,编译器会将OC代码转换成运行时代码,在运行时确定数据结构和函数。
2.通过Foundation框架的NSObject类定义的方法
Cocoa程序中绝大部分类都是NSObject类的子类 ,所以都继承了NSObject的行为。(NSProxy类是个例外,它是个抽象超类)。
一些情况下,NSObject类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如-description方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject类并不知道子类的内容,所以它只是返回类的名字和对象的地址。
还有一些NSObject类的方法可以从Runtime系统中获取信息,允许对象进行自我检查,例如:
-class //返回对象的类
-isKindOfClass //和-isMemberOfClass 方法检查对象是否存在于指定的类的继承体系中
-respondsToSelector //检查对象是否实现了指定的消息
-conformsToProtocol //检查对象是否实现了指定协议类的方法
-methodForSelector //返回指定方法实现的地址
3.通过对Runtime库函数的直接调用
Runtime系统是具有公共接口的动态共享库,头文件存入于/usr/include/objc目录下,这意味着我们使用时只需要引入objc/Runtime.h头文件即可。
Runtime相关术语
1.SEL ———— 它是selector在Objc中的表示(Swift中是Selector类>。selector是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意Objc在相同的类中不会有命名相同的两个方法。selector对方法名进行包装,以便找到对应的方法实现。它的数据结构是:
typedef struct objc_selector *SEL;
我们可以看出它是个映射到方法的C字符串,你可以通过Objc编译器器命令@selector()或者Runtime系统的 sel_registerName函数来获取一个SEL类型的方法选择器。
注意:不同类中相同名字的方法所对应的selector是相同的,由于变量的类型不同,所 以不会导致它们调用方法实现混乱。
2.id
id是一个参数类型,它是指向某个类的实例的指针。定义如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
以上定义,看到objc_object结构体包含一个isa指针,根据isa指针就可以找到 对象所属的类。
注意:isa指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类 型,要想确定类型还是需要用对象的-class方法。PS:KVO的实现机理就是将被观察对 象的isa指针指向一个中间类而不是真实类型。
3.Class
typedef struct objc_class *Class;
//Class其实是指向objc_class结构体的指针。objc_class的数据结构如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
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;
从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;
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *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不能添加属性的原因。
objc_ivar_list结构体用来存储成员变量的列表,而objc_ivar则是存储了单个成 员变量的信息;同理,objc_method_list结构体存储着方法数组的列表,而单个方法 的信息则由objc_method结构体存储。
值得注意的是,ojbc_class中也有一个isa指针,这说明Objc类本身也是一个对象。为了处理类和对象的关系,Runtime库创建了一种叫做Meta Class(元类)的东西,类对象所属的类就叫做元类。Meta Class表述了类对象本身所具备的元数据。
我们所熟悉的类方法,就源自于Meta Class。我们可以理解为类方法就是类对象的实例方法。每个类公有一个类对象,而每个类对象仅有一个与之相关的元类。
当你发出一个类似[NSObject alloc]的消息时,实际上,这个消息被发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时是一个根元类(Root Meta Class)的实例。所有元类的isa指针最终都指向根元类。
所以当[[NSObject alloc]]这条消息发送给类对象的时候,运行时代码objc_msgSend()会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象招行方法调用。
最后objc_class中还有一个objc_cache缓存,它的作用很重要。
4.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;
}
objc_method存储了方法名、方法类型和方法实现:
* 方法类型名为SEL
-
方法类型method_types是个char指针,存储方法的参数类型和返回值类型
-
method_imp指向了方法的实现,本质是一个函数指针
Ivar
Ivar表示成员变量的类型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
其中ivar_offset是基地址偏移字节
5.IMP
IMP在objc.h中定义是:
typedef id (*IMP)(id, SEL, ...);
它是一个函数指针,这是由编译器生成的。当你发起一个Objc消息之后,最终它会招行的那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。
如果得到了执行某个实例方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面的Cache中会提到。
你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现也是唯一的,通过一个id和SEL参数就能确定唯一的方法实现地址。
而一个确定的方法也只有唯一的一组id和SEL参数。
6.Cache
Cache定义如下:
typedef struct objc_cache *Cache struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在isa指针指向的类的方法列表中遍历查找 能够响应的方法,因为每次都要查找效率太低了,而是优先在Cache中查找。
Runtime系统会把调用的方法存到Cache中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中CPU绕过主存先访问Cache一样。
7.Property
typedef struct objc_property *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
/** name */
@property (strong, nonatomic) NSString *name;
/** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
@end
以上是一个Person类,有3个属性。让我们用上述方法获取类的运行时属性。
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
NSLog(@"%d", outCount);
for (NSInteger i = 0; i < outCount; i++)
{
NSString *name = @(property_getName(properties[i]));
NSString *attributes = @(property_getAttributes(properties[i]));
NSLog(@"%@--------%@", name, attributes);
}
打印结果如下:
test[2321:451525] 3
test[2321:451525] name--------T@"NSString",&,N,V_name
test[2321:451525] age--------Ti,N,V_age
test[2321:451525] 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通过给出属性名在类和协议中获取属性的引用。