前言: 在iOS中,使用引用計數來管理OC對象記憶體 一個新創建的OC對象引用計數預設是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的記憶體空間。 調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1。 記憶體管理的經驗總結 當調用alloc、new、copy、m ...
前言:
在iOS中,使用引用計數來管理OC對象記憶體
一個新創建的OC對象引用計數預設是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的記憶體空間。
調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1。
記憶體管理的經驗總結
當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調用release或者autorelease釋放它。
想擁有某個對象,就讓他的引用計數+1;不想再擁有某個對象,就讓他的引用計數-1。
一、 MRC 手動管理記憶體(Manual Reference Counting)
1、引用計數器
引用計數器:
一個整數,表示為「對象被引用的次數」。系統需要根據對象的引用計數器來判斷對象是否需要被回收。
關於「引用計數器」,有以下幾個特點:
-
每個 OC 對象都有自己的引用計數器。
-
任何一個對象,剛創建的時候,初始的引用計數為 1。
即使用 alloc、new 或者 copy 創建一個對象時,對象的引用計數器預設就是 1。
-
當沒有任何人使用這個對象時,系統才會回收這個對象。也就是說:
當對象的引用計數器為 0 時,對象占用的記憶體就會被系統回收。
如果對象的引用計數器不為 0 時,那麼在整個程式運行過程,它占用的記憶體就不可能被回收(除非整個程式已經退出)。
2、引用計數器操作
- 為保證對象的存在,每當創建引用到對象需要給對象發送一條 retain 消息,可以使引用計數器值 +1 ( retain 方法返回對象本身)。
- 當不再需要對象時,通過給對象發送一條 release 消息,可以使引用計數器值 -1。
- 給對象發送 retainCount 消息,可以獲得當前的引用計數器值。
- 當對象的引用計數為 0 時,系統就知道這個對象不再需要使用了,所以可以釋放它的記憶體,通過給對象發送 dealloc 消息發起這個過程。
- 需要註意的是:release 並不代表銷毀 / 回收對象,僅僅是將計數器 -1。
// 創建一個對象,預設引用計數器是 1
RHPerson *person1 = [[RHPerson alloc] init];
NSLog(@"retainCount = %zd", [person1 retainCount]);
// 只要給對象發送一條 retain 消息,引用計數器加1
[person1 retain];
NSLog(@"retainCount = %zd", [person1 retainCount]);
// 只要給對象發送一條 release 消息,引用計數器減1
[person1 release];
NSLog(@"retainCount = %zd", [person1 retainCount]);
// 當 retainCount 等於0時,對象被銷毀
[person1 release];
NSLog(@"--------------");
2022-07-11 16:09:24.102850+0800 Interview01-記憶體管理[8035:264221] retainCount = 1
2022-07-11 16:09:24.103083+0800 Interview01-記憶體管理[8035:264221] retainCount = 2
2022-07-11 16:09:24.103126+0800 Interview01-記憶體管理[8035:264221] retainCount = 1
2022-07-11 16:09:24.103231+0800 Interview01-記憶體管理[8035:264221] -[RHPerson dealloc]
2022-07-11 16:09:24.103259+0800 Interview01-記憶體管理[8035:264221] --------------
Program ended with exit code: 0
3、dealloc 方法
- 當一個對象的引用計數器值為 0 時,這個對象即將被銷毀,其占用的記憶體被系統回收。
- 對象即將被銷毀時系統會自動給對象發送一條 dealloc 消息(因此,從 dealloc 方法有沒有被調用,就可以判斷出對象是否被銷毀)
- dealloc 方法的重寫(註意是在 MRC 中)
- 一般會重寫 dealloc 方法,在這裡釋放相關資源,dealloc 就是對象的遺言
- 一旦重寫了 dealloc 方法,就必須調用 [super dealloc],並且放在最後面調用
- (void)dealloc {
NSLog(@"%s", __func__);
[super dealloc];
}
dealloc
使用註意:
-
不能直接調用
dealloc
方法。 -
一旦對象被回收了, 它占用的記憶體就不再可用,堅持使用會導致程式崩潰(野指針錯誤)。
4、野指針和空指針
-
只要一個對象被釋放了,我們就稱這個對象為「僵屍對象(不能再使用的對象)」。
-
當一個指針指向一個僵屍對象(不能再使用的對象),我們就稱這個指針為「野指針」。
-
只要給一個野指針發送消息就會報錯(EXC_BAD_ACCESS 錯誤)。
RHPerson *person1 = [[RHPerson alloc] init];
[person1 release];
[person1 release];
[person1 release];
-
為了避免給野指針發送消息會報錯,一般情況下,當一個對象被釋放後我們會將這個對象的指針設置為空指針。
-
空指針:
-
沒有指向存儲空間的指針(裡面存的是 nil, 也就是 0)。
-
給空指針發消息是沒有任何反應的。
RHPerson *person1 = [[RHPerson alloc] init]; [person1 release]; person1 = nil; [person1 release];
-
二、記憶體管理思想
1、單個對象記憶體管理思想
思想一:自己創建的對象,自己持有,自己負責釋放
- 通過
alloc
、new
、copy
或mutableCopy
方法創建並持有對象。 - 當自己持有的對象不再被需要時,必須調用
release
或autorelease
方法釋放對象。
id obj1 = [[NSObject alloc] init];
[obj1 release];
id obj2 = [NSObject new];
[obj2 release];
思想二:非自己創建的對象,自己也能持有
- 除了用上面方法(alloc / new / copy / mutableCopy 方法)所取得的的對象,因為非自己生成並持有,所以自己不是該對象的持有者。
- 通過調用 retain 方法,即便是非自己創建的對象,自己也能持有對象。
- 同樣當自己持有的對象不再被需要時,必須調用 release 方法來釋放對象。
id obj3 = [NSArray array];
[obj3 retain];
[obj3 release];
- 無論是否是自己創建的對象,自己都可以持有,並負責釋放。
- 計數器有加就有減。
- 曾經讓對象的計數器 +1,就必須在最後讓對象計數器 -1。
2、多個對象記憶體管理思想
多個對象之間往往是通過 setter 方法產生聯繫的,其記憶體管理的方法也是在 setter 方法、dealloc 方法中實現的。所以只有瞭解了 setter 方法是如何實現的,我們才能瞭解到多個對象之間的記憶體管理思想。
#import <Foundation/Foundation.h>
#import "RHRoom.h"
NS_ASSUME_NONNULL_BEGIN
@interface RHPerson : NSObject
{
RHRoom *_room;
}
- (void)setRoom:(RHRoom *)room;
- (RHRoom *)room;
@end
NS_ASSUME_NONNULL_END
#import "RHPerson.h"
@implementation RHPerson
- (void)setRoom:(RHRoom *)room {
if (_room != room) {
[_room release];
_room = [room retain];
}
}
- (RHRoom *)room {
return _room;
}
- (void)dealloc {
[_room release];
[super dealloc];
NSLog(@"%s", __func__);
}
@end
三、 @property
參數
- 在成員變數前加上
@property
,系統就會自動幫我們生成基本的setter
/getter
方法,但是不會生成記憶體管理相關的代碼。
@property(nonatomic) int val;
- 同樣如果在
property
後邊加上assign
,系統也不會幫我們生成setter
方法記憶體管理的代碼,僅僅只會生成普通的getter
/setter
方法,預設什麼都不寫就是assign
。
@property(nonatomic, assign) int val;
- 如果在
property
後邊加上retain
,系統就會自動幫我們生成getter
/setter
方法記憶體管理的代碼,但是仍需要我們自己重寫dealloc
方法。
@property(nonatomic, retain) RHRoom *room;
四、自動釋放池
1、自動釋放池
當我們不再使用一個對象的時候應該將其空間釋放,但是有時候我們不知道何時應該將其釋放。為瞭解決這個問題,Objective-C 提供了 autorelease
方法。
autorelease
是一種支持引用計數的記憶體管理方式,只要給對象發送一條autorelease
消息,會將對象放到一個自動釋放池中,當自動釋放池被銷毀時,會對池子裡面的「所有對象」做一次release
操作。
註意:這裡只是發送 release
消息,如果當時的引用計數(reference-counted)依然不為 0,則該對象依然不會被釋放。
autorelease
方法會返回對象本身,且調用完autorelease
方法後,對象的計數器不變。
NSObject *obj = [NSObject new];
[obj autorelease];
NSLog(@"obj.retainCount = %zd", obj.retainCount);
2、使用 autorelease 有什麼好處呢?
- 不用再關心對象釋放的時間
- 不用再關心什麼時候調用release
3、autorelease 的原理實質上是什麼?
autorelease
實際上只是把對 release
的調用延遲了,對於每一個 autorelease
,系統只是把該對象放入了當前的 autorelease pool
中,當該 pool
被釋放時,該 pool
中的所有對象會被調用 release
方法。
4、autorelease 的創建方法
// 第一種方式:使用 NSAutoreleasePool 創建
// 創建自動釋放池
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 銷毀自動釋放池
[pool release];
// 第二種方式:使用 @autoreleasepool 創建
@autoreleasepool {
// 開始代表創建自動釋放池
// 結束代表銷毀自動釋放池
}
5、autorelease 的使用方法
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
RHPerson *p = [[[RHPerson alloc] init] autorelease];
[pool release];
@autoreleasepool {
// 開始代表創建自動釋放池
RHPerson *p = [[[RHPerson alloc] init] autorelease];
// 結束代表銷毀自動釋放池
}
6、autorelease 的註意事項
- 並不是放到自動釋放池代碼中,都會自動加入到自動釋放池
@autoreleasepool {
// 因為沒有調用 autorelease,所以沒有加入到自動釋放池中
RHPerson *p = [[RHPerson alloc] init];
// 結束代表銷毀自動釋放池
}
- 在自動釋放池的外部發送 autorelease 不會被加入到自動釋放池中
autorelease
是一個方法,只有在自動釋放池中調用才有效。
@autoreleasepool {
}
// 沒有與之對應的自動釋放池, 只有在自動釋放池中調用autorelease才會放到釋放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
// 正確寫法
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
// 正確寫法
Person *p = [[Person alloc] init];
@autoreleasepool {
[p autorelease];
}
7、自動釋放池的嵌套使用
- 自動釋放池是以棧的形式存在。
- 由於棧只有一個入口,所以調用
autorelease
會將對象放到棧頂的自動釋放池。
棧頂就是離調用
autorelease
方法最近的自動釋放池。
@autoreleasepool { // 棧底自動釋放池
@autoreleasepool {
@autoreleasepool { // 棧頂自動釋放池
Person *p = [[[Person alloc] init] autorelease];
}
Person *p = [[[Person alloc] init] autorelease];
}
}
- 自動釋放池中不適宜放占用記憶體比較大的對象。
- 儘量避免對大記憶體使用該方法,對於這種延遲釋放機制,還是儘量少用。
- 不要把大量迴圈操作放到同一個
@autoreleasepool
之間,這樣會造成記憶體峰值的上升。
// 記憶體暴漲
@autoreleasepool {
for (int i = 0; i < 99999; ++i) {
Person *p = [[[Person alloc] init] autorelease];
}
}
// 記憶體不會暴漲
for (int i = 0; i < 99999; ++i) {
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
}
8、autorelease 錯誤用法
- 不要連續調用
autorelease
。
@autoreleasepool {
// 錯誤寫法, 過度釋放
Person *p = [[[[Person alloc] init] autorelease] autorelease];
}
- 調用
autorelease
後又調用release
(錯誤)。
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
[p release]; // 錯誤寫法, 過度釋放
}
五、 MRC 中避免迴圈引用
定義兩個類 Person 類和 Dog 類
- Person 類:
#import <Foundation/Foundation.h>
@class Dog;
@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end
- Dog 類:
#import <Foundation/Foundation.h>
@class Person;
@interface Dog : NSObject
@property(nonatomic, retain)Person *owner;
@end
執行以下代碼:
int main(int argc, const char * argv[]) {
Person *p = [Person new];
Dog *d = [Dog new];
p.dog = d; // retain
d.owner = p; // retain assign
[p release];
[d release];
return 0;
}
就會出現 A 對象要擁有 B 對象,而 B 對應又要擁有 A 對象,此時會形成迴圈 retain,導致 A 對象和 B 對象永遠無法釋放。
那麼如何解決這個問題呢?
不要讓 A retain B,B retain A。
讓其中一方不要做 retain 操作即可。
當兩端互相引用時,應該一端用 retain,一端用 assign。