Objective-C 关联对象与 Method Swizzling

关联对象

关联对象,顾名思义,即通过唯一键(key)连接(关联)至某个类的实例上的对象。

那么什么时候会用到关联对象呢?

比如,我们需要对内置类 NSArray 添加一个属性(不使用继承)。如何解决?分类似乎只能添加方法。当我们了解关联对象后,就可以轻松实现。

关联对象基础

设置关联对象

1
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

参数说明:

  • object: 与谁关联,通常是 self
  • key: 唯一键,在获取值时通过该键获取,通常是使用 static const void * 来声明
  • value: 关联所设置的值
  • policy: 内存管理策略

内存管理策略

1
2
3
4
5
6
7
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0, // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401, // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403 // 表示关联对象copy,不是线程安全的
};

当对象释放时,会根据设置关联对象时采用的策略来决定是否释放关联对象。当策略为 RETAIN/COPY 时,释放关联对象。当策略为 ASSIGN 时,不释放关联对象。

获取关联对象

1
id objc_getAssociatedObject(id object, const void *key)

参数说明:

  • object: 与谁关联,通常是传 self,在设置关联时所指定的与哪个对象关联的那个对象
  • key: 唯一键,在设置关联值所指定的键

取消关联对象

1
void objc_removeAssociatedObjects(id object)

取消对象的所有关联对象。如果要取消指定的关联对象,可使用 setAssociatedObject 设置为 nil 来实现。

关联对象应用

UIViewController 添加一个是否需要登录的属性。

1
2
3
4
5
@interface UIViewController (Extension)

@property (nonatomic, assign) BOOL needToLogin;

@end

1
2
3
4
5
6
7
8
9
static const char *ViewControllerNeedToLoginKey = "ViewControllerNeedToLoginKey";

- (void)setNeedToLogin:(BOOL)needToLogin {
objc_setAssociatedObject(self, ViewControllerNeedToLoginKey, @(needToLogin), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)needToLogin {
return [objc_getAssociatedObject(self, ViewControllerNeedToLoginKey) boolValue];
}

Method Swizzling

Method Swizzling,顾名思义,就是将两个方法的实现交换。

那么什么时候会用到 Method Swizzling 呢?

比如,在开发中,我们可能会遇到系统提供的 API 不能满足实际需求。我们希望能够修改它以达到期望的效果。

Method Swizzling 原理

Method Swizzling 的实现充分利用了 Objective-C runtime 动态绑定机制

在 Objective-C 中调用方法,其实是向一个对象发送消息,而查找消息的唯一依据是方法名 selector。每个类都有一个方法列表 objc_method_list,存放着其所有的方法 objc_method

1
2
3
4
5
6
7
typedef struct objc_method *Method

struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

每个方法 objc_method 保存了方法名(SEL)和方法实现(IMP)的映射关系。Method Swizzling 其实就是重置了 SELIMP 的映射关系。如下图所示:

Method Swizzling 基础

获取方法

1
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

参数说明:

  • cls: 目标类
  • name: 方法名

获取方法实现

1
IMP _Nonnull class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) 

添加方法

1
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

参数说明:

  • cls: 目标类
  • name: 要添加方法的方法名
  • imp: 要添加方法的方法实现
  • types: 方法实现的编码类型

替换方法

1
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

参数说明:

  • cls: 目标类
  • name: 目标方法的方法名
  • imp: 方法的新方法实现
  • types: 方法实现的编码类型

交换方法实现

1
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

获取方法的编码类型

1
const char * _Nullable method_getTypeEncoding(Method _Nonnull m) 

Method Swizzling 应用

通过分类允许 NSObject 对任意两个方法进行 Method Swizzling。

1
2
3
4
5
6
@interface NSObject (Swizzle)

+ (BOOL)swizzleMethod:(SEL)originalSEL withMethod:(SEL)targetSEL error:(NSError **)error;
+ (BOOL)swizzleClassMethod:(SEL)originalSEL withMethod:(SEL)targetSEL error:(NSError **)error;

@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
@implementation NSObject (Swizzle)

+ (BOOL)swizzleMethod:(SEL)originalSEL withMethod:(SEL)targetSEL error:(NSError *__autoreleasing *)error {
Method originalMethod = class_getInstanceMethod(self, originalSEL);
if (originalMethod == nil) {
return NO;
}

Method targetMethod = class_getInstanceMethod(self, targetSEL);
if (targetMethod == nil) {
return NO;
}

class_addMethod(self, originalSEL, class_getMethodImplementation(self, originalSEL), method_getTypeEncoding(originalMethod));
class_addMethod(self, targetSEL, class_getMethodImplementation(self, targetSEL), method_getTypeEncoding(targetMethod));
method_exchangeImplementations(class_getInstanceMethod(self, originalSEL), class_getInstanceMethod(self, targetSEL));

return YES;
}

+ (BOOL)swizzleClassMethod:(SEL)originalSEL withMethod:(SEL)targetSEL error:(NSError *__autoreleasing *)error {
Class metaClass = object_getClass((id)self);
return [metaClass swizzleMethod:originalSEL withMethod:targetSEL error:error];
}

参考

  1. iOS runtime实战应用:Method Swizzling
  2. iOS runtime实战应用:关联对象
  3. iOS runtime实战应用:Method Swizzling
  4. iOS运行时(Runtime)详解+Demo