Objective-C 基礎教程第九章,記憶體管理

Object-C 基礎教程第九章,記憶體管理

前言:

最近事情比較多,很久沒有來更新文章了。

剛好最近又空閑出來點時間,趕緊繼續學習OC並且做筆記,這次要學習的是OC的記憶體管理。

對象生命周期

正如現實世界中的鳥類和蜜蜂一樣,程式中你的對象也有生命周期。

對象的生命周期包括誕生(通過alloc或者new方法實現)、生存(接收消息並執行操作)、交友(通過複合以及方法傳遞參數)

以及最終死去(被釋放掉)。

當生命周期結束時,它們的原材料(記憶體)將被回收以供新的對象使用。

引用計數

現在,對象何時誕生我們已經很清楚了,而且也討論了如何使用對象,但是怎麼知道對象生命周期結束了呢?Cocoa採用了一種叫做引用計數(reference counting)的技術,有也叫做保留計數(retain counting)

每個對象都有一個關聯的整數,當某段程式碼需求訪問一個對象時候,計數器就+1,

反之當這段程式碼結束對象訪問時,計數器-1,

當計數器為0的時候系統就回收該對象(😌可憐的對象)。

  • allocnew方法或者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

image-20220416093737606

解決方案://blog.csdn.net/muy1030725645/article/details/109117668

-fno-objc-arc

image-20220416093856226

image-20220416093946829

最後輸出如下圖:

image-20220416094013295

所以,當用allocnew創建了一個對象的時候,通過用release對該對象進行釋放就能銷毀對象並且回收所佔用的記憶體。

對象所有權

對象所有權(object ownership)概念。

當我們說某個實體”擁有一個對象”時,就以為著該實體要負責確保對其他擁有的對象進行清理。

當對象裡面有其他對象實例,我們稱為該對象擁有這些對象。例如複合概念:CarParts類中,car對象擁有其指向的enginetire對象。同樣如果是一個函數創建了一個對象,則稱該函數擁有這個對象。

當多個實例擁有某個特定的對象時,對象的所有權關係就更加複雜了,這也就是是保留計數器的值大於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生成程式碼的時候見過@autoreleasepoolNSAutoreleasePool。那麼對象池到底是個什麼東西?從名字上看他大概應該是一個存放對象的池子(集合)。

-(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]);

自動釋放池的銷毀時間

  1. 自動釋放池什麼時候才能會銷毀,並向其包含的所有對象發送release消息?
  2. 還有自動釋放池應該什麼時候創建?

首先來回答第一個問題

我們看如下程式碼。自動釋放池可以用下面兩種方式來創建,那麼第一種方法用的是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後引入了自動記憶體管理機制,也就是垃圾回收。

熟悉JavaPython等語言的程式設計師應該非常熟悉垃圾回收的概念。對於已經創建和使用的對象,當你忘記清理時,系統會自動識別哪些對象仍在使用,哪些對象可以回收。

在Xcode13中,默認是開啟垃圾回收功能的。注意!垃圾回收機制只能在macOS開發中用到,iOS開發暫不支援垃圾回收機制。

自動引用計數

iOS無法使用垃圾回收機制

在iOS開發中為什麼無法使用垃圾回收機制,這是怎麼回事?

主要的原因是因為你無法知道垃圾回收器什麼時候回起作用。就像在現實生活中,你可能知道周一是垃圾回收日,但是不知道精確時間。假如你正要出門的時候,垃圾車到了該怎麼辦?垃圾回收機制會對移動設備的可用性產生非常不利的影響,因為移動設備比電腦更加私人化,資源更少。用戶可不想再玩遊戲或者打電話的時候因為系統突然進行記憶體清理而卡住。

ARC介紹

蘋果公司的解決方案被稱為自動引用計數(automatic refernce countring),簡稱:ARC

顧名思義:ARC會追蹤你的對象並確定哪一個仍會使用而哪一個不會再使用,就好像你有了一位專門負責記憶體管理的管家或私人助理。如果你啟用了ARC,只管像平常那樣按需分配並使用對象,編譯器會幫你插入retainrelease語言,無需你自己動手。

ARC不是垃圾回收器。我們已經討論過了,垃圾回收器在運行時工作,通過返回的程式碼來定期檢查對象。

與此相反,ARC是在編譯時進行工作的。它在程式碼中插入了合適的retainrelease語句,就好像是你自己手動寫好了所有的記憶體管理程式碼。不過編譯器替你完成了記憶體管理的工作。

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的強引用。

image-20220419143353071

當對象A的擁有者不再需要需要它的時候,發送release消息,這時候對象A、B的值都還是1,引發了記憶體泄露!

image-20220419143552973

解決方案:對象B通過弱應用(weak refernce)來指向對象A,並且記得清空弱引用對象。

image-20220419143936621

__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的記憶體管理方法:retainreleaseautorelease,還討論了垃圾回收和自動應用技術(ARC)。

Cocoa中有三個關於對象及其保留計數器的規則:

  • 如果使用new、alloc、copy操作獲得了一個對象,則該對象的保留計數器的值為1.
  • 如果通過其他方法獲得一個對象,則假設該對象的保留計數器的值為1,而且已經被設置為自動釋放。
  • 如果保留了其對象,則必須保持retain方法和release方法的使用次數相等。

ARC技術會在編譯過程中,編譯器自動插入retainrelease這些語句幫你完成記憶體釋放和保留。

Pwn菜雞學習小分隊

歡迎來PWN菜雞小分隊閑聊:PWN、RE 或者摸魚小技巧。
img

Tags: