Objective-C Runtime 的基本使用

发表于:, 更新于:, By Rm1210
大纲
  1. 1. 用 Associated Objects 为类动态添加属性
    1. 1.1. eg1:给 UIViewController 类添加一个 dynamicProperty 属性
    2. 1.2. eg2:给 UIAlertView 类添加一个处理操作结果的 Block
  2. 2. Method Swizzling
    1. 2.1. eg:看多了 iOS 的例子,我们来看个 OS X 的例子

最近在恶补 Objective-C 的一些知识。这篇文章记录一下 Runtime 的两个常见的使用场合。

用 Associated Objects 为类动态添加属性

其实给类添加属性的说法是错误的,因为不能给类添加实例变量,确切的说法应该是添加键值数据。实现的思路是:

  1. 为你想动态添加属性的类创建一个 Category,在该 Category 中声明想添加的 property。
  2. 在该 Category 的实现中声明 property 为 @dynamic,即声明该 property 为运行时动态存取的。
  3. 实现 property 的 gettersetter

Runtime 中涉及的方法有:

1
2
3
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
id objc_getAssociatedObject(id object, const void *key)
void objc_removeAssociatedObjects(id object)

其中的 policy 参数决定了对应的内存管理语义,大家可以看到熟悉的 RETAINCOPY 等:

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

eg1:给 UIViewController 类添加一个 dynamicProperty 属性

1
2
3
4
// .h
@interface UIViewController (DynamicProperty)
@property (nonatomic, strong) NSObject *dynamicProperty;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// .m
#import <objc/runtime.h>
@implementation UIViewController(DynamicProperty)
@dynamic dynamicProperty;
static void *StaticDynamicPropertyKey = "StaticDynamicPropertyKey";
- (void)setDynamicProperty:(NSObject *)dynamicProperty
{
[self willChangeValueForKey:@"StaticDynamicPropertyKey"];
objc_setAssociatedObject(self, &StaticDynamicPropertyKey, dynamicProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self didChangeValueForKey:@"StaticDynamicPropertyKey"];
}
- (NSObject *)dynamicProperty
{
return objc_getAssociatedObject(self, &StaticDynamicPropertyKey);
}
@end

eg2:给 UIAlertView 类添加一个处理操作结果的 Block

我们把自定义的键值数据声明为属性,可以方便使用。不过实际应用时,我们可以在适当的地方直接调用 runtime 的这些方法来满足我们的需求。此处引用《Effective Objective-C 2.0》书中第 10 条的一个例子:

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
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
};
objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
}
// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
block(buttonIndex);
}

如果一个类里有多个 UIAlertView,一般的做法可能是加 tag 然后在 delegate 方法中用 tag 判断。这样改写后,delegate 方法中不用判断做特殊处理,而且处理操作结果的代码和 UIAlertView 的代码在同一个地方更明了和方便阅读了。

不过正如原书作者的提醒:

因为产生 retain cycle 的原因很难查明,只有在其他做法行不通的时候,才考虑选用 Associated Objects 技术。


Method Swizzling

有时候我们 class-dump 一些私有库,会找到一些我们感兴趣的私有方法,甚至会想更改或者替换这些方法的实现,这时候用 Method Swizzling 就很合适了。一般会涉及到的方法有:

1
2
3
4
Method class_getInstanceMethod(Class cls, SEL name)
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
void method_exchangeImplementations(Method m1, Method m2)

eg:看多了 iOS 的例子,我们来看个 OS X 的例子

之前做 Mac 项目的时候,有自定义窗口背景的需求,就用到了下面的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取要操作的 class
id class = [[[window contentView] superview] class];
// 获取下面自定义的 drawRect:
Method m0 = class_getInstanceMethod([self class], @selector(drawRect:));
// 为这个 class 添加一个(名为 drawRectOriginal: 但是实现为下面自定义的 drawRect:)的方法
class_addMethod(class, @selector(drawRectOriginal:), method_getImplementation(m0), method_getTypeEncoding(m0));
// 获取原生的 drawRect:
Method m1 = class_getInstanceMethod(class, @selector(drawRect:));
// 获取自定义的 drawRect:,但是名字为 drawRectOriginal:
Method m2 = class_getInstanceMethod(class, @selector(drawRectOriginal:));
// 交换这两个方法,这时候这个 class 的 drawRect: 为下面的自定义实现,drawRectOriginal: 为原生的 drawRect: 实现
method_exchangeImplementations(m1, m2);
1
2
3
4
5
6
7
- (void)drawRect:(NSRect)rect
{
// 这里的 drawRectOriginal: 就是系统原来的 drawRect:
[self drawRectOriginal:rect];
...自定义的绘制
}

例子很简单,先用 class_addMethod 方法为 class 添加一个 drawRectOriginal: 方法,然后用 method_exchangeImplementations 方法交换 drawRectOriginal: 和 class 原生的 drawRect:的实现。
第一次看 Method Swizzling 的例子可能会感觉有点绕,因为一般情况我们自定义的方法都会调用一下原生的方法来 “继承” 原生的实现。

The end:
这里粗浅地介绍了两个常见的 runtime 的使用场合,有时间还要好好深入学习一下 runtime 啊!!