Runtime(一)

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通过给出属性名在类和协议中获取属性的引用。

最近的文章

VLCMediaPlayer的集成与使用

  VLC Media Player (VideoLAN) 为 Windows、Linux、OS X、Android、iOS、Windows Phone等平台提供一个视频播放器、解码器。它可以播放来自网络、摄像头、磁盘、光驱的文件,支持包括MPEG 1/2/4, H264, VC-1, DivX, WMV, Vorbis, AC3, AAC等格式的解码。在 Windows 和 Linux 上的 VLC 是使用C++/Qt写成,提供了一致的用户体验。同时 VLC 还专门为 OS X 提供了原...…

继续阅读
更早的文章

iOS10下打印NSLog syslog信息

  在iO10以前的版本越狱机器可以用socat方便的打印出NSLog信息,但在iOS10下由于日志系统发生了改变,以前一些打印方法失效。  The logging system has changed in iOS 10. Apple now uses what it calls “Unified Logging”. Below are links to a brief overview, as well as a WWDC session covering the topic: ...…

继续阅读