天天看點

記憶體管理—MRC時代的手動記憶體管理

記憶體管理系列文章

記憶體管理—MRC時代的手動記憶體管理

記憶體管理—weak的實作原理

記憶體管理——autorelease原理分析

記憶體管理——定時器問題

iOS程式的記憶體布局

MRC時代的手動記憶體管理

iOS中是通過【引用計數】來管理OC對象的記憶體的。

  • 一個新建立的OC對象引用計數預設是1,當引用計數減為0,OC對象就會銷毀,其占用的記憶體空間會被系統釋放。
  • 調用

    retain

    會讓OC對象的引用計數+1,調用

    release

    會讓OC對象的引用計數-1。

記憶體管理的原則

  • 當調用

    alloc

    new

    copy

    mutableCopy

    方法傳回了一個對象,再不需要這個對象時,要調用

    release

    或者

    autorelease

    來釋放它。
  • 想擁有某個對象,就讓它的引用計數+1,不想再擁有某個對象,就讓他的引用計數-1。

可以通過一下私有函數來檢視自動釋放池的情況

extern void _objc_autoreleasePoolPrint(void);

下面我們通過案例來分析一波

//*********  main.m   ************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CLPerson *person = [[CLPerson alloc] init];
        NSLog(@"retainCount ----- %zd",[person retainCount]);
        
    }
    return 0;
}

//*********  CLPerson.h   ************
#import <Foundation/Foundation.h>

@interface CLPerson : NSObject

@end

//*********  CLPerson.m   ************
#import "CLPerson.h"

@implementation CLPerson
-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

我們從在

main.m

裡面通過

alloc

建立一個

CLPerson

執行個體,通過列印可以看到其引用計數為

1

2019-08-27 09:12:45.362995+0800 MRCManager[10928:615055] retainCount ----- 1
           

并且沒有看到

person

調用

dealloc

方法,說明在

main

函數結束之後,

person

并沒有被釋放。那麼我們在使用完

person

之後給加上一句

release

,如下

CLPerson *person = [[CLPerson alloc] init];
NSLog(@"retainCount ----- %zd",[person retainCount]);
[person release];
           

這次的列印結果為

2019-08-27 09:12:45.362995+0800 MRCManager[10928:615055] retainCount ----- 1
2019-08-27 09:12:45.363226+0800 MRCManager[10928:615055] -[CLPerson dealloc]
           

可以看到,

person

走了

dealloc

方法,也就是成功被釋放了,原因就是通過

release

方法,使得自身的引用計數-1,1 - 1 = 0,然後系統便依據該引用計數的值将

person

釋放。OC的記憶體管理其實原理很簡單。

我們知道,Mac指令行項目,

main

函數時從上至下,線性執行,走到

return 0

,整個程式就退出結束了,是以像我們案例中的情景,很容易判斷該何時對對象進行

release

操作。

在我們常用的iOS項目裡面,由于加入了RunLoop,程式會在

main

函數裡面一直循環,直到崩潰,或者手動關閉app。是以當一個對象被建立了之後,它什麼時間會被使用,是很難确定的。如果不調用

release

,那麼可以保證任何時間使用對象都是安全的,但是帶來的問題便是,當對象不再使用之後,它便會一直存留在記憶體裡面,不被釋放,這就是我們常說的 【記憶體洩漏】。

為此,蘋果為我們提供了

autorelease

,在每次建立對象的時候調用

CLPerson *person = [[[CLPerson alloc] init] autorelease];
           

這樣,無需我們手動調用

[person release];

,系統會在某個合适的時間,自動對

person

進行

release

操作,這個“合适的時間”暫且了解成

@autoreleasepool {}

大括号結束的時候。

#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CLPerson *person = [[[CLPerson alloc] init] autorelease];
        CLPerson *person2 = [[[CLPerson alloc] init] autorelease];
        NSLog(@" @autoreleasepool即将結束");
    }
    NSLog(@" @autoreleasepool已經結束");
    return 0;
}

//********************** 列印資訊  *******************
2019-08-27 09:40:29.388495+0800 MRCManager[10970:625654]  @autoreleasepool即将結束
2019-08-27 09:40:29.388727+0800 MRCManager[10970:625654] -[CLPerson dealloc]
2019-08-27 09:40:29.388736+0800 MRCManager[10970:625654] -[CLPerson dealloc]
2019-08-27 09:40:29.388756+0800 MRCManager[10970:625654]  @autoreleasepool已經結束
Program ended with exit code: 0
           

上述案例僅僅針對一個對象這種簡單情況來讨論。在iOS實際項目中,往往對象與對象之間是有很多關聯的。我們繼續給上面的代碼添加一個

CLCat

對象

#import <Foundation/Foundation.h>

@interface CLCat : NSObject
-(void)run;
@end

***************************************************

#import "CLCat.h"

@implementation CLCat
-(void)run {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

如果

CLPerson

想擁有

CLCat

,則需要對其作如下調整

#import <Foundation/Foundation.h>
#import "CLCat.h"
@interface CLPerson : NSObject
{
    CLCat *_cat;
}
//擁有貓
-(void)setCat:(CLCat *)cat;
//擷取貓
-(CLCat *)cat;
@end

***************************************************


#import "CLPerson.h"
@implementation CLPerson

-(void)setCat:(CLCat *)cat {
    _cat = cat;
}

-(CLCat *)cat {
    return _cat;
}

-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}

@end
           

這樣就可以通過

setCat

方法,把

CLCat

對象設定到

CLPerson

_cat

成員變量上(擁有);通過

cat

方法拿到成員變量

_cat

(擷取)。也就是說

CLPerson

對象可以通過

_cat

指針操縱一個

CLCat

對象了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLCat *cat = [[CLCat alloc] init];
        CLPerson *person = [[CLPerson alloc] init];
        [person setCat:cat];
        [person.cat run];
        
        [cat release];
        [person release];
        
    }
    return 0;
}

***************** 列印結果  ****************
2019-08-27 10:22:11.086033+0800 MRCManager[11054:643966] -[CLCat run]
2019-08-27 10:22:11.086283+0800 MRCManager[11054:643966] -[CLCat dealloc]
2019-08-27 10:22:11.086294+0800 MRCManager[11054:643966] -[CLPerson dealloc]
Program ended with exit code: 0

           

從列印結果看上去,行得通。但是注意

[person.cat run];

是在

[cat release];

之前。如果是下面這樣

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLCat *cat = [[CLCat alloc] init];
        CLPerson *person = [[CLPerson alloc] init];
        [person setCat:cat];

        [cat release];
        [person.cat run];
        [person release];
        
    }
    return 0;
}
           

結果會是這樣

記憶體管理—MRC時代的手動記憶體管理

報錯的原因圖中已經表明,是以,隻需要確定

[cat release];

[person.cat run];

之後被調用即可。但是實際開發中,

[person.cat run];

何時被調用,是不确定的,并且次數也不确定,也就是說,我們無法确定

[person.cat run];

會于何時在何處被最後一次調用,是以就無法确定

[cat release];

到底該寫在哪裡。為了保證不出現

EXC_BAD_ACCESS

報錯,可以幹脆不寫

[cat release];

,但這就帶來了記憶體洩漏問題。

問題的本質就是

CLPerson

并沒有真正擁有

CLCat

。所謂“真正”擁有,就是指隻要

CLPerson

還在,那麼

CLCat

就不應該被釋放。為此,我們就可以這麼做

#import "CLPerson.h"

@implementation CLPerson


-(void)setCat:(CLCat *)cat {
    [_cat retain];//将引用計數+1
    _cat = cat;
}

-(CLCat *)cat {
    
    return _cat;
}

-(void)dealloc {
    //自己即将被釋放,不再需要cat了
    [_cat release];
    _cat = nil;
    
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

這樣即使有多個CLPerson對象在使用CLCat,也不會出問題了

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //RC+1
        CLCat *cat = [[CLCat alloc] init];
        
        CLPerson *person = [[CLPerson alloc] init];
        CLPerson *person2 = [[CLPerson alloc] init];
        
        //内部 RC+1(setCat) --> RC-1(dealloc)
        [person setCat:cat];
        
        //内部 RC+1(setCat) --> RC-1(dealloc)
        [person2 setCat:cat];
        
        //RC-1,為了對應上面的[CLCat alloc]
        [cat release];
        
        [person.cat run];
        [person2.cat run];
        [person release];
        [person2 release];
        
    }
    return 0;
}
           

CLCat

retainCount

變化過程可判斷,最後它一定會變成

,不影響CLCat執行個體對象的釋放,同時,也保證了

CLCat

retainCount

一定是在最後一個

CLPerson

執行個體對象釋放之前(意味着

CLCat

不再被需要了,可以被釋放了)被變成

,成功釋放。運作結果也可以驗證

2019-08-27 10:55:41.799859+0800 MRCManager[11120:657618] -[CLCat run]
2019-08-27 10:55:41.800096+0800 MRCManager[11120:657618] -[CLCat run]
2019-08-27 10:55:41.800111+0800 MRCManager[11120:657618] -[CLPerson dealloc]
2019-08-27 10:55:41.800117+0800 MRCManager[11120:657618] -[CLCat dealloc]
2019-08-27 10:55:41.800123+0800 MRCManager[11120:657618] -[CLPerson dealloc]
Program ended with exit code: 0
           

總的來說,手動管理記憶體的原則就是:保持執行個體對象的retainCount平衡,有+1,就有對應的-1,保證最終會變成0,執行個體對象可以被成功釋放。

到目前為止,上面對setCat方法的處理,仍然不夠完善。接下來我們繼續讨論一下相關細節。 首先看下面的場景

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //person_rc + 1 = 1
        CLPerson *person = [[CLPerson alloc] init];
        
        //cat1_rc + 1 = 1
        CLCat *cat1 = [[CLCat alloc] init];
        
        //cat2_rc + 1 = 1
        CLCat *cat2 = [[CLCat alloc] init];
        
        //cat1_rc + 1 = 2
        [person setCat:cat1];
        
        //cat2_rc + 1 = 2
        [person setCat:cat2];
        
        //cat1_rc - 1 = 1
        [cat1 release];
        
        //cat2_rc - 1 = 1
        [cat2 release];
        
        //cat2_rc - 1 = 0
        //person_rc - 1 = 0
        [person release];
    }
    return 0;
}

**************** 列印結果 ****************
2019-08-27 11:23:20.185060+0800 MRCManager[11164:667802] -[CLCat dealloc]
2019-08-27 11:23:20.185318+0800 MRCManager[11164:667802] -[CLPerson dealloc]
Program ended with exit code: 0
           

列印結果顯示,

cat1

産生了記憶體洩漏。根據代碼注釋裡對各對象

retainCount

的跟蹤,可以看出,是因為

person

setCat

方法裡設定

cat2

為成員變量的時候,造成了

cat1

最終少進行了一次

release

,進而導緻被洩漏。是以需要對

setCat

方法調整如下即可

-(void)setCat:(CLCat *)cat {
    [_cat release];//将之前_cat所指向的對象引用計數-1,不在持有
    [cat retain];//将傳進來的對象引用計數+1,保證持有
    _cat = cat;
}
           

上面我們解決了用不同

CLCat

對象進行

setCat

設定所産生的問題。接下來我們還需要看看用同一個

CLCat

對象進行

setCat

設定是否安全,代碼如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //person_rc + 1 = 1
        CLPerson *person = [[CLPerson alloc] init];
        
        //cat_rc + 1 = 1
        CLCat *cat = [[CLCat alloc] init];
        
        //cat_rc + 1 = 2
        [person setCat:cat];
        
        //cat_rc - 1 = 1
        [cat release];
        
        
        [person setCat:cat];
        
        
        [person release];
        
    }
    return 0;
}
           

上述代碼可以安全走到下圖所示的斷點處

記憶體管理—MRC時代的手動記憶體管理

我們繼續執行,就會在

setCat

方法裡看到如下報錯

記憶體管理—MRC時代的手動記憶體管理

可以看到

[cat retain]

報了

EXC_BAD_ACCESS

錯誤,說明

cat

此時已經被釋放了。我們來分析一下,進入此方法是,

cat

對象的

retainCount

1

,當再次把cat對象傳

setCat

方法是,由于

person

_cat

指向的也是

cat

,是以

[_cat release]

實際上就會導緻

cat

retainCount-1

,也就是

1-1=0

,是以

cat

被系統釋放。是以後面的代碼再次使用

[cat retain]

便造成了野指針錯誤。是以解決辦法是需要對傳如的

cat

對象判斷一下,如果等于目前

_cat

,就不需要執行引用計數的操作了,修改代碼如下

-(void)setCat:(CLCat *)cat {
    if (cat != _cat) {//隻有當傳進來的對象跟目前對象不同,才需要進行後面的操作
        [_cat release];//将之前_cat所指向的對象引用計數-1,不在持有
        [cat retain];//将傳進來的對象引用計數+1,保證持有
        _cat = cat;
    }
    
}
           

這樣,對于

setCat

方法的處理放啊就完善了。下面我貼一份完整的代碼供參考

******************* CLPerson.h ******************
#import <Foundation/Foundation.h>
#import "CLCat.h"

@interface CLPerson : NSObject
{
    CLCat *_cat;
    int _age;
}
//擁有貓
-(void)setCat:(CLCat *)cat;
//擷取貓
-(CLCat *)cat;
@end

******************* CLPerson.m ******************


#import "CLPerson.h"

@implementation CLPerson
-(void)setCat:(CLCat *)cat {
    if (cat != _cat) {//隻有當傳進來的對象跟目前對象不同,才需要進行後面的操作
        [_cat release];//将之前_cat所指向的對象引用計數-1,不在持有
        [cat retain];//将傳進來的對象引用計數+1,保證持有
        _cat = cat;
    }
    
}

-(CLCat *)cat {
    
    return _cat;
}


-(void)dealloc {
    //自己即将被釋放,不再需要cat了
//    [_cat release];
//    _cat = nil;
    self.cat = nil;//相當于上面兩句的效果
    
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end


*****************CLCat.h ***************
#import <Foundation/Foundation.h>
@interface CLCat : NSObject
-(void)run;
@end

*****************CLCat.m ***************
#import "CLCat.h"
@implementation CLCat
-(void)run {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end
           

對于非OC對象類型的成員變量,就不需要考慮記憶體管理的問題了,例如

@interface CLPerson : NSObject
{
    int _age;
}
-(void)setAge:(int)age ;
-(int)age;
@end

@implementation CLPerson
-(void)setAge:(int)age {
    _age = age;
}

-(int)age {
    return _age;
}
@end
           

上面過程中,包含了非ARC時代進行手動記憶體管理的全部核心點。後來,随着Xcode的逐漸發展,編譯器自動幫我們生成了很多代碼,讓我的代碼書寫更加簡潔。

編譯器的進化

(1)

@property

+

@synthesize

@property (nonatomic, retain) cat;

的作用:自動聲明

getter

setter

方法
-(void)setCat:(CLCat *)age ;
-(CLCat *)cat;
           

@synthesize cat = _cat;

的作用:
  • 為屬性

    cat

    生成成員變量

    _cat

{
   CLCat *_cat;
}
           
  • 自動生成

    getter

    setter

    方法的實作
-(void)setCat:(CLCat *)cat {
   if (cat != _cat) {
       [_cat release];
       [cat retain];
       _cat = cat;
   }
}

-(CLCat *)cat {
   return _cat;
}
           

(2)

@property

+

@synthesize

後來蘋果更進一步,連

@synthesize

都不需要我們寫了,一個

@property

搞定
  • 成員變量的建立
  • getter

    setter

    方法聲明
  • getter

    setter

    方法實作

但是需要注意的是,

@property

并不幫我做

dealloc

裡面的處理(對不需要使用的成員變量進行釋放),是以

dealloc

方法還是需要我們手動去寫的。

其實進入了ARC時代,iOS的記憶體管理底層機制并沒有變化,隻不過很多記憶體管理的代碼ARC(編譯器)自動替我們寫好而已。好了,上古時期的MRC手動記憶體管理就介紹到這裡。