iOS開發系列—Objective-C之記憶體管理

KenshinCui發表於2014-07-26

概述

我們知道在程式執行過程中要建立大量的物件,和其他高階語言類似,在ObjC中物件時儲存在堆中的,系統並不會自動釋放堆中的記憶體(注意基本型別是由系統自己管理的,放在棧上)。如果一個物件建立並使用後沒有得到及時釋放那麼就會佔用大量記憶體。其他高階語言如C#、Java都是通過垃圾回收來(GC)解決這個問題的,但在OjbC中並沒有類似的垃圾回收機制,因此它的記憶體管理就需要由開發人員手動維護。今天將著重介紹ObjC記憶體管理:

  1. 引用計數器
  2. 屬性引數
  3. 自動釋放池

引用計數器

在Xcode4.2及之後的版本中由於引入了ARC(Automatic Reference Counting)機制,程式編譯時Xcode可以自動給你的程式碼新增記憶體釋放程式碼,如果編寫手動釋放程式碼Xcode會報錯,因此在今天的內容中如果你使用的是Xcode4.2之後的版本(相信現在大部分朋友用的版本都比這個要高),必須手動關閉ARC,這樣才有助於你理解ObjC的記憶體回收機制。

ObjC中的記憶體管理機制跟C語言中指標的內容是同樣重要的,要開發一個程式並不難,但是優秀的程式則更測重於記憶體管理,它們往往佔用記憶體更少,執行更加流暢。雖然在新版Xcode引入了ARC,但是很多時候它並不能完全解決你的問題。在Xcode中關閉ARC:專案屬性—Build Settings--搜尋“garbage”找到Objective-C Automatic Reference Counting設定為No即可。

記憶體管理原理

我們都知道在C#、Java中都有GC在自動管理記憶體,當我們例項化一個物件之後通常會有一個變數來引用這個物件(變數中儲存物件地址),當這個引用變數不再使用之後(也就是不再引用這個物件)此時GC就會自動回收這個物件,簡單的說就是:當一個物件沒有任何變數引用的時候就會被回收。

例如下面的C#程式碼片段

using System;

namespace GC
{
    class Program
    {
        private static void Test()
        {
            object o=new object();
        }

        static void Main(string[] args)
        {
            Test();
        }
    }
}

上面是一段C#程式碼,在Test()方法中,通過new Object()建立了一個物件,o是一個物件的引用(儲存了物件的地址),它是一個區域性變數,作用範圍就是Test()方法內部。

image

當執行完Test()方法之後o就會被釋放,此時由於沒有變數在引用new Object()這個物件,因此GC會自動回收這個物件所佔用的空間。

但是在ObjC中沒有垃圾回收機制,那麼ObjC中記憶體又是如何管理的呢?其實在ObjC中記憶體的管理是依賴物件引用計數器來進行的:在ObjC中每個物件內部都有一個與之對應的整數(retainCount),叫“引用計數器”,當一個物件在建立之後它的引用計數器為1,當呼叫這個物件的alloc、retain、new、copy方法之後引用計數器自動在原來的基礎上加1(ObjC中呼叫一個物件的方法就是給這個物件傳送一個訊息),當呼叫這個物件的release方法之後它的引用計數器減1,如果一個物件的引用計數器為0,則系統會自動呼叫這個物件的dealloc方法來銷燬這個物件。

下面通過一個簡單的例子看一下引用計數器的知識:

Person.h

//
//  Person.h
//  MemoryManage
//
//  Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Person : NSObject

#pragma mark - 屬性
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;

@end

Person.m

//
//  Person.m
//  MemoryManage
//
//  Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "Person.h"

@implementation Person

#pragma mark - 覆蓋方法
#pragma mark 重寫dealloc方法,在這個方法中通常進行物件釋放操作
-(void)dealloc{
    NSLog(@"Invoke Person's dealloc method.");
    [super dealloc];//注意最後一定要呼叫父類的dealloc方法(兩個目的:一是父類可能有其他引用物件需要釋放;二是:當前物件真正的釋放操作是在super的dealloc中完成的)
}

@end

main.m

//
//  main.m
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Person.h"

void Test1(){
    Person *p=[[Person alloc]init]; //呼叫alloc,引用計數器+1
    p.name=@"Kenshin";
    p.age=28;
    
    NSLog(@"retainCount=%lu",[p retainCount]);
    //結果:retainCount=1
    
    [p release];
    //結果:Invoke Person's dealloc method.
    
    
    
    //上面呼叫過release方法,p指向的物件就會被銷燬,但是此時變數p中還存放著Person物件的地址,
    //如果不設定p=nil,則p就是一個野指標,它指向的記憶體已經不屬於這個程式,因此是很危險的
    p=nil;
    //如果不設定p=nil,此時如果再呼叫物件release會報錯,但是如果此時p已經是空指標了,
    //則在ObjC中給空指標傳送訊息是不會報錯的
    [p release];
}

void Test2(){
    Person *p=[[Person alloc]init];
    p.name=@"Kenshin";
    p.age=28;
    
    NSLog(@"retainCount=%lu",[p retainCount]);
    //結果:retainCount=1
    
    [p retain];//引用計數器+1
    NSLog(@"retainCount=%lu",[p retainCount]);
    //結果:retainCount=2
    
    [p release];//呼叫1次release引用計數器-1
    NSLog(@"retainCount=%lu",[p retainCount]);
    //結果:retainCount=1
    [p release];
    //結果:Invoke Person's dealloc method.
    p=nil;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test1();
    }
    return 0;
}

在上面的程式碼中我們可以通過dealloc方法來檢視是否一個物件已經被回收,如果沒有被回收則有可能造成記憶體洩露。如果一個物件被釋放之後,那麼最後引用它的變數我們手動設定為nil,否則可能造成野指標錯誤,而且需要注意在ObjC中給空物件傳送訊息是不會引起錯誤的。

野指標錯誤形式在Xcode中通常表現為:Thread 1:EXC_BAD_ACCESS(code=EXC_I386_GPFLT)錯誤。因為你訪問了一塊已經不屬於你的記憶體。

記憶體釋放的原則

手動管理記憶體有時候並不容易,因為物件的引用有時候是錯綜複雜的,物件之間可能互相交叉引用,此時需要遵循一個法則:誰建立,誰釋放

假設現在有一個人員Person類,每個Person可能會購買一輛汽車Car,通常情況下購買汽車這個活動我們可能會單獨抽取到一個方法中,同時買車的過程中我們可能會多看幾輛來最終確定理想的車,現在我們的程式碼如下:

Car.h

//
//  Car.h
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Car : NSObject

#pragma mark - 屬性
#pragma mark 車牌號
@property (nonatomic,copy) NSString *no;

#pragma mark - 公共方法
#pragma mark 執行方法
-(void)run;

@end

Car.m

//
//  Car.m
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "Car.h"

@implementation Car

#pragma mark - 公共方法
#pragma mark 執行方法
-(void)run{
    NSLog(@"Car(%@) run.",self.no);
}

#pragma mark - 覆蓋方法
#pragma mark 重寫dealloc方法
-(void)dealloc{
    
    NSLog(@"Invoke Car(%@) dealloc method.",self.no);
    [super dealloc];
}
@end

Person.h

//
//  Person.h
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
@class Car;

@interface Person : NSObject{
    Car *_car;
}

#pragma mark - 屬性
#pragma mark 姓名
@property (nonatomic,copy) NSString *name;

#pragma mark - 公共方法
#pragma mark Car屬性的set方法
-(void)setCar:(Car *)car;
#pragma mark  Car屬性的get方法
-(Car *)car;
@end

Person.m

//
//  Person.m
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "Person.h"
#import "Car.h"

@implementation Person

#pragma mark - 公共方法
#pragma mark Car屬性的set方法
-(void)setCar:(Car *)car{
    if (_car!=car) { //首先判斷要賦值的變數和當前成員變數是不是同一個變數
        [_car release]; //釋放之前的物件
        _car=[car retain];//賦值時重新retain
    }
}
#pragma mark  Car屬性的get方法
-(Car *)car{
    return _car;
}

#pragma mark - 覆蓋方法
#pragma mark 重寫dealloc方法
-(void)dealloc{
    NSLog(@"Invoke Person(%@) dealloc method.",self.name);
    [_car release];//在此釋放物件,即使沒有賦值過由於空指標也不會出錯
    [super dealloc];
}
@end

main.m

//
//  main.m
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"

void getCar(Person *p){
    Car *car1=[[Car alloc]init];
    car1.no=@"888888";
    
    p.car=car1;
    
    NSLog(@"retainCount(p)=%lu",[p retainCount]);
    
    Car *car2=[[Car alloc]init];
    car2.no=@"666666";
    
    [car1 release];
    car1=nil;
    
    [car2 release];
    car2=nil;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p=[[Person alloc]init];
        p.name=@"Kenshin";
        
        getCar(p);
        
        [p.car run];
        
        [p release];
        
        p=nil;
        
    }
    return 0;
}

程式執行結果:

setMethod

從執行結果來看建立的三個物件p、car1、car2都被回收了,而且[p.car run]也能順利執行,已經達到了我們的需求。但是這裡需要重點解釋一下setCar方法的實現,setCar方法中為什麼沒有寫成如下形式:

-(void)setCar:(Car *)car{
    _car=car;
}

前面在我們說到屬性的定義時不是都採用的這種方式嗎?

根據前面說到的記憶體釋放原則,getCar方法完全符合,在這個方法中定義的兩個物件car1、car2也都是在這個方法中釋放的,包括main函式中的p物件也是在main函式中定義和釋放的。但是如果發現呼叫完getCar方法之後緊接著呼叫了汽車的run方法,當然這在程式設計和開發過程中應該是再普通不過的設計了。如果setCar寫成“_car=car”的形式,當呼叫完getCar方法後,人員的car屬性被釋放了,此時呼叫run方法是會報錯的(大家自己可以試試)。但是如下的方式卻不會有問題:

-(void)setCar:(Car *)car{
    if (_car!=car) { //首先判斷要賦值的變數和當前成員變數是不是同一個變數
        [_car release]; //釋放之前的物件
        _car=[car retain];//賦值時重新retain
    }
}

因為在這個方法中我們通過[car retain]保證每次屬性賦值的時候物件引用計數器+1,這樣一來呼叫過getCar方法可以保證人員的car屬性不會被釋放,其次為了保證上一次的賦值物件(car1)能夠正常釋放,我們在賦新值之前對原有的值進行release操作。最後在Person的dealloc方法中對_car進行一次release操作(因為setCar中做了一次retain操作)保證_car能正常回收。

屬性引數

像上面這樣編寫setCar方法的情況是比較多的,那麼如何使用@property進行自動實現呢?答案就是使用屬性引數,例如上面car屬性的setter方法,可以通過@property定義如下:

@property (nonatomic,retain) Car *car;

你會發現此刻我們不必手動實現car的getter、setter方法程式仍然沒有記憶體洩露。其實大家也應該都已經看到前面Person的name屬性定義的時候我們同樣加上了(nonatomic,copy)引數,這些引數到底是什麼意思呢?

propertyParameter

@property的引數分為三類,也就是說引數最多可以有三個,中間用逗號分隔,每類引數可以從上表三類引數中人選一個。如果不進行設定或者只設定其中一類引數,程式會使用三類中的各個預設引數,預設引數:(atomic,readwrite,assign)

一般情況下如果在多執行緒開發中一個屬性可能會被兩個及兩個以上的執行緒同時訪問,此時可以考慮atomic屬性,否則建議使用nonatomic,不加鎖,效率較高;readwirte方法會生成getter、setter兩個方法,如果使用readonly則只生成getter方法;關於set方法處理需要特別說明,假設我們定義一個屬性a,這裡列出三種方式的生成程式碼:

assign,用於基本資料型別

-(void)setA:(int)a{
    _a=a;
}

retain,通常用於非字串物件

-(void)setA:(Car *)a{
    if(_a!=a){
        [_a release];
        _a=[a retain];
    }
}

copy,通常用於字串物件、block、NSArray、NSDictionary

-(void)setA:(NSString *)a{
    if(_a!=a){
        [_a release];
        _a=[a copy];
    }
}

備註:本文基於MRC進行介紹,ARC下的情況不同,請參閱其他文章,例如ARC下基本資料型別預設的屬性引數為(atomic,readwrite,assign),物件型別預設的屬性引數為(atomic,readwrite,strong)

自動釋放池

在ObjC中也有一種記憶體自動釋放的機制叫做“自動引用計數”(或“自動釋放池”),與C#、Java不同的是,這只是一種半自動的機制,有些操作還是需要我們手動設定的。自動記憶體釋放使用@autoreleasepool關鍵字宣告一個程式碼塊,如果一個物件在初始化時呼叫了autorelase方法,那麼當程式碼塊執行完之後,在塊中呼叫過autorelease方法的物件都會自動呼叫一次release方法。這樣一來就起到了自動釋放的作用,同時物件的銷燬過程也得到了延遲(統一呼叫release方法)。看下面的程式碼:

Person.h

//
//  Person.h
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Person : NSObject

#pragma mark - 屬性
#pragma mark 姓名
@property (nonatomic,copy) NSString *name;

#pragma mark - 公共方法
#pragma mark 帶引數的建構函式
-(Person *)initWithName:(NSString *)name;
#pragma mark 取得一個物件(靜態方法)
+(Person *)personWithName:(NSString *)name;
@end

Person.m

//
//  Person.m
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "Person.h"

@implementation Person

#pragma mark - 公共方法
#pragma mark 帶引數的建構函式
-(Person *)initWithName:(NSString *)name{
    if(self=[super init]){
        self.name=name;
    }
    return self;
}
#pragma mark 取得一個物件(靜態方法)
+(Person *)personWithName:(NSString *)name{
    Person *p=[[[Person alloc]initWithName:name] autorelease];//注意這裡呼叫了autorelease
    return p;
}

#pragma mark - 覆蓋方法
#pragma mark 重寫dealloc方法
-(void)dealloc{
    NSLog(@"Invoke Person(%@) dealloc method.",self.name);
    [super dealloc];
}

@end

main.m

//
//  main.m
//  MemoryManage
//
//  Created by Kenshin Cui on 14-2-15.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "Person.h"


int main(int argc, const char * argv[]) {

    @autoreleasepool {
        Person *person1=[[Person alloc]init];
        [person1 autorelease];//呼叫了autorelease方法後面就不需要手動呼叫release方法了
        person1.name=@"Kenshin";//由於autorelease是延遲釋放,所以這裡仍然可以使用person1
        
        Person *person2=[[[Person alloc]initWithName:@"Kaoru"] autorelease];//呼叫了autorelease方法
        
        Person *person3=[Person personWithName:@"rosa"];//內部已經呼叫了autorelease,所以不需要手動釋放,這也符合記憶體管理原則,因為這裡並沒有alloc所以不需要release或者autorelease
        
        Person *person4=[Person personWithName:@"jack"];
        [person4 retain];
    }
    /*結果:
     Invoke Person(rosa) dealloc method.
     Invoke Person(Kaoru) dealloc method.
     Invoke Person(Kenshin) dealloc method.
     */
    
    return 0;
}

當上面@autoreleaespool程式碼塊執行完之後,三個物件都得到了釋放,但是person4並沒有釋放,原因很簡單,由於我們手動retain了一次,當自動釋放池釋放後呼叫四個對的release方法,當呼叫完person4的release之後它的引用計數器為1,所有它並沒有釋放(這是一個反例,會造成記憶體洩露);autorelase方法將一個物件的記憶體釋放延遲到了自動釋放池銷燬的時候,因此上面person1,呼叫完autorelase之後它還存在,因此給name賦值不會有任何問題;在ObjC中通常如果一個靜態方法返回一個物件本身的話,在靜態方法中我們需要呼叫autorelease方法,因為按照記憶體釋放原則,在外部使用時不會進行alloc操作也就不需要再呼叫release或者autorelase,所以這個操作需要放到靜態方法內部完成。

對於自動記憶體釋放簡單總結一下:

  1. autorelease方法不會改變物件的引用計數器,只是將這個物件放到自動釋放池中;
  2. 自動釋放池實質是當自動釋放池銷燬後呼叫物件的release方法,不一定就能銷燬物件(例如如果一個物件的引用計數器>1則此時就無法銷燬);
  3. 由於自動釋放池最後統一銷燬物件,因此如果一個操作比較佔用記憶體(物件比較多或者物件佔用資源比較多),最好不要放到自動釋放池或者考慮放到多個自動釋放池;
  4. ObjC中類庫中的靜態方法一般都不需要手動釋放,內部已經呼叫了autorelease方法;

相關文章