OC內存管理詳解

前言

由於移動設備的內存有限,所以我們需要對內存進行嚴格的管理,以避免內存泄露造成資源浪費。在OC中,只有對象才屬於內存管理範圍,例如int、struce等基本數據類型不存在內存管理的概念。在iOS開發中,對內存的管理實際上就是對引用計數器的管理。

OC內存管理的三種方式

  1. 自動垃圾收集(Automatic Garbage Collection);
  2. 手動引用計數器(Manual Reference Counting)和自動釋放池;
  3. 自動引用計數器(Automatic Reference Counting)。

自動垃圾收集

在OC2.0中,有一種自動垃圾收集的內存管理形式,通過垃圾自動收集,系統能夠自動檢測出對象是否擁有其他的對象,當程序運行期間,不被引用的對象就會自動釋放。
說明:在iOS運行環境中不支持自動垃圾收集,在OS X環境才支持,但是Apple現在不建議使用該方法,而是推薦使用ARC進行替代。

手動引用計數器(MRC)和自動釋放池;

引用計數器的概念

顧名思義,引用計數器即一個對象被引用(使用)的次數,每個對象的引用計數器佔用4個字節
如下圖所示,當使用A創建一個對象object的時候,object的RC默認爲1,當B也指向object的時候,object的RC+1=2。然後A指針不指向object的時候,object的RC-1=2-1=1。最後當B也不指向object的時候,object的RC-1=1-1=0,此時對象object被銷燬。
說明: 當一個對象被創建的時候,該對象的RC默認爲1;當該對象被引用一次,需要調用retain方法,使RC的值+1;當指針失去對該對象的引用,需要調用release方法,使RC的值-1;當RC=0的時候,該對象被系統自動銷燬回收。
Paste_Image.png

手動引用計數器(MRC)

MRC即我們通過人爲的方式來控制引用計數器的增減,影響對象RC值得方法有以下幾種:
1. new、alloc、copy、mutableCopy,這幾種方法用來創建一個新的對象並且獲得對象的所有權,此時RC的值默認爲RC=1;
2. retain,對象調用retain方法,該對象的RC+1;
3. release,對象調用 release方法,該對象的RC-1;
4. dealloc,dealloc方法並不會影響RC的值,但是當RC的值爲0時,系統會調用dealloc方法來銷燬對象。
下面給一個例子:

//Book類的聲明和實現
@interface Book:NSObject

@end
@implementation Book

@end

//Peron類的聲明和實現
@interface Person:NSObject
{
    Book *_book;
}
- (void)setBook:(Book *)book;
@end
@implementation Person
- (void)setBook:(Book *)book
{
    if(_book != book)
    {//如果新設置的book對象不是之前指向的book對象
          [_book release];//使之前對象的RC-1
          _book = [book retain];//當前引用的RC+1  
    }
- (void)dealloc //重載dealloc方法銷燬對象
{
    [_book release];//由於_book控制了Book對象,_book調用release方法使RC-1
    [super dealloc];//調用父類的dealloc方法,而且必須放在最後一行
}
}
@end

//主函數測試
void main()
{
    Book *b=[Book new];
    Person *p = [Person new];
    p.book = b;
    [b release];//b控制了Book對象,b調用release方法使RC-1
    [p release];//p控制了Person對象,p調用release方法使RC-1
}

通過上面代碼知道,成員變量的設值和取值方法是手動生成的,而且setter方法中成員變量的引用計數器也是手動設置,我們也可以通過@property以及相應關鍵字來由編譯器生成。
關於在MRC中@property關鍵字如下:
1. assign和retain和copy
這幾個關鍵字用於setter方法的內存管理,如果使用assign(一般用於非OC對象),那麼將直接執行賦值操作;如果使用retain(一般用於OC對象),那麼將retain新值,release舊值;如果使用copy,那麼將release舊值,copy新值。不顯示使用assign爲默認值
2. nonatomic和atomic
這兩個關鍵字用於多線程管理,nonatomic的性能高,atomic的性能低。不顯示使用atomic爲默認值
3.readwrite和readonly
這兩個關鍵字用於說明是否生成setter方法,readwrite將自動生成setter和getter方法,readonly 只生成getter方法。不顯示使用readwrite爲默認值
4. getter和setter
這兩個關鍵字用於給設值和取值方法另外起一個名字。例如@property(getter=a,setter=b:) int age;相當於取值方法名爲a,設值方法名爲b:。
如果使用@property屬性,那麼上面代碼可以改爲:

//Book類的聲明和實現
@interface Book:NSObject

@end
@implementation Book

@end

//Peron類的聲明和實現
@interface Person:NSObject
- (void)dealloc;
@property(nonatomic,retain) Book *_book;
@end
@implementation Person
- (void)dealloc //重載dealloc方法銷燬對象
{
    [_book release];//用於_book控制了Book對象,_book調用release方法使RC-1
    [super dealloc];//調用父類的dealloc方法,而且必須放在最後一行
}
}
@end

//主函數測試
void main()
{
    Book *b=[Book new];
    Person *p = [Person new];
    p.book = b;
    [b release];//b控制了Book對象,b調用release方法使RC-1
    [p release];//p控制了Person對象,p調用release方法使RC-1
}

循環引用內存管理原則

對於兩個類A包含B,B包含A的循環引用情況下,看如下代碼:

//Book1類的聲明和實現
@interface Book1:NSObject
@property(nonatomic,retain) Book2 *_book2;
- (void)dealloc;
@end
@implementation Book1
- (void)dealloc //重載dealloc方法銷燬對象
{
    [_book2 release];//用於_book控制了Book對象,_book調用release方法使RC-1
    [super dealloc];//調用父類的dealloc方法,而且必須放在最後一行
}
@end

//Book2類的聲明和實現
@interface Book2:NSObject
@property(nonatomic,retain) Book1 *_book1;
- (void)dealloc;
@end
@implementation Book2
- (void)dealloc //重載dealloc方法銷燬對象
{
    [_book1 release];//用於_book控制了Book對象,_book調用release方法使RC-1
    [super dealloc];//調用父類的dealloc方法,而且必須放在最後一行
}
}
@end

//主函數測試
void main()
{
    Book1 *b1=[[Book1 alloc] init];
    Book2 *b2=[[Book2 alloc] init];
    b1.book2 = b2;
    b2.book1 = b1;
    [b1 release];
    [b2 release];
}

下面分析主函數代碼,當執行Book1 *b1=[[Book1 alloc] init]後,b1指向Book1。當執行Book2 *b2=[[Book2 alloc] init]後,b2指向Book2。 當執行b1.book2 = b2後,Book1的成員變量_b2指向Book2。當執行b2.book1 = b1後,Book2的成員變量_b1指向Book1。內存中具體關係如下圖所示。

此時Book1的引用計數器RC=2,Book2的引用計數器RC=2。
當執行 [b1 release]後,b1釋放對Book1的控制權,此時Book1的引用計數器RC=2-1=1。
當執行[b2 release]後,b2釋放對Book2的控制權,此時Book2的引用計數器RC=2-1=1。
那麼由於仍有指針指向Book1和Book2,這時內存中Book1和Book2的關係如黑色橢圓內所示。所以並不會調用dealloc函數,所以Book1和Book2並不會毀銷,這樣就造成了 內存泄露
Paste_Image.png

對於上面這種情況,只需要在Book1和Book2的@property屬性聲明中一端使用retain,一端使用assign。即將@property(nonatomic,retain) Book1 *_book1或者@property(nonatomic,retain) Book2 *_book2中的一個retian改爲assign。具體原因自己分析。

看下面這種循環引用情況,只能使用assign

@interface Book:NSObject
@property(nonatomic,assign)id  instance;  //此處必須用assign
- (void)dealloc;
@end
@implementation Book
- (void)dealloc //重載dealloc方法銷燬對象
{
    [_instance release];//用於_book控制了Book對象,_book調用release方法使RC-1
    [super dealloc];//調用父類的dealloc方法,而且必須放在最後一行
}
@end

void mian()
{
    Book b1 = [[Book alloc] init];
    Book b2 = [[Book alloc] init];
    b1.instance = b2;
    b2.instance = b1;
    [b1 release];
    [b2 release];
}

大家可以分析一下,如果@property(nonatomic,assign)id instance; 中將assign換爲retain,那麼也將造成內存泄露。

Autorelease Pool的使用

顧名思義,autorelease即自動釋放對象,不需要我們手動釋放。從上面代碼我們知道,在主函數中,創建對象obj後,總要手動調用[obj release]方法,這樣無疑使工作量變大,且對我們的技術增長毫無意義。爲了減少這種無意義的工作,可以使用Autorelease Pool方式。
Autorelease Pool即自動釋放池,在Autorelease Pool內的對象在創建時只要調用了autorelease方法,那麼在該池子內的對象的最後的release方法的調用將由編譯器完成。

Autorelease Pool的創建有兩種方式:

1.通過@autoreleasepool方法創建,如下:
@autoreleasepool{
//在大括號內創建的對象最後不需要手動調用release方法。
}
2. 通過NSAutoreleasePool類創建,如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]  init];
//此範圍爲自動釋放池
[pool release];

Autorelease Pool的使用例子如下:

void main()
{ 
    @autoreleasepool
    {
        Person p = [[[Person alloc] init] autorelease]; //調用autorelease方法
        //不需要再調用[p release];方法,超過autoreleasepool作用域會自動調用該方法。
    }    
}

在返回對象的方法中最好使用自動釋放池釋放對象,因爲如果將新創建的對象作爲返回值,由於在返回該對象之前並不能釋放該對象,所以可以通過自動釋放池來延遲該對象的釋放。示例代碼如下:

-(Person *)getNewPerson
{
    Person p* = [[[Person alloc] init] autorelease];
    //do something
    return p;
}
或者
{
    Person p* = [[Person alloc] init] ;
    //do something
    return [p autorelease];
}

注意點:

  1. release方法不能多次調用,該調用的時候調用,否則容易造成野指針錯誤。
  2. 創建對象時多次調用autorelease方法,容易造成野指針錯誤。
  3. 在自動釋放池中新創建的對象並不是一定會添加到釋放池中,例如由new、alloc、copy、mutableCopy創建的對象並不會加到自動釋放池中,並且必須手動調用release方法才能釋放對象。如果想讓新創建的對象加入到自動釋放池中,就必須調用autorelease方法。
  4. 使用autorelease方法並不會使引用計數器的值增加,只是表示將該對象加入到自動釋放池中。

自動引用計數器(ARC)

ARC將由編譯器來自動完成對象引用計數器的控制,不需要手動完成。
ARC模式下,創建的新對象通常由以下幾種關鍵字來限定。
1. __strong(默認值),由__strong修飾的爲強指針,對象只要有強指針指向就不會被銷燬;每當一個強指針指向一個對象,該對象的的RC+1;
2. __weak,由__weak修飾的爲弱指針,弱指針所指向的對象並不會改變RC值,弱指針只表示是對對象的引用;當弱指針所指向的對象銷燬時,該弱指針的值變爲nil;
3. __unsafe_unretained,__unsafe_unretained修飾的對象指針所指向的對象也不會改變RC值,也只表示是對對象的引用;當所指向的對象銷燬時,該指針的值不會變爲nil,仍是保留原有的地址;

在ARC模式下,MRC中的retain、release等方法變的不可用,因爲ARC是不需要我們手動管理內存的,一切由編譯器完成。
MRC模式下,將一個對象指針賦值給另一個對象指針如下:

Person p1 = [Person new];
Person p2 = [Person new];
[p2 release]//在p2失去對對象的控制權時需要先release
p2 = p1;//進行賦值操作

但是在ARC模式下,我們完全可以不關心具體怎麼操作,只需要直接進行賦值即可:

Person p1 = [Person new];
Person p2 = [Person new];
p2 = p1;//進行賦值操作

ARC模式下的循環引用

在ARC模式下,@property屬性關於內存管理的修飾符爲strong和weak(MRC下的retain和assign不可用),表示聲明爲強指針還是弱指針。通常情況下都是使用strong來修飾,但是在循環引用卻不是。

下面這種情況一端使用strong修飾,一端使用weak修飾。如果都使用strong修飾,那麼將造成對象的循環保持,造成內存泄露。

//Book1類的聲明和實現
@interface Book1:NSObject
@property(nonatomic,strong) Book2 *_book2;
@end
@implementation Book1

@end

//Book2類的聲明和實現
@interface Book2:NSObject
@property(nonatomic,weak) Book1 *_book1;
@end
@implementation Book2

@end

//主函數測試
void main()
{
    Book1 *b1=[[Book1 alloc] init];
    Book2 *b2=[[Book2 alloc] init];
    b1.book2 = b2;
    b2.book1 = b1;
}

下面這種循環引用情況,只能使用weak。如果使用strong修飾,那麼將造成對象的循環保持,造成內存泄露。

@interface Book:NSObject
@property(nonatomic,weak)id  instance;  //此處必須用assign
@end
@implementation Book

@end

void mian()
{
    Book b1 = [[Book alloc] init];
    Book b2 = [[Book alloc] init];
    b1.instance = b2;
    b2.instance = b1;
}

注意點:

  1. ARC模式下仍能使用自動釋放池;
  2. MRC下的retain、release、retainCount、autorelease等方法不可使用。
  3. 注意循環引用下strong和weak的選擇。

總結

內存管理的本質是對對象引用計數器的操作,理解MRC模式下內存管理操作有助於我們對OC內存管理的理解。內存管理只針對對象而言,注意MRC和ARC下@property屬性關鍵字的選擇,在MRC模式下,OC對象通常使用retain關鍵字,非OC對象使用assign關鍵字,但是循環引用是一個例外,通常需要一端使用assign,一端使用retain;在ARC模式下,OC對象通常使用strong關鍵字,非OC對象使用assign關鍵字,但是循環引用是一個例外,通常需要一端使用strong,一端使用weak。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章