首页 iOS开发:Runtime
文章
取消

iOS开发:Runtime

在Apple的官方文档中,runtime有以下定义:

Objective-C runtime is the collection of functions that are responsible for the dynamic behavior of an Objective-C program.

翻译过来就是:Objective-C运行时(runtime)是一组函数,负责Objective-C程序的动态行为。

Runtime 是 Objective-C 的运行时系统,是一组 C 函数和数据结构,主要负责在程序运行时处理对象、类、方法等信息。它是 Objective-C 实现的核心之一,也是 iOS 开发中不可或缺的一部分。

在 Objective-C 中,消息传递机制是一种很重要的特性。当我们向一个对象发送消息时,Runtime 会根据对象所属的类以及方法名来查找对应的方法。如果找到了方法,则执行该方法;如果没有找到方法,则进行消息转发。

除此之外,Runtime 还提供了一些强大的功能,例如:

  • 动态创建对象、类、方法
  • 替换或交换方法的实现
  • 添加属性、变量等成员
  • 修改类继承关系等

Objective-C Runtime的核心组成部分

Objective-C 的 Runtime 由以下几个核心部分组成:

Class 和 Object

Class 是 Objective-C 中表示类的数据类型,它包含了所有实例方法和类方法的定义信息。每个 Class 对象都有一个 isa 指针,指向其元类(Meta Class)。Object 则是所有类的根类,在 Objective-C 程序中,所有对象都是 Object 类或其子类的实例。

Method 和 SEL

Method 是 Objective-C 中表示方法的数据类型,它包含了方法名、返回值类型、参数类型及方法实现等信息。SEL 则是方法名的一个替代符号,它是一个在运行时被注册的唯一标识符。

Ivar 和 IMP

Ivar 是 Objective-C 中表示实例变量的数据类型,它包含了变量名、类型等信息。IMP(Implementation)则是方法实现的函数指针,它指向了方法的具体实现。

以上这些组件都是在 Runtime 库中实现的,并且可以通过 Runtime API 来进行访问和操作。

Objective-C消息传递机制

Objective-C的消息传递机制是指从发送消息到接收消息的整个过程。在Objective-C中,方法调用都是通过消息传递来实现的。当我们调用一个方法时,实际上是向该对象发送了一个消息,然后该对象接收到消息后会根据消息中的方法名来执行相应的方法实现。

Objective-C消息传递机制的核心是Selector和IMP。当我们向一个对象发送消息时,实际上是将消息中的方法名转换为一个Selector对象,然后再将Selector对象与该对象进行匹配,找到对应的IMP,并执行该IMP。

动态创建对象和类

在Objective-C中,类的创建是在运行时完成的。当我们使用一个类时,Objective-C Runtime会自动为该类创建一个Class对象并初始化它,然后将该Class对象与该类的名称进行关联。下面是Objective-C类的创建过程:

  1. 调用 objc_allocateClassPair 函数,并传入需要创建的类的名称和父类的Class对象。这个函数会创建一个新的Class对象并返回。
  2. 使用 class_addMethod 函数向该Class对象添加方法。
  3. 使用 class_addIvar 函数向该Class对象添加实例变量。
  4. 使用 objc_registerClassPair 函数注册该Class对象,使其能够被Objective-C Runtime使用。
1
2
3
4
5
6
7
8
9
// 普通方式创建对象obj
id obj = [[NSObject alloc] init];

// 通过 Runtime API 创建对象myObj
// 首先使用 objc_allocateClassPair 函数创建一个新类 MyClass,并指定其父类为 NSObject。然后调用 objc_registerClassPair 函数注册该类,使其可用于实例化对象。最后使用该类的 alloc 和 init 方法创建一个新对象 myObj。
Class cls = objc_allocateClassPair([NSObject class], "MyClass", 0);
objc_registerClassPair(cls);

id myObj = [[cls alloc] init];

替换方法实现

通过 Runtime API 可以动态地替换方法的实现。我们定义一个名为 myMethod 的方法,在运行时获取该方法的实现并与另一个名为 newMethod 的方法实现进行交换。这样,当我们调用 myMethod 方法时,实际上调用的是 newMethod 方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation MyClass

- (void)myMethod {
  // 原始方法实现
}

@end

MyClass *obj = [[MyClass alloc] init];

Method m1 = class_getInstanceMethod([MyClass class], @selector(myMethod));
Method m2 = class_getInstanceMethod([MyClass class], @selector(newMethod));

method_exchangeImplementations(m1, m2);

[obj myMethod]; // 调用的实际上是 newMethod 方法的实现

添加属性和变量

Objective-C 中的对象是动态的,可以在运行时添加新的成员变量或属性。使用 Runtime API 可以实现添加属性和变量的功能。我们首先定义一个名为 name 的属性,然后使用 class_addProperty 函数将该属性添加到类中。接着使用 class_getInstanceVariable 函数获取该属性所对应的实例变量 _name,并使用 object_setIvar 函数设置其值为 @”John”。最后使用 obj.name 获取该属性的值并输出。

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

@property (nonatomic, strong) NSString *name;

@end

@implementation MyObject

@end

MyObject *obj = [[MyObject alloc] init];

objc_property_attribute_t type = { "T", "@\"NSString\"" };
objc_property_attribute_t ownership = { "C", "" };
objc_property_attribute_t backingivar = { "V", "_name" };
objc_property_attribute_t attrs[] = { type, ownership, backingivar };
class_addProperty([MyObject class], "name", attrs, 3);

Ivar ivar = class_getInstanceVariable([MyObject class], "_name");
object_setIvar(obj, ivar, @"John");
NSLog(@"%@", obj.name); // 输出 "John"

Hook

Hook 是指在运行时替换原始方法实现的过程,可以用于修改方法的行为或者增加一些额外的逻辑。比如,我们可以使用 Hook 在原始方法执行前后打印日志信息。我们定义名为 myMethod 和 newMethod 的两个方法,前者为原始方法实现,后者为 Hook 实现。然后使用 method_exchangeImplementations 函数交换这两个方法的实现。当我们调用 myMethod 方法时,实际上调用的是 newMethod 方法的实现,从而实现了 Hook 的目的。

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
@implementation MyClass

- (void)myMethod {
  NSLog(@"original myMethod");
}

@end

@implementation MyClass (Hook)

- (void)newMethod {
  NSLog(@"hooked myMethod before");
  [self newMethod];
  NSLog(@"hooked myMethod after");
}

@end

MyClass *obj = [[MyClass alloc] init];

Method originalMethod = class_getInstanceMethod([MyClass class], @selector(myMethod));
Method hookedMethod = class_getInstanceMethod([MyClass class], @selector(newMethod));
method_exchangeImplementations(originalMethod, hookedMethod);

[obj myMethod]; // 输出 "hooked myMethod before", "original myMethod", "hooked myMethod after"

消息转发

当向一个对象发送消息时,如果该对象无法响应该消息,则会触发消息转发机制。消息转发机制允许我们在运行时动态地处理未知消息。定义一个名为 unknownMethod 的方法,并向一个对象发送该消息。由于该对象无法响应该消息,因此触发了消息转发机制。在 forwardInvocation 方法中,我们可以自定义处理未知消息的逻辑。在 methodSignatureForSelector 方法中,我们可以返回指定方法选择器所对应的方法签名。

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

@end

@implementation MyClass

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  NSLog(@"forwardInvocation: %@", anInvocation);
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  if ([NSStringFromSelector(aSelector) isEqualToString:@"unknownMethod"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  }
  return [super methodSignatureForSelector:aSelector];
}

@end

MyClass *obj = [[MyClass alloc] init];

[obj performSelector:@selector(unknownMethod)]; // 输出 "forwardInvocation:"

关联对象

关联对象是一种将任意对象与另一个对象关联起来的机制,可以用于给对象添加存储空间。使用 Runtime API 可以实现关联对象的功能。我们定义一个名为 kAssociatedObjectKey 的静态字符变量,并使用 objc_setAssociatedObject 函数将一个 NSString 对象与 obj 关联起来。然后使用 objc_getAssociatedObject 函数获取关联对象并输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface MyObject : NSObject

@end

@implementation MyObject

@end

MyObject *obj = [[MyObject alloc] init];

static char kAssociatedObjectKey;
NSString *associatedObject = @"associated object";
objc_setAssociatedObject(obj, &kAssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

NSString *getAssociatedObject = objc_getAssociatedObject(obj, &kAssociatedObjectKey);
NSLog(@"%@", getAssociatedObject); // 输出 "associated object"

Runtime的应用场景

模型转换

在 iOS 开发中,经常需要将数据模型转换为字典或者 JSON 数据。使用 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
@interface MyModel : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

@implementation MyModel

@end

@implementation MyModel (JSON)

- (NSDictionary *)toDictionary {
  NSMutableDictionary *dict = [NSMutableDictionary dictionary];
  unsigned int count = 0;
  objc_property_t *properties = class_copyPropertyList([self class], &count);
  for (int i = 0; i < count; i++) {
    const char *name = property_getName(properties[i]);
    NSString *propertyName = [NSString stringWithUTF8String:name];
    id value = [self valueForKey:propertyName];
    if (value != nil) {
      [dict setObject:value forKey:propertyName];
    }
  }
  free(properties);
  return dict;
}

@end

MyModel *model = [[MyModel alloc] init];
model.name = @"John";
model.age = 20;

NSDictionary *dict = [model toDictionary];
NSLog(@"%@", dict); // 输出 "{ age = 20; name = John; }"

字典转模型

与模型转换类似,我们也可以使用 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
@interface MyModel : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

@implementation MyModel

+ (instancetype)modelWithDictionary:(NSDictionary *)dict {
    MyModel *model = [[MyModel alloc] init];
    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList([model class], &count);
    for (int i = 0; i < count; i++) {
        const char *name = property_getName(properties[i]);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        id value = [dict objectForKey:propertyName];
        if (value != nil) {
            [model setValue:value forKey:propertyName];
        }
    }
    free(properties);
    return model;
}

@end



NSDictionary *dict = @{
    @"name": @"John",
    @"age": @20
};

MyModel *model = [MyModel modelWithDictionary:dict];
NSLog(@"%@, %ld", model.name, (long)model.age); // 输出 "John, 20"

利用 Runtime 实现 KVO

在 iOS 中,KVO(键值观察)是一种常见的编程技术,可以用于观察对象的属性变化并在发生变化时执行相应的操作。利用 Runtime 技术可以实现自己的 KVO 功能,例如:

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
@interface MyObject : NSObject

@property (nonatomic, strong) NSString *name;

@end


@implementation MyObject

@end


static void *kNameObservationContext = &kNameObservationContext;

@interface MyObserver : NSObject

@property (nonatomic, weak) MyObject *obj;
@property (nonatomic, copy) void (^handler)(NSString *);

@end


@implementation MyObserver

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
  if (context == kNameObservationContext && [keyPath isEqualToString:@"name"]) {
    NSString *newName = [change objectForKey:NSKeyValueChangeNewKey];
    self.handler(newName);
  } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  }
}

- (void)dealloc {
  [self.obj removeObserver:self forKeyPath:@"name" context:kNameObservationContext];
}

@end

MyObject *obj = [[MyObject alloc] init];

MyObserver *observer = [[MyObserver alloc] init];
observer.obj = obj;
observer.handler = ^(NSString *newName){
  NSLog(@"new name: %@", newName);
};

[obj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kNameObservationContext];

obj.name = @"John"; // 输出 "new name: John"

Block

Block 是一种封装了函数及其执行上下文的对象,可以用于实现闭包和回调等功能。在 Objective-C 中,使用 Block 可以方便地实现异步任务、事件响应等场景。

Block 是一种匿名函数,可以在方法内部或外部定义,并具有自身的实现代码和作用域。Block 通常在以下场景中使用:

  • 实现回调函数或委托
  • 将代码块传递给异步执行的 API
  • 简化函数嵌套
  • 创建弱引用和避免循环引用

在 Objective-C 中,Block 可以使用 ^ 符号进行声明。其中,return_type 表示返回值类型,parameters 表示参数列表,implementation 表示 Block 的实现代码块。例如,下面是一个简单的 Block 示例:

1
2
3
4
5
6
int (^multiply)(int, int) = ^(int a, int b) {
  return a * b;
};

int result = multiply(2, 3);
NSLog(@"%d", result); // 输出 "6"

从 Runtime 的角度来看,Block 实际上是一种结构体,包含了函数指针和捕获的变量等信息。在底层实现中,Block 主要通过三个部分来组成:

  1. isa 指针:指向 Block 类型对象。
  2. flags:标志位,用于表示 Block 的属性信息。
  3. invoke 函数指针:指向 Block 内部实现代码的函数指针。

其中,isa 指针和 flags 部分与 Objective-C 对象非常类似,但是 Block 在底层存储时并不是一个真正的对象,而是作为一个内存块存放在栈或者堆上。它的存储位置取决于 Block 的使用方式和生命周期。

对于 Block 的实现代码,其主要通过捕获变量的方式来获取执行上下文,并将这些变量封装到 Block 对象内部。Block 可以捕获三种类型的变量:

  • 局部变量(auto variable)
  • 静态变量(static variable)
  • 全局变量(global variable)

另外,Block 在捕获变量时,会对局部变量和静态变量进行存储和复制操作,确保在 Block 被拷贝或移动时仍然可以访问到这些变量;而对于全局变量,则直接引用其地址即可。这些操作也涉及到了 Runtime 技术。

除此之外,在使用 Block 时我们还可以通过 runtime 中的函数进行相关操作,例如:

  • Block_copy: 复制一个 Block 对象
  • Block_release: 释放一个 Block 对象
  • Block_object_assign: 将一个对象赋值给 Block 的变量
  • Block_object_dispose: 释放 Block 的变量

当 Block 从栈上拷贝至堆上时,会触发 Block 的复制操作,此时会调用 copy 或者 Block 内部的 _Block_copy 函数。在复制过程中,Block 会将其捕获的变量一并复制到堆上,并将内部的 isa 指针指向 _NSConcreteMallocBlock 类型对象。当 Block 释放时,也会调用 release 或者 _Block_release 函数。若 Block 对象是在全局区创建的,则不会被复制或释放,因为其生命周期与程序相同。

本文由作者按照 CC BY 4.0 进行授权