【iOS开发】——MRC(手动内存管理)的一些补充
【iOS開發】——MRC(手動內存管理)的一些補充
- 前言
- 野指針與空指針
- 多個對象內存管理的思想
- 玩家沒有使用過房間
- 一個玩家使用一個游戲房間的情況
- 一個玩家使用一個房間 r 后,換到另一個房間 r2 的情況
- 一個玩家使用一個房間,不再使用房間,將房間釋放掉之后,再次使用該房間的情況
- MRC需要注意的一些知識點
- @property 參數
- 自動釋放池(AutoreleasePool)
- 使用 autorelease 有什么好處呢?
- autorelease 的原理實質上是什么?
- autorelease 的創建方法
- autorelease的使用方法
- autorelease 的注意事項
- 自動釋放池的嵌套使用
- autorelease 錯誤用法
- MRC 中避免循環引用
前言
上學期的時候我總結過關于MRC的一些知識,具體可以看這篇:iOS開發——MRC(手動內存管理)
最近在復習MRC,發現當時總結的時候有幾個點沒有總結上,今天在寫一篇補充記錄一下
野指針與空指針
空指針:
- 空指針指的是沒有指向存儲空間的指針(里面存的是 nil, 也就是 0)。
- 給空指針發消息是沒有任何反應的
野指針:
- 只要一個對象被釋放了,我們就稱這個對象為「僵尸對象(不能再使用的對象)」。
- 當一個指針指向一個僵尸對象(不能再使用的對象),我們就稱這個指針為「野指針」。
- 只要給一個野指針發送消息就會報錯(EXC_BAD_ACCESS 錯誤)。
多個對象內存管理的思想
多個對象之間往往是通過setter方法產生聯系的,其內存管理的方法也是通過setter、delloc方法實現管理的。接下來我們學習一下setter方法的具體實現過程
我們可以舉一個例子來幫助我們理解這個過程:
我記得很早以前,騰訊出過一個叫qq游戲大廳的功能好像,具體記不清了,反正就是有好幾種游戲,比如斗地主,我們打麻將需要三個人和一個房間,所以我們可以定義房間為Room類對象,然后定義玩家為Person類對象,玩家對象擁有 _room 作為成員變量。
一個玩家對象,如果想要玩游戲,就要持有一個房間對象,并保證在使用房間期間,這個房間對象一直存在,并且在游戲房間沒人的時候,還需要將這個房間對象釋放。
那么房間具體的引用情況有哪些呢:
- 只要一個玩家想使用房間(進入房間),就需要對這個游戲房間的引用計數器 +1。
- 只要一個玩家不想再使用房間(離開房間),就需要對這個游戲房間的引用計數器 -1。
- 只要還有至少一個玩家在用某個房間,那么這個游戲房間就不會被回收,引用計數至少為 1。
- 只要沒有玩家在房子里了,那么這個房間就會被回收
我們可以看到玩家三個玩家對象都持有房間對象,所以房間對象的引用為3。
我們將剛剛說的兩個類對的代碼寫出來:
Room類:
Person類:
#import <Foundation/Foundation.h> #import "Room.h" NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject {Room *_room; }- (void)setRoom:(Room *)room; - (Room *)room; @endNS_ASSUME_NONNULL_END玩家沒有使用過房間
#import <Foundation/Foundation.h> #import "Person.h" #import "Room.h"int main(int argc, const char * argv[]) {@autoreleasepool {//1.創建兩個對象Room *r = [[Room alloc] init];Person *p = [[Person alloc] init];//給房間號賦值r.number = 808;//釋放兩個對象[r release];[p release];}return 0; }我們可以看到在上述代碼中Person類創建的對象沒有對房間進行持有,也就是玩家雖然創建出來了但是卻沒有使用過房間,上述代碼運行時內存使用情況如圖所示:
在這里復習兩個知識點:
- 棧:存放基本類型 的變量數據和對象的引用,但對象本身不存放在棧中,而是存放在堆(new出來的對象)或者常量池中(字符串常量對象存放的常量池中),局部變量【注意:(方法中的局部變量使用final修飾后,放在堆中,而不是棧中)】
- 堆:存放使用new創建的對象,全局變量
我們在來看上面的例子,通過上圖可以發現Room 實例對象和 Person 實例對象之間沒有相互聯系,所以各自釋放不會報錯。等兩個對象釋放以后,內存的情況如圖所示:
最后由于引用計數變為0了,各自實例對象的內存就會被系統回收。
一個玩家使用一個游戲房間的情況
在調用 setter 方法的時候,因為 Room 實例對象多了一個 Person 對象引用,所以應將 Room 實例對象的引用計數 +1 才對,即 setter 方法應該像下邊一樣,對 room 進行一次 retain 操作。
- (void)setRoom:(Room *)room { // 調用 room = r;// 對房間的引用計數器 +1[room retain];_room = room; }然后我們在main函數里完成一下一個玩家使用一個游戲房間的情況:
#import <Foundation/Foundation.h> #import "Person.h" #import "Room.h"int main(int argc, const char * argv[]) {@autoreleasepool {Room *r = [[Room alloc] init];Person *p = [[Person alloc] init];r.number = 808;// 將房間賦值給玩家,表示玩家在使用房間// 玩家需要使用這間房,只要玩家在,房間就一定要在p.room = r;// [p setRoom:r][r release];// 在這行代碼之前,玩家都沒有被釋放,但是因為玩家還在,那么房間就不能銷毀[p release];}return 0; }此時我們的內存分配情況就應該為:
其實還是很好理解的,我們主要來理解一下引用計數這部分,Room創建實例對象引用計數?1,然后Person創建實例對象Person的引用計數也?1同時Person通過setter方法對Room實例對象進行了持有,所以此時Room的引用計數再?1變為了2。
然后我們看,Room的實例對象釋放了對應的Room的引用計數就要?1,此時內存的分配情況為:
然后執行代碼 [p release];,釋放Person實例對象。這時候因為玩家不在房間里了,房間也沒有用了,所以在釋放玩家的時候,要把房間也釋放掉,也就是在 delloc 里邊對房間再進行一次 release 操作。
這樣對房間對象來說,每一次 retain / alloc 操作都對應一次 release 操作。
- (void)dealloc {// 人釋放了, 那么房間也需要釋放[_room release];NSLog(@"%s", __func__);[super dealloc]; }最終內存情況變為了:
一個玩家使用一個房間 r 后,換到另一個房間 r2 的情況
#import <Foundation/Foundation.h> #import "Person.h" #import "Room.h"int main(int argc, const char * argv[]) {@autoreleasepool {Room *r = [[Room alloc] init];Person *p = [[Person alloc] init];r.number = 808;// 將房間賦值給玩家,表示玩家在使用房間// 玩家需要使用這間房,只要玩家在,房間就一定要在p.room = r;[r release];Room *r2 = [[Room alloc] init];r2.number = 404;p.room = r2;[r2 release]; // 釋放房間 r2// 在這行代碼之前,玩家都沒有被釋放,但是因為玩家還在,那么房間就不能銷毀[p release];}return 0; }在第一個Room實例對象釋放后,內存情況為:
接著我們進行了第二個房間的創建以及Person實例對象通過setter方法持有第二個Room實例對象。此時我們的內存情況變為了:
在我們執行完所有代碼,我們可以發現內存情況變為了:
此時為什么r還持有Room的實例對象呢,原因其實很簡單,我們調用了兩次Person的setter方法但是只delloc了一次,問題出在哪呢?當r釋放的時候,我們的p并沒有釋放,所以不會調用delloc方法,所以就造成了上述結果,那我們應該怎么辦呢?我們可以在調用 setter 方法的時候,對之前的變量進行一次 release 操作。具體 setter 方法代碼如下:
- (void)setRoom:(Room *)room { // room = r// 將以前的房間釋放掉 -1[_room release];// 對房間的引用計數器 +1[room retain];_room = room;} }這樣我們在第二次調用setter方法的時候會先將之前通過setter方法增加的引用計數減掉,就不會出現剛剛那種情況了。
所以內存情況就變為了:
一個玩家使用一個房間,不再使用房間,將房間釋放掉之后,再次使用該房間的情況
int main(int argc, const char * argv[]) {@autoreleasepool {// 1. 創建兩個對象Person *p = [[Person alloc] init];Room *r = [[Room alloc] init];r.number = 808;// 2. 將房間 r 賦值給玩家 pp.room = r; // [p setRoom:r][r release]; // 釋放房間 r// 3. 再次使用房間 rp.room = r;[r release]; // 釋放房間 r[p release]; // 釋放玩家 p}return 0; }執行完以下代碼:
// 1.創建兩個對象 Person *p = [[Person alloc] init]; Room *r = [[Room alloc] init]; r.number = 808;// 2.將房間賦值給人 p.room = r; // [p setRoom:r] [r release]; // 釋放房間 r內存情況為:
然后再執行 p.room = r;,因為 setter 方法會將之前的 Room 實例對象先釋放掉,所以此時內存表現為:
此時 _room、r 已經變成了一個野指針。之后再對野指針 r 發出 retain 消息,程序就會崩潰。所以我們在進行 setter 方法的時候,要先判斷一下是否是重復賦值,如果是同一個實例對象,就不需要重復進行 release 和 retain。換句話說,如果我們使用的還是之前的房間,那換房的時候就不需要對這個房間再進行 release 和 retain。則 setter 方法具體代碼如下:
因為 retain 不僅僅會對引用計數器 +1, 而且還會返回當前對象,所以上述代碼可最終簡化成:
- (void)setRoom:(Room *)room { // room = r// 只有房間不同才需用 release 和 retainif (_room != room) { // 0ffe1 != 0ffe1// 將以前的房間釋放掉 -1[_room release];_room = [room retain];} }所以就這樣我們得到了setter最終的形式,這也是多個對象內存管理的思想。
MRC需要注意的一些知識點
@property 參數
- 在成員變量前加上 @property,系統就會自動幫我們生成基本的 setter / getter 方法,但是不會生成內存管理相關的代碼。
- 同樣如果在 property 后邊加上 assign,系統也不會幫我們生成 setter 方法內存管理的代碼,僅僅只會生成普通的 getter / setter 方法,默認什么都不寫就是 assign。
- 如果在 property 后邊加上 retain,系統就會自動幫我們生成 getter / setter 方法內存管理的代碼,但是仍需要我們自己重寫 dealloc 方法。
自動釋放池(AutoreleasePool)
以前學MRC的時候了解過一點自動釋放池但是沒有做過系統的總結,今天總結一下關于自動釋放池的一些知識點
當我們不再使用一個對象的時候應該將其空間釋放,但是有時候我們不知道何時應該將其釋放。為了解決這個問題,Objective-C 提供了 autorelease 方法。
- autorelease 是一種支持引用計數的內存管理方式,只要給對象發送一條 autorelease 消息,會將對象放到一個自動釋放池中,當自動釋放池被銷毀時,會對池子里面的「所有對象」做一次 release 操作。
注意:這里只是發送 release 消息,如果當時的引用計數(reference-counted)依然不為 0,則該對象依然不會被釋放
- autorelease 方法會返回對象本身,且調用完 autorelease 方法后,對象的計數器不變。
使用 autorelease 有什么好處呢?
- 不用再關心對象釋放的時間
- 不用再關心什么時候調用release
autorelease 的原理實質上是什么?
autorelease 實際上只是把對 release 的調用延遲了,對于每一個 autorelease,系統只是把該對象放入了當前的 autorelease pool 中,當該 pool 被釋放時,該 pool 中的所有對象會被調用 release 方法。
autorelease 的創建方法
- 使用 NSAutoreleasePool 創建
- 使用 @autoreleasepool 創建
autorelease的使用方法
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; Person *p = [[[Person alloc] init] autorelease]; [autoreleasePool drain]; @autoreleasepool { // 創建一個自動釋放池Person *p = [[Person new] autorelease];// 將代碼寫到這里就放入了自動釋放池 } // 銷毀自動釋放池(會給池子中所有對象發送一條 release 消息)autorelease 的注意事項
- 并不是放到自動釋放池代碼中,都會自動加入到自動釋放池
- 在自動釋放池的外部發送 autorelease 不會被加入到自動釋放池中
-
- autorelease 是一個方法,只有在自動釋放池中調用才有效。
自動釋放池的嵌套使用
- 自動釋放池是以棧的形式存在
- 由于棧只有一個入口,所以調用 autorelease 會將對象放到棧頂的自動釋放池(棧頂就是離調用 autorelease 方法最近的自動釋放池)
- 自動釋放池中不適宜放占用內存比較大的對象
-
- 盡量避免對大內存使用該方法,對于這種延遲釋放機制,還是盡量少用
-
- 不要把大量循環操作放到同一個 @autoreleasepool 之間,這樣會造成內存峰值的上升。
autorelease 錯誤用法
- 不要連續調用 autorelease
- 調用 autorelease 后又調用 release(錯誤)
MRC 中避免循環引用
定義兩個類 Person 類和 Dog 類
Person 類:
#import <Foundation/Foundation.h> @class Dog;@interface Person : NSObject @property(nonatomic, retain)Dog *dog; @endDog類:
#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; // retaind.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。
總結
以上是生活随笔為你收集整理的【iOS开发】——MRC(手动内存管理)的一些补充的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文看懂「生成对抗网络 - GAN」基本
- 下一篇: 缓冲技术之三:Linux下I/O操作bu