1. 关联对象和类别添加属性的关系
一提到类别添加属性,就会联想到在get set方法里面通过关联对象来实现,有没有想过为什么,不这样做行吗?
先说为什么?
给类别添加属性get、set方法是有了,那么得有空间去存储数据,同时我们还要能够处理数据的内存避免发生内存泄漏,而此时association就恰好能满足我们的诉求。
我们通过系统提供的apiobjc_setAssociatedObject``objc_getAssociatedObject
在set、get方法中进行写和读的操作;并且在宿主对象释放的时候,也会判断是否有关联对象,从而释放掉关联的对象,一切都那么的完美契合,所以一般我们都通过关联对象来实现
不用关联对象行吗?
我觉得是没有必然关系的,我们只要解决了类别中属性对应的数据的读写和内存管理就能实现类别添加属性的完整的能力,懂得自然就懂就不多说了。
2. 关联对象策略policy的retain和retain_nonatomic的区别
以前一直以为关联对象的OBJC_ASSOCIATION_RETAIN_NONATOMIC
和OBJC_ASSOCIATION_RETAIN
跟属性的类似,就是原子和非原子操作,直到在实际用的过程中,发现2者的差异,才决定去源码里面看看究竟
关联对象的整体实现可以参照探索AssociatedObject关联对象的内部实现;这里就不细说,主要探究这2者的差异
2.1 先看几个枚举定义
关联的策略
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
内部存取的策略
enum {
OBJC_ASSOCIATION_SETTER_ASSIGN = 0,
OBJC_ASSOCIATION_SETTER_RETAIN = 1,
OBJC_ASSOCIATION_SETTER_COPY = 3, // NOTE: both bits are set, so we can simply test 1 bit in releaseValue below.
OBJC_ASSOCIATION_GETTER_READ = (0 << 8),
OBJC_ASSOCIATION_GETTER_RETAIN = (1 << 8),
OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8)
};
内部在存取关联对象的时候会根据这两个策略的组合来做不同的逻辑处理
2.2 存的逻辑
_object_set_associative_reference
中会调用association.acquireValue()
来retain新值
inline void acquireValue() {
if (_value) {
switch (_policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN: // 1
_value = objc_retain(_value);
break;
case OBJC_ASSOCIATION_SETTER_COPY:
_value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
break;
}
}
}
看代码retain和retain_nonatomic是都会执行objc_retain
的
在设置完了之后,会判断是否需要释放旧值
association.releaseHeldValue()
inline void releaseHeldValue() {
if (_value && (_policy & OBJC_ASSOCIATION_SETTER_RETAIN)) { // retain or retain_nonatomic都会执行
objc_release(_value);
}
}
看代码retain和retain_nonatomic是都会执行objc_release
前提是之前已经设置过key对应的数据
2.3 取的逻辑
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
ObjectAssociationMap &refs = i->second;
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
association = j->second;
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();
}
主要看retainReturnedValue``autoreleaseReturnedValue
的差异
inline void retainReturnedValue() { // retain_nonatomic的不会走objc_retain而retain的policy则会执行
if (_value && (_policy & OBJC_ASSOCIATION_GETTER_RETAIN)) { // OBJC_ASSOCIATION_GETTER_RETAIN = 1<<8
objc_retain(_value);
}
}
inline id autoreleaseReturnedValue() {
if (slowpath(_value && (_policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE))) {
return objc_autorelease(_value);
}
return _value;
}
OBJC_ASSOCIATION_GETTER_RETAIN = 1<<8
OBJC_ASSOCIATION_GETTER_AUTORELEASE = 2<<8
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1
OBJC_ASSOCIATION_RETAIN = 01401
对照这几个值我lldb打印了一下:
# retain_nonatomic
(lldb) p 1 & 1<<8
(int) $0 = 0
(lldb) p 1 & 2<<8
(int) $1 = 0
# retain
(lldb) p 01401 & 1<<8
(int) $0 = 256
(lldb) p 01401 & 2<<8
(int) $1 = 512
(lldb)
差别就来了,OBJC_ASSOCIATION_RETAIN
策略的是会走objc_retain
和objc_autorelease
流程,相当于返回了([[_value retain] autorelease]),而OBJC_ASSOCIATION_RETAIN_NONATOMIC
则直接返回了_value
跟我了解的属性的nonatomic不是一个概念。
接下来来讲下association在实际开发中的运用,了解原理不只是为了了解原理,得灵活运用才有实际意义
3. 在对象释放的时候做些事情
- 一般我们在做kvo、notification添加observer之后,dealloc里需要去removeObserver,忘记了就尴尬了,会发生异常
- 假如我在分类中做了监听了,我不能在分类中覆写dealloc而且苹果也不建议我们去swizzle dealloc方法,那么我们就需要一个机制在对象释放的时候去做些清理的工作
其实这个需求可以简单描述为在对象释放的时候做一些额外的工作,了解对象dealloc的流程,我们发现对象释放时会移除关联对象如果有的话、移除weak引用如果有的话,那么我们就可以从这里来切入;
dealloc不是会移除关联对象吗,那么我们可以在关联对象释放的时候来做这些操作;
思路就是:给对象添加一个关联对象(关联对象弱引用宿主对象同时提供一个block回调可供外部设置),在关联对象释放的时候回调回来做额外的操作。
代码实现如下
typedef void(^HCBlock)(__unsafe_unretained NSObject *target);
/// 这个类主要是实现一些Association的应用场景
@interface NSObject (HCWillDealloc)
- (void)hc_doSthWhenDeallocWithBlock:(HCBlock)block;
@end
@interface HCAssociatedObject : NSObject
- (instancetype)initWithTarget:(NSObject *)target;
//- (instancetype)initWithBlock:(HCBlock)block target:(NSObject *)target;
- (void)addActionBlock:(HCBlock)block;
@end
这里支持设置多个回调,内部存储在关联对象的数组中,dealloc的时候遍历去执行回调
static char kHCAssociatedObjectKey;
@implementation NSObject (HCWillDealloc)
- (void)hc_doSthWhenDeallocWithBlock:(HCBlock)block {
if (block) {
// 这里尝试过设置一个HCBlock就生成一个HCAssociatedObject对象,然后将其追加到对象已有的NSMutableArray<HCAssociatedObject *>数组中,后面调试发现,在kvo remove observer的时候会crash;采用下面这种方式则没有该问题
HCAssociatedObject *associatedObject = objc_getAssociatedObject(self, &kHCAssociatedObjectKey);
if (!associatedObject) {
associatedObject = [[HCAssociatedObject alloc] initWithTarget:self];
objc_setAssociatedObject(self, &kHCAssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 这里用下面这句,在测试移除kvo的时候会异常。TODO:什么原因
// objc_setAssociatedObject(self, &kHCAssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN);
}
[associatedObject addActionBlock:block];
}
}
@end
@interface HCAssociatedObject ()
/*
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj); // clear association
if (dealloc) obj->clearDeallocating(); // clear weak and other
}
return obj;
}
*/
@property (nonatomic, unsafe_unretained) NSObject *target;//这里不用weak是由于在target释放的时候,先释放关联对象,然后有weak引用会清除weak表数据,回调的地方拿到的就是nil了,使用unsafe_unretained
//@property (nonatomic, copy) HCBlock deallocBlock;
@property (nonatomic, strong) NSMutableArray<HCBlock> *deallocBlocks;
@end
@implementation HCAssociatedObject
- (instancetype)initWithTarget:(NSObject *)target {
self = [super init];
if (self) {
_deallocBlocks = [NSMutableArray arrayWithCapacity:0];
_target = target;
}
return self;
}
- (void)addActionBlock:(HCBlock)block {
[self.deallocBlocks addObject:[block copy]];
}
//- (instancetype)initWithBlock:(HCBlock)block target:(NSObject *)target {
// self = [super init];
// if (self) {
// _deallocBlock = block;
// _target = target;
// }
//
// return self;
//}
- (void)dealloc {
[_deallocBlocks enumerateObjectsUsingBlock:^(HCBlock _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj ? obj(_target) : nil;
}];
}
@end
来个例子
- (void)testDoSthWhenDealloc {
UIScrollView *tmpView = [UIScrollView new];
[tmpView addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
[tmpView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
[tmpView hc_doSthWhenDeallocWithBlock:^(NSObject * _Nonnull target) {
[target removeObserver:self forKeyPath:@"backgroundColor"];
NSLog(@"removeObserver:forKeyPath:backgroundColor");
}];
[tmpView hc_doSthWhenDeallocWithBlock:^(NSObject * _Nonnull target) {
[target removeObserver:self forKeyPath:@"frame"];
NSLog(@"removeObserver:forKeyPath:frame");
}];
/*
使用OBJC_ASSOCIATION_RETAIN设置关联对象会有一下异常
'Cannot remove an observer <ViewController 0x7f97a5f05e60> for the key path "backgroundColor" from <UIScrollView 0x7f97a787b000> because it is not registered as an observer.'
*/
}
在scrollView释放的时候就会执行设置的block回调了。
实现细节代码中有注释,有2个需要注意的点
- 关联对象弱引用宿主对象target不能声明为
weak
,要使用unsafe_unretained
-- 这是由于:在target释放的时候,先释放关联对象,然后有weak引用会清除weak表数据,回调的地方拿到的就是nil了 - 在设置关联对象的时候,用
OBJC_ASSOCIATION_RETAIN_NONATOMIC
,用OBJC_ASSOCIATION_RETAIN
会出现异常,回调的时候target已经是空了,这个可以参照上面关于这两个策略的内部实现区别
4. association属性的weak实现
看了关联对象的policy,发现咋没有weak,weak这么好用,assign又有时会出问题;能不能自己实现一个了?
4.1 先看assign的问题
- (void)testAssignCase {
static char kTestAssignKey;
{
{
UILabel *associatedLabel = [UILabel new];
objc_setAssociatedObject(self, &kTestAssignKey, associatedLabel, OBJC_ASSOCIATION_ASSIGN);
}
UILabel *label = objc_getAssociatedObject(self, &kTestAssignKey); // EXC_BAD_ACCESS
}
}
这个用例在关联对象associatedLabel
是assign出了作用域没有持有者强持有它进而就释放了,然后去读就EXC_BAD_ACCESS
,要是有weak就好了,释放了就置为空了,避免了异常的发生。
4.2 自己实现一个weak关联
我们要做的就是关联对象在释放的时候将宿主的该关联对象也移除,就可以避免由于assign的方式访问了非法内存的异常了
前面已经介绍了一个应用,在对象释放的时候做些事情,那么我们在关联对象释放的时候,将宿主对象对应的该key的关联对象设置为nil,那么外部读的时候就是个nil,就避免了异常
上代码:
/// 设置关联对象不支持weak的方式
/// @param object 宿主对象
/// @param key 关联key
/// @param value 关联的对象
extern void objc_setWeakAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value);
void objc_setWeakAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value) {
if (value) {
//__weak typeof(object) weakObj = object;
[value hc_doSthWhenDeallocWithBlock:^(NSObject *__unsafe_unretained _Nonnull target) {
objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_ASSIGN); // clear association
}];
}
objc_setAssociatedObject(object, key, value, OBJC_ASSOCIATION_ASSIGN); // call system imp
}
这里实现细节就是在关联对象释放的时候,调用objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_ASSIGN)
这样就把宿主对象的该key的关联对象清除了,外部读这个key的关联对象就是nil
测试用例:
- (void)testWeakCase {
// 如果关联对象也支持weak这种特性就好了,关联的对象释放了,自动置空,宿主对象再次获取拿到的是个nil
static char kTestWeakKey;
{
{
UILabel *associatedLabel = [UILabel new];
objc_setWeakAssociatedObject(self, &kTestWeakKey, associatedLabel);
//objc_setAssociatedObject(self, &kTestWeakKey, associatedLabel, OBJC_ASSOCIATION_ASSIGN);
//objc_setAssociatedObject(self, &kTestWeakKey, nil, OBJC_ASSOCIATION_ASSIGN);
}
UILabel *label = objc_getAssociatedObject(self, &kTestWeakKey);
NSLog(@"label = %@", label); // 输出结果:null
}
}
输出结果符合预期,完事。