前言
有經驗的iOS開發者都知道,ARC中的weak關鍵字可以在對象銷毀時 指針自動置成nil,在OC中向nil發消息是安全的,所以不會造成野指針錯誤。
在category中擴展屬性時,一般會使用runtime的關聯對象(AssociatedObject)技術,關聯對象的策略(Policy)有5個:
OBJC_ASSOCIATION_ASSIGN = 0, //弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,//強引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//copy,非原子性
OBJC_ASSOCIATION_RETAIN = 01401,//強引用,原子性
OBJC_ASSOCIATION_COPY = 01403//copy,原子性
我們可以發現,在5個策略中并沒有weak類型,OBJC_ASSOCIATION_ASSIGN 策略雖然可以弱引用,但是在對象銷毀的時候不能自動將指針置nil。
現象舉例
我們使用Category給UIViewController擴展一個UILabel類型的屬性aLabel,并使用OBJC_ASSOCIATION_ASSIGN 策略,看看會發生什么。代碼如下:
// 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進行了賦值,出了viewDidLoad作用域后,aLabel指向的對象會被銷毀。運行后,我們發現程序崩潰了。打開僵尸對象調試,報錯如下:
*** -[UILabel retain]: message sent to deallocated instance
對象被銷毀了,指針沒有置nil,造成了崩潰。
解決方案
我們需要做的是在獲取到對象銷毀的時機,然后將相應的指針指向nil。如果是我們自己創建的類,可以在dealloc方法中進行block回調。但是系統早已創建好的類,開發者沒有地方可以寫dealloc回調。
與蘋果系統對KVO的實現原理參考我這篇文章類似,我們可以在屬性的set方法中,動態創建一個關聯對象的子類,重寫新類的dealloc方法,在新類的dealloc中將指針置nil,并將關聯對象的isa指針指向新類。
沿著這個思路,我們可以寫出以下代碼:
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);
//如果子類不存在,動態創建子類
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指向動態創建的子類
object_setClass(value, class);
objc_setAssociatedObject(object, key, value, OBJC_ASSOCIATION_ASSIGN);
}
在進行關聯對象的操作時,我們使用自己新寫的方法,不再使用系統關聯對象方法:
- (void)setALabel:(UILabel *)aLabel {
objc_setAssociatedObject_weak(self, @selector(aLabel), aLabel);
再運行程序看看打印結果:
-viewDidLoad-
self.aLabel = <AssociationWeak_UILabel: 0x7fd174f0b770; baseClass = UILabel; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000818780>>
-viewDidAppear-
self.aLabel = (null)
我們發現這樣成功捕捉到了對象被銷毀的時機,并將指針指向了nil,沒有出現崩潰的情況。
至此,我們成功做到了弱引用對象銷毀后,指針自動置空的操作。我將方法封裝到了NSObject的分類中。任何繼承自NSObject的OC對象,在關聯對象時,都可以使用。