iOS Runtime 基础原理
关于 Runtime ,网上已经有很多很好的文章,写得很详尽。本篇主要是从新手的角度出发,逐步介绍 Runtime 的原理、常用方法、应用场景等。
相关链接:
苹果维护的Runtime开源代码
GNU维护一个开源的runtime 版本
官方Api
一、Runtime 是什么
在 C 语言中,将代码转换为可执行程序,一般要经历三个步骤,即编译、链接、运行。在链接的时候,对象的类型、方法的实现就已经确定好了。
而在 Objective-C 中,却将一些在编译和链接过程中的工作,放到了运行阶段。也就是说,就算是一个编译好的 .ipa 包,在程序没运行的时候,也不知道调用一个方法会发生什么。这也为后来大行其道的「热修复」提供了可能。因此我们称 Objective-C为一门动态语言。
这样的设计使 Objective-C 变得灵活,甚至可以让我们在程序运行的时候,去动态修改一个方法的实现。而实现这一切的基础就是 Runtime。
简单来说, Runtime 是一个库,这个库使我们可以在程序运行时创建对象、检查对象,修改类和对象的方法。
至于这个库是怎么实现的,请紧张刺激地往下看。
二、Runtime 是怎么工作的
要了解 Runtime 是怎么工作的,首先要知道类和对象在 Objective-C 中是怎么定义的。
注意:以下会用到
C语言中结构体的内容,包括结构体的定义、为结构体定义别名等。如果你对这块不熟悉,建议先复习一下这块的语法。传送门
1. Class 和 Object
在 objc.h 中, Class 被定义为指向 objc_class 的指针,定义如下:
typedef struct objc_class *Class;
而 objc_class 是一个结构体,在 runtime.h 中的定义如下:
struct objc_class {
Class isa; // 实现方法调用的关键
Class super_class; // 父类
const char * name; // 类名
long version; // 类的版本信息,默认为0
long info; // 类信息,供运行期使用的一些位标识
long instance_size; // 该类的实例变量大小
struct objc_ivar_list * ivars; // 该类的成员变量链表
struct objc_method_list ** methodLists; // 方法定义的链表
struct objc_cache * cache; // 方法缓存
struct objc_protocol_list * protocols; // 协议链表
};
为了方便理解,我这里去掉了一些声明,主要是和
Objective-C语言版本相关,这里可以暂时忽略。完整的定义可以自己去runtime.h中查看。
提示:在 Xcode 中,使用快捷键
command + shift + o,可以打开搜索窗口,输入objc_class即可看到头文件定义。
可以看到,一个类保存了自身所有的成员变量( ivars )、所有的方法( methodLists )、所有实现的协议( objc_protocol_list )。
比较重要的字段还有 isa 和 cache ,它们是什么东西,先不着急,我们来看下 Objective-C 中对象的定义。
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
这里看到了我们熟悉的 id ,一般我们用它来实现类似于 C++ 中泛型的一些操作,该类型的对象可以转换为任意一种对象。在这里 id 被定义为一个指向 objc_object 的指针。说明 objc_object 就是我们平时常用的对象的定义,它只包含一个 isa 指针。
也就是说,一个对象唯一保存的信息就是它的 Class 的地址 isa。当我们调用一个对象的方法时,它会通过 isa 去找到对应的 objc_class,然后再在 objc_class 的 methodLists 中找到我们调用的方法,然后执行。
再说说 cache ,因为调用方法的过程是个查找 methodLists 的过程,如果每次调用都去查找,效率会非常低。所以对于调用过的方法,会以 map 的方式保存在 cache 中,下次再调用就会快很多。
2. Meta Class 元类
上一小节讲了 Objective-C 中类和对象的定义,也讲了调用对象方法的实现过程。但还留下了许多问题,比如调用一个对象的类方法的过程是怎么样的?还有 objc_class 中也有一个 isa 指针,它是干嘛用的?
现在划重点,在 Objective-C 中,类也被设计为一个对象。
其实观察 objc_class 和 objc_object 的定义,会发现两者其实本质相同(都包含 isa 指针),只是 objc_class 多了一些额外的字段。相应的,类也是一个对象,只是保存了一些字段。
既然说类也是对象,那么类的类型是什么呢?这里就引出了另外一个概念 —— Meta Class(元类)。
在 Objective-C 中,每一个类都有对应的元类。而在元类的 methodLists 中,保存了类的方法链表,即所谓的「类方法」。并且类的 isa 指针指向对应的元类。因此上面的问题答案就呼之欲出,调用一个对象的类方法的过程如下:
- 通过对象的
isa指针找到对应的类。 - 通过类的
isa指针找到对应元类。 - 在元类的
methodLists中,找到对应的方法,然后执行。
注意:上面类方法的调用过程不考虑继承的情况,这里只是说明一下类方法的调用原理,完整的调用流程在后面会提到。
这么说来元类也有一个 isa 指针,元类也应该是一个对象。的确是这样。那么元类的 isa 指向哪里呢?为了不让这种结构无限延伸下去, Objective-C 的设计者让所有的元类的 isa 指向基类(比如 NSObject )的元类。而基类的元类的 isa 指向自己。这样就形成了一个完美的闭环。
下面这张图可以清晰地表示出这种关系。
1852765-244b037923a6c2aa.jpg
同时注意 super_class 的指向,基类的 super_class 指向 nil 。
3. Method
上面讲到,「找到对应的方法,然后执行」,那么这个「执行」是怎样进行的呢?下面就来介绍一下 Objective-C 中的方法调用。
先来看一下 Method 在头文件中的定义:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char * method_types;
IMP method_imp;
};
Method 被定义为一个 objc_method 指针,在 objc_method 结构体中,包含一个 SEL 和一个 IMP ,同样来看一下它们的定义:
// SEL
typedef struct objc_selector *SEL;
// IMP
typedef id (*IMP)(id, SEL, ...);
1、先说一下 SEL 。 SEL 是一个指向 objc_selector 的指针,而 objc_selector 在头文件中找不到明确的定义。
我们来测试以下代码:
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 输出:viewDidLoad
SEL sel1 = @selector(viewDidLoad1);
NSLog(@"%s", sel1); // 输出:viewDidLoad1
可以看到, SEL 不过是保存了方法名的一串字符。因此我们可以认为, SEL 就是一个保存方法名的字符串。
由于一个 Method 只保存了方法的方法名,并最终要根据方法名来查找方法的实现,因此在 Objective-C 中不支持下面这种定义。
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
2、再来说 IMP 。可以看到它是一个「函数指针」。简单来说,「函数指针」就是用来找到函数地址,然后执行函数。(「函数指针」了解一下)
这里要注意, IMP 指向的函数的前两个参数是默认参数, id 和 SEL 。这里的 SEL 好理解,就是函数名。而 id ,对于实例方法来说, self 保存了当前对象的地址;对于类方法来说, self 保存了当前对应类对象的地址。后面的省略号即是参数列表。
3、到这里, Method 的结构就很明了了。 Method 建立了 SEL 和 IMP 的关联,当对一个对象发送消息时,会通过给出的 SEL 去找到 IMP ,然后执行。
在 Objective-C 中,所有的方法调用,都会转化成向对象发送消息。发送消息主要是使用 objc_msgSend 函数。看一下头文件定义:
id objc_msgSend(id self, SEL op, ...);
可以看到参数列表和 IMP 指向的函数参数列表是相对应的。 Runtime 会将方法调用做下面的转换,所以一般也称 Objective-C 中的调用方法为「发送消息」。
[self doSomething];
objc_msgSend(self, @selector(doSomething));
4、上面看到 objc_msgSend 会默认传入 id 和 SEL 。这对应了两个隐含参数, self 和 _cmd 。这意味着我们可以在方法的实现过程中拿到它们,并使用它们。下面来看个例子:
- (void)testCmd:(NSNumber *)num {
NSLog(@"%ld", (long)num.integerValue);
num = [NSNumber numberWithInteger:num.integerValue-1];
if (num.integerValue > 0) {
[self performSelector:_cmd withObject:num];
}
}
尝试调用:
[self testCmd:@(5)];
上面会按顺序输出 5, 4, 3, 2, 1 ,然后结束。即我们可以在方法内部用 _cmd 来调用方法自身。
5、上面已经介绍了方法调用的大致过程,下面来讨论类之间继承的情况。重新回去看 objc_class 结构体的定义,当中包含一个指向父类的指针 super_class 。
即当向一个对象发送消息时,会去这个类的 methodLists 中查找相应的 SEL ,如果查不到,则通过 super_class 指针找到父类,再去父类的 methodLists 中查找,层层递进。最后仍然找不到,才走抛异常流程。
下面的图演示了一个基本的消息发送框架:
1852765-d5c23b880cf2a7c5.jpg
6、当一个方法找不到的时候,会走拦截调用和消息转发流程。我们可以重写 +resolveClassMethod: 和 +resolveInstanceMethod: 方法,在程序崩溃前做一些处理。通常的做法是动态添加一个方法,并返回 YES 告诉程序已经成功处理消息。如果这两个方法返回 NO ,这个流程会继续往下走,完整的流程如下图所示:
1852765-3a683919c57a9cda.jpg
4. Category
我们来看一下 Category 在头文件中的定义:
typedef struct objc_category *Category;
struct objc_category {
char * category_name;
char * class_name;
struct objc_method_list * instance_methods;
struct objc_method_list * class_methods;
struct objc_protocol_list * protocols;
}
Category是一个指向 objc_category结构体的指针,在 objc_category 中包含对象方法列表、类方法列表、协议列表。从这里我们也可以看出, Category 支持添加对象方法、类方法、协议,但不能保存成员变量。
注意:在
Category中是可以添加属性的,但不会生成对应的成员变量、getter和setter。因此,调用Category中声明的属性时会报错。
我们可以通过「关联对象」的方式来添加可用的属性。具体操作如下:
- 1、在
UIViewController+Tag.h文件中声明property。
@property (nonatomic, strong) NSString *tag;
- 2、在
UIViewController+Tag.m中实现getter和setter。记得添加头文件#import <objc/runtime.h>。主要是用到objc_setAssociatedObject和objc_getAssociatedObject这两个方法。
static void *tag = &tag;
@implementation UIViewController (Tag)
- (void)setTag:(NSString *)t {
objc_setAssociatedObject(self, &tag, t, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tag {
return objc_getAssociatedObject(self, &tag);
}
@end
- 3、在子类中调用。
// 子类 ViewController.m
- (void)testCategroy {
self.tag = @"TAG";
NSLog(@"%@", self.tag); // 这里输出:TAG
}
注意:当一个对象被释放后,
Runtime回去查找这个对象是否有关联的对象,有的话,会将它们释放掉。因此不需要我们手动去释放。