上一篇博客iOS/OS X內(nèi)存管理(一):基本概念與原理主要講了iOS/OSX 內(nèi)存管理中引用計數(shù)和內(nèi)存管理規(guī)則计济,以及引入ARC新的內(nèi)存管理機制之后如何選擇ownership qualifiers(__strong
粱年、__weak
成榜、__unsafe_unretained
和__autoreleasing
)來管理內(nèi)存逆皮。這篇我們主要關(guān)注在實際開發(fā)中會遇到哪些內(nèi)存管理問題西设,以及如何使用工具來調(diào)試和解決柳譬。
在往下看之前請下載實例MemoryProblems群扶,我們將以這個工程展開如何檢查和解決內(nèi)存問題透葛。
懸掛指針問題
懸掛指針(Dangling Pointer)就是當指針指向的對象已經(jīng)釋放或回收后笨使,但沒有對指針做任何修改(一般來說,將它指向空指針)僚害,而是仍然指向原來已經(jīng)回收的地址硫椰。如果指針指向的對象已經(jīng)釋放,但仍然使用,那么就會導致程序crash靶草。
當你運行MemoryProblems后蹄胰,點擊懸掛指針那個選項,就會出現(xiàn)EXC_BAD_ACCESS
崩潰信息
我們看看這個NameListViewController
是做什么的奕翔?它繼承UITableViewController
裕寨,主要顯示多個名字的信息。它的實現(xiàn)文件如下:
static NSString *const kNameCellIdentifier = @"NameCell";
@interface NameListViewController ()
#pragma mark - Model
@property (strong, nonatomic) NSArray *nameList;
#pragma mark - Data source
@property (assign, nonatomic) ArrayDataSource *dataSource;
@end
@implementation NameListViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self.dataSource;
}
#pragma mark - Lazy initialization
- (NSArray *)nameList
{
if (!_nameList) {
_nameList = @[@"Sam", @"Mike", @"John", @"Paul", @"Jason"];
}
return _nameList;
}
- (ArrayDataSource *)dataSource
{
if (!_dataSource) {
_dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
cellIdentifier:kNameCellIdentifier
tableViewStyle:UITableViewCellStyleDefault
configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
cell.textLabel.text = item;
}];
}
return _dataSource;
}
@end
要想通過tableView顯示數(shù)據(jù)派继,首先要實現(xiàn)UITableViewDataSource
這個協(xié)議宾袜,為了瘦身controller和復用data source,我將它分離到一個類ArrayDataSource
來實現(xiàn)UITableViewDataSource
這個協(xié)議互艾。然后在viewDidLoad
方法里面將dataSource
賦值給tableView.dataSource
试和。
解釋完NameListViewController
的職責后,接下來我們需要思考出現(xiàn)EXC_BAD_ACCESS
錯誤的原因和位置信息纫普。
一般來說阅悍,出現(xiàn)EXC_BAD_ACCESS
錯誤的原因都是懸掛指針導致的,但具體是哪個指針是懸掛指針還不確定昨稼,因為控制臺并沒有給出具體crash信息节视。
啟用NSZombieEnabled
要想得到更多的crash信息,你需要啟動NSZombieEnabled
假栓。具體步驟如下:
-
選中
Edit Scheme
寻行,并點擊
-
Run -> Diagnostics -> Enable Zombie Objects
設(shè)置完之后,再次運行和點擊懸掛指針匾荆,雖然會再次crash拌蜘,但這次控制臺打印了以下有用信息:
信息message sent to deallocated instance 0x7fe19b081760
大意是向一個已釋放對象發(fā)送信息,也就是已釋放對象還調(diào)用某個方法⊙览觯現(xiàn)在我們大概知道什么原因?qū)е鲁绦驎rash简卧,但是具體哪個對象被釋放還仍然使用呢?
點擊上面紅色框的Continue program execution
按鈕繼續(xù)運行烤芦,截圖如下:
留意上面的兩個紅色框举娩,它們兩個地址是一樣,而且ArrayDataSource
前面有個_NSZombie_
修飾符构罗,說明dataSource
對象被釋放還仍然使用铜涉。
再進一步看dataSource
聲明屬性的修飾符是assign
#pragma mark - Data source
@property (assign, nonatomic) ArrayDataSource *dataSource;
而assign
對應就是__unsafe_unretained
,它跟__weak相似遂唧,被它修飾的變量都不持有對象的所有權(quán)芙代,但當變量指向的對象的RC為0時,變量并不設(shè)置為nil盖彭,而是繼續(xù)保存對象的地址纹烹。
因此事甜,在viewDidLoad
方法中
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self.dataSource;
/* 由于dataSource是被assign修飾,self.dataSource賦值后滔韵,它對象的對象就馬上釋放逻谦,
* 而self.tableView.dataSource也不是strong,而是weak陪蜻,此時仍然使用邦马,所有會導致程序crash
*/
}
分析完原因和定位錯誤代碼后,至于如何修改宴卖,我想大家都心知肚明了滋将,如果還不知道的話,留言給我症昏。
內(nèi)存泄露問題
還記得上一篇iOS/OS X內(nèi)存管理(一):基本概念與原理的引用循環(huán)例子嗎随闽?它會導致內(nèi)存泄露,上次只是文字描述肝谭,不怎么直觀掘宪,這次我們嘗試使用Instruments里面的子工具Leaks來檢查內(nèi)存泄露。
靜態(tài)分析
一般來說攘烛,在程序未運行之前我們可以先通過Clang Static Analyzer(靜態(tài)分析)來檢查代碼是否存在bug魏滚。比如,內(nèi)存泄露坟漱、文件資源泄露或訪問空指針的數(shù)據(jù)等鼠次。下面有個靜態(tài)分析的例子來講述如何啟用靜態(tài)分析以及靜態(tài)分析能夠查找哪些bugs。
啟動程序后芋齿,點擊靜態(tài)分析腥寇,馬上就出現(xiàn)crash
此時,即使啟用NSZombieEnabled觅捆,控制臺也不能打印出更多有關(guān)bug的信息赦役,具體原因是什么,等下會解釋惠拭。
打開StaticAnalysisViewController
扩劝,里面引用Facebook Infer工具的代碼例子庸论,包含個人日常開發(fā)中會出現(xiàn)的bugs:
@implementation StaticAnalysisViewController
#pragma mark - Lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self memoryLeakBug];
[self resoureLeakBug];
[self parameterNotNullCheckedBlockBug:nil];
[self npeInArrayLiteralBug];
[self prematureNilTerminationArgumentBug];
}
#pragma mark - Test methods from facebook infer iOS Hello examples
- (void)memoryLeakBug
{
CGPathRef shadowPath = CGPathCreateWithRect(self.inputView.bounds, NULL);
}
- (void)resoureLeakBug
{
FILE *fp;
fp=fopen("info.plist", "r");
}
-(void) parameterNotNullCheckedBlockBug:(void (^)())callback {
callback();
}
-(NSArray*) npeInArrayLiteralBug {
NSString *str = nil;
return @[@"horse", str, @"dolphin"];
}
-(NSArray*) prematureNilTerminationArgumentBug {
NSString *str = nil;
return [NSArray arrayWithObjects: @"horse", str, @"dolphin", nil];
}
@end
下面我們通過靜態(tài)分析來檢查代碼是否存在bugs职辅。有兩個方式:
- 手動靜態(tài)分析:每次都是通過點擊菜單欄的Product -> Analyze或快捷鍵shift + command + b
- 自動靜態(tài)分析:在Build Settings啟用Analyze During 'Build',每次編譯時都會自動靜態(tài)分析
靜態(tài)分析結(jié)果如下:
通過靜態(tài)分析結(jié)果聂示,我們來分析一下為什么NSZombieEnabled不能定位EXC_BAD_ACCESS
的錯誤代碼位置域携。由于callback傳入進來的是null指針,而NSZombieEnabled只能針對某個已經(jīng)釋放對象的地址鱼喉,所以啟動NSZombieEnabled是不能定位的秀鞭,不過可以通過靜態(tài)分析可得知趋观。
啟動Instruments
有時使用靜態(tài)分析能夠檢查出一些內(nèi)存泄露問題,但是有時只有運行時使用Instruments才能檢查到锋边,啟動Instruments步驟如下:
-
點擊Xcode的菜單欄的 Product -> Profile 啟動Instruments
-
此時皱坛,出現(xiàn)Instruments的工具集,選中Leaks子工具點擊
-
打開Leaks工具之后豆巨,點擊紅色圓點按鈕啟動
Leaks
工具剩辟,在Leaks工具啟動同時,模擬器或真機也跟著啟動
啟動Leaks工具后往扔,它會在程序運行時記錄內(nèi)存分配信息和檢查是否發(fā)生內(nèi)存泄露贩猎。當你點擊引用循環(huán)進去那個頁面后,再返回到主頁萍膛,就會發(fā)生內(nèi)存泄露
如果發(fā)生內(nèi)存泄露吭服,我們怎么定位哪里發(fā)生和為什么會發(fā)生內(nèi)存泄露?
定位內(nèi)存泄露
借助Leaks能很快定位內(nèi)存泄露問題蝗罗,在這個例子中艇棕,步驟如下:
- 首先點擊Leak Checks時間條那個紅色叉
-
然后雙擊某行內(nèi)存泄露調(diào)用棧,會直接跳到內(nèi)存泄露代碼位置
分析內(nèi)存泄露原因
上面已經(jīng)定位好內(nèi)存泄露代碼的位置串塑,至于原因是什么欠肾?可以查看上一篇的iOS/OS X內(nèi)存管理(一):基本概念與原理的循環(huán)引用例子,那里已經(jīng)有詳細的解釋拟赊。
難以檢測Block引用循環(huán)
大多數(shù)的內(nèi)存問題都可以通過靜態(tài)分析和Instrument Leak工具檢測出來刺桃,但是有種block引用循環(huán)是難以檢測的,看我們這個Block內(nèi)存泄露例子吸祟,跟上面的懸掛指針例子差不多瑟慈,只是在configureCellBlock
里面調(diào)用一個方法configureCell
。
- (ArrayDataSource *)dataSource
{
if (!_dataSource) {
_dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
cellIdentifier:kNameCellIdentifier
tableViewStyle:UITableViewCellStyleDefault
configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
cell.textLabel.text = item;
[self configureCell];
}];
}
return _dataSource;
}
- (void)configureCell
{
NSLog(@"Just for test");
}
- (void)dealloc
{
NSLog(@"release BlockLeakViewController");
}
我們首先用靜態(tài)分析來看看能不能檢查出內(nèi)存泄露:
結(jié)果是沒有任何內(nèi)存泄露的提示屋匕,我們再用Instrument Leak工具在運行時看看能不能檢查出:
結(jié)果跟使用靜態(tài)分析一樣葛碧,還是沒有任何內(nèi)存泄露信息的提示。
那么我們怎么知道這個BlockLeakViewController
發(fā)生了內(nèi)存泄露呢过吻?還是根據(jù)iOS/OS X內(nèi)存管理機制的一個基本原理:當某個對象的引用計數(shù)為0時进泼,它就會自動調(diào)用- (void)dealloc
方法。
在這個例子中纤虽,如果BlockLeakViewController
被navigationController pop出去后乳绕,沒有調(diào)用dealloc
方法,說明它的某個屬性對象仍然被持有逼纸,未被釋放洋措。而我在dealloc
方法打印release BlockLeakViewController信息:
- (void)dealloc
{
NSLog(@"release BlockLeakViewController");
}
在我點擊返回按鈕后,其并沒有打印出來杰刽,因此這個BlockLeakViewController
存在內(nèi)存泄露問題的菠发。至于如何解決block內(nèi)存泄露這個問題王滤,很多基本功扎實的同學都知道如何解決,不懂的話滓鸠,自己查資料解決吧雁乡!
總結(jié)
一般來說,在創(chuàng)建工程的時候糜俗,我都會在Build Settings啟用Analyze During 'Build'蔗怠,每次編譯時都會自動靜態(tài)分析。這樣的話吩跋,寫完一小段代碼之后寞射,就馬上知道是否存在內(nèi)存泄露或其他bug問題,并且可以修bugs锌钮。而在運行過程中桥温,如果出現(xiàn)EXC_BAD_ACCESS
,啟用NSZombieEnabled梁丘,看出現(xiàn)異常后侵浸,控制臺能否打印出更多的提示信息。如果想在運行時查看是否存在內(nèi)存泄露氛谜,使用Instrument Leak工具掏觉。但是有些內(nèi)存泄露是很難檢查出來,有時只有通過手動覆蓋dealloc
方法值漫,看它最終有沒有調(diào)用澳腹。