iOS Runtime

iOS Runtime简称运行时,对于一个iOS开发者来说深刻理解它至关重要,可以毫不夸张的说Runtime是OC这门语言的灵魂。那么什么是运行时呢?接下来这篇文章会向你详细讲解,以及如何使用。

什么是Runtime

  • 简单回顾一下C语言的顺序执行的特性:对于C语言,函数的调用在编译的时候会决定调用哪个函数。编译完成后直接顺序的执行,没有任何的二义性。
  • Runtime就是系统在运行的时候的一些机制,其中最主要的是消息转发的机制。OC的函数调用实际就是消息发送的过程,即使这个函数并未实现,只要申明过就不会报错。只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • OC中一切都被设计成一个对象,当一个类A被初始化为一个实例a,a是一个对象,实际上类A本质上也是一个对象。
  • 代码在程序运行过程中都会被转化成runtime的C代码执行,例如:[target doSomething];会被转化成objc_msgSend(target,@selector(doSomething));的方式发送消息。

OC中类的定义

打开objc.h文件,会发现关于类的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;//OC中类的定义,一个对象

/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;//OC中类的实例定义
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;//OC中类的实例

typedef struct objc_selector *SEL;//方法

typedef id (*IMP)(id, SEL, ...); //方法的指针

runtime.h中会发现objc_class实际上是一个结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;//指针,实例的isa指向类对象,类对象的isa指向元类

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;//指向父类
const char *name OBJC2_UNAVAILABLE;//类名称
long version OBJC2_UNAVAILABLE;//类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
long info OBJC2_UNAVAILABLE;//一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size OBJC2_UNAVAILABLE;//该类的实例变量大小(包括从父类继承下来的实例变量);
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;//用于存储每个成员变量的地址
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;//与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache *cache OBJC2_UNAVAILABLE;//指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//协议列表
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

以及其它等定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;//方法的描述

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;//实例变量的描述

/// An opaque type that represents a category.
typedef struct objc_category *Category;//类别的描述

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;//属性的描述

typedef struct objc_object Protocol;//协议的描述

...

获取列表

有时候会有这样的需求,我们需要知道当前类中每个属性的名称(比喻字典转模型的操作,避免后台传递null的问题,检查字典的Key和模型对象的属性名字不匹配问题,swizzing的操作等),在此可以通过runtime的一系列的方法获取类的信息(属性列表,方法的名称列表,成员变量列表,协议列表)等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}

//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}

//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}

//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}

方法调用

让我们看一下方法调用在运行时的过程

如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。

1.首先,在相应操作的对象中的cache缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。

2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行。并且将method放置cache中

3.如果没找到,去父类指针所指向的对象中执行1,2.

4.以此类推,如果一直到根类还没找到,转向拦截调用。

5.如果没有重写拦截调用的方法,程序报错。

  • 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
  • 如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。

拦截调用

在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢。

拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。

1
2
3
4
5
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
  • 第二个方法和第一个方法相似,只不过处理的是实例方法。
  • 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
  • 第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。

动态添加方法

重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?有一个办法是根据传进来的SEL类型的selector动态添加一个方法。如下图:

General preferences pane

关联对象

就是给对象添加内容,假如有如下场景:现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性,现在你能想到的就是继承或者采用分类的方式去做,这样是否太麻烦。这时我们可以通过runtime的属性关联来实现

设置内容:

1
2
3
UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showRulePage:)];
objc_setAssociatedObject(tapGR, "ruleUrl", ruleUrl, OBJC_ASSOCIATION_COPY_NONATOMIC);
[messageLabel addGestureRecognizer:tapGR];

objc_setAssociatedObject的四个参数:

1.id object给谁设置关联对象。

2.const void *key关联对象唯一的key,获取时会用到。

3.id value关联对象。

4.objc_AssociationPolicy关联策略,有以下几种策略(retainCount):

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

获取内容:

1
2
3
- (void)showRulePage:(UITapGestureRecognizer *)tapGr {
NSString *ruleUrl = objc_getAssociatedObject(tapGr, "ruleUrl");
}

objc_getAssociated

1.id object获取谁的关联对象。

2.const void *key根据这个唯一的key获取关联对象。Object的两个参数

方法交换

方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。这是参考Mattt大神在NSHipster上的文章自己写的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (swizzling)

//load方法会在类第一次加载的时候被调用
//调用的时间比较靠前,适合在这个方法里做方法交换
+ (void)load{
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

//获得viewController的生命周期方法的selector
SEL systemSel = @selector(viewWillAppear:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(swiz_viewWillAppear:);
//两个方法的Method
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);

//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAdd) {
//如果成功,说明类中不存在这个方法的实现
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}else{
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}

});
}

- (void)swiz_viewWillAppear:(BOOL)animated{
//这时候调用自己,看起来像是死循环
//但是其实自己的实现已经被替换了
[self swiz_viewWillAppear:animated];
NSLog(@"swizzle");
}

@end

基本的讲解已经完成,下一章会着重讲解swizzle的相关内容。

参考

1.Objective-C总Runtime的那点事儿(一)消息机制