iOS記憶體管理詳解

jackyshan_發表於2018-03-30

iOS記憶體管理詳解

從上圖可以看到,棧裡面存放的是值型別,堆裡面存放的是物件型別。物件的引用計數是在堆記憶體中操作的。下面我們講講堆和棧怎麼存放和運算元據, 還有MRCARC怎麼管理引用計數。

Heap(堆)和stack(棧)

堆是什麼

引自維基百科(英語:Heap)是電腦科學中一類特殊的資料結構的統稱。堆通常是一個可以被看做一棵樹的陣列物件。在佇列中,排程程式反覆提取佇列中第一個作業並執行,因為實際情況中某些時間較短的任務將等待很長時間才能結束,或者某些不短小,但具有重要性的作業,同樣應當具有優先權。堆即為解決此類問題設計的一種資料結構。

堆(Heap)又被為優先佇列(priority queue)。儘管名為優先佇列,但堆並不是佇列。回憶一下,在佇列中,我們可以進行的限定操作是dequeue和enqueue。dequeue是按照進入佇列的先後順序來取出元素。而在堆中,我們不是按照元素進入佇列的先後順序取出元素的,而是按照元素的優先順序取出元素。

這就好像候機的時候,無論誰先到達候機廳,總是頭等艙的乘客先登機,然後是商務艙的乘客,最後是經濟艙的乘客。每個乘客都有頭等艙、商務艙、經濟艙三種個鍵值(key)中的一個。頭等艙->商務艙->經濟艙依次享有從高到低的優先順序。

總的來說,堆是一種資料結構,資料的插入和刪除是根據優先順序定的,他有幾個特性:

  • 任意節點的優先順序不小於它的子節點
  • 每個節點值都小於或等於它的子節點
  • 主要操作是插入和刪除最小元素(元素值本身為優先順序鍵值,小元素享有高優先順序)

舉個例子,就像疊羅漢,體重大(優先順序低、值大)的站在最下面,體重小的站在最上面(優先順序高,值小)。 為了讓堆穩固,我們每次都讓最上面的參與者退出堆,也就是每次取出優先順序最高的元素

iOS記憶體管理詳解

棧是什麼

引自維基百科是電腦科學中一種特殊的串列形式的抽象資料型別,其特殊之處在於只能允許在連結串列或陣列的一端(稱為堆疊頂端指標,英語:top)進行加入資料(英語:push)和輸出資料(英語:pop)的運算。另外棧也可以用一維陣列或連結串列的形式來完成。堆疊的另外一個相對的操作方式稱為佇列。 由於堆疊資料結構只允許在一端進行操作,因而按照後進先出(LIFO, Last In First Out)的原理運作。

舉個例子,一把54式手槍的子彈夾,你往裡面裝子彈,最先射擊出來的子彈肯定是最後裝進去的那一個。 這就是棧的結構,後進先出。

iOS記憶體管理詳解

棧中的每個元素稱為一個frame。而最上層元素稱為top frame棧只支援三個操作, pop, top, push

  • pop取出棧中最上層元素(8),棧的最上層元素變為早先進入的元素(9)。
  • top檢視棧的最上層元素(8)。
  • push將一個新的元素(5)放在棧的最上層。

棧不支援其他操作。如果想取出元素12, 必須進行3次pop操作。

iOS記憶體管理詳解

記憶體分配中的棧和堆

堆疊空間分配

棧(作業系統):由作業系統自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。 堆(作業系統): 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收,分配方式倒是類似於連結串列。

堆疊快取方式

棧使用的是一級快取, 他們通常都是被呼叫時處於儲存空間中,呼叫完畢立即釋放。 堆則是存放在二級快取中,生命週期由虛擬機器的垃圾回收演算法來決定(並不是一旦成為孤兒物件就能被回收)。所以呼叫這些物件的速度要相對來得低一些。

一般情況下程式存放在Rom(只讀記憶體,比如硬碟)或Flash中,執行時需要拷到RAM(隨機儲存器RAM)中執行,RAM會分別儲存不同的資訊,如下圖所示:

iOS記憶體管理詳解

記憶體中的棧區處於相對較高的地址以地址的增長方向為上的話,棧地址是向下增長的。

棧中分配區域性變數空間,堆區是向上增長的用於分配程式設計師申請的記憶體空間。另外還有靜態區是分配靜態變數,全域性變數空間的;只讀區是分配常量和程式程式碼空間的;以及其他一些分割槽。

也就是說,在iOS中,我們的值型別是放在棧空間的,記憶體分配和回收不需要我們關係,系統會幫我處理。在堆空間的物件型別就要有程式設計師自己分配,自己釋放了。

引用計數

引用計數是什麼

引自維基百科引用計數是計算機程式語言中的一種記憶體管理技術,是指將資源(可以是物件、記憶體或磁碟空間等等)的被引用次數儲存起來,當被引用次數變為零時就將其釋放的過程。使用引用計數技術可以實現自動資源管理的目的。同時引用計數還可以指使用引用計數技術回收未使用資源的垃圾回收演算法。 當建立一個物件的例項並在堆上申請記憶體時,物件的引用計數就為1,在其他物件中需要持有這個物件時,就需要把該物件的引用計數加1,需要釋放一個物件時,就將該物件的引用計數減1,直至物件的引用計數為0,物件的記憶體會被立刻釋放。

正常情況下,當一段程式碼需要訪問某個物件時,該物件的引用的計數加1;當這段程式碼不再訪問該物件時,該物件的引用計數減1,表示這段程式碼不再訪問該物件;當物件的引用計數為0時,表明程式已經不再需要該物件,系統就會回收該物件所佔用的記憶體。

  • 當程式呼叫方法名以allocnewcopymutableCopy開頭的方法來建立物件時,該物件的引用計數加1
  • 程式呼叫物件的retain方法時,該物件的引用計數加1
  • 程式呼叫物件的release方法時,該物件的引用計數減1

NSObject 中提供了有關引用計數的如下方法:

  • retain:將該物件的引用計數器加1
  • release:將該物件的引用計數器減1
  • autorelease:不改變該物件的引用計數器的值,只是將物件新增到自動釋放池中。
  • retainCount:返回該物件的引用計數的值。

引用計數記憶體管理的思考方式

看到“引用計數”這個名稱,我們便會不自覺地聯想到“某處有某物多少多少”而將注意力放到計數上。但其實,更加客觀、正確的思考方式:

  • 自己生成的物件,自己持有。
  • 非自己生成的物件,自己也能持有。
  • 不再需要自己持有的物件時釋放。
  • 非自己持有的物件無法釋放。

引用計數式記憶體管理的思考方式僅此而已。按照這個思路,完全不必考慮引用計數。 上文出現了“生成”、“持有”、“釋放”三個詞。而在Objective-C記憶體管理中還要加上“廢棄”一詞。各個詞標書的Objective-C方法如下表。

物件操作 Objective-C方法
生成並持有物件 alloc/new/copy/mutableCopy等方法
持有物件 retain方法
釋放物件 release方法
廢棄物件 dealloc方法

這些有關Objective-C記憶體管理的方法,實際上不包括在該語言中,而是包含在Cocoa框架中用於macOSiOS應用開發。Cocoa框架中Foundation框架類庫的NSObject類擔負記憶體管理的職責。Objective-C記憶體管理中的alloc/retain/release/dealloc方法分別指代NSObject類的alloc類方法、retain例項方法、release例項方法和dealloc例項方法。

iOS記憶體管理詳解

Cocoa框架、Foundation框架和NSObject類的關係

MRC(手動管理引用計數)

顧名思義,MRC就是呼叫Objective-C的方法(alloc/new/copy/mutableCopy/retain/release等)實現引用計數的增加和減少。

下面通過Objective-C的方法實現記憶體管理的思考方式。

自己生成的物件,自己持有

使用以下名稱開頭的方法名意味著自己生成的物件只有自己持有:

  • alloc
  • new
  • copy
  • mutableCopy
alloc的實現
// 自己生成並持有物件
id obj = [[NSObject alloc] init];
複製程式碼

使用NSObject類的alloc方法就能自己生成並持有物件。指向生成並持有物件的指標被賦給變數obj

new的實現
// 自己生成並持有物件
id obj = [NSObject new];
複製程式碼

[NSObject new][[NSObject alloc] init]是完全一致的。

copy的實現

copy方法利用基於NSCopying方法約定,由各類實現的copyWithZone:方法生成並持有物件的副本。

#import "ViewController.h"

@interface Person: NSObject<NSCopying>

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

- (id)copyWithZone:(NSZone *)zone {
    Person *obj = [[[self class] allocWithZone:zone] init];
    obj.name = self.name;
    return obj;
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //alloc生成並持有物件
    Person *p = [[Person alloc] init];
    p.name = @"testname";
    
    //copy生成並持有物件
    id obj = [p copy];
    
    //列印物件
    NSLog(@"p物件%@", p);
    NSLog(@"obj物件%@", obj);
}

@end
複製程式碼

列印結果: 2018-03-28 23:01:32.321661+0800 ocram[4466:1696414] p物件<Person: 0x1c0003320> 2018-03-28 23:01:32.321778+0800 ocram[4466:1696414] obj物件<Person: 0x1c0003370>

從列印可以看到objp物件的副本。兩者的引用計數都是1

說明:在- (id)copyWithZone:(NSZone *)zone方法中,一定要通過[self class]方法返回的物件呼叫allocWithZone:方法。因為指標可能實際指向的是Person的子類。這種情況下,通過呼叫[self class],就可以返回正確的類的型別物件。

mutableCopy的實現

copy方法類似,mutableCopy方法利用基於NSMutableCopying方法約定,由各類實現的mutableCopyWithZone:方法生成並持有物件的副本。

#import "ViewController.h"

@interface Person: NSObject<NSMutableCopying>

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

- (id)mutableCopyWithZone:(NSZone *)zone {
    Person *obj = [[[self class] allocWithZone:zone] init];
    obj.name = self.name;
    return obj;
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //alloc生成並持有物件
    Person *p = [[Person alloc] init];
    p.name = @"testname";
    
    //copy生成並持有物件
    id obj = [p mutableCopy];
    
    //列印物件
    NSLog(@"p物件%@", p);
    NSLog(@"obj物件%@", obj);
}

@end
複製程式碼

列印結果: 2018-03-28 23:08:17.382538+0800 ocram[4476:1699096] p物件<Person: 0x1c4003c20> 2018-03-28 23:08:17.382592+0800 ocram[4476:1699096] obj物件<Person: 0x1c4003d70>

從列印可以看到objp物件的副本。兩者的引用計數都是1

copymutableCopy的區別在於,copy方法生成不可變更的物件,而mutableCopy方法生成可變更的物件。

淺拷貝和深拷貝

既然講到copymutableCopy,那就要談一下深拷貝和淺拷貝的概念和實踐。

什麼是淺拷貝、深拷貝?

簡單理解就是,淺拷貝是拷貝了指向物件的指標, 深拷貝不但拷貝了物件的指標,還在系統中再分配一塊記憶體,存放拷貝物件的內容。

淺拷貝:拷貝物件本身,返回一個物件,指向相同的記憶體地址。 深層複製:拷貝物件本身,返回一個物件,指向不同的記憶體地址。

如何判斷淺拷貝、深拷貝?

深淺拷貝取決於拷貝後的物件的是不是和被拷貝物件的地址相同,如果不同,則產生了新的物件,則執行的是深拷貝,如果相同,則只是指標拷貝,相當於retain一次原物件, 執行的是淺拷貝。

iOS記憶體管理詳解

深拷貝和淺拷貝的判斷要注意兩點:

  • 源物件型別是否是可變的
  • 執行的拷貝是copy還是mutableCopy
淺拷貝深拷貝的實現
  • NSArray呼叫copy方法,淺拷貝
id obj = [NSArray array];
id obj1 = [obj copy];

NSLog(@"obj是%p", obj);
NSLog(@"obj1是%p", obj1);
複製程式碼

列印結果: 2018-03-29 20:48:56.087197+0800 ocram[5261:2021415] obj是0x1c0003920 2018-03-29 20:48:56.087250+0800 ocram[5261:2021415] obj1是0x1c0003920

指標一樣obj是淺拷貝。

  • NSArray呼叫mutableCopy方法,深拷貝
id obj = [NSArray array];
id obj1 = [obj mutableCopy];

NSLog(@"obj是%p", obj);
NSLog(@"obj1是%p", obj1);
複製程式碼

列印結果: 2018-03-29 20:42:16.508134+0800 ocram[5244:2018710] obj是0x1c00027d0 2018-03-29 20:42:16.508181+0800 ocram[5244:2018710] obj1是0x1c0453bf0

指標不一樣obj是深拷貝。

  • NSMutableArray呼叫copy方法,深拷貝
id obj = [NSMutableArray array];
id obj1 = [obj copy];

NSLog(@"obj是%p", obj);
NSLog(@"obj1是%p", obj1);
複製程式碼

列印結果: 2018-03-29 20:50:36.936054+0800 ocram[5265:2022249] obj是0x1c0443f90 2018-03-29 20:50:36.936097+0800 ocram[5265:2022249] obj1是0x1c0018580

指標不一樣obj是深拷貝。

  • NSMutableArray呼叫mutableCopy方法,深拷貝
id obj = [NSMutableArray array];
id obj1 = [obj mutableCopy];

NSLog(@"obj是%p", obj);
NSLog(@"obj1是%p", obj1);
複製程式碼

列印結果: 2018-03-29 20:52:30.057542+0800 ocram[5268:2023155] obj是0x1c425e6f0 2018-03-29 20:52:30.057633+0800 ocram[5268:2023155] obj1是0x1c425e180

指標不一樣obj是深拷貝。

  • 深拷貝的陣列裡面的元素依然是淺拷貝
id obj = [NSMutableArray arrayWithObject:@"test"];
id obj1 = [obj mutableCopy];

NSLog(@"obj是%p", obj);
NSLog(@"obj內容是%p", obj[0]);
NSLog(@"obj1是%p", obj1);
NSLog(@"obj1內容是%p", obj1[0]);
複製程式碼

列印結果: 2018-03-29 20:55:18.196597+0800 ocram[5279:2025743] obj是0x1c0255120 2018-03-29 20:55:18.196647+0800 ocram[5279:2025743] obj內容是0x1c02551e0 2018-03-29 20:55:18.196665+0800 ocram[5279:2025743] obj1是0x1c0255210 2018-03-29 20:55:18.196682+0800 ocram[5279:2025743] obj1內容是0x1c02551e0

可以看到objobj1雖然指標是不一樣的(深拷貝),但是他們的元素的指標是一樣的,所以陣列裡的元素依然是淺拷貝

其他實現

使用上述使用一下名稱開頭的方法,下面名稱也意味著自己生成並持有物件。

  • allocMyObject
  • newThatObject
  • copyThis
  • mutableCopyYourObject

使用駝峰拼寫法來命名。

#import "ViewController.h"

@interface Person: NSObject

@property (nonatomic, strong) NSString *name;

+ (id)allocObject;

@end

@implementation Person

+ (id)allocObject {
    //自己生成並持有物件
    id obj = [[Person alloc] init];
    
    return obj;
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //取得非自己生成並持有的物件
    Person *p = [Person allocObject];
    p.name = @"testname";
    
    NSLog(@"p物件%@", p);
}

@end
複製程式碼

列印結果: 2018-03-28 23:33:37.044327+0800 ocram[4500:1706677] p物件<Person: 0x1c0013770>

allocObject名稱符合上面的命名規則,因此它與用alloc方法生成並持有物件的情況完全相同,所以使用allocObject方法也意味著“自己生成並持有物件”。

非自己生成的物件,自己也能持有

//非自己生成的物件,暫時沒有持有
id obj = [NSMutableArray array];

//通過retain持有物件
[obj retain];
複製程式碼

上述程式碼中NSMutableArray通過類方法array生成了一個物件賦給變數obj,但變數obj自己並不持有該物件。使用retain方法可以持有物件。

不再需要自己持有的物件時釋放

自己持有的物件,一旦不再需要,持有者有義務釋放該物件。釋放使用release方法。

自己生成並持有物件的釋放
// 自己生成並持有物件
id obj = [[NSObject alloc] init];

//釋放物件
[obj release];
複製程式碼

如此,用alloc方法由自己生成並持有的物件就通過realse方法釋放了。自己生成而非自己所持有的物件,若用retain方法變為自己持有,也同樣可以用realse方法釋放。

非自己生成的物件持有物件的釋放
//非自己生成的物件,暫時沒有持有
id obj = [NSMutableArray array];

//通過retain持有物件
[obj retain];

//釋放物件
[obj release];
複製程式碼
非自己生成的物件本身的釋放

像呼叫[NSMutableArray array]方法使取得的物件存在,但自己並不持有物件,是如何實現的呢?

+ (id)array {
    //生成並持有物件
    id obj = [[NSMutableArray alloc] init];
    
    //使用autorelease不持有物件
    [obj autorelease];
    
    //返回物件
    return obj;
}
複製程式碼

上例中,我們使用了autorelease方法。用該方法,可以使取得的物件存在,但自己不持有物件。autorelease提供這樣的功能,使物件在超出指定的生存範圍時能夠自動並正確的釋放(呼叫release方法)。

iOS記憶體管理詳解

在後面會對autorelease做更為詳細的介紹。使用NSMutableArray類的array類方法等可以取得誰都不持有的物件,這些方法都是通過autorelease實現的。根據上文的命名規則,這些用來取得誰都不持有的物件的方法名不能以alloc/new/copy/mutableCopy開頭,這點需要注意。

非自己持有的物件無法釋放

對於用alloc/new/copy/mutableCopy方法生成並持有的物件,或是用retain方法持有的物件,由於持有者是自己,所以在不需要該物件時需要將其釋放。而由此以外所得到的物件絕對不能釋放。倘若在程式中釋放了非自己所持有的物件就會造成崩潰。

// 自己生成並持有物件
id obj = [[NSObject alloc] init];

//釋放物件
[obj release];

//再次釋放已經非自己持有的物件,應用程式崩潰
[obj release];
複製程式碼

釋放了非自己持有的物件,肯定會導致應用崩潰。因此絕對不要去釋放非自己持有的物件。

autorelease

autorelease介紹

說到Objective-C記憶體管理,就不能不提autorelease。 顧名思義,autorelease就是自動釋放。這看上去很像ARC,單實際上它更類似於C語言中自動變數(區域性變數)的特性。

在C語言中,程式程式執行時,若區域性變數超出其作用域,該區域性變數將被自動廢棄。

{
    int a;
}

//因為超出變數作用域,程式碼執行到這裡,自動變數`a`被廢棄,不可再訪問。
複製程式碼

autorelease會像C語言的區域性變數那樣來對待物件例項。當其超出作用域時,物件例項的release例項方法被呼叫。另外,同C語言的區域性變數不同的是,程式設計人員可以設定變數的作用域。

autorelease的具體使用方法如下:

  • 生成並持有NSAutoreleasePool物件。
  • 呼叫已分配物件的autorelease例項方法。
  • 廢棄NSAutoreleasePool物件。

iOS記憶體管理詳解

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

id obj = [[NSObject alloc] init];

[obj autorelease];

[pool drain];
複製程式碼

上述程式碼中最後一行的[pool drain]等同於[obj release]

autorelease實現

呼叫NSObject類的autorelease例項方法。

[obj autorelease];
複製程式碼

呼叫autorelease方法的內部實現

- (id) autorelease {
    [NSAutoreleasePool addObject: self];
}
複製程式碼

autorelease例項方法的本質就是呼叫NSAutoreleasePool物件的addObject類方法。

autorelease注意

autoreleaseNSObject的例項方法,NSAutoreleasePool也是繼承NSObject的類。那能不能呼叫autorelease呢?

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

[pool release];
複製程式碼

執行結果發生崩潰。通常在使用Objective-C,也就是Foundation框架時,無論呼叫哪一個物件的autorelease例項方法,實現上是呼叫的都是NSObject類的autorelease例項方法。但是對於NSAutoreleasePool類,autorelease例項方法已被該類過載,因此執行時就會出錯。

ARC(自動管理引用計數)

ARC介紹

上面講了“引用計數記憶體管理的思考方式”的本質部分在ARC中並沒有改變。就像“自動引用計數”這個名稱表示的那樣,ARC只是自動地幫助我們處理“引用計數”的相關部分。

在編譯單位上,可設定ARC有效或無效,即設定特定類是否啟用ARC。 在Project裡面找到Build Phases-Compile Sources,這裡是所有你的編譯檔案。指定編譯器屬性為-fobjc-arc即為該檔案使用ARC,指定編譯器屬性為-fno-objc-arc即為該檔案不使用ARC,如下圖所示。

iOS記憶體管理詳解

編譯器在編譯時會幫我們自動插入,包括 retainreleasecopyautoreleaseautoreleasepool

ARC有效的程式碼實現

所有權修飾符

Objective-C程式設計中為了處理物件,可將變數型別定義為id型別或各種物件型別。 ARC中,id型別和物件類其型別必須附加所有權修飾符。

  • __strong修飾符
  • __weak修飾符
  • __unsafe_unretained修飾符
  • __autoreleasing修飾符
__strong修飾符

__strong修飾符是id型別和物件型別預設的所有權修飾符。也就是說,不寫修飾符的話,預設物件前面被附加了__strong所有權修飾符。

id obj = [[NSObject alloc] init];
等同於
id __strong obj = [[NSObject alloc] init];
複製程式碼

__strong修飾符的變數obj在超出其變數作用域時,即在該變數被廢棄時,會釋放其被賦予的物件。 __strong修飾符表示對物件的“強引用”。持有強引用的變數在超出其作用域時被廢棄,隨著強引用的失效,引用的物件會隨之釋放。

當然,__strong修飾符也可以用在Objective-C類成員變數和方法引數上。

@interface Test: NSObject
{
    id __strong obj_;
}

- (void)setObject:(id __strong)obj;

@end

@implementation Test

- (instancetype)init {
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj {
    obj_ = obj
}

@end
複製程式碼

無需額外的工作便可以使用於類成員變數和方法引數中。__strong修飾符和後面要講的__weak修飾符和__autoreleasing修飾符一起,可以保證將附有這些修飾符的自動變數初始化為nil

正如蘋果宣稱的那樣,通過__strong修飾符再鍵入retainrelease,完美地滿足了“引用計數式記憶體管理的思考方式”。

__weak修飾符

通過__strong修飾符並不能完美的進行記憶體管理,這裡會發生“迴圈引用”的問題。

iOS記憶體管理詳解

通過上面的例子程式碼實現迴圈引用。

{
      id test0 = [[Test alloc] init];
      id test1 = [[Test alloc] init];
      [test0 setObject:test1];
      [test1 setObject:test0];
}
複製程式碼

可以看到test0tets1互相持有對方,誰也釋放不了誰。

迴圈引用容易發生記憶體洩露。所謂記憶體洩露就是應當廢棄的物件在超出其生命週期後繼續存在。

__weak修飾符可以避免迴圈引用,與__strong修飾符相反,提供弱引用。弱引用不能持有物件例項,所以在超出其變數作用域時,物件即被釋放。像下面這樣將之前的程式碼修改,就可以避免迴圈引用了。

@interface Test: NSObject
{
    id __weak obj_;
}

- (void)setObject:(id __strong)obj;
複製程式碼

使用__weak修飾符還有另外一個優點。在持有某物件的弱引用時,若該物件被廢棄,則此弱引用將自動失效且處於nil賦值的狀態(空弱引用)。

id __weak obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    
    obj1 = obj0;
    
    NSLog(@"%@", obj1);
}

NSLog(@"%@", obj1);
複製程式碼

列印結果: 2018-03-30 21:47:50.603814+0800 ocram[51624:22048320] <NSObject: 0x60400001ac10> 2018-03-30 21:47:50.604038+0800 ocram[51624:22048320] (null)

可以看到因為obj0超出作用域就被釋放了,弱引用也被至為nil狀態。

__unsafe_unretained修飾符

__unsafe_unretained修飾符是不安全的修飾符,儘管ARC式的記憶體管理是編譯器的工作,但附有__unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理物件。__unsafe_unretained__weak一樣不能持有物件。

id __unsafe_unretained obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    
    obj1 = obj0;
    
    NSLog(@"%@", obj1);
}

NSLog(@"%@", obj1);
複製程式碼

列印結果: 2018-03-30 21:58:28.033250+0800 ocram[51804:22062885] <NSObject: 0x604000018e80>

可以看到最後一個列印沒有列印出來,程式崩潰了。這是因為超出了作用域,obj1已經變成了一個野指標,然後我們去操作野指標的時候會發生崩潰。

所以在使用__unsafe_unretained修飾符時,賦值給__strong修飾符的變數時有必要確保被賦值的物件確實存在。

__autoreleasing修飾符

ARC中,我也可以使用autorelease功能。指定“@autoreleasepool塊”來代替“NSAutoreleasePool類物件生成、持有以及廢棄這一範圍,使用附有__autoreleasing修飾符的變數替代autorelease方法。

iOS記憶體管理詳解

其實我們不用顯示的附加 __autoreleasing修飾符,這是由於編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開始,如果不是則自動將返回值的物件註冊到autoreleasepool

有時候__autoreleasing修飾符要和__weak修飾符配合使用。

id __weak obj1 = obj0;

id __autoreleasing tmp = obj1;
複製程式碼

為什麼訪問附有__weak修飾符的變數時必須訪問註冊到autoreleasepool的物件呢?這是因為__weak修飾符只持有物件的弱引用,而在訪問引用物件的過程中,該物件有可能被廢棄。如果把訪問的物件註冊到autoreleasepool中,那麼在@autoreleasepool塊結束之前都能確保該物件存在。

屬性與所有權修飾符的對應關係

iOS記憶體管理詳解

以上各種屬性賦值給指定的屬性中就相當於賦值給附加各屬性對應的所有權修飾符的變數中。只有copy不是簡單的賦值,它賦值的是通過NSCopying介面的copyWithZone:方法複製賦值源所生成的物件。

ARC規則

ARC有效的情況下編譯原始碼,必須遵守一定的規則。

不能使用retain/release/retainCount/autorelease

ARC有效時,實現retain/release/retainCount/autorelease會引起編譯錯誤。程式碼會標紅,編譯不通過。

不能使用NSAllocateObject/NSDeallocateObject
須遵守記憶體管理的方法命名規則

alloc,new,copy,mutableCopy,initinit開始的方法的規則要比alloc,new,copy,mutableCopy更嚴格。該方法必須是例項方法,並且要返回物件。返回的物件應為id型別或方法宣告類的物件型別,抑或是該類的超型別或子型別。該返回物件並不註冊到autoreleasepool上。基本上只是對alloc方法返回值的物件進行初始化處理並返回該物件。

//符合命名規則
- (id) initWithObject;

//不符合命名規則
- (void) initThisObject;
複製程式碼
不要顯式呼叫dealloc

當物件的引用計數為0,所有者不持有該物件時,該物件會被廢棄,同時呼叫物件的dealloc方法。ARC會自動對此進行處理,因此不必書寫[super dealloc]

使用@autoreleasepool塊替代NSAutoreleasePool
不能使用區域(NSZone)
物件型變數不能作為C語言結構體(struct、union)的成員

C語言結構體(struct、union)的成員中,如果存在Objective-C物件型變數,便會引起編譯錯誤。

struct Data {
    NSMutableArray *array;
};
複製程式碼

顯示警告: ARC forbids Objective-C objects in struct

C語言的規約上沒有方法來管理結構體成員的生命週期。因為ARC把記憶體管理的工資分配給編譯器,所以編譯器必須能夠知道並管理物件的生命週期。例如C語言的區域性變數可使用該變數的作用域管理物件。但是對於C語言的結構體成員來說,這在標準上就是不可實現的。

要把物件型別新增到結構體成員中,可以強制轉換為void *或是附加__unsafe_unretained修飾符。

struct Data {
    NSMutableArray __unsafe_unretained *array;
};
複製程式碼

__unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理物件。如果管理時不注意賦值物件的所有者,便可能遭遇記憶體洩露或者程式崩潰。

顯示轉換idvoid *

在MRC時,將id變數強制轉換void *變數是可以的。

id obj = [[NSObject alloc] init];

void *p = obj;

id o = p;

[o release];
複製程式碼

但是在ARC時就會編譯報錯,id型或物件型變數賦值給void *或者逆向賦值時都需要進行特定的轉換。如果只想單純的賦值,則可以使用“__bridge轉換”

__bridge轉換中還有另外兩種轉換,分部是“__bridge_retained”和“__bridge_transfer轉換” __bridge_retained轉換與retain類似,__bridge_transfer轉換與release類似。

void *p = (__bridge_retained void *)[[NSObject alloc] init];
NSLog(@"class = %@", [(__bridge id)p class]);
(void)(__bridge_transfer id)p;
複製程式碼

ARC記憶體的洩露和檢測

ARC記憶體洩露常見場景

物件型變數作為C語言結構體(struct、union)的成員
struct Data {
    NSMutableArray __unsafe_unretained *array;
};
複製程式碼

__unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理物件。如果管理時不注意賦值物件的所有者,便可能遭遇記憶體洩露或者程式崩潰。

迴圈引用

迴圈引用常見有三種現象:

  • 兩個物件互相持有物件,這個可以設定弱引用解決。
@interface Test: NSObject
{
    id __weak obj_;
}

- (void)setObject:(id __strong)obj;
複製程式碼
  • block持有self物件,這個要在block塊外面和裡面設定弱引用和強引用。
__weak __typeof(self) wself = self;
obj.block = ^{
    __strong __typeof(wself) sself = wself;
    
    [sself updateSomeThing];
}
複製程式碼
  • NSTimer的target持有self

NSTimer會造成迴圈引用,timer會強引用target即self,一般self又會持有timer作為屬性,這樣就造成了迴圈引用。 那麼,如果timer只作為區域性變數,不把timer作為屬性呢?同樣釋放不了,因為在加入runloop的操作中,timer被強引用。而timer作為區域性變數,是無法執行invalidate的,所以在timer被invalidate之前,self也就不會被釋放。

單例屬性不釋放

嚴格來說這個不算是記憶體洩露,主要就是我們在單例裡面設定一個物件的屬性,因為單例是不會釋放的,所以單例會有一直持有這個物件的引用。

[Instanse shared].obj = self;
複製程式碼

可以看到單例持有了當前物件self,這個self就不會釋放了。

ARC記憶體洩露的檢測

使用Xcode自帶工具Instrument

開啟Xcode8自帶的Instruments

iOS記憶體管理詳解

或者

iOS記憶體管理詳解

或者:長按執行按鈕,然後出現如圖所示列表,點選Profile.

iOS記憶體管理詳解

按上面操作,build成功後跳出Instruments工具,選擇Leaks選項

iOS記憶體管理詳解

選擇之後介面如下圖:

到這裡之後,我們前期的準備工作做完啦,下面開始正式的測試!

(有一個注意的點,最好選擇真機進行測試,模擬器是執行在mac上的,mac跟手機還是有區別的嘛。)

1.選中Xcode先把程式(command + R)執行起來(如果Xcode左上角已經是instrument的圖示就不用執行這一步了)

2.再選中Xcode,按快捷鍵(command + control + i)執行起來,此時Leaks已經跑起來了

3.由於Leaks是動態監測,所以我們需要手動操作APP,一邊操作,一邊觀察Leaks的變化,當出現紅色叉時,就監測到了記憶體洩露,點選左上角的第二個,進行暫停檢測(也可繼續檢測).如圖所示:

iOS記憶體管理詳解

4.下面就是定位修改了,此時選中有紅色柱子的Leaks,下面有個"田"字方格,點開,選中Call Tree

iOS記憶體管理詳解

顯示如下圖介面

iOS記憶體管理詳解

5.下面就是最關鍵的一步,在這個介面的右下角有若干選框,選中Invert Call Tree 和Hide System Libraries,(紅圈範圍內)顯示如下:

iOS記憶體管理詳解

如果右下角找不到此設定視窗,可以在底部點選Call Tree,顯示如下:

iOS記憶體管理詳解

到這裡就算基本完成啦,這裡顯示的就是記憶體洩露程式碼部分,那麼現在還差一步:定位!

6.選中顯示的若干條中的一條,雙擊,會自動跳到記憶體洩露程式碼處,如圖所示

iOS記憶體管理詳解

在選擇call tree後,可能你會發現檢視不到原始碼從而無法定位記憶體洩漏的位置,只是顯示16進位制的資料。此時需要你在Xcode中檢查是否有dSYM File生成,如下圖所示選擇第二項DWARF with dSYM File.

iOS記憶體管理詳解

在物件dealloc中進行列印

我們生成的物件,在即將釋放的時候,會呼叫dealloc方法。所以我們可以在dealloc列印當前物件已經釋放的訊息。如果沒有釋放,物件的dealloc方法就永遠不會執行,此時我們知道發生了記憶體洩露。

通過這個思路,我寫了一個小工具用來檢查當前controller沒有釋放的,然後列印出來。

寫個簡單的Swift檢測Controller沒有銷燬的工具

關注我

歡迎關注公眾號:jackyshan,技術乾貨首發微信,第一時間推送。

iOS記憶體管理詳解

相關文章