內(nèi)存管理概念與原理以及解決辦法

一、內(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呜象。

image

二、 引用計(jì)數(shù)(Reference Count)

為了解釋引用計(jì)數(shù)碑隆,我們做一個(gè)類比:員工在辦公室使用燈的情景恭陡。

image
  • 當(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)存空間。

image

三毫炉、內(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)燈 釋放對象
image
  • 而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)建誰釋放的原則鲜滩。

image
  • 注意下以下情況是不會(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)用的是NSStringstringWithFormat并不不滿足內(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對象
image

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)存不足 
       */
}
image

像上面這種情況你就可以這么寫:

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)用retainrelease方法來管理Objective-C對象的內(nèi)存,而是引入一種新的內(nèi)存管理機(jī)制Automatic Reference Counting(ARC)皿桑,簡單來說毫目,它讓編譯器來代替程序員來自動(dòng)加入retainrelease方法來持有放棄對象的所有權(quán)。

image

在ARC內(nèi)存管理機(jī)制中诲侮,id和其他對象類型變量必須是以下四個(gè)ownership qualifiers其中一個(gè)來修飾:

  • __strong(默認(rèn)镀虐,如果不指定其他,編譯器就默認(rèn)加入)
  • __weak
  • __unsafe_unretained
  • __autoreleasing
image

比方說下面這段程序

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)存泄露問題。

image

舉一個(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)存地址相同

image

如果將__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é)果將不同*/
image
  • 5.4、 __autoreleasing ownership qualifier

MRCautorelease 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修飾變量即可吱涉。

image

但是我們很少或基本上不使用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)贼穆、nonatomicreadonly兰粉、readwrite故痊。
image

只能一個(gè)線程訪問,線程安全的(相當(dāng)于線程鎖的概念玖姑,一個(gè)時(shí)間段只有一個(gè)線程訪問不容易出錯(cuò)愕秫,所以線程安全),低性能

image

七、內(nèi)存管理問題解決辦法

image

在往下看之前請下載實(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崩潰信息

image

我們看看這個(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)擊

image

2唉锌、Run -> Diagnostics -> Enable Zombie Objects

image

設(shè)置完之后,再次運(yùn)行和點(diǎn)擊懸掛指針竿奏,雖然會(huì)再次crash,但這次控制臺(tái)打印了以下有用信息:

image

信息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)行荠雕,截圖如下:

image

留意上面的兩個(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

image

此時(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

  • image
  • 自動(dòng)靜態(tài)分析:在Build Settings啟用Analyze During 'Build'硼莽,每次編譯時(shí)都會(huì)自動(dòng)靜態(tài)分析

image

靜態(tài)分析結(jié)果如下:

image

通過靜態(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步驟如下:

  1. 點(diǎn)擊Xcode的菜單欄的 Product -> Profile 啟動(dòng)Instruments

    image
  2. 此時(shí)甥雕,出現(xiàn)Instruments的工具集挟阻,選中Leaks子工具點(diǎn)擊

    image
  3. 打開Leaks工具之后脱拼,點(diǎn)擊紅色圓點(diǎn)按鈕啟動(dòng)Leaks工具熄浓,在Leaks工具啟動(dòng)同時(shí),模擬器或真機(jī)也跟著啟動(dòng)

image
  1. 啟動(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)存泄露

    image
    image

如果發(fā)生內(nèi)存泄露,我們怎么定位哪里發(fā)生和為什么會(huì)發(fā)生內(nèi)存泄露订歪?

九刷晋、定位內(nèi)存泄露
借助Leaks能很快定位內(nèi)存泄露問題,在這個(gè)例子中捏悬,步驟如下:

  • 首先點(diǎn)擊Leak Checks時(shí)間條那個(gè)紅色叉

    image
  • 然后雙擊某行內(nèi)存泄露調(diào)用棧甥厦,會(huì)直接跳到內(nèi)存泄露代碼位置

    image

十刀疙、難以檢測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)存泄露:
image
  • 結(jié)果是沒有任何內(nèi)存泄露的提示溯泣,我們再用Instrument Leak工具在運(yùn)行時(shí)看看能不能檢查出:
image

結(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è)例子中手幢,如果BlockLeakViewControllernavigationController 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)存問題

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桦卒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匿又,更是在濱河造成了極大的恐慌方灾,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碌更,死亡現(xiàn)場離奇詭異拢蛋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贾陷,“玉大人顶燕,你說我怎么就攤上這事。” “怎么了较性?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵速和,是天一觀的道長碰凶。 經(jīng)常有香客問我,道長扫步,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮灾馒,結(jié)果婚禮上花盐,老公的妹妹穿的比我還像新娘熙揍。我一直安慰自己饺汹,他們只是感情好压语,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布蚀浆。 她就那樣靜靜地躺著曼追,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天病曾,我揣著相機(jī)與錄音逼蒙,去河邊找鬼社搅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛互亮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播余素,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼豹休,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了桨吊?” 一聲冷哼從身側(cè)響起威根,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎视乐,沒想到半個(gè)月后洛搀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佑淀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年留美,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伸刃。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谎砾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出捧颅,到底是詐尸還是另有隱情棺榔,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布隘道,位于F島的核電站症歇,受9級特大地震影響郎笆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜忘晤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一宛蚓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧设塔,春花似錦凄吏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至序六,卻和暖如春任连,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背例诀。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工随抠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人繁涂。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓拱她,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扔罪。 傳聞我的和親對象是個(gè)殘疾皇子秉沼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容