iOS開發系列—Objective-C以內存管理

概述

咱們知道在程序運行過程當中要建立大量的對象,和其餘高級語言相似,在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便可。ui

內存管理原理

咱們都知道在C#、Java中都有GC在自動管理內存,當咱們實例化一個對象以後一般會有一個變量來引用這個對象(變量中存儲對象地址),當這個引用變量再也不使用以後(也就是再也不引用這個對象)此時GC就會自動回收這個對象,簡單的說就是:當一個對象沒有任何變量引用的時候就會被回收。atom

例以下面的C#代碼片斷spa

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方法來銷燬這個對象。指針

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

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、car一、car2都被回收了,並且[p.car run]也能順利運行,已經達到了咱們的需求。可是這裏須要重點解釋一下setCar方法的實現,setCar方法中爲何沒有寫成以下形式:

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

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

根據前面說到的內存釋放原則,getCar方法徹底符合,在這個方法中定義的兩個對象car一、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方法;
相關文章
相關標籤/搜索