iOS內存管理詳解

從上圖能夠看到,棧裏面存放的是值類型,堆裏面存放的是對象類型。對象的引用計數是在堆內存中操做的。下面咱們講講堆和棧怎麼存放和操做數據, 還有MRCARC怎麼管理引用計數。git

Heap(堆)和stack(棧)

堆是什麼

引自維基百科(英語:Heap)是計算機科學中一類特殊的數據結構的統稱。堆一般是一個能夠被看作一棵樹的數組對象。在隊列中,調度程序反覆提取隊列中第一個做業並運行,由於實際狀況中某些時間較短的任務將等待很長時間才能結束,或者某些不短小,但具備重要性的做業,一樣應當具備優先權。堆即爲解決此類問題設計的一種數據結構。程序員

堆(Heap)又被爲優先隊列(priority queue)。儘管名爲優先隊列,但堆並非隊列。回憶一下,在隊列中,咱們能夠進行的限定操做是dequeue和enqueue。dequeue是按照進入隊列的前後順序來取出元素。而在堆中,咱們不是按照元素進入隊列的前後順序取出元素的,而是按照元素的優先級取出元素。github

這就好像候機的時候,不管誰先到達候機廳,老是頭等艙的乘客先登機,而後是商務艙的乘客,最後是經濟艙的乘客。每一個乘客都有頭等艙、商務艙、經濟艙三種個鍵值(key)中的一個。頭等艙->商務艙->經濟艙依次享有從高到低的優先級。算法

總的來講,堆是一種數據結構,數據的插入和刪除是根據優先級定的,他有幾個特性:編程

  • 任意節點的優先級不小於它的子節點
  • 每一個節點值都小於或等於它的子節點
  • 主要操做是插入和刪除最小元素(元素值自己爲優先級鍵值,小元素享有高優先級)

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

棧是什麼

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

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

棧中的每一個元素稱爲一個frame。而最上層元素稱爲top frame棧只支持三個操做, pop, top, pushbash

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

棧不支持其餘操做。若是想取出元素12, 必須進行3次pop操做。微信

內存分配中的棧和堆

堆棧空間分配

棧(操做系統):由操做系統自動分配釋放 ,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。 堆(操做系統): 通常由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收,分配方式卻是相似於鏈表。

堆棧緩存方式

棧使用的是一級緩存, 他們一般都是被調用時處於存儲空間中,調用完畢當即釋放。 堆則是存放在二級緩存中,生命週期由虛擬機的垃圾回收算法來決定(並非一旦成爲孤兒對象就能被回收)。因此調用這些對象的速度要相對來得低一些。

通常狀況下程序存放在Rom(只讀內存,好比硬盤)或Flash中,運行時須要拷到RAM(隨機存儲器RAM)中執行,RAM會分別存儲不一樣的信息,以下圖所示:

內存中的棧區處於相對較高的地址以地址的增加方向爲上的話,棧地址是向下增加的。

棧中分配局部變量空間,堆區是向上增加的用於分配程序員申請的內存空間。另外還有靜態區是分配靜態變量,全局變量空間的;只讀區是分配常量和程序代碼空間的;以及其餘一些分區。

也就是說,在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實例方法。

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一次原對象, 執行的是淺拷貝。

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

  • 源對象類型是不是可變的
  • 執行的拷貝是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方法)。

在後面會對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對象。

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,以下圖所示。

編譯器在編譯時會幫咱們自動插入,包括 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修飾符並不能完美的進行內存管理,這裏會發生「循環引用」的問題。

經過上面的例子代碼實現循環引用。

{
      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方法。

其實咱們不用顯示的附加 __autoreleasing修飾符,這是因爲編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開始,若是不是則自動將返回值的對象註冊到autoreleasepool

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

id __weak obj1 = obj0;

id __autoreleasing tmp = obj1;
複製代碼

爲何訪問附有__weak修飾符的變量時必須訪問註冊到autoreleasepool的對象呢?這是由於__weak修飾符只持有對象的弱引用,而在訪問引用對象的過程當中,該對象有可能被廢棄。若是把訪問的對象註冊到autoreleasepool中,那麼在@autoreleasepool塊結束以前都能確保該對象存在。

屬性與全部權修飾符的對應關係

以上各類屬性賦值給指定的屬性中就至關於賦值給附加各屬性對應的全部權修飾符的變量中。只有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

或者

或者:長按運行按鈕,而後出現如圖所示列表,點擊Profile.

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

選擇以後界面以下圖:

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

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

1.選中Xcode先把程序(command + R)運行起來(若是Xcode左上角已是instrument的圖標就不用執行這一步了)

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

3.因爲Leaks是動態監測,因此咱們須要手動操做APP,一邊操做,一邊觀察Leaks的變化,當出現紅色叉時,就監測到了內存泄露,點擊左上角的第二個,進行暫停檢測(也可繼續檢測).如圖所示:

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

顯示以下圖界面

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

若是右下角找不到此設置窗口,能夠在底部點擊Call Tree,顯示以下:

到這裏就算基本完成啦,這裏顯示的就是內存泄露代碼部分,那麼如今還差一步:定位!

6.選中顯示的若干條中的一條,雙擊,會自動跳到內存泄露代碼處,如圖所示

在選擇call tree後,可能你會發現查看不到源碼從而沒法定位內存泄漏的位置,只是顯示16進制的數據。此時須要你在Xcode中檢查是否有dSYM File生成,以下圖所示選擇第二項DWARF with dSYM File.

在對象dealloc中進行打印

咱們生成的對象,在即將釋放的時候,會調用dealloc方法。因此咱們能夠在dealloc打印當前對象已經釋放的消息。若是沒有釋放,對象的dealloc方法就永遠不會執行,此時咱們知道發生了內存泄露。

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

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

關注我

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

相關文章
相關標籤/搜索