一、內(nèi)存管理的基本規(guī)則
在Objective-C的內(nèi)存管理中弯予,其實(shí)就是引用計(jì)數(shù)(reference count)的管理。內(nèi)存管理就是在程序需要時(shí)程序員分配一段內(nèi)存空間,而當(dāng)使用完之后將它釋放杀狡。如果程序員對內(nèi)存資源使用不當(dāng),有時(shí)不僅會(huì)造成內(nèi)存資源浪費(fèi)贰镣,甚至?xí)?dǎo)致程序crach呜象。
二、 引用計(jì)數(shù)(Reference Count)
為了解釋引用計(jì)數(shù)碑隆,我們做一個(gè)類比:員工在辦公室使用燈的情景恭陡。
- 當(dāng)第一個(gè)人進(jìn)入辦公室時(shí),他需要使用燈上煤,于是開燈休玩,引用計(jì)數(shù)為1
- 當(dāng)另一個(gè)人進(jìn)入辦公室時(shí),他也需要燈楼入,引用計(jì)數(shù)為2哥捕;每當(dāng)多一個(gè)人進(jìn)入辦公室時(shí),引用計(jì)數(shù)加1
- 當(dāng)有一個(gè)人離開辦公室時(shí)嘉熊,引用計(jì)數(shù)減1遥赚,當(dāng)引用計(jì)數(shù)為0時(shí),也就是最后一個(gè)人離開辦公室時(shí)阐肤,他不再需要使用燈凫佛,關(guān)燈離開辦公室讲坎。
從底層的實(shí)現(xiàn)來講就是一個(gè)class結(jié)構(gòu)體
,結(jié)構(gòu)體內(nèi)部有個(gè)值(retain count
)記錄了對象擁有者(ownship
)的個(gè)數(shù)愧薛,當(dāng)計(jì)數(shù)值為0時(shí)晨炕,系統(tǒng)將自動(dòng)釋放這個(gè)對象占用的內(nèi)存空間。
三毫炉、內(nèi)存管理基本規(guī)則
從上面燈的例子瓮栗,我們對比一下燈的動(dòng)作與Objective-C對象的動(dòng)作有什么相似之處:
燈的動(dòng)作 | Objective-C對象的動(dòng)作 |
---|---|
開燈 | 創(chuàng)建一個(gè)對象并獲取它的所有權(quán)(ownership) |
使用燈 | 獲取對象的所有權(quán) |
不使用燈 | 放棄對象的所有權(quán) |
關(guān)燈 | 釋放對象 |
-
而Objective-C對象的動(dòng)作對應(yīng)有哪些方法以及這些方法對引用計(jì)數(shù)有什么影響?
Objective-C對象的動(dòng)作 | Objective-C對象的方法 |
---|---|
1. 創(chuàng)建一個(gè)對象并獲取它的所有權(quán) | alloc/new/copy/mutableCopy (RC = 1) |
2. 獲取對象的所有權(quán) | retain (RC + 1) |
3. 放棄對象的所有權(quán) | release (RC - 1) |
4. 釋放對象 | dealloc (RC = 0 瞄勾,此時(shí)會(huì)調(diào)用該方法) |
當(dāng)你alloc
一個(gè)對象objc
费奸,此時(shí)RC=1
;在某個(gè)地方你又retain
這個(gè)對象objc
进陡,此時(shí)RC
加1愿阐,也就是RC=2
;由于調(diào)用alloc/retain
一次趾疚,對應(yīng)需要調(diào)用release
一次來釋放對象objc
缨历,所以你需要release
對象objc
兩次,此時(shí)RC=0
糙麦;而當(dāng)RC=0
時(shí)辛孵,系統(tǒng)會(huì)自動(dòng)調(diào)用dealloc
方法釋放對象。
除了alloc/new/copy/mutableCopy/retain
這幾種方法可以獲取對象的所有權(quán)(ownship)外喳资,當(dāng)對象被添加到集合對象(array, dictionary, set)中時(shí)觉吭,集合對象會(huì)獲取集合中所有對象的所有權(quán)(RC+1
),當(dāng)集合對象釋放時(shí),也會(huì)默認(rèn)向集合中所有對象發(fā)送release
消息(RC -1)
仆邓,符合誰創(chuàng)建誰釋放
的原則鲜滩。
- 注意下以下情況是不會(huì)獲取對象的所有權(quán):
(1)不使用alloc/new/copy/mutableCopy
方法引用的對象將不會(huì)獲取對象的擁有權(quán)
- (NSString *)fullName {
NSString *string = [NSString stringWithFormat:@"%@ %@",
self.firstName, self.lastName];
return string;
}
這個(gè)函數(shù)調(diào)用的是NSString
的 stringWithFormat
并不不滿足內(nèi)存管理基本原則,不會(huì)擁有對象的所有權(quán)节值,所以可以放心的返回徙硅。而不用調(diào)用release方法或者autorelease方法。
(2)引用對象指針地址
的方式(指針(*)搞疗、取地址(&)
)嗓蘑,不會(huì)獲取對象的擁有權(quán)。
這個(gè)比較好理解匿乃,比如我們常見的error桩皿。
NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
// Deal with error...
}
// ...
[string release];
四、 Autorelease Pool
在開發(fā)中幢炸,我們常常都會(huì)使用到局部變量
泄隔,局部變量一個(gè)特點(diǎn)就是當(dāng)它超過作用域時(shí),就會(huì)自動(dòng)釋放宛徊。而autorelease pool
跟局部變量
類似佛嬉,當(dāng)執(zhí)行代碼超過autorelease pool
塊時(shí)逻澳,所有放在autorelease pool
的對象都會(huì)自動(dòng)調(diào)用release
。它的工作原理如下:
- 創(chuàng)建一個(gè)
NSAutoreleasePool
對象 - 在autorelease pool塊的對象調(diào)用
autorelease
方法 - 釋放
NSAutoreleasePool
對象
iOS 5/OS X Lion前的(等下會(huì)介紹引入ARC的寫法)實(shí)例代碼如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// put object into pool
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
/* 超過autorelease pool作用域范圍時(shí)暖呕,obj會(huì)自動(dòng)調(diào)用release方法 */
由于放在autorelease pool的對象并不會(huì)馬上釋放斜做,如果有大量圖片數(shù)據(jù)放在這里的話,將會(huì)導(dǎo)致內(nèi)存不足湾揽。
for (int i = 0; i < numberOfImages; i++)
{
/* 處理圖片瓤逼,例如加載
* 太多autoreleased objects存在
* 由于NSAutoreleasePool對象沒有被釋放
* 在某個(gè)時(shí)刻,會(huì)導(dǎo)致內(nèi)存不足
*/
}
像上面這種情況你就可以這么寫:
for (int i = 0; i < numberOfImages; i++)
{
@autoreleasepool {
/*
*這樣臨時(shí)的autoreleased objects就會(huì)在autoreleasepool 結(jié)束時(shí)釋放達(dá)到最少的內(nèi)存占用钝腺。
*/
}
}
五抛姑、ARC管理方法
iOS/OS X內(nèi)存管理方法有兩種:手動(dòng)引用計(jì)數(shù)(Manual Reference Counting
)和自動(dòng)引用計(jì)數(shù)(Automatic Reference Counting
)。從OS X Lion和iOS 5開始艳狐,不再需要程序員手動(dòng)調(diào)用retain
和release
方法來管理Objective-C
對象的內(nèi)存,而是引入一種新的內(nèi)存管理機(jī)制Automatic Reference Counting(ARC)
皿桑,簡單來說毫目,它讓編譯器
來代替程序員來自動(dòng)加入retain
和release
方法來持有
和放棄
對象的所有權(quán)。
在ARC內(nèi)存管理機(jī)制中诲侮,id和其他對象類型變量必須是以下四個(gè)ownership qualifiers其中一個(gè)來修飾:
- __strong(默認(rèn)镀虐,如果不指定其他,編譯器就默認(rèn)加入)
- __weak
- __unsafe_unretained
- __autoreleasing
比方說下面這段程序
NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
// Report the error.
// ...
開啟ARC經(jīng)過編譯器處理后將會(huì)變成下面這樣:
NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
// Report the error.
// ...
接下來看幾個(gè)例子:
- 5.1沟绪、__strong ownership qualifier
如果變量var被__strong修飾刮便,當(dāng)變量var指向某個(gè)對象objc,那么變量var持有某個(gè)對象objc的所有權(quán)
如果我想創(chuàng)建一個(gè)字符串绽慈,使用完之后將它釋放調(diào)用恨旱,使用MRC管理內(nèi)存的寫法應(yīng)該是這樣:
{
NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"]; //@"Hello, world"對象的RC=1
NSLog(@"%@", text);
[text release]; //@"Hello, world"對象的RC=0
}
而如果是使用ARC方式的話,text對象無需調(diào)用release
方法坝疼,而是當(dāng)text
變量超過作用域時(shí)搜贤,編譯器來自動(dòng)加入[text release]
方法來釋放內(nèi)存
{
NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"]; //@"Hello, world"對象的RC=1
NSLog(@"%@", text);
}
/*
* 當(dāng)text超過作用域時(shí),@"Hello, world"對象會(huì)自動(dòng)釋放钝凶,RC=0
*/
而當(dāng)你將text賦值給其他變量anotherText時(shí)仪芒,MRC需要retain一下來持有所有權(quán),當(dāng)text和anotherText使用完之后耕陷,各個(gè)調(diào)用release方法來釋放掂名。
{
NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"]; //@"Hello, world"對象的RC=1
NSLog(@"%@", text);
NSString *anotherText = text; //@"Hello, world"對象的RC=1
[anotherText retain]; //@"Hello, world"對象的RC=2
NSLog(@"%@", anotherText);
[text release]; //@"Hello, world"對象的RC=1
[anotherText release]; //@"Hello, world"對象的RC=0
}
而使用ARC的話,則不需要調(diào)用retain和release方法來持有跟釋放對象哟沫。
{
NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"]; //@"Hello, world"對象的RC=1
NSLog(@"%@", text);
NSString *anotherText = text; //@"Hello, world"對象的RC=2
NSLog(@"%@", anotherText);
}
/*
* 當(dāng)text和anotherText超過作用域時(shí)饺蔑,會(huì)自動(dòng)調(diào)用[text release]和[anotherText release]方法, @"Hello, world"對象的RC=0
*/
除了當(dāng)__strong
變量超過作用域時(shí)南用,編譯器會(huì)自動(dòng)加入release
語句來釋放內(nèi)存膀钠,如果你將__strong
變量重新賦給它其他值掏湾,那么編譯器也會(huì)自動(dòng)加入release
語句來釋放變量指向之前的對象。例如:
{
NSString *text = [[NSString alloc] initWithFormat:@"Hello, world"]; //@"Hello, world"對象的RC=1
NSString *anotherText = text; //@"Hello, world"對象的RC=2
NSString *anotherText = [[NSString alloc] initWithFormat:@"Sam Lau"]; // 由于anotherText對象引用另一個(gè)對象@"Sam Lau"肿嘲,那么就會(huì)自動(dòng)調(diào)用[anotherText release]方法融击,使得@"Hello, world"對象的RC=1, @"Sam Lau"對象的RC=1
}
/*
* 當(dāng)text和anotherText超過作用域時(shí),會(huì)自動(dòng)調(diào)用[text release]和[anotherText release]方法雳窟,
* @"Hello, world"對象的RC=0和@"Sam Lau"對象的RC=0
*/
前面已經(jīng)提過內(nèi)存管理的四條規(guī)則:
Objective-C對象的動(dòng)作 | Objective-C對象的方法 |
---|---|
1. 創(chuàng)建一個(gè)對象并獲取它的所有權(quán) | alloc/new/copy/mutableCopy (RC = 1) |
2. 獲取對象的所有權(quán) | retain (RC + 1) |
3. 放棄對象的所有權(quán) | release (RC - 1) |
4. 釋放對象 | dealloc (RC = 0 尊浪,此時(shí)會(huì)調(diào)用該方法) |
我們總結(jié)一下編譯器是按以下方法來實(shí)現(xiàn)的:
- 對于規(guī)則1和規(guī)則2,是通過
__strong
變量來實(shí)現(xiàn)封救, - 對于規(guī)則3來說拇涤,當(dāng)變量超過它的作用域或被賦值或成員變量被丟棄時(shí)就能實(shí)現(xiàn)
- 對于規(guī)則4,當(dāng)
RC=0
時(shí)誉结,系統(tǒng)就會(huì)自動(dòng)調(diào)用
- 5.2鹅士、__weak ownership qualifier
__strong
不能解決引用循環(huán)(Reference Cycle)
問題:對象A持有對象B,反過來惩坑,對象B持有對象A掉盅;這樣會(huì)導(dǎo)致不能釋放內(nèi)存造成內(nèi)存泄露問題。
舉一個(gè)簡單的例子以舒,有一個(gè)類Test有個(gè)屬性objc趾痘,有兩個(gè)對象test1和test2的屬性objc互相引用test1和test2:
@interface Test : NSObject
@property (strong, nonatomic) id objc;
@end
{
Test *test1 = [Test new]; /* 對象a */
/* test1有一個(gè)強(qiáng)引用到對象a */
Test *test2 = [Test new]; /* 對象b */
/* test2有一個(gè)強(qiáng)引用到對象b */
test1.objc = test2; /* 對象a的成員變量objc有一個(gè)強(qiáng)引用到對象b */
test2.objc = test1; /* 對象b的成員變量objc有一個(gè)強(qiáng)引用到對象a */
}
/* 當(dāng)變量test1超過它作用域時(shí),它指向a對象會(huì)自動(dòng)release
* 當(dāng)變量test2超過它作用域時(shí)蔓钟,它指向b對象會(huì)自動(dòng)release
*
* 此時(shí)永票,b對象的objc成員變量仍持有一個(gè)強(qiáng)引用到對象a
* 此時(shí),a對象的objc成員變量仍持有一個(gè)強(qiáng)引用到對象b
* 于是發(fā)生內(nèi)存泄露
*/
如何解決滥沫?于是我們引用一個(gè)__weak ownership qualifier
侣集,被它修飾的變量都不持有對象的所有權(quán),而且當(dāng)變量指向的對象的RC為0時(shí)佣谐,變量設(shè)置為nil肚吏。例如:
__weak NSString *text = [[NSString alloc] initWithFormat:@"Sam Lau"];
NSLog(@"%@", text);
由于text變量被__weak修飾,text并不持有@"Sam Lau"對象的所有權(quán)狭魂,@"Sam Lau"對象一創(chuàng)建就馬上被釋放罚攀,并且編譯器給出警告??,所以打印結(jié)果為(null)雌澄。
所以斋泄,針對剛才的引用循環(huán)問題,只需要將Test類的屬性objc設(shè)置weak修飾符镐牺,那么就能解決炫掐。
@interface Test : NSObject
@property (weak, nonatomic) id objc;//修改成weak修飾符
@end
以及我們常用的block防止內(nèi)存泄漏也可以使用__weak 修飾符,引用官方給的例子如下:
MyViewController *myController = [[MyViewController alloc] init…];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler = ^(NSInteger result) {
MyViewController *strongMyController = weakMyController;
if (strongMyController) {
// ...
[strongMyController dismissViewControllerAnimated:YES completion:nil];
// ...
}
else {
// Probably nothing...
}
};
- 5.3睬涧、__unsafe_unretained ownership qualifier
__unsafe_unretained ownership qualifier
募胃,正如名字所示旗唁,它是不安全的。它跟__weak
相似痹束,被它修飾的變量都不持有對象的所有權(quán)检疫,但當(dāng)變量指向的對象的RC
為0時(shí),變量并不設(shè)置為nil
祷嘶,而是繼續(xù)保存對象的地址屎媳;這樣的話,對象有可能已經(jīng)釋放论巍,但繼續(xù)訪問烛谊,就會(huì)造成非法訪問(Invalid Access)。例子如下:
__unsafe_unretained id obj0 = nil;
{
id obj1 = [[NSObject alloc] init]; // 對象A
/* 由于obj1是強(qiáng)引用嘉汰,所以obj1持有對象A的所有權(quán)丹禀,對象A的RC=1 */
obj0 = obj1;
/* 由于obj0是__unsafe_unretained,它不持有對象A的所有權(quán)鞋怀,但能夠引用它湃崩,對象A的RC=1 */
NSLog(@"A: %@", obj0);
}
/* 當(dāng)obj1超過它的作用域時(shí),它指向的對象A將會(huì)自動(dòng)釋放 */
NSLog(@"B: %@", obj0);
/* 由于obj0是__unsafe_unretained接箫,當(dāng)它指向的對象RC=0時(shí),它會(huì)繼續(xù)保存對象的地址朵诫,所以兩個(gè)地址相同 */
打印結(jié)果是內(nèi)存地址相同
:
如果將__unsafe_unretained改為weak的話辛友,兩個(gè)打印結(jié)果將不同
__weak id obj0 = nil;
{
id obj1 = [[NSObject alloc] init]; // 對象A
/* 由于obj1是強(qiáng)引用,所以obj1持有對象A的所有權(quán)剪返,對象A的RC=1 */
obj0 = obj1;
/* 由于obj0是__unsafe_unretained废累,它不持有對象A的所有權(quán),但能夠引用它脱盲,對象A的RC=1 */
NSLog(@"A: %@", obj0);
}
/* 當(dāng)obj1超過它的作用域時(shí)邑滨,它指向的對象A將會(huì)自動(dòng)釋放 */
NSLog(@"B: %@", obj0);
/* 由于obj0是__weak, 當(dāng)它指向的對象RC=0時(shí)钱反,它會(huì)自動(dòng)設(shè)置為nil掖看,所以兩個(gè)打印結(jié)果將不同*/
- 5.4、 __autoreleasing ownership qualifier
MRC
下autorelease pool
寫法如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// put object into pool
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
/* 超過autorelease pool作用域范圍時(shí)面哥,obj會(huì)自動(dòng)調(diào)用release方法 */
引入ARC
之后哎壳,寫法更加簡潔:
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
相比之前的創(chuàng)建、使用和釋放NSAutoreleasePool
對象尚卫,現(xiàn)在你只需要將代碼放在@autoreleasepool
塊即可归榕。你也不需要調(diào)用autorelease
方法了,只需要用__autoreleasing
修飾變量即可吱涉。
但是我們很少或基本上不使用autorelease pool
刹泄。當(dāng)我們使用XCode
創(chuàng)建工程后外里,有一個(gè)app
的入口文件main.m
使用了它:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
六、 Property(屬性)與ownership qualifier
有了ARC之后特石,新的property modifier也被引入到Objective-C類的property盅蝗,例如:
@property (strong, nonatomic) NSString *text;
下面有張表來展示property modifier與ownership qualifier的對應(yīng)關(guān)系
Property modifier | Ownership qualifier |
---|---|
strong | __strong |
retain | __strong |
copy | __strong |
weak | __weak |
assign | __unsafe_unretained |
unsafe_unretained | __unsafe_unretained |
- 先看下命名一個(gè)屬性為
retain
時(shí),然后調(diào)用@synthesize
時(shí)編譯器將會(huì)做什么
setter方法:
- (void)setCount:(NSNumber *)newCount {
[newCount retain];
[_count release];
// Make the new assignment.
_count = newCount;
}
也就是這里會(huì)首先retain新值,然后釋放舊值(舊值RC -1)县匠,然后再賦值新值风科。
- 相同的當(dāng)命名一個(gè)屬性為copy類型時(shí)
setter方法:
- (void)setCount:(NSNumber *)newCount {
[_count release];
// Make the new assignment.
_count = [newCount copy];
}
retain與copy的區(qū)別在于,一個(gè)不產(chǎn)生新的對象只是對對象的RC + 1,另一個(gè)產(chǎn)生新的對象乞旦。
- 除了上面屬性修飾符外還有
atomic (default)
贼穆、nonatomic
、readonly
兰粉、readwrite
故痊。
只能一個(gè)線程訪問
,線程安全的(相當(dāng)于線程鎖的概念玖姑,一個(gè)時(shí)間段只有一個(gè)線程訪問不容易出錯(cuò)愕秫,所以線程安全),低性能
七、內(nèi)存管理問題解決辦法
在往下看之前請下載實(shí)例MemoryProblems焰络,我們將以這個(gè)工程展開如何檢查和解決內(nèi)存問題戴甩。
-
懸掛指針問題
懸掛指針(Dangling Pointer)就是當(dāng)指針指向的對象已經(jīng)釋放或回收后,但沒有對指針做任何修改(一般來說闪彼,將它指向空指針)甜孤,而是仍然指向原來已經(jīng)回收的地址。如果指針指向的對象已經(jīng)釋放畏腕,但仍然使用缴川,那么就會(huì)導(dǎo)致程序crash。
當(dāng)你運(yùn)行MemoryProblems后描馅,點(diǎn)擊懸掛指針那個(gè)選項(xiàng)把夸,就會(huì)出現(xiàn)EXC_BAD_ACCESS
崩潰信息
我們看看這個(gè)NameListViewController
是做什么的?它繼承UITableViewController
铭污,主要顯示多個(gè)名字的信息恋日。它的實(shí)現(xiàn)文件如下:
static NSString *const kNameCellIdentifier = @"NameCell";
@interface NameListViewController ()
#pragma mark - Model
@property (strong, nonatomic) NSArray *nameList;
#pragma mark - Data source
@property (assign, nonatomic) ArrayDataSource *dataSource;
@end
@implementation NameListViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self.dataSource;
}
#pragma mark - Lazy initialization
- (NSArray *)nameList
{
if (!_nameList) {
_nameList = @[@"Sam", @"Mike", @"John", @"Paul", @"Jason"];
}
return _nameList;
}
- (ArrayDataSource *)dataSource
{
if (!_dataSource) {
_dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
cellIdentifier:kNameCellIdentifier
tableViewStyle:UITableViewCellStyleDefault
configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
cell.textLabel.text = item;
}];
}
return _dataSource;
}
@end
要想通過tableView
顯示數(shù)據(jù),首先要實(shí)現(xiàn)UITableViewDataSource
這個(gè)協(xié)議况凉,為了瘦身controller
和復(fù)用data source
谚鄙,我將它分離到一個(gè)類ArrayDataSource
來實(shí)現(xiàn)UITableViewDataSource
這個(gè)協(xié)議。然后在viewDidLoad
方法里面將dataSource
賦值給tableView.dataSource
刁绒。
解釋完NameListViewController
的職責(zé)后闷营,接下來我們需要思考出現(xiàn)EXC_BAD_ACCESS
錯(cuò)誤的原因和位置信息。
一般來說,出現(xiàn)EXC_BAD_ACCESS
錯(cuò)誤的原因都是懸掛指針導(dǎo)致的傻盟,但具體是哪個(gè)指針是懸掛指針還不確定速蕊,因?yàn)榭刂婆_(tái)并沒有給出具體crash
信息。
-
啟用NSZombieEnabled
要想得到更多的crash
信息娘赴,你需要啟動(dòng)NSZombieEnabled
规哲。具體步驟如下:
1、選中Edit Scheme
诽表,并點(diǎn)擊
2唉锌、Run -> Diagnostics -> Enable Zombie Objects
設(shè)置完之后,再次運(yùn)行和點(diǎn)擊懸掛指針竿奏,雖然會(huì)再次crash,但這次控制臺(tái)打印了以下有用信息:
信息message sent to deallocated instance 0x7fe19b081760
大意是向一個(gè)已釋放對象發(fā)送信息绿语,也就是已釋放對象還調(diào)用某個(gè)方法『蛑罚現(xiàn)在我們大概知道什么原因?qū)е鲁绦驎?huì)crash
岗仑,但是具體哪個(gè)對象被釋放還仍然使用呢?
點(diǎn)擊上面紅色框的Continue program execution
按鈕繼續(xù)運(yùn)行荠雕,截圖如下:
留意上面的兩個(gè)紅色框泌神,它們兩個(gè)地址是一樣,而且ArrayDataSource
前面有個(gè)_NSZombie_
修飾符舞虱,說明dataSource
對象被釋放還仍然使用。
再進(jìn)一步看dataSource
聲明屬性的修飾符是assign
#pragma mark - Data source
@property (assign, nonatomic) ArrayDataSource *dataSource;
而assign對應(yīng)就是__unsafe_unretained母市,它跟__weak相似矾兜,被它修飾的變量都不持有對象的所有權(quán),但當(dāng)變量指向的對象的RC為0時(shí)患久,變量并不設(shè)置為nil椅寺,而是繼續(xù)保存對象的地址。
因此蒋失,在viewDidLoad方法中
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self.dataSource;
/* 由于dataSource是被assign修飾返帕,self.dataSource賦值后,它對象的對象就馬上釋放篙挽,
* 而self.tableView.dataSource也不是strong荆萤,而是weak,此時(shí)仍然使用,所有會(huì)導(dǎo)致程序crash
*/
}
解決:
@property (strong, nonatomic) ArrayDataSource *dataSource; //strong代替assign
八链韭、內(nèi)存泄露問題
使用Instruments里面的子工具Leaks來檢查內(nèi)存泄露偏竟。
-
靜態(tài)分析
一般來說,在程序未運(yùn)行之前我們可以先通過Clang Static Analyzer(靜態(tài)分析)來檢查代碼是否存在bug敞峭。比如踊谋,內(nèi)存泄露、文件資源泄露或訪問空指針的數(shù)據(jù)等旋讹。下面有個(gè)靜態(tài)分析的例子來講述如何啟用靜態(tài)分析以及靜態(tài)分析能夠查找哪些bugs鲁捏。
啟動(dòng)程序后捍掺,點(diǎn)擊靜態(tài)分析琅拌,馬上就出現(xiàn)crash
此時(shí)进宝,即使啟用NSZombieEnabled谭胚,控制臺(tái)也不能打印出更多有關(guān)bug的信息,具體原因是什么旁趟,等下會(huì)解釋锡搜。
打開StaticAnalysisViewController
,里面引用Facebook Infer工具的代碼例子肠缔,包含個(gè)人日常開發(fā)中會(huì)出現(xiàn)的bugs:
@implementation StaticAnalysisViewController
#pragma mark - Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self memoryLeakBug];
[self resoureLeakBug];
[self parameterNotNullCheckedBlockBug:nil];
[self npeInArrayLiteralBug];
[self prematureNilTerminationArgumentBug];
}
#pragma mark - Test methods from facebook infer iOS Hello examples
- (void)memoryLeakBug
{
CGPathRef shadowPath = CGPathCreateWithRect(self.inputView.bounds, NULL);
}
- (void)resoureLeakBug
{
FILE *fp;
fp=fopen("info.plist", "r");
}
-(void) parameterNotNullCheckedBlockBug:(void (^)())callback {
callback();
}
-(NSArray*) npeInArrayLiteralBug {
NSString *str = nil;
return @[@"horse", str, @"dolphin"];
}
-(NSArray*) prematureNilTerminationArgumentBug {
NSString *str = nil;
return [NSArray arrayWithObjects: @"horse", str, @"dolphin", nil];
}
@end
下面我們通過靜態(tài)分析來檢查代碼是否存在bugs
。有兩個(gè)方式:
手動(dòng)靜態(tài)分析:每次都是通過點(diǎn)擊菜單欄的
Product -> Analyze
或快捷鍵shift + command + b
自動(dòng)靜態(tài)分析:在Build Settings啟用Analyze During 'Build'硼莽,每次編譯時(shí)都會(huì)自動(dòng)靜態(tài)分析
靜態(tài)分析結(jié)果如下:
通過靜態(tài)分析結(jié)果行疏,我們來分析一下為什么NSZombieEnabled不能定位EXC_BAD_ACCESS
的錯(cuò)誤代碼位置酿联。由于callback傳入進(jìn)來的是null指針
,而NSZombieEnabled
只能針對某個(gè)已經(jīng)釋放對象的地址
,所以啟動(dòng)NSZombieEnabled是不能定位的续镇,不過可以通過靜態(tài)分析可得知摸航。
-
啟動(dòng)Instruments
有時(shí)使用靜態(tài)分析
能夠檢查出一些內(nèi)存泄露
問題,但是有時(shí)只有運(yùn)行時(shí)
使用Instruments
才能檢查到逢净,啟動(dòng)Instruments
步驟如下:
-
點(diǎn)擊Xcode的菜單欄的 Product -> Profile 啟動(dòng)Instruments
-
此時(shí)甥雕,出現(xiàn)Instruments的工具集挟阻,選中Leaks子工具點(diǎn)擊
打開Leaks工具之后脱拼,點(diǎn)擊
紅色圓點(diǎn)
按鈕啟動(dòng)Leaks工具熄浓,在Leaks工具啟動(dòng)同時(shí),模擬器或真機(jī)也跟著啟動(dòng)
-
啟動(dòng)Leaks工具后娃惯,它會(huì)在程序
運(yùn)行時(shí)
記錄內(nèi)存分配信息和檢查是否發(fā)生內(nèi)存泄露趾浅。當(dāng)你點(diǎn)擊引用循環(huán)進(jìn)去那個(gè)頁面后,再返回到主頁往史,就會(huì)發(fā)生內(nèi)存泄露
如果發(fā)生內(nèi)存泄露,我們怎么定位哪里發(fā)生和為什么會(huì)發(fā)生內(nèi)存泄露订歪?
九刷晋、定位內(nèi)存泄露
借助Leaks能很快定位內(nèi)存泄露問題,在這個(gè)例子中捏悬,步驟如下:
-
首先點(diǎn)擊Leak Checks時(shí)間條那個(gè)紅色叉
-
然后雙擊某行內(nèi)存泄露調(diào)用棧甥厦,會(huì)直接跳到內(nèi)存泄露代碼位置
十刀疙、難以檢測Block引用循環(huán)
大多數(shù)的內(nèi)存問題都可以通過靜態(tài)分析
和Instrument Leak工具
檢測出來,但是有種block引用循環(huán)
是難以檢測的油够,看我們這個(gè)Block內(nèi)存泄露例子石咬,跟上面的懸掛指針例子差不多,只是在configureCellBlock
里面調(diào)用一個(gè)方法configureCell
焕窝。
- (ArrayDataSource *)dataSource
{
if (!_dataSource) {
_dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
cellIdentifier:kNameCellIdentifier
tableViewStyle:UITableViewCellStyleDefault
configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
cell.textLabel.text = item;
[self configureCell];
}];
}
return _dataSource;
}
- (void)configureCell
{
NSLog(@"Just for test");
}
- (void)dealloc
{
NSLog(@"release BlockLeakViewController");
}
- 我們首先用靜態(tài)分析來看看能不能檢查出內(nèi)存泄露:
- 結(jié)果是沒有任何內(nèi)存泄露的提示溯泣,我們再用Instrument Leak工具在運(yùn)行時(shí)看看能不能檢查出:
結(jié)果跟使用靜態(tài)分析一樣客给,還是沒有任何內(nèi)存泄露信息的提示靶剑。
那么我們怎么知道這個(gè) BlockLeakViewController
發(fā)生了內(nèi)存泄露呢桩引?還是根據(jù)iOS/OS X
內(nèi)存管理機(jī)制的一個(gè)基本原理:當(dāng)某個(gè)對象的引用計(jì)數(shù)為0
時(shí),它就會(huì)自動(dòng)調(diào)用- (void)dealloc
方法笛辟。
在這個(gè)例子中手幢,如果BlockLeakViewController
被navigationController pop
出去后,沒有調(diào)用dealloc
方法监透,因?yàn)锽lockLeakViewController 強(qiáng)引用了dataSource,而dataSource的configureCellBlock屬性又強(qiáng)引用了BlockLeakViewController的實(shí)例方法粪狼,導(dǎo)致循環(huán)引用再榄,BlockLeakViewController不能被釋放。
我們在dealloc
方法打印release BlockLeakViewController
信息:
- (void)dealloc
{
NSLog(@"release BlockLeakViewController");
}
在點(diǎn)擊返回按鈕后疾就,其并沒有打印出來,因此這個(gè)BlockLeakViewController存在內(nèi)存泄露問題的漆诽。解決:
BlockLeakViewController *__weak controller = self;
[controller configureCell]; //代替[self configureCell];
總結(jié)
在創(chuàng)建工程的時(shí)候,在Build Settings
啟用Analyze During Build
供鸠,每次編譯時(shí)都會(huì)自動(dòng)靜態(tài)分析
薄坏。編碼時(shí)及時(shí)知道是否存在內(nèi)存泄露
或其他bug問題胶坠,并且進(jìn)行修改。而在運(yùn)行過程中椭蹄,如果出現(xiàn)EXC_BAD_ACCESS
罩润,則啟用NSZombieEnabled
哨啃,看出現(xiàn)異常后,在控制臺(tái)是否能打印出更多的提示信息祝峻。如果想在運(yùn)行時(shí)
查看是否存在內(nèi)存泄露
,使用Instrument Leak工具
。但是有些內(nèi)存泄露
是很難檢查出來浮定,只有通過手動(dòng)覆蓋dealloc方法
,看它最終有沒有調(diào)用來判斷是否存在內(nèi)存泄漏层亿。
參考借鑒文獻(xiàn):
IOS內(nèi)存管理(一)基本概念與原理
IOS內(nèi)存管理(二)借助工具解決內(nèi)存問題