內存管理剖析(三)——iOS程序的內存佈局markdown
iOS中是經過 【引用計數】 來管理OC對象的內存的。oop
retain
會讓OC對象的引用計數+1,調用release
會讓OC對象的引用計數-1。alloc
、new
、copy
、mutableCopy
方法返回了一個對象,再不須要這個對象時,要調用release
或者autorelease
來釋放它。能夠經過一下私有函數來查看自動釋放池的狀況佈局
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
實例,經過打印能夠看到其引用計數爲1
,atom
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;
}
複製代碼
從CLCat
的retainCount
變化過程可判斷,最後它必定會變成0
,不影響CLCat
實例對象的釋放,同時,也保證了CLCat
的retainCount
必定是在最後一個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
的跟蹤,能夠看出,是由於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;
}
複製代碼
上述代碼能夠安全走到下圖所示的斷點處咱們繼續執行,就會在setCat
方法裏看到以下報錯能夠看到[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的逐步發展,編譯器自動幫咱們生成了不少代碼,讓個人代碼書寫更加簡潔。
@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; } 複製代碼
@property
+ @synthesize
後來蘋果更進一步,連
@synthesize
都不須要咱們寫了,一個@property
搞定
- 成員變量的建立
getter
、setter
方法聲明getter
、setter
方法實現
可是須要注意的是,@property
並不幫我作dealloc
裏面的處理(對不須要使用的成員變量進行釋放),所以dealloc
方法仍是須要咱們手動去寫的。
好了,上古時期的MRC手動內存管理就介紹到這裏。