前言
有經(jīng)驗(yàn)的iOS開發(fā)者都知道,ARC中的weak關(guān)鍵字可以在對(duì)象銷毀時(shí) 指針自動(dòng)置成nil,在OC中向nil發(fā)消息是安全的,所以不會(huì)造成野指針錯(cuò)誤。
在category中擴(kuò)展屬性時(shí),一般會(huì)使用runtime的關(guān)聯(lián)對(duì)象(AssociatedObject)技術(shù),關(guān)聯(lián)對(duì)象的策略(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 策略雖然可以弱引用,但是在對(duì)象銷毀的時(shí)候不能自動(dòng)將指針置nil。
現(xiàn)象舉例
我們使用Category給UIViewController擴(kuò)展一個(gè)UILabel類型的屬性aLabel,并使用OBJC_ASSOCIATION_ASSIGN 策略,看看會(huì)發(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方法中對(duì)aLabel進(jìn)行了賦值,出了viewDidLoad作用域后,aLabel指向的對(duì)象會(huì)被銷毀。運(yùn)行后,我們發(fā)現(xiàn)程序崩潰了。打開僵尸對(duì)象調(diào)試,報(bào)錯(cuò)如下:
*** -[UILabel retain]: message sent to deallocated instance
對(duì)象被銷毀了,指針沒有置nil,造成了崩潰。
解決方案
我們需要做的是在獲取到對(duì)象銷毀的時(shí)機(jī),然后將相應(yīng)的指針指向nil。如果是我們自己創(chuàng)建的類,可以在dealloc方法中進(jìn)行block回調(diào)。但是系統(tǒng)早已創(chuàng)建好的類,開發(fā)者沒有地方可以寫dealloc回調(diào)。
與蘋果系統(tǒng)對(duì)KVO的實(shí)現(xiàn)原理參考我這篇文章類似,我們可以在屬性的set方法中,動(dòng)態(tài)創(chuàng)建一個(gè)關(guān)聯(lián)對(duì)象的子類,重寫新類的dealloc方法,在新類的dealloc中將指針置nil,并將關(guān)聯(lián)對(duì)象的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);
//如果子類不存在,動(dòng)態(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指向動(dòng)態(tài)創(chuàng)建的子類
object_setClass(value, class);
objc_setAssociatedObject(object, key, value, OBJC_ASSOCIATION_ASSIGN);
}
在進(jìn)行關(guān)聯(lián)對(duì)象的操作時(shí),我們使用自己新寫的方法,不再使用系統(tǒng)關(guān)聯(lián)對(duì)象方法:
- (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)這樣成功捕捉到了對(duì)象被銷毀的時(shí)機(jī),并將指針指向了nil,沒有出現(xiàn)崩潰的情況。
至此,我們成功做到了弱引用對(duì)象銷毀后,指針自動(dòng)置空的操作。我將方法封裝到了NSObject的分類中。任何繼承自NSObject的OC對(duì)象,在關(guān)聯(lián)對(duì)象時(shí),都可以使用。