問題現(xiàn)象:
<br />有這樣兩個(gè)方法:
- (ErrorViewType)viewType {
NSNumber *number = objc_getAssociatedObject(self, @selector(viewType));
return [number integerValue];
}
l
- (void)setViewType:(ErrorViewType)viewType {
objc_setAssociatedObject(self, @selector(viewType),@(viewType), OBJC_ASSOCIATION_ASSIGN) ;
}
其中ErrorViewType
為枚舉變量類型NSUInteger,值為0-14
有這樣兩個(gè)方法在验,某一次setViewType
傳入的值為14掌敬,但是在之后某次get的時(shí)候直接崩了包券。報(bào)的錯(cuò)誤是EXC_BAD_ACCESS
介袜,即野指針訪問痹兜,那么這個(gè)對(duì)象又是什么時(shí)候被釋放蛮粮,又為什么被釋放了呢益缎?
寫了一個(gè)簡(jiǎn)單的Demo在32bit真機(jī)上驗(yàn)證了一下,iPhone4s,iOS7系統(tǒng):
可見當(dāng)被關(guān)聯(lián)的對(duì)象NSNumer的值為13的時(shí)候然想,在下一輪runloop去訪問這個(gè)對(duì)象會(huì)引發(fā)野指針訪問:
編譯器的代碼大概是莺奔,通過rewrite以及匯編代碼整理:
id tmp1 = _objc_retainAutoreleasedReturnValue([NSNumber numberWithInt:13]);//ARC環(huán)境下對(duì)于autorelease對(duì)象的優(yōu)化 這個(gè)時(shí)候tmp1指向的對(duì)象retainCount為1
_objc_setAssociatedObject(self,_SEL(testMagicNumber),tmp1,0);
_objc_release(temp1);//因?yàn)閠emp是作為一個(gè)入?yún)簵5模@里出了上面方法的作用域就會(huì)被釋放
id tmp2 = _objc_retainAutoreleasedReturnValue(_objc_getAssociatedObject(self,_SEL(testMagicNumber)));
objc_storeStrong(&tmp2,nil);//release temp2指向的對(duì)象变泄,并且把temp2指向nil
其中對(duì)于ARC下對(duì)autorelease對(duì)象的優(yōu)化可以參考我上一篇博客:
ARC環(huán)境下編譯器到底對(duì)autorelease對(duì)象做了怎樣的優(yōu)化
可見temp1指向的對(duì)象在_objc_setAssociatedObject
方法調(diào)用結(jié)束之后就立即被釋放了令哟,而對(duì)于關(guān)聯(lián)對(duì)象的修飾符又是assign,沒有被強(qiáng)引用妨蛹,所以最后temp1指向的對(duì)象release1次引用計(jì)數(shù)就變成了0励饵,也就隨即被釋放了。所以下一輪runloop訪問的時(shí)候就引發(fā)野指針訪問的崩潰了滑燃。在當(dāng)前runloop訪問的話也會(huì)崩潰,但是如果把上面的set和get方法抽出來之后現(xiàn)象有點(diǎn)詭異颓鲜,這個(gè)待會(huì)說表窘。
然后我們把NSNumer的值改為12之后典予,一切正常, 這其中有什么貓膩呢乐严。
http://stackoverflow.com/questions/2533355/nsnumber-13-wont-retain-everything-else-will
上面這個(gè)回答里有提到瘤袖,NSNumber在32bit設(shè)備之上0-12都是存在內(nèi)存共享區(qū),類似于[NSArray array]無論調(diào)用多少次指針指向的都是相同的一塊內(nèi)存區(qū)域昂验,永遠(yuǎn)不會(huì)被銷毀捂敌。而只要大于12就是正常的創(chuàng)建在堆上的對(duì)象。為了驗(yàn)證這個(gè)問題既琴,再寫個(gè)Demo占婉,還是4s真機(jī)測(cè)試:
果然,驗(yàn)證了猜想甫恩,在iPhone5 iOS10系統(tǒng)上同樣的現(xiàn)象逆济。
同時(shí)也發(fā)現(xiàn)了一個(gè)有意思的現(xiàn)象:
將get操作封裝在一個(gè)方法中,不會(huì)崩潰磺箕。這個(gè)原因暫時(shí)沒想通奖慌,但是只要點(diǎn)了Xcode的停止按鈕,還是會(huì)提示有Crash
在get操作之后再打印一下取到的對(duì)象松靡,這種case下就會(huì)崩潰简僧,然后取到的對(duì)象居然是@(20),也就是雖然@(14)被釋放了雕欺,但是在他的內(nèi)存區(qū)域由重新new了一個(gè)@(20)的對(duì)象岛马,鳩占鵲巢,@(20)這個(gè)對(duì)象被釋放之后20這個(gè)值還是存在的阅茶,所以還能夠打印出來蛛枚,但是因?yàn)閳?zhí)行的對(duì)象以及被銷毀,所以再對(duì)其調(diào)用release方法就自然會(huì)報(bào)double free的錯(cuò)誤了脸哀。
這里到底為什么蹦浦,一塊內(nèi)存被銷毀之后內(nèi)存中到底是如何標(biāo)記的,拋磚引玉一下撞蜂,歡迎大神指導(dǎo)~
那么對(duì)于64bit的設(shè)備呢盲镶?
可見無論NSInterger
保存的這個(gè)數(shù)有多大,只要在正常范圍之內(nèi)蝌诡,一定是存放在常量區(qū)的溉贿,也就是永遠(yuǎn)不會(huì)釋放。
可見浦旱,是系統(tǒng)在32bit設(shè)備上對(duì)NSNumber
類型的對(duì)象做的優(yōu)化不夠徹底宇色,然后我們?cè)谑褂藐P(guān)聯(lián)對(duì)象時(shí)內(nèi)存修飾符又使用不當(dāng),造成了崩潰的問題。猜測(cè)對(duì)于32bit的設(shè)備宣蠕,同時(shí)存在大量的共享內(nèi)存會(huì)比較消耗資源例隆,因此只對(duì)0-12這少數(shù)的幾個(gè)數(shù)做了優(yōu)化,而出問題時(shí)候我們傳入的參數(shù)剛好是14抢蚀,所以就掉進(jìn)了坑里镀层。
解決辦法及結(jié)論
將OBJC_ASSOCIATION_ASSIGN
改為OBJC_ASSOCIATION_RETAIN
,這樣在本對(duì)象有一個(gè)強(qiáng)引用皿曲,這個(gè)被關(guān)聯(lián)的對(duì)象也就不會(huì)釋放唱逢,生命周期也和本對(duì)象相同了。我認(rèn)為既然關(guān)聯(lián)對(duì)象傳入的都是對(duì)象屋休,那么其實(shí)絕大多時(shí)候用的都應(yīng)該是是OBJC_ASSOCIATION_RETAIN
坞古,在我們項(xiàng)目中傳入的對(duì)象很多是NSNumber類型(包裝的bool或則int)的時(shí)候都是用的OBJC_ASSOCIATION_ASSIGN,以前沒暴露問題也是誤打誤撞錯(cuò)進(jìn)錯(cuò)出博投。所以除了一些需要破解循環(huán)引用的場(chǎng)景绸贡,關(guān)聯(lián)對(duì)象的內(nèi)存操作修飾符建議都用OBJC_ASSOCIATION_RETAIN
。