Damon

宏愿纵未了 奋斗总不太晚

0%

runtime快速入门和实战

什么是runtime?

简单来说,runtime就是一个C语言库,包含了很多底层C语言的API。Objective-C语言是一门动态语言,我们平时变编写的Objective-C代码,在程序运行时,最终都是转成了runtimeC语言代码。所以,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法。

利用runtime能做什么?

利用runtime机制让我们可以在程序运行时动态修改类对象、对象中的所有属性、方法,就算是私有方法以及私有属性都是可以动态修改的。KVO的底层实现就是利用runtime来实现的

类和对象基本数据结构

首先,我们来认识一下classesobjects的概念。我们都知道,Objects是由Classes生成,但是在Objective-C中,Classes本身也是objects,也能处理消息,这也就是为什么有类方法和实例方法。

Class

打开objc.h可以看到Class的定义

1
typedef struct objc_class *Class;

Class 实际上就是一个指向 objc_class 结构体的指针,打开 objc/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;

#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;
/* Use `Class` instead of `struct objc_class *` */

但是,上面的代码有 OBJC2_UNAVAILABLE 标志,这是说明这个结构体在 OC 2.0 版本(2006年发布)已经是不可用的,现在已经变成这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
private:
isa_t isa;
}

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用来获取类的具体信息
}

objc_objectobjc_class 就是我们经常会用到的 idClass 类型。

objc_object 只包含一个 isa_t 类型的结构体,objc_class 继承于 objc_object,可以得出结论,对象都是有 isa 的,OC中类也是一个对象,那 isa 有什么作用?

当一个对象的实例方法被调用的时候,就会通过 isa 找到对应的类,然后通过该类的 class_data_bits_t 找到对应的实现。如果调用类方法的话,类对象的 isa 就是元类(meta-class)。对象,类,元类之间的关系:

实线是 super_class 指针,虚线是 isa 指针。

  • Root class 其实就是 NSObject,它是没有超类的,superclasss 指向 nil。
  • NSObject 的元类的 superclass 指向自己。
  • 每个元类对象的 isa 都指向 根元类对象。

cache_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits

typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;

struct cache_t {
struct bucket_t *_buckets; // 散列表
mask_t _mask; // 散列表的长度 -1
mask_t _occupied; // 已经缓存的方法数量
}

struct bucket_t {
private:
cache_key_t _key; // SEL 作为key
IMP _imp; // 指向函数的指针
}

cache_t 里面用散列表来缓存曾经调用过的方法,可以提高方法的查找速度。

class_data_bits_t

使用 bits & FAST_DATA_MASK 可以得到 class_rw_t

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
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // 只读
method_array_t methods; // 方法列表(分类和类的都在这里)
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name; // 类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};
  • class_rw_t 里面的 methos、properties、protocols是可读可写的二维数组,包含了类的初始内容、分类的内容。
  • class_ro_t 里面的 baseMethodListbaseProtocolsivarsbaseProperties 是只读的一维数组,包含了类的初始内容。

method_t

method_t 是对方法的封装。

1
2
3
4
5
struct method_t {
SEL name; // 函数名
const char *types; // 函数编码(返回值类型、参数类型)
IMP imp; // 指向函数的指针 (函数地址)
};

runtime 的源码是开源,可以在官方下载,但是官方的是编译不过的,可以到这里下载可以编译的版本

objc_msgSend

平常我们使用OC代码来调用方法

1
2
3
4
5
6
// 创建一个People实例
People *p = [[People alloc] init];

// 调用方法
[p doSomething];

对象调用方法,本质上就是给对象”发送消息”,所以以上的代码,在编译时就会转换成runtimeobjc_msgSend函数调用

1
objc_msgSend(p, @selector(doSomething));

objc_msgSend函数会根据接收者和SEL选择器的类型来调用适当的方法,为了完成这个操作,它首先会在自身的isa指针找到自己所属的类(class),然后在这个类(class)的”方法列表”(list of methods)查找该方法,如果找到,就会去执行这个方法。如果找不到,会继续通过自身的super_class指针找到父类的类对象继续查找。如果到最后还是找不到相对应的方法,就会执行”消息转发”操作(将在下文详细介绍)

objc_msgSend函数只能处理部分消息的调用过程,如果有其他“特殊”的情况就会调用另外一些函数来处理,具体会调用哪些函数呢?

1
2
3
objc_msgSend_stret: 发送的消息要返回结构体,就可以用这个函数来发送和接收返回值
objc_msgSend_fpret : 发送的消息要返回浮点数,就可以用这个函数来发送和接收返回值
objc_msgSendSuper : 如果要给超类发送信息,例如[super doSomething]就可以用这个函数来处理

Runtime实战

终于写到这里了,上文说过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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#import "AppDelegate.h"
#import <UIKit/UIKit.h>
#import <objc/message.h>

// 函数的两个默认参数:self和_cmd
void cooking(id self,SEL _cmd,id dish){

// 通过runtime函数获取成员变量 _name
Ivar nameIvar = class_getInstanceVariable([self class], "_name");
id name = object_getIvar(self, nameIvar);

// 通过KVC获取成员变量 _age
NSInteger age = [[self valueForKeyPath:@"age"] integerValue];

NSLog(@"动态添加的女朋友名字是%@,年龄:%ld",name,age);
}

int main(int argc, char * argv[]) {
@autoreleasepool {

// 动态创建一个继承自NSObject的类
Class GirlFriend = objc_allocateClassPair([NSObject class], "GirlFriend", 0);

// 给“女朋友"添加成员变量 _name
class_addIvar(GirlFriend, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));

// 给“女朋友”添加成员变量 _age
class_addIvar(GirlFriend, "_age", sizeof(NSInteger), sizeof(NSInteger), @encode(NSInteger));

// 注册一个名为“cooking”的方法
SEL cook = sel_registerName("cooking");

// 给“女朋友”添加方法
class_addMethod(GirlFriend, cook, (IMP)cooking, "v@:@");

// 注册“女朋友”类
objc_registerClassPair(GirlFriend);

// 创建类的实例
id gfInstance = [[GirlFriend alloc] init];
// class_createInstance这个函数也能创建类实例,不过ARC环境下不能使用

// 获取类中指定名称的成员变量的信息
Ivar nameIvar = class_getInstanceVariable(GirlFriend, "_name");

// 利用runtime函数为这个成员变量赋值
object_setIvar(gfInstance, nameIvar, @"娃娃");

// 利用KVC赋值
[gfInstance setValue:@18 forKeyPath:@"age"];

// 给对象发送消息
objc_msgSend(gfInstance,cook,@"鱼香肉丝");

gfInstance = nil;

// 销毁类
objc_disposeClassPair(GirlFriend);

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

以上的函数有几个需要解释一下:

  • class_addIvar:除了第四个参数,相信其他的参数大家都能知道是什么意思.第四个参数我看了下官方文档的解释

    The instance variable’s minimum alignment in bytes is 1<<align. The minimum alignment of an instance variable depends on the ivar’s type and the machine architecture. For variables of any pointer type, pass log2(sizeof(pointer_type)).

成员变量的最小对齐长度是1<<align.这个取决于ivar的类型和机器的架构。任何指针类型的变量,通过log2(sizeof(pointer_type))来进行赋值,而且这个函数只能在objc_allocateClassPair和objc_disposeClassPair之间调用.

  • class_addMethod:最后一个参数,要赋值的是函数的类型。一个函数至少有两个参数(self和_cmd),这两个参数是隐式参数。函数的类型= 返回值+参数类型,v 表示 void,@ 表示对象,: 表示SEL
  • objc_disposeClassPair : 如果要销毁的类的实例或者它子类的实例还存在,不能调用这个函数。所以一定要先销毁实例再去销毁类

额外补充

在上面的代码中,我们动态创建了类和成员变量,给类添加方法..但是还没有尝试过添加属性。因为添加属性比较麻烦,所以我打算单独给大家说明一下添加属性这个函数的具体使用.

1
class_addProperty(__unsafe_unretained Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)

上面的函数用来添加属性,但是第三个参数和第四个参数我们该怎么赋值呢。第三个参数是一个指向objc_property_attribute_t的指针,首先看下它到底是什么

1
2
3
4
5
typedef struct {
const char *name; /**< The name of the attribute */
const char *value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

知道了这是一个结构体,看下文档说明

attributes:An array of property attributes.
attributeCount:The number of attributes in attributes.

attributes 就是一个数组,里面装着property的attributes(属性).
attributeCount 可以理解为数组的个数

知道了attributes是什么了之后,那么问题来了, property 的 attributes是什么呢? 要怎么填?

runtime有一个函数可以拿到property的attributes(属性).那么我们可以试试打印一下看看那是个什么东西.首先,创建一个”MyClass”文件

1
2
3
@interface MyClass : NSObject
@property (nonatomic,copy) NSString *name;
@end

然后随便找个地打印一下

1
2
3
4
5
// 获取MyClass类中的name属性
objc_property_t property = class_getProperty([MyClass class], "name");
// 获取property的属性
const char* attribute = property_getAttributes(property);
NSLog(@"%s",attribute);

输出结果:T@”NSString”,C,N,V_name
看不懂,好吧..我们找官方文档看看这个字符串是什么意思

The string starts with a T followed by the @encode type and a comma, and finishes with a V followed by the name of the backing instance variable. Between these, the attributes are specified by the following descriptors, separated by commas

就是说这个字符串是以T跟着类型编码和逗号开头,以V成员变量的名称结束
@ 代表这个property是一个对象 C 代表copy N 代表nonatomic

看到这里我们有能明白了,attributes要赋值一个objc_property_attribute_t结构体类型的数组,而objc_property_attribute_t结构体要赋值的是property的属性。so 我们动手尝试一下吧

1
2
3
4
5
6
objc_property_attribute_t type1 = {"T","@\"NSString\""};
objc_property_attribute_t policy = {"C",""};
objc_property_attribute_t rmw = {"N",""};
objc_property_attribute_t ivar = {"V","_introduction"};
objc_property_attribute_t att[] = {type1,policy,rmw,ivar};
class_addProperty(GirlFriend, "introduction", att, 4);

了解女朋友

了解自己的女朋友是必不可少的,那么我们怎么通过代码来了解呢?
首先,创建一个GirlFriend文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface GirlFriend : NSObject

{
NSString *_nativeplace;
NSString *_background;
}


@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) NSUInteger age;
@property (nonatomic,assign) double height;


- (void)AllIvars;
- (void)AllPropertys;
- (void)cooking;
- (void)fitness;
- (void)AllMethods;

@end

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
43
44
45
46
47
48
49
50
51
52
53
54
#import "GirlFriend.h"
#import <objc/message.h>

@implementation GirlFriend

// 获取所有的成员变量
- (void)AllIvars
{
unsigned int outCount = 0;

Ivar *ivars = class_copyIvarList(self.class, &outCount);

for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char* ivarName = ivar_getName(ivar);
NSLog(@"ivarName--%s",ivarName);
}

free(ivars);
}

// 获取所有的property
- (void)AllPropertys
{
unsigned int outCount = 0;

objc_property_t *propertys = class_copyPropertyList(self.class, &outCount);

for (int i = 0; i < outCount; i++) {
objc_property_t property = propertys[i];
const char* propertyName = property_getName(property);
NSLog(@"propertyName--%s",propertyName);
}

free(propertys);
}

// 获取所有方法
- (void)AllMethods
{
unsigned int outCount = 0;

Method *methods = class_copyMethodList(self.class, &outCount);

for (int i = 0; i < outCount; i++) {
Method method = methods[i];
SEL methodName = method_getName(method);
NSLog(@"methodName--%@",NSStringFromSelector(methodName));
}

free(methods);
}

@end
1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char * argv[]) {
@autoreleasepool {

GirlFriend *gf = [[GirlFriend alloc] init];
[gf AllIvars];
[gf AllPropertys];
[gf AllMethods];

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

输出结果:

以上的函数都比较简单.但是很实用,比如快速归档和解档.而且要注意一点,因为runtime是C语言的函数,所以遇到copy创建的对象基本上都要用free()函数来释放.

关联对象的使用

我们可以通过关联对象给女朋友添加属性。首先创建一个女朋友的分类Extension

1
2
3
4
5
#import "GirlFriend.h"
@interface GirlFriend (Extension)
// 随便写的
@property (nonatomic,copy) NSString *personality;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "GirlFriend+Extension.h"
#import <objc/runtime.h>
@implementation GirlFriend (Extension)

- (void)setPersonality:(NSString *)personality
{
// 第一个参数:要给哪个对象添加关联对象
// 第二个参数:关联的key,注意: void* 相当于 OC中的id类型,所以这个key不是固定的
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, @selector(personality), personality, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)personality
{
// 通过key来获取value
return objc_getAssociatedObject(self, @selector(personality));
}

关联对象是比较简单的,用得场景也很多.可以看下第三方库的源码.关联对象很常用,.不仅仅是添加属性.

字典转模型

相信大家都使用过第三方库来实现这个功能,下面我们来了解一下这个功能的具体实现
首先新建一个NSObject分类

1
2
3
4
#import <Foundation/Foundation.h>
@interface NSObject (KeyValue)
+ (instancetype)modelWithDictonary:(NSDictionary *)dict;
@end
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
#import "NSObject+KeyValue.h"
#import <objc/message.h>

@implementation NSObject (KeyValue)


+ (instancetype)modelWithDictonary:(NSDictionary *)dict
{
id instance = [[self alloc] init];
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(self, &outCount);

for (int i = 0 ; i < outCount; i++) {
Ivar ivar = ivars[i];
// 获取成员属性名字
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 去掉下划线
NSString *key = [name substringFromIndex:1];
// 根据key获取value
id value = dict[key];
if (value) { // 如果value有值
[instance setValue:value forKey:key];
}
}
return instance;
}
@end

简单实现了具体功能,而且也解决了字典中的key找不到对应的属性也不会Crash

method swizzling

之前提到了方法调用其实就是发消息给对象,那么当对象接收到了消息之后怎么调用对应的方法呢?

消息机制原理:对象根据方法编号SEL在”方法映射表”中找到对应的方法实现.因为Objective-C是动态语言,具体要调用哪个方法要到运行时才决定。所以,我们可以在运行时把SEL对应的方法实现改变一下.我们可以在不修改源码和继承子类覆写方法就能修改一个类本身的功能。这种方法叫method swizzling(俗称”黑魔法”)

回到GirlFriend.h文件,我们给女朋友添加“做家务”的功能

1
- (void)housework;
1
2
3
4
5
6
7
8
- (void)housework
{
NSLog(@"做家务");
}
- (void)cooking
{
NSLog(@"做饭");
}

需求:让女朋友做饭,她却去做家务

1
2
3
4
5
6
7
8
9
10
11
+ (void)load
{
// 拿到“做饭”方法地址(实例方法,如果要拿到类方法使用class_getClassMethod)
Method cooking = class_getInstanceMethod(self, @selector(cooking));

// 拿到"做家务"方法地址
Method housework = class_getInstanceMethod(self, @selector(housework));

// 交换地址方法
method_exchangeImplementations(housework, cooking);
}

然后随便找个地验证一下

1
2
GirlFriend *gf = [[GirlFriend alloc] init];
[gf cooking]; // 输出结果: 做家务

上面的例子只是简单的交换方法。method swizzling也可以交换系统自带的方法.也能在系统原有的功能里,添加更多的功能。或者迭代开发中处理版本兼容问题也能用到

消息转发机制

上文说了消息传递机制的原理,那么我们可能还会遇到另一种情况,当发送消息给对象的时候,对象不能解析这条消息(找不到对应的方法)会方法什么情况呢?

当一个对象收到不能解析的消息之后,就会启动”消息转发”机制。我们可以在这个过程告诉对象应该怎么处理!

  • 动态方法解析
    对象在接收到无法解读的消息后,首先将调用其所属类的下列类方法
1
+ (BOOL)resolveInstanceMethod:(SEL)sel

其返回值是BOOL类型,表示这个是否能新增一个实例方法来处理此选择子,返回NO的话进入下一步

  • 备援接收者
    当前接收者还有第二次机会能处理不能解析的消息,在这一步中,运行期就会问它,能不能把这条消息转给其他接收者来处理
1
- (id)forwardingTargetForSelector:(SEL)aSelector

如果返回nil的话就进入下一步

  • 完整的消息转发
    如果已经到了这个步骤的话,就会启动”完整的转发机制”,需要创建NSInvocation对象来进行处理(下文详解)。这个步骤会调用下面方法来进行转发消息
1
- (void)forwardInvocation:(NSInvocation *)invocation

看张图片加深理解

模拟场景:如果我们找了一个“女朋友”,谈的时候她跟你说她做饭很好吃!!,等你带回家的时候,发现她根本就不会做饭.这不是坑爹吗?那我们应该怎么办呢?
首先在GirlFriend.m文件 把cooking方法的实现给注释掉.
随便找块地打印

1
2
GirlFriend *gf = [[GirlFriend alloc] init];
[gf cooking];

报错:

1
2
-[GirlFriend cooking]: unrecognized selector sent to instance 0x7fbef2e0b490
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[GirlFriend cooking]: unrecognized selector sent to instance 0x7fbef2e0b490'

报错的原因是:你的女朋友根本就不会做饭..

第一步 让女朋友会做饭

在GrilFriend.m文件

1
2
3
4
5
6
7
8
9
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
class_addMethod(self, sel, (IMP)cooking, "v@:");
return YES;
}
static void cooking(id self,SEL _cmd)
{
NSLog(@"会做饭啦");
}

随便找块地打印

1
2
GirlFriend *gf = [[GirlFriend alloc] init];
[gf cooking]; // 输出结果:会做饭啦

第二步 让别人来做饭

如果你的女朋友死活不肯去做饭呢?那我们请个保姆做呗..
首先新建Nurse文件
.h文件

1
2
3
@interface Nurse : NSObject
- (void)cooking;
@end

.m文件

1
2
3
4
5
6
7
@implementation Nurse

// 保姆必须会做饭
- (void)cooking
{
NSLog(@"保姆去做饭啦");
}

回到GirlFriend.m文件

1
2
3
4
5
6
7
8
9
10
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
Nurse *n = [[Nurse alloc] init];

return n;
}

好了,那我们随便找块地打印验证一下

1
2
GirlFriend *gf = [[GirlFriend alloc] init];
[gf cooking]; // 输出结果:保姆去做饭啦

第三步 完整的消息转发

来到这一步的话,说明就要创建并且处理NSInvocation对象。这个对象用起来比较麻烦。所以一般都会在前两步来处理(最好在第一步)

下面我们来使用一下NSInvocation对象
GirlFriend.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 因为Invocation对象需要一个函数签名来创建,所以,首先会来到这一步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
// 创建函数签名
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sig;
}

// anInvocation:根据返回的函数签名创建的
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setTarget:self];

[anInvocation setSelector:@selector(invocation)];

// 函数调用
[anInvocation invoke];
}

- (void)invocation
{
NSLog(@"通过invocation调用");
}

打印一下

1
2
3
GirlFriend *gf = [[GirlFriend alloc] init];
[gf cooking]; // 输出结果:通过invocation调用

额外补充

1
2
3
4
// 如果方法要传参数的话,可以用这个方法
// 第一个参数:要传的参数
// 第二个参数:参数的位置(注意:要从第2个开始,0和1被self和_cmd占了)
anInvocation setArgument:(nonnull void *) atIndex:(NSInteger)

参考文献

运行时之一:类与对象
Effective Objective C 2.0:编写高质量iOS与OS X代码的52个有效方法
让你快速上手Runtime