Object-C 基礎教程第九章,記憶體管理
前言:
最近事情比較多,很久沒有來更新文章了。
剛好最近又空閒出來點時間,趕緊繼續學習OC並且做筆記,這次要學習的是OC的記憶體管理。
物件生命週期
正如現實世界中的鳥類和蜜蜂一樣,程式中你的物件也有生命週期。
物件的生命週期包括誕生(通過alloc或者new方法實現)、生存(接收訊息並執行操作)、交友(通過複合以及方法傳遞引數)
以及最終死去(被釋放掉)。
當生命週期結束時,它們的原材料(記憶體)將被回收以供新的物件使用。
引用計數
現在,物件何時誕生我們已經很清楚了,而且也討論瞭如何使用物件,但是怎麼知道物件生命週期結束了呢?Cocoa採用了一種叫做引用計數(reference counting)
的技術,有也叫做保留計數(retain counting)
。
每個物件都有一個關聯的整數,當某段程式碼需求訪問一個物件時候,計數器就+1,
反之當這段程式碼結束物件訪問時,計數器-1,
當計數器為0的時候系統就回收該物件(?可憐的物件)。
alloc
、new
方法或者copy訊息
會建立一個物件,物件引用計數器被設定為1-(id) retain;
增加計數器-(oneway void) release
減少計數器dealloc
不要直接呼叫,系統會呼叫該方法-(NSUInteger) retainCount
返回當前引用計數器的值
RetainCount1專案例子
//宣告
@interface RetainTracker: NSObject
@end
//實現
@implementation RetainTracker
-(id) init
{
if(self = [super init])
{
//當物件被建立的時候,呼叫retainCount來獲取當前引用計數器的值.
NSLog(@"init: Retain count of %lu.",[self retainCount]);
return (self);
}
}
-(void) dealloc
{
//dealloc 方法無需我們自己呼叫,當計數器為0時候,系統自動呼叫dealloc回收物件。
NSLog(@"銷燬方法被呼叫!");
[super dealloc];
}
@end
int main(int argc,const char *argv[])
{
//當通過new建立物件的時候,會將引用計數器設定為1,也會預設呼叫init方法。
RetainTracker *tracker = [RetainTracker new];
//增加引用計數器 count:2
[tracker retain];
NSLog(@"%d",[tracker retainCount]);
//增加引用計數器 count:3
[tracker retain];
NSLog(@"%d",[tracker retainCount]);
//減少引用計數器 count:2
[tracker release];
NSLog(@"%d",[tracker retainCount]);
//減少引用計數器 count:1
[tracker release];
NSLog(@"%d",[tracker retainCount]);
//增加引用計數器 count:2
[tracker retain];
NSLog(@"%d",[tracker retainCount]);
//減少引用計數器 count:1
[tracker release];
NSLog(@"%d",[tracker retainCount]);
//減少引用計數器 count:0
[tracker release];
NSLog(@"%d",[tracker retainCount]);
//當引用計數器為0的時候,系統將自動呼叫dealloc方法
//並且輸出我們自定義dealloc方法裡面的銷燬方法被呼叫。
return(0);
}
但是當我們要編譯的時候會報錯,提示:retainCount' is unavailable: not available in automatic reference counting mode
。
解決方案:https://blog.csdn.net/muy1030725645/article/details/109117668
-fno-objc-arc
最後輸出如下圖:
所以,當用alloc
、new
建立了一個物件的時候,通過用release
對該物件進行釋放就能銷燬物件並且回收所佔用的記憶體。
物件所有權
物件所有權(object ownership)
概念。
當我們說某個實體"擁有一個物件"時,就以為著該實體要負責確保對其他擁有的物件進行清理。
當物件裡面有其他物件例項,我們稱為該物件擁有這些物件
。例如複合概念:CarParts
類中,car
物件擁有其指向的engine
和tire
物件。同樣如果是一個函式建立了一個物件,則稱該函式擁有這個物件。
當多個例項擁有某個特定的物件時,物件的所有權關係就更加複雜了,這也就是是保留計數器的值大於1的原因。
-(void) setEngine:(Engine*) newEngine;
int main()
{
Engine *engine = [Engine new];
[car setEngine: engine];//car設定新的引擎
}
現在我們參看如上程式碼,並且進行思考。
- 現在哪個實體物件擁有engine物件?是
main()
函式還是Car
類? - 哪個實體負責確保當engine物件不再被使用時能夠收到release訊息?
答:
1.Car類 因為Car類正在使用engine物件,所以不可能是main函式。
2.main()函式 因為main()函式隨後可能還會用到engine物件,所以不可能是由Car類實體來收到release訊息。
解決方案:
讓Car類保留engine物件,將engine物件的保留計數器的值增加到2.
Car類應該在setEngine方法中保留engine物件
main()函式應該負責釋放engine物件
當Car類完成其任務時再釋放engine物件(在某dealloc方法中),最後engine物件佔用的資源被回收。
訪問方法中的保留和釋放
編寫setEngine
方法的第一個記憶體管理版本。
-(void) setEngine:(Engine* )newEngine
{
engine = [newEngine retain];//增加引用計數器
}
int main()
{
Engine *engine1 = [Engine new];//new會建立一個物件,並且保留計數器會被設定為1
[car setEngine:engine1];//setEngine方法會呼叫retain,所以保留計數器+1 = 2
[engine1 release];//釋放物件,保留計數器會被-1 = 1 ,這樣main函式還能訪問engine1物件
Engine *engine2 = [Engine new];//1
[car setEngine:engine2];//2
}
//如上程式碼有個bug,因為[engine1 release]的時候,保留計數器還是1,所以導致了記憶體洩露。
修改後
-(void) setEngine:(Engine*) newEngine
{
[newEngine retain];//保留計數器值+1
[engine release];
engine = newEngine;
}
自動釋放
我們都知道,當我們不再使用物件的時候必須將其釋放,但是在某些情況下需要弄清楚什麼時候不再使用一個物件並不容易,比如:
-(NSString *)description
{
NSString *description;
description = [[NSString alloc] initWithFormat:@"hello"];//alloc 建立物件保留計數器值=1
return (description);
}
int main()
{
//可以用如下的程式碼進行釋放,但是要寫成這樣看起來就很麻煩。
NSString *desc = [someObject description];
NSLog(@"%@",desc);
[desc release];
}
所有物件放入池中
Cocoa中有個自動釋放池(autorelease pool
)的概念。你可能已經在Xcode生成程式碼的時候見過@autoreleasepool
或NSAutoreleasePool
。那麼物件池到底是個什麼東西?從名字上看他大概應該是一個存放物件的池子(集合)。
-(id) autorelease;
該方法是NSObject類提供的,他預先設定了一條會在未來某個時間傳送的release
訊息,其返回值是接收這條訊息的物件。
當給一個物件傳送autorelease
訊息的時候,實際上是將該物件新增到了自動釋放池中。當自動釋放池被銷燬時,會向該池中的所有物件傳送release訊息。
改進後的之前description方法程式碼。
-(NSString*) description
{
NSString *description;
description = [[NSString alloc] initWithFormat:@"hello"];//保留計數器值= 1
return [description autorelease];//將description物件新增到自動釋放池中,當自動釋放池被銷燬,物件也被銷燬
}
//NSLog函式呼叫完畢後,自動釋放池被銷燬,所以物件也被銷燬,記憶體被回收。
NSLog(@"%@",[someObject description]);
自動釋放池的銷燬時間
- 自動釋放池什麼時候才能會銷燬,並向其包含的所有物件傳送release訊息?
- 還有自動釋放池應該什麼時候建立?
首先來回答第一個問題
我們看如下程式碼。自動釋放池可以用下面兩種方式來建立,那麼第一種方法用的是OC的關鍵字,他會在{}結束後進行銷燬並且傳送release訊息。第二種方法則是用NSAutoreleasePool
類,來進行建立一個活的池。他會在release後回收並銷燬池。
@autoreleasepool
{
//....Your Code
}
NSAutoreleasePool *pool = [NSAutoreleasePool new];
//....Your Code
[pool release];
回答第二個問題,我們需要先了解了解自動釋放池的工作流程。
自動釋放池的工作流程
如下程式碼展示了自動釋放池的工作流程。
int main(int argc, char const *argv[])
{
//NSAutoReleasePool方式自動釋放池.
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init];
RetainTracker *tracker = [RetainTracker new];//Count = 1
[tracker retain];//Count = 2 (Count+1)
[tracker autorelease];//將tracker物件新增到自動釋放池, Count = 2
[tracker release];//Count = 1 (Count-1)
NSLog(@"釋放掉自動釋放池(release pool)");
[pool release];
//@autorelease 關鍵字方式自動釋放池
@autorelease
{
RetainTracker *tracker2 = [RetainTracker new];//count = 1
[tracker2 retain];//count = 2
[tracker2 autorelease];//count = 2 //將tracker2物件新增到自動釋放池
[tracker2 release];//count = 1
NSLog(@"@autorelease關鍵字,自動釋放池!");
}
return 0;
}
Cocoa的記憶體管理規則
- 當你使用new、alloc、或copy方法建立一個物件時,該物件的保留計數器的值為1。當不再使用該物件時,你應該向物件傳送一條release或autorelease訊息。
- 當你通過其他方法獲得一個物件時,假設該物件的保留計數器的值為1,而且已經被設定為自動釋放,那麼你不需要執行任何操作來確保該物件得到清理。如果你打算在一段時間內擁有該物件,則需要保留它並確保在操作完成時釋放它。
- 如果你保留了某個物件,就需要(最終)釋放會自動釋放該物件。必須保持
retain
方法和release
方法的使用次數相等。
臨時物件
接下來我們通過程式碼來看看一些常用的記憶體管理生命週期例子。
//用new、alloc、copy建立的物件要自己來釋放。
NSMutableArray *array;
array = [[NSMutalbleArray alloc] init];//呼叫alloc,保留計數器值=1
[array release];//傳送release訊息,保留計數器值=0
NSMutableArray *array = [NSMutableArray arrayWithCapacity:16];//count = 1,並且設定為了autorelease
//這個arrayWithCapacity建立的物件,不需要我們手動去release釋放它,它會自動新增到releasepool,在自動釋放池銷燬掉的時候自動給array物件傳送release訊息,來進行釋放。
NSColor *color;
color = [NSColor blueColor];
//blueColor方法也不屬於alloc、new、copy這三個方法,所以也不需要進行手動釋放,當它用blueColor建立物件的時候,會被新增到自動釋放池,我們不需要手動來對他進行釋放。
擁有物件
有時候,你可能希望在多段程式碼中一直擁有某個物件。典型的方法是把它們加入到諸如NSArray或者NSDicrionary等集合中,作為其他物件的例項變數來使用。
手動釋放
-(void) doStuff
{
flonkArray = [NSMutableArray new];
}
-(void) dealloc
{
[flonkArray release];
[super dealloc];
}
自動釋放
-(void) doStuff
{
//通過非alloc、new、copy函式建立的物件會新增到autorelease中。
flonkArray = [NSMutableArray arrayWithCapacity: 16];
[flonkArray retain];//count = 2
//autorelease後變成1
}
-(void) dealloc
{
[flonkArray release];
[super dealloc];
}
仔細觀察這一段程式碼,指出哪裡有問題?
int i;
for(i=0;i<1000000;i++)
{
id object = [someArray objectAtIndex:i];
NSString *desc = [object description];
}
首先,可以看出這段程式碼會迴圈1000000次,然後someArray類傳送objectAtIndex訊息建立了一個物件object。
object物件呼叫description訊息會呼叫NSLog輸出訊息,接著也會建立一個物件desc。
所以說,這裡兩個物件都是通過非alloc、new、copy建立的,他們會新增到自動釋放池。這就建立了1000000個自動釋放池,大量的字串佔用了記憶體。自動釋放池在for迴圈中並不會被銷燬,所以這段期間電腦記憶體佔用率會很高,從而影響使用者體驗。
改良後:
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];//建立自動釋放池
int i;
for(i = 0;i<1000000;i++)
{
id object = [someArray objectAtIndex:i];
NSString *desc = [object description];
if(i % 1000 == 0)
{
[pool release];//當i=1000時候,銷燬自動釋放池。也就是當字串超過1000就開始釋放記憶體了!
pool = [[NSAutoreleasePool alloc] init];//再建立新的自動釋放池
}
}
[pool release];
改見後的程式碼在迴圈1000次以後,就會釋放自動釋放池。這樣就解決了字串太多佔用記憶體的問題。
垃圾回收
Object-C 2.0
後引入了自動記憶體管理機制,也就是垃圾回收。
熟悉Java
或Python
等語言的程式設計師應該非常熟悉垃圾回收的概念。對於已經建立和使用的物件,當你忘記清理時,系統會自動識別哪些物件仍在使用,哪些物件可以回收。
在Xcode13中,預設是開啟垃圾回收功能的。注意!垃圾回收機制只能在macOS開發中用到,iOS開發暫不支援垃圾回收機制。
自動引用計數
iOS無法使用垃圾回收機制
在iOS開發中為什麼無法使用垃圾回收機制,這是怎麼回事?
主要的原因是因為你無法知道垃圾回收器什麼時候回起作用。就像在現實生活中,你可能知道週一是垃圾回收日,但是不知道精確時間。假如你正要出門的時候,垃圾車到了該怎麼辦?垃圾回收機制會對移動裝置的可用性產生非常不利的影響,因為移動裝置比電腦更加私人化,資源更少。使用者可不想再玩遊戲或者打電話的時候因為系統突然進行記憶體清理而卡住。
ARC介紹
蘋果公司的解決方案被稱為自動引用計數
(automatic refernce countring
),簡稱:ARC
。
顧名思義:ARC
會追蹤你的物件並確定哪一個仍會使用而哪一個不會再使用,就好像你有了一位專門負責記憶體管理的管家或私人助理。如果你啟用了ARC
,只管像平常那樣按需分配並使用物件,編譯器會幫你插入retain
和release
語言,無需你自己動手。
ARC
不是垃圾回收器。我們已經討論過了,垃圾回收器在執行時工作,通過返回的程式碼來定期檢查物件。
與此相反,ARC
是在編譯時進行工作的。它在程式碼中插入了合適的retain
和release
語句,就好像是你自己手動寫好了所有的記憶體管理程式碼。不過編譯器替你完成了記憶體管理的工作。
ARC條件
如果你想要在程式碼中使用ARC
,必須滿足以下三個條件:
-
能夠確定哪些物件需要進行記憶體管理;
-
能夠表明如何去管理物件;
-
有可行的辦法傳遞物件的所有權。
第一個條件是最上層集合知道如何去管理他的子物件。
第一個條件例子:
這段程式碼建立了指向10個字串的C型陣列。因為C型陣列是不可保留的物件,所以你無法在這個結構體裡使ARC特性。
NSString **myString;
myString = malloc(10 * sizeof(NSString *));
第二個條件是你必須能夠對某個物件的保留計數器的值進行加1或減1的操作。也就是說所有NSObject
類的子類都能進行記憶體管理。這包括了大部分你需要管理的物件。
第三個條件是在傳遞物件的時候,你的程式需要能夠在呼叫者和接收者(後面會詳細介紹)之間傳遞所有權。
弱引用(Weak)、強引用
強引用:當用指標指向某個物件時,你可以管理他的記憶體(retain、release),如果你管理了那麼你就擁有了這個物件的強引用(strong refernce)
。如果你沒有管理,那麼你就擁有的是弱引用(weak refernce)
。
當物件A建立出了物件B,然後物件B有一個指向物件A的強引用。
當物件A的擁有者不再需要需要它的時候,傳送release訊息,這時候物件A、B的值都還是1,引發了記憶體洩露!
解決方案:物件B通過弱應用(weak refernce)
來指向物件A,並且記得清空弱引用物件。
__weak NSString *myString;
@proerty(weak) NSString* myString;
//如果有些比較老舊的系統不支援arc,就用如下方法
__unsafe_unretained
使用ARC的時候兩種命名規則需要注意:
- 屬性不能以new 開頭,比如說@property NSString *newString;//是不被允許的? Why????
- 屬性不能只有一個read-only而沒有記憶體管理特性。如果你沒有啟用ARC,可以使用@property(readonly) NSString *title,
擁有者許可權
之前說過指標支援ARC的一個條件是必須是可保留物件指標(ROP)。
這意味著你不能簡單的 將一個ROP表示成不可保留物件指標(non-ROP),因為指標的所有權會移交。
NSString *theString = @"Learn Objective-C";
CFStringRef cfString = (CFStringRef) theString;
theString指標是一個ROP,而另外一個cfString則不是。為了讓ARC便於工作,需要告訴編譯器哪個物件是指標的擁有者。
//(__bridge型別)操作符
//這種型別的轉換會傳遞指標但不會傳遞它的所有權。
{
NSString *theString = @"Learn Objective-C";
CFStringRef cfString = (__bridge CFStringRef)theString;
}
//(__bridge_retained型別)操作符
//這種型別,所有權會轉移到non-ROp上。
{
NSString *theString = @"Lean Objective-C";
CFStringRef cfString = (__bridge_retained CFStringRef)theString;
}
//(__bridge_transfer型別)操作符
//這種轉換型別與上一個相反,它把所有權交給ROP
{
NSString *theString = @"Lean Objective-C";
CFStringRef cfString = (__bridge CFStringRef)theString;
}
異常
與異常有關的關鍵字
- @try:定義用來測試的程式碼塊是否要丟擲異常。
- @catch():定義用來處理已丟擲異常的程式碼塊。
- @finally:定義無論如何是否有丟擲異常都會執行程式碼塊。
- @throw:丟擲異常。
捕捉不同型別的異常
@try
{
}@catch(NSException *exception){
}@catch(MyCustomException *custom){
}@catch(id value){
}@finally
{
}
丟擲異常
@try
{
NSException *e = @"error";
@throw e;
}@catch(NSException *e){
@throw;
}
異常也需要記憶體管理
-(void) mySimpleMethod
{
NSDictionary *dictionary = [[NSDictionary alloc] initWith....];
@try{
[self processDictionary:dictionary];
}
@finally{
[dictionary release];//finally中的程式碼會比trhow之前執行。
}
}
異常和自動釋放池
-(void) myMethod
{
id savedException = nil;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *myDictionary = [[NSDictionary alloc] initWith....];
@try{
[self processDictionary:myDictionary];
}@catch(NSException *e){
savedException = [e retain];
@throw;
}@finally{
[pool release];
[savedException autorelease];
}
}
通過使用retain方法,我們在當前池中保留了異常。當池被釋放時,我們早已儲存了一個異常指標,它會同當前池一同釋放。
小結
本章介紹了Cocoa
的記憶體管理方法:retain
、release
、autorelease
,還討論了垃圾回收和自動應用技術
(ARC
)。
Cocoa中有三個關於物件及其保留計數器的規則:
- 如果使用new、alloc、copy操作獲得了一個物件,則該物件的保留計數器的值為1.
- 如果通過其他方法獲得一個物件,則假設該物件的保留計數器的值為1,而且已經被設定為自動釋放。
- 如果保留了其物件,則必須保持
retain
方法和release
方法的使用次數相等。
ARC技術會在編譯過程中,編譯器自動插入retain
和release
這些語句幫你完成記憶體釋放和保留。
Pwn菜雞學習小分隊
歡迎來PWN菜雞小分隊閒聊:PWN、RE 或者摸魚小技巧。