Runtime应用之关联对象和MethodSwizzling

最近用到了sunnyxx的forkingdog系列(UIView-FDCollapsibleConstraints),纪录下关联对象和MethodSwizzling在实际场景中的应用。

基本概念

关联对象

  • 关联对象操作函数

    • 设置关联对象:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 设置关联对象
    *
    * @param object 源对象
    * @param key 关联对象的key
    * @param value 关联的对象
    * @param policy 关联策略
    */
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    • 获取关联对象:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 获取关联对象
    *
    * @param object 源对象
    * @param key 关联对象的key
    *
    * @return 关联的对象
    */
    id objc_getAssociatedObject(id object, const void *key)

其中设置关联对象的策略有以下5种:

  • 和MRC的内存操作retain、assign方法效果差不多
    • 比如设置的关联对象是一个UIView,并且这个UIView已经有父控件时,可以使用OBJC_ASSOCIATION_ASSIGN
1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN				// 对关联对象进行弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC // 对关联对象进行强引用(非原子)
OBJC_ASSOCIATION_COPY_NONATOMIC // 对关联对象进行拷贝引用(非原子)
OBJC_ASSOCIATION_RETAIN // 对关联对象进行强引用
OBJC_ASSOCIATION_COPY // 对关联对象进行拷贝引用

关联对象在一些第三方框架的分类中常常见到,这里在分析前先看下分类的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct category_t {
// 类名
const char *name;
// 类
classref_t cls;
// 实例方法
struct method_list_t *instanceMethods;
// 类方法
struct method_list_t *classMethods;
// 协议
struct protocol_list_t *protocols;
// 属性
struct property_list_t *instanceProperties;
};

从以上的分类结构,可以看出,分类中是不能添加成员变量的,也就是Ivar类型。所以,如果想在分类中存储某些数据时,关联对象就是在这种情况下的常用选择。

需要注意的是,关联对象并不是成员变量,关联对象是由一个全局哈希表存储的键值对中的值。

全局哈希表的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { spinlock_lock(&_lock); }
~AssociationsManager() { spinlock_unlock(&_lock); }

AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};

其中的AssociationsHashMap就是那个全局哈希表,而注释中也说明的很清楚了:哈希表中存储的键值对是(源对象指针 : 另一个哈希表)。而这个value,即ObjectAssociationMap对应的哈希表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// hash_map和unordered_map是模版类
// 查看源码后可以看出AssociationsHashMap的key是disguised_ptr_t类型,value是ObjectAssociationMap *类型
// ObjectAssociationMap的key是void *类型,value是ObjcAssociation类型

#if TARGET_OS_WIN32
typedef hash_map ObjectAssociationMap;
typedef hash_map AssociationsHashMap;
#else
typedef ObjcAllocator > ObjectAssociationMapAllocator;
class ObjectAssociationMap : public std::map {
public:
void *operator new(size_t n) { return ::_malloc_internal(n); }
void operator delete(void *ptr) { ::_free_internal(ptr); }
};
typedef ObjcAllocator > AssociationsHashMapAllocator;

class AssociationsHashMap : public unordered_map {
public:
void *operator new(size_t n) { return ::_malloc_internal(n); }
void operator delete(void *ptr) { ::_free_internal(ptr); }
};
#endif

其中的ObjectAssociationMap就是value的类型。同时,也可以知道ObjectAssociationMap的键值对类型为(关联对象对应的key : 关联对象),也就是函数objc_setAssociatedObject的对应的key:value参数。

大部分情况下,关联对像会使用getter方法的SEL当作key(getter方法中可以这样表示:_cmd)。

更多和关联对象有关的底层信息,可以查看Dive into Category

MethodSwizzling

MethodSwizzling主要原理就是利用runtime的动态特性,交换方法对应的实现,也就是IMP
通常,MethodSwizzling的封装为:

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
+ (void)load
{
// 源方法--原始的方法
// 目的方法--我们自己实现的,用来替换源方法

static dispatch_once_t onceToken;
// MethodSwizzling代码只需要在类加载时调用一次,并且需要线程安全环境
dispatch_once(&onceToken, ^{
Class class = [self class];

// 获取方法的SEL
SEL origionSel = @selector(viewDidLoad);
SEL swizzlingSel = @selector(tpc_viewDidLoad);
// IMP origionMethod = class_getMethodImplementation(class, origionSel);
// IMP swizzlingMethod = class_getMethodImplementation(class, swizzlingSel);
// 根据SEL获取对应的Method
Method origionMethod = class_getInstanceMethod(class, origionSel);
Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSel);

// 向类中添加目的方法对应的Method
BOOL hasAdded = class_addMethod(class, origionSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));

// 交换源方法和目的方法的Method方法实现
if (hasAdded) {
class_replaceMethod(class, swizzlingSel, method_getImplementation(origionMethod), method_getTypeEncoding(origionMethod));
} else {
method_exchangeImplementations(origionMethod, swizzlingMethod);
}
});
}

为了便于区别,这里列出Method的结构:

1
2
3
4
5
6
7
8
9
typedef struct method_t *Method;

// method_t
struct method_t {
SEL name;
const char *types;
IMP imp;
...
}

实现MethodSwizzling需要了解的有以下几个常用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name )

// 返回方法描述
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 )

// 返回方法的实现
IMP method_getImplementation ( Method m );

// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );

// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );

介绍MethodSwizzling的文章很多,更多和MethodSwizzling有关的信息,可以查看Objective-C的hook方案(一): Method Swizzling

针对UIView-FDCollapsibleConstraints的应用

UIView-FDCollapsibleConstraints是sunnyxx阳神写的一个UIView分类,可以实现仅在IB中对UIView上的约束进行设置,就达到以下效果,而不需要编写改变约束的代码:(图片来源[UIView-FDCollapsibleConstraints]




源代码解析

  • 实现思路
    • 将需要和view关联且需要动态修改的约束添加进一个特定的数组里面
    • 根据view的内容是否为nil,对特定数组中的约束值进行统一设置
  • 头文件
    • IBOutletCollection表示xib中的相同的控件连接到一个数组中(介绍链接
      • 这里表示将NSLayoutConstraint控件添加到fd_collapsibleConstraints数组中
      • IBOutletCollectionh和IBOutlet操作方式一样,需要在IB中进行相应的拖拽才能把对应的控件加到数组中(UIView->NSLayoutConstraint
      • 设置了IBOutletCollection之后,当从storybooard或者xib中加载时,根据KVC原理最终会调用fd_collapsibleConstraints的setter方法,然后就可以在其setter方法中做相应的操作了
    • IBInspectable 表示这个属性可以在IB中更改,如下图
      • 还有一个这里没用,IB_DESIGNABLE,这个表示可以在IB中实时显示修改的效果
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
@interface UIView (FDCollapsibleConstraints)

/// Assigning this property immediately disables the view's collapsible constraints'
/// by setting their constants to zero.
@property (nonatomic, assign) BOOL fd_collapsed;

/// Specify constraints to be affected by "fd_collapsed" property by connecting in
/// Interface Builder.
@property (nonatomic, copy) IBOutletCollection(NSLayoutConstraint) NSArray *fd_collapsibleConstraints;

@end

@interface UIView (FDAutomaticallyCollapseByIntrinsicContentSize)

/// Enable to automatically collapse constraints in "fd_collapsibleConstraints" when
/// you set or indirectly set this view's "intrinsicContentSize" to {0, 0} or absent.
///
/// For example:
/// imagesView.images = nil;
/// label.text = nil, label.text = @"";
///
/// "NO" by default, you may enable it by codes.
@property (nonatomic, assign) BOOL fd_autoCollapse;

/// "IBInspectable" property, more friendly to Interface Builder.
/// You gonna find this attribute in "Attribute Inspector", toggle "On" to enable.
/// Why not a "fd_" prefix? Xcode Attribute Inspector will clip it like a shit.
/// You should not assgin this property directly by code, use "fd_autoCollapse" instead.
@property (nonatomic, assign, getter=fd_autoCollapse) IBInspectable BOOL autoCollapse;
  • _FDOriginalConstantStorage
    • 在这个分类中,给NSLayoutConstraint约束关联一个存储约束初始值的浮点数,以便在修改约束值后,可以还原
      • objc_setAssociatedObject 设置关联对象
      • objc_getAssociatedObject 获取关联对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// A stored property extension for NSLayoutConstraint's original constant.
@implementation NSLayoutConstraint (_FDOriginalConstantStorage)

// 给NSLayoutConstraint关联一个初始约束值
- (void)setFd_originalConstant:(CGFloat)originalConstant
{
objc_setAssociatedObject(self, @selector(fd_originalConstant), @(originalConstant), OBJC_ASSOCIATION_RETAIN);
}

- (CGFloat)fd_originalConstant
{
#if CGFLOAT_IS_DOUBLE
return [objc_getAssociatedObject(self, _cmd) doubleValue];
#else
return [objc_getAssociatedObject(self, _cmd) floatValue];
#endif
}

@end
  • FDCollapsibleConstraints
    • 实现fd_collapsibleConstraints属性的setter和getter方法 (关联一个存储约束的对象)
      • getter方法中创建关联对象constraints(和懒加载的方式类似,不过不是创建成员变量)
      • setter方法中设置约束的初始值,并添加进关联对象constraints中,方便统一操作
    • 从IB中关联的约束,根据KVC地层原理,最终会调用setFd_collapsibleConstraints:方法,也就是这一步不需要手动调用,系统自己完成(在awakeFromNib之前完成IB这些值的映射)
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
- (NSMutableArray *)fd_collapsibleConstraints
{
// 获取对象的所有约束关联值
NSMutableArray *constraints = objc_getAssociatedObject(self, _cmd);
if (!constraints) {
constraints = @[].mutableCopy;
// 设置对象的所有约束关联值
objc_setAssociatedObject(self, _cmd, constraints, OBJC_ASSOCIATION_RETAIN);
}

return constraints;
}

// IBOutletCollection表示xib中的相同的控件连接到一个数组中
// 因为设置了IBOutletCollection,所以从xib使用KVC加载时,最终会调用set方法
// 然后就来到了这个方法
- (void)setFd_collapsibleConstraints:(NSArray *)fd_collapsibleConstraints
{
// Hook assignments to our custom `fd_collapsibleConstraints` property.
// 返回保存原始约束的数组,使用关联对象
NSMutableArray *constraints = (NSMutableArray *)self.fd_collapsibleConstraints;

[fd_collapsibleConstraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
// Store original constant value
// 保存原始的约束
constraint.fd_originalConstant = constraint.constant;
[constraints addObject:constraint];
}];
}
  • 使用Method Swizzling交换自己的和系统的-setValue:forKey:方法
    • 实现自己的KVC的-setValue:forKey:方法
    • Method Swizzling的完全体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    + (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL origionSel = @selector(viewDidLoad);
SEL swizzlingSel = @selector(tpc_viewDidLoad);
// IMP origionMethod = class_getMethodImplementation(class, origionSel);
// IMP swizzlingMethod = class_getMethodImplementation(class, swizzlingSel);
Method origionMethod = class_getInstanceMethod(class, origionSel);
Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSel);

BOOL hasAdded = class_addMethod(class, origionSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));

if (hasAdded) {
class_replaceMethod(class, swizzlingSel, method_getImplementation(origionMethod), method_getTypeEncoding(origionMethod));
} else {
method_exchangeImplementations(origionMethod, swizzlingMethod);
}
});
}
  • 这一步作者的意思是这种类型的IBOutlet不会触发其setter方法,但是经过测试,注释掉这段代码后,系统还是自己触发了setter方法,说明这种IBOutlet还是可以触发setter方法的。所以,即使没有这一段代码,应该也是可行的


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
   #pragma mark - Hacking KVC

// load先从原类,再调用分类的开始调用
// 也就是调用的顺序是
// 原类
// FDCollapsibleConstraints
// FDAutomaticallyCollapseByIntrinsicContentSize
// 所以并不冲突

+ (void)load
{
// Swizzle setValue:forKey: to intercept assignments to `fd_collapsibleConstraints`
// from Interface Builder. We should not do so by overriding setvalue:forKey:
// as the primary class implementation would be bypassed.
SEL originalSelector = @selector(setValue:forKey:);
SEL swizzledSelector = @selector(fd_setValue:forKey:);

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

method_exchangeImplementations(originalMethod, swizzledMethod);
}


// xib也就是xml,再加载进行decode时,会调用setValue:forKey:,把他的方法替换成自身的,然后获取添加的约束
// 不使用重写这个KVC方法的方式,是因为这样会覆盖view本身在这个方法中进行的操作

- (void)fd_setValue:(id)value forKey:(NSString *)key
{
NSString *injectedKey = [NSString stringWithUTF8String:sel_getName(@selector(fd_collapsibleConstraints))];

if ([key isEqualToString:injectedKey]) {
// This kind of IBOutlet won't trigger property's setter, so we forward it.
// 作者的意思是,IBOutletCollection不会触发对应属性的setter方法,所以这里执行手动调用
self.fd_collapsibleConstraints = value;
} else {
// Forward the rest of KVC's to original implementation.
[self fd_setValue:value forKey:key];
}
}
  • 设置对应的约束值
    • 注意,这里只要传入的是YES,那么,这个UIView对应的存入constraints关联对象的所有约束,都会置为0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma mark - Dynamic Properties

- (void)setFd_collapsed:(BOOL)collapsed
{
[self.fd_collapsibleConstraints enumerateObjectsUsingBlock:
^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
if (collapsed) {
// 如果view的内容为nil,则将view关联的constraints对象所有值设置为0
constraint.constant = 0;
} else {
// 如果view的内容不为nil,则将view关联的constraints对象所有值返回成原值
constraint.constant = constraint.fd_originalConstant;
}
}];
// 设置fd_collapsed关联对象,供自动collapsed使用
objc_setAssociatedObject(self, @selector(fd_collapsed), @(collapsed), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)fd_collapsedFDAutomaticallyCollapseByIntrinsicContentSize{
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
@end
  • FDAutomaticallyCollapseByIntrinsicContentSize
    • 使用Method Swizzling交换自己的和系统的-fd_updateConstraints方法
    • [self fd_updateConstraints]调用的是self的updateConstraints方法,fd_updateConstraints和updateConstraints方法的Method(映射SEL和IMP)已经调换了
    • intrinsicContentSize(控件的内置大小)默认为UIViewNoIntrinsicMetric,当控件中没有内容时,调用intrinsicContentSize返回的即为默认值介绍链接
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
 #pragma mark - Hacking "-updateConstraints"

+ (void)load
{
// Swizzle to hack "-updateConstraints" method
SEL originalSelector = @selector(updateConstraints);
SEL swizzledSelector = @selector(fd_updateConstraints);

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

method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)fd_updateConstraints
{
// Call primary method's implementation
[self fd_updateConstraints];

if (self.fd_autoCollapse && self.fd_collapsibleConstraints.count > 0) {

// "Absent" means this view doesn't have an intrinsic content size, {-1, -1} actually.
const CGSize absentIntrinsicContentSize = CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);

// 当设置控件显示内容为nil时,计算出来的contentSize和上面的相等
// Calculated intrinsic content size
const CGSize contentSize = [self intrinsicContentSize];

// When this view doesn't have one, or has no intrinsic content size after calculating,
// it going to be collapsed.
if (CGSizeEqualToSize(contentSize, absentIntrinsicContentSize) ||
CGSizeEqualToSize(contentSize, CGSizeZero)) {
// 当控件没有内容时,则设置控件关联对象constraints的所有约束值为0
self.fd_collapsed = YES;
} else {
// 当控件有内容时,则设置控件关联对象constraints的所有约束值返回为原值
self.fd_collapsed = NO;
}
}
}
  • 设置一些动态属性(关联对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  #pragma mark - Dynamic Properties

- (BOOL)fd_autoCollapse
{
return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_autoCollapse:(BOOL)autoCollapse
{
objc_setAssociatedObject(self, @selector(fd_autoCollapse), @(autoCollapse), OBJC_ASSOCIATION_RETAIN);
}

- (void)setAutoCollapse:(BOOL)collapse
{
// Just forwarding
self.fd_autoCollapse = collapse;
}

总结

总体来说,在分类中要想实现相对复杂的逻辑,却不能添加成员变量,也不想对需要操作的类进行继承,这时就需要runtime中的关联对象和MethodSwizzling技术了。

forkingdog系列分类都用到了runtime的一些知识,代码简洁注释齐全风格也不错,比较适合需要学习runtime应用知识的我。