之前寫了一篇文章總結了OC中弱引用容器實現,在小米面試中提到其中CFFoundation的做法,面試官問了我一個問題,這樣實現后在這些元素在被銷毀后,還保留在容器中會有什么問題么?我馬上意識到,這些元素會變成野指針,且之前只實現了引用計數的不變,而沒有實現Weak特質,也就是沒有在銷毀后置nil,也沒有被移除,那么容器外界再訪問時就會崩潰。看來之前考慮得還是太片面,也沒有做更周全的實驗。
所以看了Runtime源碼和文章后,訂正弱引用容器的一些實現方法。
Runtime源碼的weak關鍵字實現
源碼基于Runtime-709分析,當我們使用__weak
關鍵字時,實際上調用的是NSObject.mm中的objc_initWeak()
方法,然后進入核心方法storeWeak
,傳入核心參數是location(弱引用指針的地址)和newobj(弱引用指針應該指向的)核心方法源碼如下:
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
//template關鍵字類似于泛型,傳入三個參數其實都是bool類型,為各種情況提供組合優化
static id //返回id類型
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
//新值沒有的情況應該被上層的方法攔截,所以這里有個斷言
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
//SideTable結構是引用計數表,記錄著對象的weak表,目的就是為了獲取weak表
SideTable *oldTable;
SideTable *newTable;
retry:
if (haveOld) {
oldObj = *location;//因為是個指向地址的指針,用*解引用返回地址的對象
oldTable = &SideTables()[oldObj];//獲得舊值對象所在的表的指針
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];//獲得新值對象生意所在的表的指針
} else {
newTable = nil;
}
//加鎖
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
//再次確認保證線程安全
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// 防止弱引用機制的死鎖并用init構造保證弱引用的isa指針非空
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
previouslyInitializedClass = cls;
goto retry;
}
}
//清除舊址
if (haveOld) {
//在weakTable中反注冊
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
//分配新值
if (haveNew) {
//在weakTable中注冊新值并返回對象
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// 對TaggedPointer的優化
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
}
else {
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
而WeakTable是一個Hash表設計,以對象的地址為key,value是所有指向這個對象的weak指針的地址集合。通過這種設計,在廢棄對象時,可以通過weak表快速找到value即所有weak指針并統一設置為nil并刪除該記錄。
所以蘋果對于weak的實現其實類似于通知的實現,指明誰(weak指針)要監聽誰(賦值對象)什么事件(dealloc操作)執行什么操作(置nil)。
動手實現弱引用置nil
對于弱引用不增加引用計數,之前文章已經探討過,現在目的是如何仿照蘋果這種設計,去實現置nil的操作。蘋果是為了多個弱引用指針指向同一個對象才使用了表,而需要其中一個指針置nil的關鍵在于,監聽dealloc操作。而其實在ARC里,重寫dealloc方法就可以,但是怎么樣不入侵整個類的dealloc方法呢?這時突破點在于,dealloc中做了什么,有沒有辦法在dealloc調用的其它方法入手。
而MRC中的dealloc方法實際上做了這些事情:
- 對自己所有強引用的屬性發送release消息
- 對自己所有強引用的關聯對象發送release消息
- ….
- 調用
[super dealloc]
這時我們發現,只要此時關聯對象引用計數為1,那么發送release消息后則會調用它的dealloc方法并銷毀。
在它dealloc時置我們需要的指針為nil是合適的選擇,因為之后的時機指向的對象也會被銷毀,銷毀之前置nil剛剛好。
為了保證關聯對象的引用指針為1,在weak賦值時z只要創建一次就好了。由于我們需要置nil這個操作,關聯對象的dealloc跑一個block是靈活性最大的選擇了,也就是由關聯對象持有一個block,并在weak賦值時順便告訴這個block里面執行什么。
@interface CDZDeallocObserver : NSObject
@property (nonatomic, copy) void (^block)(void);
@end
@implementation CDZDeallocObserver
- (void)dealloc{
if (self.block) {
self.block();
}
}
@end
用分類實現一個setBlock的方法,拓展給NSObject類。
const void *CDZDellocBlockKey = &CDZDellocBlockKey;
@implementation NSObject (DeallocBlock)
- (void)cdz_deallocBlock:(void(^)(void))block{
CDZDeallocObserver *observer = objc_getAssociatedObject(self, CDZDellocBlockKey);
if (!observer) {
observer = [CDZDeallocObserver new];
objc_setAssociatedObject(self, CDZDellocBlockKey, observer, OBJC_ASSOCIATION_RETAIN);
}
observer.block = block;
}
@end
弱引用容器的拓展
之前我們知道CoreFoundation和MRC法都可以使引用計數不變,那么其實我們只要在Add的時候順便關聯上釋放時移除對象即可,因為保留一堆nil指針在容器內并不是一個好的選擇。置nil其實更希望是表示這個元素不存在了。
//與CoreFoundation配套的add方法,用unsafe_unretain是不希望被置nil,系統置nil的時機可能比實際dealloc的時機早
- (void)cf_addObject:(id)object{
[self addObject:object];
__unsafe_unretained typeof (object) unRetainObject = object;
__weak typeof(self) weakSelf = self;
[object cdz_deallocBlock:^{
if (unRetainObject) {
[weakSelf removeObject:unRetainObject];
}
}];
}
//跟mrc配套的
- (void)mrc_addObject:(id)object{
[self addObject:object];
__unsafe_unretained typeof (object) unRetainObject = object;
__weak typeof(self) weakSelf = self;
[object cdz_deallocBlock:^{
if (unRetainObject) {
[weakSelf removeObject:unRetainObject];
}
}];
CFRelease((__bridge CFTypeRef)(object));
}
這樣才能保證后續在對象銷毀時,用容器獲取的對象是正確的(置nil或被移除)。
最后
更改后的方案Demo已經更新,之前對于內存管理的理解不夠深入而導致錯誤的文章希望不要誤導了太多人。
如果您覺得有幫助,不妨給個star鼓勵一下,歡迎關注&交流
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz