Objective-C Runtime

0x00 前言

这篇文章是自己在学习 CFHipsterRef Chapter 2 Objective-C Runtime 时做的笔记。

0x01 libobjc

libobjc 是 Objective-C 2.0 运行时的共享库。

要想使用 runtime,可以导入 <objc/runtime.h> 头文件。

0x02 发送消息(Message Sending)

在 Objective-C 中,调用一个对象的某个方法,一般的写法是这样的:

1
[object message];

然后编译器最后会将该语句翻译成:

1
objc_msgSend(object, @selector(message));

objc_msgSend 有几种形式:

  • objc_msgSend:向一个类发送简单返回值类型的消息
  • objc_msgSend_stret:向一个类发送返回值类型为结构体的消息
  • objc_msgSendSuper:向父类发送简单返回值类型的消息
  • objc_msgSendSuper_stert:向父类发送返回值类型为结构体的消息

0x03 属性元编程(Metaprogramming with Properties)

属性定义了对象状态的公共接口。

例如,访问一个对象的属性列表,我们可以通过属性元编程来避免手动实现 NSCoding

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
#pragma mark - NSCoding

- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
unsigned in count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (NSUInteger i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *key = [NSString stringWithUTF8String:property_getName(property)];
[self setValue:[decoder decodeObjectForKey:key] forKey:key];
}

free(properties);
}

return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
unsigned int count;
objc_property_t *properties = class_getPropertyList([self class], &count);
for (NSUInteger i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *key = [NSString stringWithUTF8String:property_getName(property)];
[coder encodeObject:[self valueForKey:key] forKey:key];
}

free(properties);
}

0x04 关联对象(Associated Objects)

关联对象是 Objective-C 运行时的一个特性,允许开发者对已经存在的类在扩展中添加自定义的属性,这非常有用。

关联对象使用非常简单,用 objc_setAssociatedObject 存储一个关联值,用 objc_getAssociatedObject 获取这个关联值。

任何关联对象都是通过一个 key 来关联的,这个 key 可以是任何常量值。最简单的方法就是传入 getter 方法选择器(方法选择器在运行时保证是唯一且不变的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface NSObject (AssociatedObject)

@property (strong, nonatomic) id associatedObject;

@end

@implementation NSObject (AssociatedObject)

- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}

0x05 动态添加方法(Dynamically Adding a Method)

正如属性描述了一个对象的状态,方法则构成了对象的行为。

在 Objective-C 中,方法以 +- 前置声明,表示该方法是类方法还是实例方法(也就是说是通过类调用的还是类的对象来调用的)。

一个类可以通过创建一个 category 来添加新的方法,然后可以在包含了这个 category 的地方使用这些新的方法。

不过,方法也可以在运行时创建:

1
2
3
4
5
6
7
Class c = [NSObject class];
IMP addingMethodIMP = imp_implementationWithBlock((NSString *)^(id self, id arg1) {
return [NSString stringWithFormat:@"arg1 = %@", arg1];
});

const char *addingMethodTypes = [[NSString stringWithFormat:@"%s%s%s", @encode(id), @encode(id), @encode(SEL)] UTF8String];
class_addMethod(c, @selector(addingMethodWithArg:), addingMethodIMP, addingMethodTypes);

0x06 Method Swizzling

Method Swizzling 是改变一个已存在的方法的实现的过程。这个技术是改变一个映射到一个类的调度表中的底层函数的方法,在运行时修改为调用自己定义的方法。

比如在某些时候想要跟踪一个应用里面的每个 ViewController 在一次生命周期内打开的次数。想要实现这个功能有几种方法:

  1. 每个 ViewController 都添加跟踪代码到重载的 viewDidAppear 方法里面,但是这样会有很多重复的代码。
  2. 子类化 UIViewController,但是这样的话,还要子类化 UITableViewControllerUINavigationController 等其他用到的 ViewController,还是要添加很多重复的代码。

有了 Method Swizzling,解决办法就非常优雅了:

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
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(xxx_viewDidAppear:);

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMthod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling

- (void)xxx_viewDidAppear:(BOOL)animated {
[self xxx_viewDidAppear:animated];
NSLog(@"viewDidAppear: %@", self);
}

@end

现在,当任何 UIViewController 对象或者 它的子类对象调用 viewDidAppear 方法时,都会打印出一个 log。

因为 Method Swizzling 是影响全局状态的,所以尽可能减少竞态条件的可能性非常重要。

+ load 方法保证在类初始化的时候执行,相比之下,+ initialize 不保证什么时候它会被执行(事实上,除非 app 直接对一个类发送消息,否则这个类的 + initialize 方法不会被调用)。

0x07 动态创建一个类(Dynamically Creating a Class)

结合之前提到的动态属性和方法定义,libobjc 的终极大招是运行时创建一个类。

创建一个 Product 类,有两个属性:nameprice

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

@property (readonly) NSString *name;
@property (readonly) double price;

- (instancetype)initWithName:(NSString *)name price:(double)price;

@end

@implementation Product

- (instancetype)initWithName:(NSString *)name price:(double)price {
if (self = [super init]) {
self.name = name;
self.price = price;
}

return self;
}

运行时创建:

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
Class c = objc_allocateClassPair([NSObject class], "Product", 0);
class_addIvar(c, "name", sizeof(id), log2(sizeof(id)), @encode(id));
class_addIvar(c, "price", sizeof(double), log2(sizeof(double)), @encode(double));

Ivar nameIvar = class_getInstanceVariable(c, "name");
ptrdiff_t priceIvarOffset = ivar_getOffset(class_getInstanceVariable(c, "price"));

IMP initIMP = imp_implementationWithBlock(^(id self, NSString *name, double price) {
object_setIvar(self, nameIvar, name);

char *ptr = ((char *)(__bridge void *)self) + priceIvarOffset;
memcpy(ptr, &price, sizeof(price));

return self;
});

const char *initTypes = [[NSString stringWithFormat:@"%s%s%s%s%s%s", @encode(id), @encode(id), @encode(SEL), @encode(id), @encode(id), @encode(NSUInteger)] UTF8String];
class_addMethod(c, @selector(initWithFirstName:lastName:age), initIMP, initTypes);

IMP nameIMP = imp_implementationWithBlock(^(id self) {
return object_getIvar(self, nameIvar);
});
const char *nameTypes = [[NSString stringWithFormat:@"%s%s%s", @encode(id), @encode(id), @encode(SEL)] UTF8String];
class_addMethod(c, @selector(name), nameIMP, nameTypes);

IMP priceIMP = imp_implementationWithBlock(^(id self) {
char *ptr = ((char *)(__bridge void *)self) + priceIvarOffset;
double price;
memcpy(&price, ptr, sizeof(price));

return price;
});
const char *priceTypes = [[NSString stringWithFormat:@"%s%s%s", @encode(double), @encode(id), @encode(SEL)] UTF8String];
class_addMethod(c, @selector(age), priceIMP, priceTypes);

objc_registerClassPair(c);

这里代码很多,我们来一点一点说。

首先,一个类用 objc_allocateClassPair 被分配了内存空间,指定了这个类的父类和类名。

然后,使用 class_addIvar 给这个类添加了实例变量。第四个参数用于确定变量的最小对齐方式(variable’s minimum alignment),这取决于 ivar 的类型和目标平台架构。

接下来是用 imp_implementationWithBlock: 定义初始化函数(initializer)的实现。调用 object_setIvar 设置 nameprice 是通过之前计算的 offset 执行 memcpy 来设置的。

为了添加初始化函数,需要计算每个参数的类型编码。@encode 和 字符串插值有点尴尬混乱,但是就是这样的。

添加 ivar 的 getter 方法类似。

最后,一旦所有的方法都被添加,类就被注册到运行时。从那时起,Product 就可以像其他任何 Objective-C 类一样交互了:

1
2
Product *widget = [[Product alloc] initWithName:@"Widget" price:50.00];
NSLog(@"%@: %g", widget.name, widget,price);
打赏点钱买猫粮,喂流浪猫去~