內存管理剖析(一)—MRC時代的手動內存管理

內存管理傳送門🦋🦋🦋

內存管理剖析(二)——定時器問題安全

內存管理剖析(三)——iOS程序的內存佈局markdown

內存管理剖析(四)——autorelease原理分析app

內存管理剖析(五)—— weak指針實現原理函數

MRC時代的手動內存管理

iOS中是經過 【引用計數】 來管理OC對象的內存的。oop

  • 一個新建立的OC對象引用計數默認是1,當引用計數減爲0,OC對象就會銷燬,其佔用的內存空間會被系統釋放。
  • 調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1。

內存管理的原則

  • 當調用allocnewcopymutableCopy方法返回了一個對象,再不須要這個對象時,要調用release或者autorelease來釋放它。
  • 想擁有某個對象,就讓它的引用計數+1,不想再擁有某個對象,就讓他的引用計數-1。

能夠經過一下私有函數來查看自動釋放池的狀況佈局

extern void _objc_autoreleasePoolPrint(void);post

下面咱們經過案例來分析一波ui

//********* 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實例,經過打印能夠看到其引用計數爲1atom

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

而且沒有看到person調用dealloc方法,說明在main函數結束以後,person並無被釋放。那麼咱們在使用完person以後給加上一句release,以下spa

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;
}
複製代碼

結果會是這樣對象提早釋放報錯的緣由圖中已經代表,因此,只須要確保[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;
}
複製代碼

CLCatretainCount變化過程可判斷,最後它必定會變成0,不影響CLCat實例對象的釋放,同時,也保證了CLCatretainCount必定是在最後一個CLPerson實例對象釋放以前(意味着CLCat再也不被須要了,能夠被釋放了)被變成0,成功釋放。運行結果也能夠驗證

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的跟蹤,能夠看出,是由於personsetCat方法裏設置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;
}
複製代碼

上述代碼能夠安全走到下圖所示的斷點處咱們繼續執行,就會在setCat方法裏看到以下報錯能夠看到[cat retain]報了EXC_BAD_ACCESS錯誤,說明cat此時已經被釋放了。咱們來分析一下,進入此方法是,cat對象的retainCount1,當再次把cat對象傳setCat方法是,因爲person_cat指向的也是cat,所以[_cat release]實際上就會致使catretainCount-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;的做用:自動聲明gettersetter方法

-(void)setCat:(CLCat *)age ;
-(CLCat *)cat;
複製代碼

@synthesize cat = _cat;的做用:

  • 爲屬性cat生成成員變量_cat
{
   CLCat *_cat;
}
複製代碼
  • 自動生成gettersetter方法的實現
-(void)setCat:(CLCat *)cat {
   if (cat != _cat) {
       [_cat release];
       [cat retain];
       _cat = cat;
   }
}

-(CLCat *)cat {
   return _cat;
}
複製代碼
(2)@property + @synthesize

後來蘋果更進一步,連@synthesize都不須要咱們寫了,一個@property搞定

  • 成員變量的建立
  • gettersetter方法聲明
  • gettersetter方法實現

可是須要注意的是,@property並不幫我作dealloc裏面的處理(對不須要使用的成員變量進行釋放),所以dealloc 方法仍是須要咱們手動去寫的。

好了,上古時期的MRC手動內存管理就介紹到這裏。

內存管理傳送門🦋🦋🦋

內存管理剖析(二)——定時器問題

內存管理剖析(三)——iOS程序的內存佈局

內存管理剖析(四)——autorelease原理分析

內存管理剖析(五)—— weak指針實現原理

相關文章
相關標籤/搜索