記憶體管理系列文章
記憶體管理—MRC時代的手動記憶體管理
記憶體管理—weak的實作原理
記憶體管理——autorelease原理分析
記憶體管理——定時器問題
iOS程式的記憶體布局
MRC時代的手動記憶體管理
iOS中是通過【引用計數】來管理OC對象的記憶體的。
- 一個新建立的OC對象引用計數預設是1,當引用計數減為0,OC對象就會銷毀,其占用的記憶體空間會被系統釋放。
- 調用
會讓OC對象的引用計數+1,調用retain
會讓OC對象的引用計數-1。release
記憶體管理的原則
- 當調用
、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;
}
結果會是這樣
報錯的原因圖中已經表明,是以,隻需要確定
[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;
}
上述代碼可以安全走到下圖所示的斷點處
我們繼續執行,就會在
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的逐漸發展,編譯器自動幫我們生成了很多代碼,讓我的代碼書寫更加簡潔。
編譯器的進化
(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手動記憶體管理就介紹到這裡。