iOS內存管理詳解

從上圖可以看到,棧里面存放的是值類型,堆里面存放的是對象類型。對象的引用計數是在堆內存中操作的。下面我們講講堆和棧怎么存放和操作數據, 還有MRCARC怎么管理引用計數。

Heap(堆)和stack(棧)

堆是什么

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

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

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

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

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

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

棧是什么

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

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

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

  • 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,init
init開始的方法的規則要比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,技術干貨首發微信,第一時間推送。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容