之前寫了一篇文章總結(jié)了OC中弱引用容器實現(xiàn)淡诗,在小米面試中提到其中CFFoundation的做法,面試官問了我一個問題懂牧,這樣實現(xiàn)后在這些元素在被銷毀后坤学,還保留在容器中會有什么問題么?我馬上意識到肢簿,這些元素會變成野指針靶剑,且之前只實現(xiàn)了引用計數(shù)的不變,而沒有實現(xiàn)Weak特質(zhì)池充,也就是沒有在銷毀后置nil桩引,也沒有被移除,那么容器外界再訪問時就會崩潰收夸】咏常看來之前考慮得還是太片面,也沒有做更周全的實驗卧惜。
所以看了Runtime源碼和文章后厘灼,訂正弱引用容器的一些實現(xiàn)方法。
Runtime源碼的weak關(guān)鍵字實現(xiàn)
源碼基于Runtime-709分析咽瓷,當(dāng)我們使用__weak
關(guān)鍵字時设凹,實際上調(diào)用的是NSObject.mm中的objc_initWeak()
方法,然后進入核心方法storeWeak
茅姜,傳入核心參數(shù)是location(弱引用指針的地址)和newobj(弱引用指針應(yīng)該指向的)核心方法源碼如下:
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
//template關(guān)鍵字類似于泛型闪朱,傳入三個參數(shù)其實都是bool類型,為各種情況提供組合優(yōu)化
static id //返回id類型
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
//新值沒有的情況應(yīng)該被上層的方法攔截,所以這里有個斷言
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
//SideTable結(jié)構(gòu)是引用計數(shù)表奋姿,記錄著對象的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構(gòu)造保證弱引用的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的優(yōu)化
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
}
else {
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
而WeakTable是一個Hash表設(shè)計胀蛮,以對象的地址為key院刁,value是所有指向這個對象的weak指針的地址集合。通過這種設(shè)計粪狼,在廢棄對象時退腥,可以通過weak表快速找到value即所有weak指針并統(tǒng)一設(shè)置為nil并刪除該記錄。
所以蘋果對于weak的實現(xiàn)其實類似于通知的實現(xiàn)再榄,指明誰(weak指針)要監(jiān)聽誰(賦值對象)什么事件(dealloc操作)執(zhí)行什么操作(置nil)狡刘。
動手實現(xiàn)弱引用置nil
對于弱引用不增加引用計數(shù)困鸥,之前文章已經(jīng)探討過嗅蔬,現(xiàn)在目的是如何仿照蘋果這種設(shè)計,去實現(xiàn)置nil的操作疾就。蘋果是為了多個弱引用指針指向同一個對象才使用了表,而需要其中一個指針置nil的關(guān)鍵在于猬腰,監(jiān)聽dealloc操作姑荷。而其實在ARC里添寺,重寫dealloc方法就可以懈费,但是怎么樣不入侵整個類的dealloc方法呢计露?這時突破點在于票罐,dealloc中做了什么,有沒有辦法在dealloc調(diào)用的其它方法入手寨闹。
而MRC中的dealloc方法實際上做了這些事情:
- 對自己所有強引用的屬性發(fā)送release消息
- 對自己所有強引用的關(guān)聯(lián)對象發(fā)送release消息
- ….
- 調(diào)用
[super dealloc]
這時我們發(fā)現(xiàn)胶坠,只要此時關(guān)聯(lián)對象引用計數(shù)為1,那么發(fā)送release消息后則會調(diào)用它的dealloc方法并銷毀繁堡。
在它dealloc時置我們需要的指針為nil是合適的選擇沈善,因為之后的時機指向的對象也會被銷毀乡数,銷毀之前置nil剛剛好。
為了保證關(guān)聯(lián)對象的引用指針為1闻牡,在weak賦值時z只要創(chuàng)建一次就好了净赴。由于我們需要置nil這個操作,關(guān)聯(lián)對象的dealloc跑一個block是靈活性最大的選擇了罩润,也就是由關(guān)聯(lián)對象持有一個block玖翅,并在weak賦值時順便告訴這個block里面執(zhí)行什么。
@interface CDZDeallocObserver : NSObject
@property (nonatomic, copy) void (^block)(void);
@end
@implementation CDZDeallocObserver
- (void)dealloc{
if (self.block) {
self.block();
}
}
@end
用分類實現(xiàn)一個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法都可以使引用計數(shù)不變,那么其實我們只要在Add的時候順便關(guān)聯(lián)上釋放時移除對象即可严沥,因為保留一堆nil指針在容器內(nèi)并不是一個好的選擇猜极。置nil其實更希望是表示這個元素不存在了。
//與CoreFoundation配套的add方法消玄,用unsafe_unretain是不希望被置nil跟伏,系統(tǒng)置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));
}
這樣才能保證后續(xù)在對象銷毀時,用容器獲取的對象是正確的(置nil或被移除)翩瓜。
最后
更改后的方案Demo已經(jīng)更新受扳,之前對于內(nèi)存管理的理解不夠深入而導(dǎo)致錯誤的文章希望不要誤導(dǎo)了太多人。
如果您覺得有幫助,不妨給個star鼓勵一下,歡迎關(guān)注&交流
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz