前言
有經(jīng)驗(yàn)的iOS開發(fā)者都知道玖姑,ARC中的weak關(guān)鍵字可以在對象銷毀時(shí) 指針自動置成nil缎谷,在OC中向nil發(fā)消息是安全的,所以不會造成野指針錯(cuò)誤蚁孔。
在category中擴(kuò)展屬性時(shí)屑埋,一般會使用runtime的關(guān)聯(lián)對象(AssociatedObject)技術(shù)豪筝,關(guān)聯(lián)對象的策略(Policy)有5個(gè):
OBJC_ASSOCIATION_ASSIGN = 0, //弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,//強(qiáng)引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//copy摘能,非原子性
OBJC_ASSOCIATION_RETAIN = 01401,//強(qiáng)引用续崖,原子性
OBJC_ASSOCIATION_COPY = 01403//copy,原子性
我們可以發(fā)現(xiàn)团搞,在5個(gè)策略中并沒有weak類型严望,OBJC_ASSOCIATION_ASSIGN 策略雖然可以弱引用,但是在對象銷毀的時(shí)候不能自動將指針置nil逻恐。
現(xiàn)象舉例
我們使用Category給UIViewController擴(kuò)展一個(gè)UILabel類型的屬性aLabel像吻,并使用OBJC_ASSOCIATION_ASSIGN 策略,看看會發(fā)生什么复隆。代碼如下:
// UIViewController+Category.h
@interface UIViewController (Category)
@property (nonatomic, strong) UILabel *aLabel;
@end
// UIViewController+Category.m
@implementation UIViewController (Category)
- (void)setALabel:(UILabel *)aLabel {
objc_setAssociatedObject(self, @selector(aLabel), aLabel, OBJC_ASSOCIATION_ASSIGN);
}
- (UILabel *)aLabel {
return objc_getAssociatedObject(self, @selector(aLabel));
}
@end
然后賦值使用:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
UILabel *label = [[UILabel alloc] init];
self.aLabel = label;
NSLog(@"-viewDidLoad-\nself.aLabel = %@", self.aLabel);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"-viewDidAppear-\nself.aLabel = %@", self.aLabel);
}
@end
在viewDidLoad方法中對aLabel進(jìn)行了賦值拨匆,出了viewDidLoad作用域后,aLabel指向的對象會被銷毀挽拂。運(yùn)行后惭每,我們發(fā)現(xiàn)程序崩潰了。打開僵尸對象調(diào)試亏栈,報(bào)錯(cuò)如下:
*** -[UILabel retain]: message sent to deallocated instance
對象被銷毀了台腥,指針沒有置nil宏赘,造成了崩潰。
解決方案
我們需要做的是在獲取到對象銷毀的時(shí)機(jī)览爵,然后將相應(yīng)的指針指向nil。如果是我們自己創(chuàng)建的類镇饮,可以在dealloc方法中進(jìn)行block回調(diào)蜓竹。但是系統(tǒng)早已創(chuàng)建好的類,開發(fā)者沒有地方可以寫dealloc回調(diào)储藐。
與蘋果系統(tǒng)對KVO的實(shí)現(xiàn)原理參考我這篇文章類似俱济,我們可以在屬性的set方法中,動態(tài)創(chuàng)建一個(gè)關(guān)聯(lián)對象的子類钙勃,重寫新類的dealloc方法蛛碌,在新類的dealloc中將指針置nil,并將關(guān)聯(lián)對象的isa指針指向新類辖源。
沿著這個(gè)思路蔚携,我們可以寫出以下代碼:
void objc_setAssociatedObject_weak(id _Nonnull object, const void * _Nonnull key, id _Nullable value) {
//子類的名字
NSString *name = [NSString stringWithFormat:@"AssociationWeak_%@", NSStringFromClass([value class])];
Class class = objc_getClass(name.UTF8String);
//如果子類不存在,動態(tài)創(chuàng)建子類
if (!class) {
class = objc_allocateClassPair([value class], name.UTF8String, 0);
objc_registerClassPair(class);
}
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([value class], deallocSEL);
const char *types = method_getTypeEncoding(deallocMethod);
//在子類dealloc方法中將object的指針置為nil
IMP imp = imp_implementationWithBlock(^(id _s, int k) {
objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_ASSIGN);
});
//添加子類的dealloc方法
class_addMethod(class, deallocSEL, imp, types);
//將value的isa指向動態(tài)創(chuàng)建的子類
object_setClass(value, class);
objc_setAssociatedObject(object, key, value, OBJC_ASSOCIATION_ASSIGN);
}
在進(jìn)行關(guān)聯(lián)對象的操作時(shí)克饶,我們使用自己新寫的方法酝蜒,不再使用系統(tǒng)關(guān)聯(lián)對象方法:
- (void)setALabel:(UILabel *)aLabel {
objc_setAssociatedObject_weak(self, @selector(aLabel), aLabel);
再運(yùn)行程序看看打印結(jié)果:
-viewDidLoad-
self.aLabel = <AssociationWeak_UILabel: 0x7fd174f0b770; baseClass = UILabel; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000818780>>
-viewDidAppear-
self.aLabel = (null)
我們發(fā)現(xiàn)這樣成功捕捉到了對象被銷毀的時(shí)機(jī),并將指針指向了nil矾湃,沒有出現(xiàn)崩潰的情況亡脑。
至此,我們成功做到了弱引用對象銷毀后邀跃,指針自動置空的操作霉咨。我將方法封裝到了NSObject的分類中。任何繼承自NSObject的OC對象拍屑,在關(guān)聯(lián)對象時(shí)途戒,都可以使用。