给分类添加weak属性的几种方法。


众所周知,通过Runtime的关联属性来给分类添加“属性”,这里的属性缺少了严格意义上的成员变量,而且是自己手动实现了getter方法和setter方法。几种关联策略中并没有与weak效果相媲美的选项,OBJC_ASSOCIATION_ASSIGN策略与weak效果的主要区别在于weak自动能将指向已销毁对象的指针指为nil。

危险的ASSIGN

单纯使用ASSIGN容易诱发坏内存访问,原因无需多言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface NSObject(Default)
@property (nonatomic) id strongObj;
@end

@implementation NSObject(Default)
- (void)setStrongObj:(id)strongObj {
objc_setAssociatedObject(self, @selector(strongObj), strongObj, OBJC_ASSOCIATION_ASSIGN);
}

- (id)strongObj {
return objc_getAssociatedObject(self, @selector(strongObj));
}
@end

// 示例如下
{
NSObject *obj = [NSObject new];
NSObject *main = [NSObject new];
main.strongObj = obj;
obj = nil;
NSLog(@"%@", main.strongObj); // Crash
}

极简方案

这是一种极好的给分类添加weak属性的实现方式。看到这种实现方式后极为兴奋,实在太简洁、巧妙了。__weak本身就会把指针指向nil,那直接利用就是了。使用OBJC_ASSOCIATION_COPY关联策略将block copy到堆上,利用block把持有的weak对象返回,如果对象不存在了,返回的便是空值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface NSObject(Weak)
@property (nonatomic) id object;
@end

@implementation NSObject(Weak)
- (void)setObject:(id)object {
id __weak weakObject = object;
id (^block)(void) = ^{ return weakObject; };
objc_setAssociatedObject(self, @selector(object), block, OBJC_ASSOCIATION_COPY);
}

- (id)object {
id (^block)(void) = objc_getAssociatedObject(self, @selector(object));
return (block ? block() : nil);
}
@end

包装类

这种方式是通过包装一个对象实现的。要求设置的关联对象是YAWeakObject类型。当这个对象销毁的时候调用deallocBlock,而在这个block中把关联的对象重新设置为nil(不可使用objc_removeAssociatedObjects直接移除关联对象),这样访问这个关联对象的时候得到的就是nil值了。

这种方式会污染weak属性,要求被设置为weak属性的对象必须是某种类型,不是太好。当然根据这种思路,还可以进一步封装,最终的落脚点无非是提供新的方法接口替代原生的运行时方法(见参考文章)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface NSObject(WeakClass)
@property (nonatomic) YAWeakObject *weakObject;
@end

@implementation NSObject(WeakClass)
- (YAWeakObject *)weakObject {
return objc_getAssociatedObject(self, @selector(weakObject));
}

- (void)setWeakObject:(YAWeakObject *)weakObject {
objc_setAssociatedObject(self, @selector(weakObject), weakObject, OBJC_ASSOCIATION_ASSIGN);
typeof(self) slf = self;
void (^block)(void) = ^{
typeof(slf) self = slf;
objc_setAssociatedObject(self, @selector(weakObject), nil, OBJC_ASSOCIATION_ASSIGN);
};
[weakObject setDeallocBlock:block];
}
@end

使用容器

实际上使用支持弱引用的容器如NSHashTableNSMapTableNSPointerArray都是可以实现的。原理很简单,使用容器持有关联的对象,当该对象不存在时,容器自身便有自动移除已销毁对象的特性,这样就实现了weak属性。

NSMapTable 可以持有键和值的弱引用,当键或者值当中的一个被释放时,整个这一项就会被移除掉。
NSHashTable 可以持有成员的弱引用。
NSPointerArray 可以持有成员的弱引用,当成员不存在时自动把所在index置为NULL。

这种做法需要创建一个容器,相对比较麻烦。

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
@interface NSObject(WeakContainer)
@property (nonatomic) id weakObj;
@end

@implementation NSObject(WeakContainer)
static NSPointerArray *gPointerArray = nil;
- (id)weakObj {
if (!gPointerArray) return nil;
// Removes NULL values from the receiver.(sometimes doesn't work as documented)
[gPointerArray compact];
for (id obj in gPointerArray) {
if (obj != NULL) {
return objc_getAssociatedObject(self, @selector(weakObj));
}
}
gPointerArray = nil;
return nil;
}

- (void)setWeakObj:(id)weakObj {
if (weakObj) {
if (!gPointerArray) gPointerArray = [NSPointerArray weakObjectsPointerArray];
[gPointerArray addPointer:(__bridge void *)weakObj];
objc_setAssociatedObject(self, @selector(weakObj), weakObj, OBJC_ASSOCIATION_ASSIGN);
}
}
@end

小结

其实看到作者的思路(极简方案)确实挺有感触的,完全利用现有的__weak关键字配合block,没有冗余的包装,方法精简且巧妙。去年在想这个问题的时候,也是考虑很多,在《Runtime基础》一文中描述了我当时的思路,基本上也是从把指针置为nil这个角度出发,或者派生子类重写dealloc,或者使用弱引用容器,都不够巧妙。

很多时候,好的思路,灵光一现的想法,真的无比重要。就像bang哥在写JSPatch时:

当时继续苦苦寻找解决方案,若按 JS 语法,这是唯一的方法,但若不按 JS 语法呢?突然脑洞开了下,CoffieScript/JSX 都可以用 JS 实现一个解释器实现自己的语法,我也可以通过类似的方式做到,再进一步想到其实我想要的效果很简单,就是调用一个不存在方法时,能转发到一个指定函数去执行,就能解决一切问题了,这其实可以用简单的字符串替换,把 JS 脚本里的方法调用都替换掉。

还有一个东西在作者的文章里看到,比较有意思,这里提一下。

Weak Singleton

1
2
3
4
5
6
7
8
9
10
11
+ (instancetype)sharedWeakInstance {
static __weak id weakObj = nil;
id strongObj = weakObj;
@synchronized (self) {
if (!strongObj) {
strongObj = [[self class] new];
weakObj = strongObj;
}
}
return strongObj;
}

应用场景:不需要保存公共的信息、用户状态等,符合“用完就走”。如果类似LoginManager管理登录状态,继承自 AFHttpSessionManager的NetworkManager单例,App单例ClientManager等则不适用这种方式。

参考资料:
如何使用 Runtime 给现有的类添加 weak 属性
iOS给类别添加weak属性
iOS weak 关键字漫谈