关于 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
,这个流程会继续往下走,完整的流程如下图所示:
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
回去查找这个对象是否有关联的对象,有的话,会将它们释放掉。因此不需要我们手动去释放。
网友评论