最近用到了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 | OBJC_ASSOCIATION_ASSIGN // 对关联对象进行弱引用 |
关联对象在一些第三方框架的分类中常常见到,这里在分析前先看下分类的结构:
1 | struct category_t { |
从以上的分类结构,可以看出,分类中是不能添加成员变量的,也就是Ivar类型。所以,如果想在分类中存储某些数据
时,关联对象就是在这种情况下的常用选择。
需要注意的是,关联对象并不是成员变量
,关联对象是由一个全局哈希表
存储的键值对中的值。
全局哈希表的定义如下:
1 | class AssociationsManager { |
其中的AssociationsHashMap就是那个全局哈希表,而注释中也说明的很清楚了:哈希表中存储的键值对是(源对象指针 : 另一个哈希表)
。而这个value,即ObjectAssociationMap对应的哈希表如下:
1 | // hash_map和unordered_map是模版类 |
其中的ObjectAssociationMap就是value的类型。同时,也可以知道ObjectAssociationMap的键值对类型为(关联对象对应的key : 关联对象)
,也就是函数objc_setAssociatedObject的对应的key:value参数。
大部分情况下,关联对像会使用getter方法的SEL当作key
(getter方法中可以这样表示:_cmd)。
更多和关联对象有关的底层信息,可以查看Dive into Category
MethodSwizzling
MethodSwizzling主要原理就是利用runtime的动态特性,交换方法对应的实现
,也就是IMP
。
通常,MethodSwizzling的封装为:
1 | + (void)load |
为了便于区别,这里列出Method的结构:
1 | typedef struct method_t *Method; |
实现MethodSwizzling需要了解的有以下几个常用函数:
1 | // 返回方法的具体实现 |
介绍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 | @interface UIView (FDCollapsibleConstraints) |
- _FDOriginalConstantStorage
- 在这个分类中,给NSLayoutConstraint约束关联一个存储
约束初始值
的浮点数,以便在修改约束值后,可以还原- objc_setAssociatedObject 设置关联对象
- objc_getAssociatedObject 获取关联对象
- 在这个分类中,给NSLayoutConstraint约束关联一个存储
1 | /// A stored property extension for NSLayoutConstraint's original constant. |
- FDCollapsibleConstraints
- 实现fd_collapsibleConstraints属性的setter和getter方法 (
关联一个存储约束的对象
)- 在
getter方法中创建关联对象constraints
(和懒加载
的方式类似,不过不是创建成员变量) - 在
setter方法中设置约束的初始值
,并添加进关联对象constraints
中,方便统一操作
- 在
- 从IB中关联的约束,根据KVC地层原理,最终会调用setFd_collapsibleConstraints:方法,也就是这一步不需要手动调用,系统自己完成(在awakeFromNib之前完成IB这些值的映射)
- 实现fd_collapsibleConstraints属性的setter和getter方法 (
1 | - (NSMutableArray *)fd_collapsibleConstraints |
- 使用Method Swizzling交换自己的和系统的-setValue:forKey:方法
- 实现自己的KVC的-setValue:forKey:方法
- Method Swizzling的完全体
1 | + (void)load |
- 这一步作者的意思是这种类型的IBOutlet不会触发其setter方法,但是经过测试,注释掉这段代码后,系统还是自己触发了setter方法,说明这种IBOutlet还是可以触发setter方法的。所以,即使没有这一段代码,应该也是可行的
1 |
|
- 设置对应的约束值
- 注意,这里只要传入的是YES,那么,这个UIView对应的存入
constraints关联对象的所有约束
,都会置为0
- 注意,这里只要传入的是YES,那么,这个UIView对应的存入
1 |
|
- FDAutomaticallyCollapseByIntrinsicContentSize
- 使用Method Swizzling交换自己的和系统的-fd_updateConstraints方法
- [self fd_updateConstraints]调用的是self的updateConstraints方法,fd_updateConstraints和updateConstraints方法的Method(映射SEL和IMP)已经调换了
intrinsicContentSize(控件的内置大小)
默认为UIViewNoIntrinsicMetric,当控件中没有内容时
,调用intrinsicContentSize返回的即为默认值
(介绍链接)
1 |
|
- 设置一些动态属性(关联对象)
1 |
|
总结
总体来说,在分类中要想实现相对复杂的逻辑,却不能添加成员变量
,也不想对需要操作的类进行继承
,这时就需要runtime中的关联对象和MethodSwizzling
技术了。
forkingdog系列分类都用到了runtime的一些知识,代码简洁注释齐全风格也不错,比较适合需要学习runtime应用知识的我。