從上圖可以看到,棧里面存放的是值類型,堆里面存放的是對象類型。對象的引用計數是在堆內存中操作的。下面我們講講堆和棧怎么存放和操作數據, 還有MRC
和ARC
怎么管理引用計數。
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時,表明程序已經不再需要該對象,系統就會回收該對象所占用的內存。
- 當程序調用方法名以
alloc
、new
、copy
、mutableCopy
開頭的方法來創建對象時,該對象的引用計數加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框架中用于macOS
、iOS
應用開發。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>
從打印可以看到obj
是p
對象的副本。兩者的引用計數都是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>
從打印可以看到obj
是p
對象的副本。兩者的引用計數都是1
。
copy
和mutableCopy
的區別在于,copy
方法生成不可變更的對象,而mutableCopy
方法生成可變更的對象。
淺拷貝和深拷貝
既然講到copy
和mutableCopy
,那就要談一下深拷貝和淺拷貝的概念和實踐。
什么是淺拷貝、深拷貝?
簡單理解就是,淺拷貝是拷貝了指向對象的指針, 深拷貝不但生成了新的對象的指針,還在系統中再分配一塊內存,存放拷貝對象的內容。
淺拷貝:拷貝對象本身,返回一個對象,指向相同的內存地址。
深層復制:拷貝對象本身,返回一個對象,指向不同的內存地址。
如何判斷淺拷貝、深拷貝?
深淺拷貝取決于拷貝后的對象的是不是和被拷貝對象的地址相同,如果不同,則產生了新的對象,則執行的是深拷貝,如果相同,則只是指針拷貝,相當于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
可以看到obj
和obj1
雖然指針是不一樣的(深拷貝),但是他們的元素的指針是一樣的,所以數組里的元素依然是淺拷貝。
其他實現
使用上述使用一下名稱開頭的方法,下面名稱也意味著自己生成并持有對象。
- 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注意
autorelease
是NSObject
的實例方法,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
,如下圖所示。
編譯器在編譯時會幫我們自動插入,包括 retain
、release
、copy
、autorelease
、autoreleasepool
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
修飾符再鍵入retain
和release
,完美地滿足了“引用計數式內存管理的思考方式”。
__weak修飾符
通過__strong
修飾符并不能完美的進行內存管理,這里會發生“循環引用”的問題。
通過上面的例子代碼實現循環引用。
{
id test0 = [[Test alloc] init];
id test1 = [[Test alloc] init];
[test0 setObject:test1];
[test1 setObject:test0];
}
可以看到test0
和tets1
互相持有對方,誰也釋放不了誰。
循環引用容易發生內存泄露。所謂內存泄露就是應當廢棄的對象在超出其生命周期后繼續存在。
__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
修飾符的變量不屬于編譯器的內存管理對象。如果管理時不注意賦值對象的所有者,便可能遭遇內存泄露或者程序崩潰。
顯示轉換id
和void *
在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
沒有釋放的,然后打印出來。
關注我
歡迎關注公眾號:jackyshan,技術干貨首發微信,第一時間推送。