iOS內(nèi)功篇:內(nèi)存管理
前言
現(xiàn)在iOS開(kāi)發(fā)已經(jīng)是arc甚至是swift的時(shí)代,但是內(nèi)存管理仍是一個(gè)重點(diǎn)關(guān)注的問(wèn)題,如果只知盲目開(kāi)發(fā)而不知個(gè)中原理,踩坑就跳不出來(lái)了,理解好內(nèi)存管理,能讓我們寫(xiě)出更有質(zhì)量的代碼。
內(nèi)存管理是程序設(shè)計(jì)中很重要的一部分,程序在運(yùn)行的過(guò)程中消耗內(nèi)存,運(yùn)行結(jié)束后釋放占用的內(nèi)存。如果程序運(yùn)行時(shí)一直分配內(nèi)存而不及時(shí)釋放無(wú)用的內(nèi)存,會(huì)造成這樣的后果:程序占用的內(nèi)存越來(lái)越大,直至內(nèi)存消耗殫盡,程序因無(wú)內(nèi)存可用導(dǎo)致崩潰,這樣的情況我們稱之為內(nèi)存泄漏。
ObjC的內(nèi)存管理比較簡(jiǎn)潔,然而要深刻理解也不是一件易事,本文將介紹如何使用ObjC進(jìn)行內(nèi)存管理。
1、引用計(jì)數(shù)
在ObjC中,對(duì)象什么時(shí)候會(huì)被釋放(或者對(duì)象占用的內(nèi)存什么時(shí)候會(huì)被回收利用)?
答案是:當(dāng)對(duì)象沒(méi)有被任何變量引用(也可以說(shuō)是沒(méi)有指針指向該對(duì)象)的時(shí)候,就會(huì)被釋放。
那怎么知道對(duì)象已經(jīng)沒(méi)有被引用了呢?
ObjC采用引用計(jì)數(shù)(reference counting)的技術(shù)來(lái)進(jìn)行管理:
1)每個(gè)對(duì)象都有一個(gè)關(guān)聯(lián)的整數(shù),稱為引用計(jì)數(shù)器
2)當(dāng)代碼需要使用該對(duì)象時(shí),則將對(duì)象的引用計(jì)數(shù)加1
3)當(dāng)代碼結(jié)束使用該對(duì)象時(shí),則將對(duì)象的引用計(jì)數(shù)減1
4)當(dāng)引用計(jì)數(shù)的值變?yōu)?時(shí),表示對(duì)象沒(méi)有被任何代碼使用,此時(shí)對(duì)象將被釋放。
與之對(duì)應(yīng)的消息發(fā)送方法如下:
1)當(dāng)對(duì)象被創(chuàng)建(通過(guò)alloc、new或copy等方法)時(shí),其引用計(jì)數(shù)初始值為1
2)給對(duì)象發(fā)送retain消息,其引用計(jì)數(shù)加1
3)給對(duì)象發(fā)送release消息,其引用計(jì)數(shù)減1
4)當(dāng)對(duì)象引用計(jì)數(shù)歸0時(shí),ObjC給對(duì)象發(fā)送dealloc消息銷毀對(duì)象
下面通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明:
場(chǎng)景:有一個(gè)寵物中心(內(nèi)存),可以派出小動(dòng)物(對(duì)象)陪小朋友們玩耍(對(duì)象引用者),現(xiàn)在xiaoming想和小狗一起玩耍。
新建Dog類,重寫(xiě)其創(chuàng)建和銷毀的方法:
- @implementation Dog
- - (instancetype)init {
- if (self = [super init]) {
- NSLog(@"小狗被派出去啦!初始引用計(jì)數(shù)為 %ld",self.retainCount);
- }
- return self;
- }
- - (void)dealloc {
- NSLog(@"小狗回到寵物中心");
- [super dealloc];
- }
- @end
在main方法中創(chuàng)建dog對(duì)象,給dog發(fā)送消息
- //模擬:寵物中心派出小狗
- Dog * dog = [[Dog alloc]init];
- //模擬:xiaoming需要和小狗玩耍,需要將其引用計(jì)數(shù)加1
- [dog retain];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaoming不和小狗玩耍了,需要將其引用計(jì)數(shù)減1
- [dog release];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //沒(méi)人需要和小狗玩耍了,將其引用計(jì)數(shù)減1
- [dog release];
- //將指針置nil,否則變?yōu)橐爸羔?/span>
- dog = nil;
輸出結(jié)果為
- [34691:7638855] 初始引用計(jì)數(shù)為 1
- [34691:7638855] 小狗的引用計(jì)數(shù)為 2
- [34691:7638855] 小狗的引用計(jì)數(shù)為 1
- [34691:7638855] 銷毀Dog
可以看到,引用計(jì)數(shù)幫助寵物中心很好的標(biāo)記了小狗的使用狀態(tài),在完成任務(wù)的時(shí)候及時(shí)收回到寵物中心。
思考幾個(gè)問(wèn)題:
1)NSString引用計(jì)數(shù)問(wèn)題
如果我們嘗試查看一個(gè)string的引用計(jì)數(shù)
- NSString * str = @"hello guys";
- NSLog(@"%ld", str.retainCount);
會(huì)發(fā)現(xiàn)引用計(jì)數(shù)為-1,這可以理解為NSString實(shí)際上是一個(gè)字符串常量,是沒(méi)有引用計(jì)數(shù)的(或者它的引用計(jì)數(shù)是一個(gè)很大的值(使用%lu可以打印查看),對(duì)它做引用計(jì)數(shù)操作沒(méi)實(shí)質(zhì)上的影響)。
2)賦值不會(huì)擁有某個(gè)對(duì)象
- NSString * name = dog.name;
這里僅僅是指針賦值操作,并不會(huì)增加name的引用計(jì)數(shù),需要持有對(duì)象必須要發(fā)送retain消息。
3)dealloc
由于釋放對(duì)象是會(huì)調(diào)用dealloc方法,因此重寫(xiě)dealloc方法來(lái)查看對(duì)象釋放的情況,如果沒(méi)有調(diào)用則會(huì)造成內(nèi)存泄露。在上面的例子中我們通過(guò)重寫(xiě)dealloc讓小狗被釋放的時(shí)候打印日志來(lái)告訴我們已經(jīng)完成釋放。
4)在上面例子中,如果我們?cè)黾舆@樣一個(gè)操作
- //沒(méi)人需要和小狗玩耍了,將其引用計(jì)數(shù)減1
- [dog release];
- NSLog(@"%ld",dog.retainCount);
會(huì)發(fā)現(xiàn)獲取到的引用計(jì)數(shù)為1,為什么不是0呢?
這是因?yàn)閷?duì)引用計(jì)數(shù)為1的對(duì)象release時(shí),系統(tǒng)知道該對(duì)象將被回收,就不會(huì)再對(duì)該對(duì)象的引用計(jì)數(shù)進(jìn)行減1操作,這樣可以增加對(duì)象回收的效率。
另外,對(duì)已釋放的對(duì)象發(fā)送消息是不可取的,因?yàn)閷?duì)象的內(nèi)存已被回收,如果發(fā)送消息時(shí),該內(nèi)存已經(jīng)被其他對(duì)象使用了,得到的結(jié)果是無(wú)法確定的,甚至?xí)斐杀罎ⅰ?/p>
2、自動(dòng)釋放池
現(xiàn)在已經(jīng)明確了,當(dāng)不再使用一個(gè)對(duì)象時(shí)應(yīng)該將其釋放,但是在某些情況下,我們很難理清一個(gè)對(duì)象什么時(shí)候不再使用(比如xiaoming和小狗玩耍結(jié)束的時(shí)間不確定),這可怎么辦?
ObjC提供autorelease方法來(lái)解決這個(gè)問(wèn)題,當(dāng)給一個(gè)對(duì)象發(fā)送autorelease消息時(shí),方法會(huì)在未來(lái)某個(gè)時(shí)間給這個(gè)對(duì)象發(fā)送release消息將其釋放,在這個(gè)時(shí)間段內(nèi),對(duì)象還是可以使用的。
那autorelease的原理是什么呢?
原理就是對(duì)象接收到autorelease消息時(shí),它會(huì)被添加到了當(dāng)前的自動(dòng)釋放池中,當(dāng)自動(dòng)釋放池被銷毀時(shí),會(huì)給池里所有的對(duì)象發(fā)送release消息。
這里就引出了自動(dòng)釋放池這個(gè)概念,什么是自動(dòng)釋放池呢? 顧名思義,就是一個(gè)池,這個(gè)池可以容納對(duì)象,而且可以自動(dòng)釋放,這就大大增加了我們處理對(duì)象的靈活性。
自動(dòng)釋放池怎樣創(chuàng)建?
ObjC提供兩種方法創(chuàng)建自動(dòng)釋放池:
方法一:使用NSAutoreleasePool來(lái)創(chuàng)建
- NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
- //這里寫(xiě)代碼
- [pool release];
方法二:使用@autoreleasepool創(chuàng)建
- @autoreleasepool {
- //這里寫(xiě)代碼
- }
自動(dòng)釋放池創(chuàng)建后,就會(huì)成為活動(dòng)的池子,釋放池子后,池子將釋放其所包含的所有對(duì)象。
以上兩種方法推薦***種,因?yàn)閷?nèi)存交給ObjC管理更高效。
自動(dòng)釋放池什么時(shí)候創(chuàng)建?
app使用過(guò)程中,會(huì)定期自動(dòng)生成和銷毀自動(dòng)釋放池,一般是在程序事件處理之前創(chuàng)建,當(dāng)然我們也可以自行創(chuàng)建自動(dòng)釋放池,來(lái)達(dá)到我們一些特定的目的。
自動(dòng)釋放池什么時(shí)候銷毀?
自動(dòng)釋放池的銷毀時(shí)間是確定的,一般是在程序事件處理之后釋放,或者由我們自己手動(dòng)釋放。
下面舉例說(shuō)明自動(dòng)釋放池的工作流程:
場(chǎng)景:現(xiàn)在xiaoming和xiaohong都想和小狗一起玩耍,但是他們的需求不一樣,他們的玩耍時(shí)間不一樣,流程如下
- //創(chuàng)建一個(gè)自動(dòng)釋放池
- NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
- //模擬:寵物中心派出小狗
- Dog * dog = [[Dog alloc]init];
- //模擬:xiaoming需要和小狗玩耍,需要將其引用計(jì)數(shù)加1
- [dog retain];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaohong需要和小狗玩耍,需要將其引用計(jì)數(shù)加1
- [dog retain];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaoming確定不想和小狗玩耍了,需要將其引用計(jì)數(shù)減1
- [dog release];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaohong不確定何時(shí)不想和小狗玩耍了,將其設(shè)置為自動(dòng)釋放
- [dog autorelease];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //沒(méi)人需要和小狗玩耍了,將其引用計(jì)數(shù)減1
- [dog release];
- NSLog(@"釋放池子");
- [pool release];
- //創(chuàng)建一個(gè)自動(dòng)釋放池
- @autoreleasepool {
- //模擬:寵物中心派出小狗
- Dog * dog = [[Dog alloc]init];
- //模擬:xiaoming需要和小狗玩耍,需要將其引用計(jì)數(shù)加1
- [dog retain];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaohong需要和小狗玩耍,需要將其引用計(jì)數(shù)加1
- [dog retain];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaoming確定不想和小狗玩耍了,需要將其引用計(jì)數(shù)減1
- [dog release];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //模擬:xiaohong不確定何時(shí)不想和小狗玩耍了,將其設(shè)置為自動(dòng)釋放
- [dog autorelease];
- NSLog(@"小狗的引用計(jì)數(shù)為 %ld",dog.retainCount);
- //沒(méi)人需要和小狗玩耍了,將其引用計(jì)數(shù)減1
- [dog release];
- NSLog(@"釋放池子");
- }
輸出結(jié)果如下:
- [34819:7801589] 初始引用計(jì)數(shù)為 1
- [34819:7801589] 小狗的引用計(jì)數(shù)為 2
- [34819:7801589] 小狗的引用計(jì)數(shù)為 3
- [34819:7801589] 小狗的引用計(jì)數(shù)為 2
- [34819:7801589] 小狗的引用計(jì)數(shù)為 2
- [34819:7801589] 釋放池子
- [34819:7801589] 銷毀Dog
可以看到,當(dāng)池子釋放后,dog對(duì)象才被釋放,因此在池子釋放之前,xiaohong都可以盡情地和小狗玩耍。
使用自動(dòng)釋放池需要注意:
1)自動(dòng)釋放池實(shí)質(zhì)上只是在釋放的時(shí)候給池中所有對(duì)象對(duì)象發(fā)送release消息,不保證對(duì)象一定會(huì)銷毀,如果自動(dòng)釋放池向?qū)ο蟀l(fā)送release消息后對(duì)象的引用計(jì)數(shù)仍大于1,對(duì)象就無(wú)法銷毀。
2)自動(dòng)釋放池中的對(duì)象會(huì)集中同一時(shí)間釋放,如果操作需要生成的對(duì)象較多占用內(nèi)存空間大,可以使用多個(gè)釋放池來(lái)進(jìn)行優(yōu)化。比如在一個(gè)循環(huán)中需要?jiǎng)?chuàng)建大量的臨時(shí)變量,可以創(chuàng)建內(nèi)部的池子來(lái)降低內(nèi)存占用峰值。
3)autorelease不會(huì)改變對(duì)象的引用計(jì)數(shù)
自動(dòng)釋放池的常見(jiàn)問(wèn)題:
在管理對(duì)象釋放的問(wèn)題上,自動(dòng)幫助我們釋放池節(jié)省了大量的時(shí)間,但是有時(shí)候它卻未必會(huì)達(dá)到我們期望的效果,比如在一個(gè)循環(huán)事件中,如果循環(huán)次數(shù)較大或者事件處理占用內(nèi)存較大,就會(huì)導(dǎo)致內(nèi)存占用不斷增長(zhǎng),可能會(huì)導(dǎo)致不希望看到的后果。
示例代碼:
- for (int i = 0; i < 100000; i ++) {
- NSString * log = [NSString stringWithFormat:@"%d", i];
- NSLog(@"%@", log);
- }
前面講過(guò),自動(dòng)釋放池的釋放時(shí)間是確定的,這個(gè)例子中自動(dòng)釋放池會(huì)在循環(huán)事件結(jié)束時(shí)釋放,那問(wèn)題來(lái)了:在這個(gè)十萬(wàn)次的循環(huán)中,每次都會(huì)生成一個(gè)字符串并打印,這些字符串對(duì)象都放在池子中并直到循環(huán)結(jié)束才會(huì)釋放,因此在循環(huán)期間內(nèi)存不增長(zhǎng)。
這類問(wèn)題的解決方案是在循環(huán)中創(chuàng)建新的自動(dòng)釋放池,多少個(gè)循環(huán)釋放一次由我們自行決定。
- for (int i = 0; i < 100000; i ++) {
- @autoreleasepool {
- NSString * log = [NSString stringWithFormat:@"%d", i];
- NSLog(@"%@", log);
- }
- }
3、iOS的內(nèi)存管理規(guī)則
3.1 基本原則
無(wú)規(guī)矩不成方圓,在iOS開(kāi)發(fā)中也存在規(guī)則來(lái)約束開(kāi)發(fā)者進(jìn)行內(nèi)存管理,總的來(lái)講有三點(diǎn):
1)當(dāng)你通過(guò)new、alloc或copy方法創(chuàng)建一個(gè)對(duì)象時(shí),它的引用計(jì)數(shù)為1,當(dāng)不再使用該對(duì)象時(shí),應(yīng)該向?qū)ο蟀l(fā)送release或者autorelease消息釋放對(duì)象。
2)當(dāng)你通過(guò)其他方法獲得一個(gè)對(duì)象時(shí),如果對(duì)象引用計(jì)數(shù)為1且被設(shè)置為autorelease,則不需要執(zhí)行任何釋放對(duì)象的操作;
3)如果你打算取得對(duì)象所有權(quán),就需要保留對(duì)象并在操作完成之后釋放,且必須保證retain和release的次數(shù)對(duì)等。
應(yīng)用到文章開(kāi)頭的例子中,小朋友每申請(qǐng)一個(gè)小狗(生成對(duì)象),***都要?dú)w還到寵物中心(釋放對(duì)象),如果只申請(qǐng)而不歸還(對(duì)象創(chuàng)建了沒(méi)有釋放),那寵物中心的小狗就會(huì)越來(lái)越少(可用內(nèi)存越來(lái)越少),到***一個(gè)小狗都沒(méi)有了(內(nèi)存被耗盡),其他小朋友就再也沒(méi)有小狗可申請(qǐng)了(無(wú)資源可申請(qǐng)使用),因此,必須要遵守規(guī)則:申請(qǐng)必須歸還(規(guī)則1),申請(qǐng)幾個(gè)必須歸還幾個(gè)(規(guī)則3),如果小狗被設(shè)定歸還時(shí)間則不用小朋友主動(dòng)歸還(規(guī)則2)。
有興趣的讀者可以思考:
以上原則可以總結(jié)成一句簡(jiǎn)潔的話,是什么呢?
3.2 ARC
在MRC時(shí)代,必須嚴(yán)格遵守以上規(guī)則,否則內(nèi)存問(wèn)題將成為惡魔一樣的存在,然而來(lái)到ARC時(shí)代,事情似乎變得輕松了,不用再寫(xiě)無(wú)止盡的ratain和release似乎讓開(kāi)發(fā)變得輕松了,對(duì)初學(xué)者變得更友好。
ObjC2.0引入了垃圾回收機(jī)制,然而由于垃圾回收機(jī)制會(huì)對(duì)移動(dòng)設(shè)備產(chǎn)生某些不好的影響(例如由于垃圾清理造成的卡頓),iOS并不支持這個(gè)機(jī)制,蘋(píng)果的解決方案就是ARC(自動(dòng)引用計(jì)數(shù))。
iOS5以后,我們可以開(kāi)啟ARC模式,ARC可以理解成一位管家,這個(gè)管家會(huì)幫我們向?qū)ο蟀l(fā)送retain和release語(yǔ)句,不再需要我們手動(dòng)添加了,我們可以更舒心地創(chuàng)建或引用對(duì)象,簡(jiǎn)化內(nèi)存管理步驟,節(jié)省大量的開(kāi)發(fā)時(shí)間。
實(shí)際上,ARC不是垃圾回收,也并不是不需要內(nèi)存管理了,它是隱式的內(nèi)存管理,編譯器在編譯的時(shí)候會(huì)在代碼插入合適的ratain和release語(yǔ)句,相當(dāng)于在背后幫我們完成了內(nèi)存管理的工作。
下面將自動(dòng)釋放池的例子轉(zhuǎn)化成ARC來(lái)看看
- @autoreleasepool {
- Dog * dog = [[Dog alloc]init];
- [xiaoming playWithDog:dog];
- [xiaohong playWithDog:dog];
- NSLog(@"釋放池子");
- }
怎么樣,是不是簡(jiǎn)潔了很多,是不是很熟悉的感覺(jué)呢。
注意:
1)如果你的工程歷史比較久,可以將其從MRC轉(zhuǎn)換成ARC,跟上時(shí)代的步伐更好地維護(hù)
2)如果你的工程引用了某些不支持ARC的庫(kù),可以在Build Phases的Compile Sources將對(duì)應(yīng)的m文件的編譯器參數(shù)配置為-fno-objc-arc
3)ARC能幫我們簡(jiǎn)化內(nèi)存管理問(wèn)題,但不代表它是***的,還是有它不能處理的情況,這就需要我們自己手動(dòng)處理,比如循環(huán)引用、非ObjC對(duì)象、Core Foundation中的malloc()或者free()等等
有興趣的讀者可以思考:
MRC有什么缺點(diǎn)?ARC有什么局限性?請(qǐng)列舉。
3.3 ARC的修飾符
ARC提供四種修飾符,分別是strong, weak, autoreleasing, unsafe_unretained。
__strong:強(qiáng)引用,持有所指向?qū)ο蟮乃袡?quán),無(wú)修飾符情況下的默認(rèn)值。如需強(qiáng)制釋放,可置nil。
比如我們常用的定時(shí)器
- NSTimer * timer = [NSTimer timerWith...];
相當(dāng)于
- NSTimer * __strong timer = [NSTimer timerWith...];
當(dāng)不需要使用時(shí),強(qiáng)制銷毀定時(shí)器
- [timer invalidate];
- timer = nil;
__weak:弱引用,不持有所指向?qū)ο蟮乃袡?quán),引用指向的對(duì)象內(nèi)存被回收之后,引用本身會(huì)置nil,避免野指針。
比如避免循環(huán)引用的弱引用聲明:
- __weak __typeof(self) weakSelf = self;
__autoreleasing:自動(dòng)釋放對(duì)象的引用,一般用于傳遞參數(shù)
比如一個(gè)讀取數(shù)據(jù)的方法
- - (void)loadData:(NSError **)error;
當(dāng)你調(diào)用時(shí)會(huì)發(fā)現(xiàn)這樣的提示
- NSError * error;
- [dataTool loadData:(NSError *__autoreleasing *)]
這是編譯器自動(dòng)幫我們插入以下代碼
- NSError * error;
- NSError * __autoreleasing tmpErr = error;
- [dataTool loadData:&tmpErr];
__unsafe_unretained:為兼容iOS5以下版本的產(chǎn)物,可以理解成MRC下的weak,現(xiàn)在基本用不到,這里不作描述。
有興趣的讀者可以思考:
1)__strong NSTimer * timer 和 NSTimer * __strong timer哪個(gè)寫(xiě)法是正確的, 為什么編譯器不報(bào)錯(cuò)?
2)使用__autoreleasing可能會(huì)遇到哪些問(wèn)題?
3.4 屬性的內(nèi)存管理
ObjC2.0引入了@property,提供成員變量訪問(wèn)方法、權(quán)限、環(huán)境、內(nèi)存管理類型的聲明,下面主要說(shuō)明ARC中屬性的內(nèi)存管理。
屬性的參數(shù)分為三類,基本數(shù)據(jù)類型默認(rèn)為(atomic,readwrite,assign),對(duì)象類型默認(rèn)為(atomic,readwrite,strong),其中第三個(gè)參數(shù)就是該屬性的內(nèi)存管理方式修飾,修飾詞可以是以下之一:
1)assign:直接賦值
assign一般用來(lái)修飾基本數(shù)據(jù)類型
@property (nonatomic, assign) NSInteger count;
當(dāng)然也可以修飾ObjC對(duì)象,但是不推薦,因?yàn)楸籥ssign修飾的對(duì)象釋放后,指針還是指向釋放前的內(nèi)存,在后續(xù)操作中可能會(huì)導(dǎo)致內(nèi)存問(wèn)題引發(fā)崩潰。
2)retain:release舊值,再retain新值(引用計(jì)數(shù)+1)
retain和strong一樣,都用來(lái)修飾ObjC對(duì)象。
使用set方法賦值時(shí),實(shí)質(zhì)上是會(huì)先保留新值,再釋放舊值,再設(shè)置新值,避免新舊值一樣時(shí)導(dǎo)致對(duì)象被釋放的的問(wèn)題。
MRC寫(xiě)法如下
- - (void)setCount:(NSObject *)count {
- [count retain];
- [_count release];
- _count = count;
- }
ARC對(duì)應(yīng)寫(xiě)法
- - (void)setCount:(NSObject *)count {
- _count = count;
- }
3)copy:release舊值,再copy新值(拷貝內(nèi)容)
一般用來(lái)修飾String、Dict、Array等需要保護(hù)其封裝性的對(duì)象,尤其是在其內(nèi)容可變的情況下,因此會(huì)拷貝(深拷貝)一份內(nèi)容給屬性使用,避免可能造成的對(duì)源內(nèi)容進(jìn)行改動(dòng)。
使用set方法賦值時(shí),實(shí)質(zhì)上是會(huì)先拷貝新值,再釋放舊值,再設(shè)置新值。
實(shí)際上,遵守NSCopying的對(duì)象都可以使用copy,當(dāng)然,如果你確定是要共用同一份可變內(nèi)容,你也可以使用strong或retain。
@property (nonatomic, copy) NSString * name;
4)weak:ARC新引入修飾詞,可代替assign,比assign多增加一個(gè)特性(置nil,見(jiàn)上文)。
weak和strong一樣用來(lái)修飾ObjC對(duì)象。
使用set方法賦值時(shí),實(shí)質(zhì)上不保留新值,也不釋放舊值,只設(shè)置新值。
比如常用的代理的聲明
- @property (weak) id<mydelegate> delegate;</mydelegate>
Xib控件的引用
- @property (weak, nonatomic) IBOutlet UIImageView *productImage;
5)strong:ARC新引入修飾詞,可代替retain
可參照retain,這里不再作描述。
有興趣的讀者可以思考:
1)各個(gè)屬性修飾詞和3.3中的修飾詞的對(duì)應(yīng)關(guān)系?
2)屬性的本質(zhì)是什么?
3.5 block的內(nèi)存管理
iOS中使用block必須自己管理內(nèi)存,錯(cuò)誤的內(nèi)存管理將導(dǎo)致循環(huán)引用等內(nèi)存泄漏問(wèn)題,這里主要說(shuō)明在ARC下block聲明和使用的時(shí)候需要注意的兩點(diǎn):
1)如果你使用@property去聲明一個(gè)block的時(shí)候,一般使用copy來(lái)進(jìn)行修飾(當(dāng)然也可以不寫(xiě),編譯器自動(dòng)進(jìn)行copy操作),盡量不要使用retain。
@property (nonatomic, copy) void(^block)(NSData * data);
2)block會(huì)對(duì)內(nèi)部使用的對(duì)象進(jìn)行強(qiáng)引用,因此在使用的時(shí)候應(yīng)該確定不會(huì)引起循環(huán)引用,當(dāng)然保險(xiǎn)的做法就是添加弱引用標(biāo)記。
__weak typeof(self) weakSelf = self;
有興趣的讀者可以深入了解:
1、block的內(nèi)部實(shí)現(xiàn)原理是什么?
2、從內(nèi)存位置來(lái)看block有幾種類型?它們的內(nèi)存管理方式各是怎樣的?
3、對(duì)于不同類型的外部變量,block的內(nèi)存管理都是怎樣的?
4 經(jīng)典內(nèi)存泄漏及其解決方案
雖然ARC好處多多,然而也并無(wú)法避免內(nèi)存泄漏問(wèn)題,下面介紹在ARC中常見(jiàn)的內(nèi)存泄漏。
4.1 僵尸對(duì)象和野指針
僵尸對(duì)象:內(nèi)存已經(jīng)被回收的對(duì)象。
野指針:指向僵尸對(duì)象的指針,向野指針發(fā)送消息會(huì)導(dǎo)致崩潰。
野指針錯(cuò)誤形式在Xcode中通常表現(xiàn)為:Thread 1:EXC_BAD_ACCESS,因?yàn)槟阍L問(wèn)了一塊已經(jīng)不屬于你的內(nèi)存。
例子代碼:(沒(méi)有出現(xiàn)錯(cuò)誤的筒子多運(yùn)行幾遍,因?yàn)楂@取野指針指向的結(jié)果是不確定的)
- Dog * dog = [[Dog alloc]init];
- NSLog(@"before");
- NSLog(@"%s",object_getClassName(dog));
- [dog release];
- NSLog(@"after");
- NSLog(@"%s",object_getClassName(dog));
運(yùn)行結(jié)果:
- [15184:5811062] before
- [15184:5811062] Dog
- [15184:5811062] after
- (lldb)
可以看到,當(dāng)運(yùn)行到第六行的時(shí)候崩潰了,并給出了EXC_BAD_ACCESS的提示。
解決方案:
對(duì)象已經(jīng)被釋放后,應(yīng)將其指針置為空指針(沒(méi)有指向任何對(duì)象的指針,給空指針發(fā)送消息不會(huì)報(bào)錯(cuò))。
然而在實(shí)際開(kāi)發(fā)中實(shí)際遇到EXC_BAD_ACCESS錯(cuò)誤時(shí),往往很難定位到錯(cuò)誤點(diǎn),幸好Xcode提供方便的工具給我們來(lái)定位及分析錯(cuò)誤。
1)在product-scheme-edit scheme-diagnostics中將enable zombie objects勾選上,下次再出現(xiàn)這樣的錯(cuò)誤就可以準(zhǔn)確定位了。
運(yùn)行結(jié)果:
- [15169:5801945] before
- [15169:5801945] Dog
- [15169:5801945] after
- [15169:5801945] _NSZombie_Dog
可以看到,當(dāng)運(yùn)行到第六行時(shí)并沒(méi)有崩潰,并給出了NSZombie的提示。
2)在Xcode-open developer tool-Instruments打開(kāi)工具集,選擇Zombies工具可以對(duì)已安裝的應(yīng)用進(jìn)行僵尸對(duì)象檢測(cè)。
4.2 循環(huán)引用
循環(huán)引用是ARC中最常出現(xiàn)的問(wèn)題,對(duì)于可能引發(fā)循環(huán)引用的一些原因在前一篇文章iOS總結(jié)篇:影響控制器正常釋放的常見(jiàn)問(wèn)題中有提及,大家可以看看。
一般來(lái)講循環(huán)引用也是可以使用工具來(lái)檢測(cè)到的,分為兩種:
1)在product-Analyze中使用靜態(tài)分析來(lái)檢測(cè)代碼中可能存在循環(huán)引用的問(wèn)題。
2)在Xcode-open developer tool-Instruments打開(kāi)工具集,選擇Leaks工具可以對(duì)已安裝的應(yīng)用進(jìn)行內(nèi)存泄漏檢測(cè),此工具能檢測(cè)靜態(tài)分析不會(huì)提示,但是到運(yùn)行時(shí)才會(huì)出現(xiàn)的內(nèi)存泄漏問(wèn)題。
Leaks工具雖然強(qiáng)大,但是它不能檢測(cè)到block循環(huán)引用導(dǎo)致的內(nèi)存泄漏,這種情況一般需要自行排查問(wèn)題(考驗(yàn)?zāi)愕幕竟r(shí)候到了),傻瓜式的方案當(dāng)然是重寫(xiě)對(duì)象的dealloc方法來(lái)監(jiān)測(cè)對(duì)象是否正常釋放,來(lái)確認(rèn)沒(méi)有形成循環(huán)引用。
由于ARC中循環(huán)引用出現(xiàn)的幾率相對(duì)較大,很多大神或者團(tuán)隊(duì)都提供了很多解決此問(wèn)題的思路和方法,甚至開(kāi)發(fā)了插件和類庫(kù)來(lái)幫助開(kāi)發(fā)者更好地檢測(cè)問(wèn)題,有興趣的讀者可以研究一下,是否好用,孰好孰壞就由讀者自行評(píng)判了。
4.3 循環(huán)中對(duì)象占用內(nèi)存大
這個(gè)問(wèn)題常見(jiàn)于循環(huán)次數(shù)較大,循環(huán)體生成的對(duì)象占用內(nèi)存較大的情景。
例子代碼:我需要10000個(gè)演員來(lái)打仗
- for (int i = 0; i < 10000; i ++) {
- Person * soldier = [[Person alloc]init];
- [soldier fight];
- }
該循環(huán)內(nèi)產(chǎn)生大量的臨時(shí)對(duì)象,直至循環(huán)結(jié)束才釋放,可能導(dǎo)致內(nèi)存泄漏,解決方法和上文中提到的自動(dòng)釋放池常見(jiàn)問(wèn)題類似:在循環(huán)中創(chuàng)建自己的autoReleasePool,及時(shí)釋放占用內(nèi)存大的臨時(shí)變量,減少內(nèi)存占用峰值。
- for (int i = 0; i < 10000; i ++) {
- @autoreleasepool {
- Person * soldier = [[Person alloc]init];
- [soldier fight];
- }
- }
然而有時(shí)候autoReleasePool也不是***的:
例子:假如有2000張圖片,每張1M左右,現(xiàn)在需要獲取所有圖片的尺寸,你會(huì)怎么做?
如果這樣做
- for (int i = 0; i < 2000; i ++) {
- CGSize size = [UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]].size;
- //add size to array
- }
用imageNamed方法加載圖片占用Cache的內(nèi)存,autoReleasePool也不能釋放,對(duì)此問(wèn)題需要另外的解決方法,當(dāng)然保險(xiǎn)的當(dāng)然是雙管齊下了
- for (int i = 0; i < 2000; i ++) {
- @autoreleasepool {
- CGSize size = [UIImage imageWithContentsOfFile:filePath].size;
- //add siez to array
- }
- }
4.4 ***循環(huán)
這個(gè)是比4.3更極端的情況,無(wú)論你出于什么原因,當(dāng)你啟動(dòng)了一個(gè)***循環(huán)的時(shí)候,ARC會(huì)默認(rèn)該方法用不會(huì)執(zhí)行完畢,方法里面的對(duì)象就永不釋放,內(nèi)存***上漲,導(dǎo)致內(nèi)存泄漏。
例子:
- NSLog(@"start !");
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- BOOL isSucc = YES;
- while (isSucc) {
- [NSThread sleepForTimeInterval:1.0];
- NSLog(@"create an obj");
- }
- });
輸出結(jié)果為
- [7026:3555827] start !
- [7026:3556236] create an obj
- [7026:3556236] create an obj
- [7026:3556236] create an obj
- [7026:3556236] create an obj
- [7026:3555827] dealloc
- [7026:3556236] create an obj
- [7026:3556236] create an obj
- [7026:3556236] create an obj
可以看到,當(dāng)控制器釋放后該循環(huán)還在繼續(xù)。
對(duì)于這類問(wèn)題解決方案是什么呢?留給讀者思考吧~ ^_^
提示:解決方法有autoreleasepool、block、timer等等
后記
關(guān)于iOS內(nèi)存管理的知識(shí)點(diǎn)很多,如果展開(kāi)來(lái)講,本文涉及的知識(shí)點(diǎn)都可以寫(xiě)成一篇長(zhǎng)文,因此,本文只是做一個(gè)概述,試圖起到拋磚引玉的作用,幫助iOS開(kāi)發(fā)的初學(xué)者更快地理解內(nèi)存管理。
關(guān)于第四點(diǎn)“經(jīng)典內(nèi)存泄漏及其解決方案”,將專門(mén)寫(xiě)一篇文章在本文的基礎(chǔ)上詳細(xì)介紹(圖文并茂),敬請(qǐng)期待。