歡迎訪問(wèn)我的博客原文
內(nèi)存泄漏指的是程序中已動(dòng)態(tài)分配的堆內(nèi)存(程序員自己管理的空間)由于某些原因未能釋放或無(wú)法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi)夭委,導(dǎo)致程序運(yùn)行速度變慢甚至系統(tǒng)崩潰。
在 iOS 開發(fā)中會(huì)遇到的內(nèi)存泄漏場(chǎng)景可以分為幾類:
循環(huán)引用
當(dāng)對(duì)象 A 強(qiáng)引用對(duì)象 B演侯,而對(duì)象 B 又強(qiáng)引用對(duì)象 A憔四,或者多個(gè)對(duì)象互相強(qiáng)引用形成一個(gè)閉環(huán),這就是循環(huán)引用措拇。
Block
Block 會(huì)對(duì)其內(nèi)部的對(duì)象強(qiáng)引用我纪,因此使用的時(shí)候需要確保不會(huì)形成循環(huán)引用。
舉個(gè)例子,看下面這段代碼:
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", self.name);
});
};
self.block();
block
是 self
的屬性浅悉,因此 self
強(qiáng)引用了 block
趟据,而 block
內(nèi)部又調(diào)用了 self
,因此 block
也強(qiáng)引用了 self
术健。要解決這個(gè)循環(huán)引用的問(wèn)題汹碱,有兩種思路。
使用 Weak-Strong Dance
先用 __weak
將 self
置為弱引用荞估,打破“循環(huán)”關(guān)系咳促,但是 weakSelf
在 block
中可能被提前釋放,因此還需要在 block
內(nèi)部勘伺,用 __strong
對(duì) weakSelf
進(jìn)行強(qiáng)引用跪腹,這樣可以確保 strongSelf
在 block
結(jié)束后才會(huì)被釋放。
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf.name);
});
};
self.block();
斷開持有關(guān)系
使用 __block
關(guān)鍵字設(shè)置一個(gè)指針 vc
指向 self
飞醉,重新形成一個(gè) self → block → vc → self
的循環(huán)持有鏈冲茸。在調(diào)用結(jié)束后,將 vc
置為 nil
缅帘,就能斷開循環(huán)持有鏈轴术,從而令 self
正常釋放。
__block UIViewController *vc = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
vc = nil;
});
};
self.block();
這里還要補(bǔ)充一個(gè)問(wèn)題股毫,為什么要用 __block
修飾 vc
膳音?
首先,block
本身不允許修改外部變量的值铃诬。但被 __block
修飾的變量會(huì)被存在了一個(gè)棧的結(jié)構(gòu)體當(dāng)中祭陷,成為結(jié)構(gòu)體指針。當(dāng)這個(gè)對(duì)象被 block
持有趣席,就將“外部變量”在棧中的內(nèi)存地址放到堆中兵志,進(jìn)而可以在 block
內(nèi)部修改外部變量的值。
還有一種方式可以斷開持有關(guān)系宣肚。就是將 self
以傳參的形式傳入 block
內(nèi)部想罕,這樣 self
就不會(huì)被 block
持用,也就不會(huì)形成循環(huán)持有鏈霉涨。
self.block = ^(UIViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
});
};
self.block(self);
NSTimer
我們知道 NSTimer
對(duì)象是采用 target-action
方式創(chuàng)建的按价,通常 target
就是類本身赡艰,而我們?yōu)榱朔奖阌殖0?NSTimer
聲明為屬性属韧,像這樣:
// 第一種創(chuàng)建方式,timer 默認(rèn)添加進(jìn) runloop
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
// 第二種創(chuàng)建方式啥么,需要手動(dòng)將 timer 添加進(jìn) runloop
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
這就形成了 self → timer → self(target)
的循環(huán)持有鏈往枷。只要 self
不釋放框产,dealloc
就不會(huì)執(zhí)行凄杯,timer
就無(wú)法在 dealloc
中銷毀,self
始終被強(qiáng)引用秉宿,永遠(yuǎn)得不到釋放戒突,循環(huán)矛盾,最終造成內(nèi)存泄漏描睦。
那么如果只把 timer
作為局部變量膊存,而不是屬性呢?
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self
同樣釋放不了忱叭。
因?yàn)樵诩尤?runloop 的操作中膝舅,timer
被強(qiáng)引用,這就形成了一條 runloop → timer → self(target)
的持有鏈窑多。而 timer
作為局部變量仍稀,無(wú)法執(zhí)行 invalidate
,所以在 timer
被銷毀之前埂息,self
也不會(huì)被釋放技潘。
所以只要申請(qǐng)了 timer
,加入了 runloop千康,并且 target
是 self
享幽,就算不是循環(huán)引用,也會(huì)造成內(nèi)存泄漏拾弃,因?yàn)?self
沒(méi)有釋放的時(shí)機(jī)值桩。
解決這個(gè)問(wèn)題有好幾種方式,開發(fā)者可以自行選擇豪椿。
在合適的時(shí)機(jī)銷毀 NSTimer
當(dāng) NSTimer
初始化之后奔坟,加入 runloop 會(huì)導(dǎo)致被當(dāng)前的頁(yè)面強(qiáng)引用,因此不會(huì)執(zhí)行 dealloc
搭盾。所以需要在合適的時(shí)機(jī)銷毀 _timer
咳秉,斷開 _timer
、runloop 和當(dāng)前頁(yè)面之間的強(qiáng)引用關(guān)系鸯隅。
[_timer invalidate];
_timer = nil;
ViewController
中的時(shí)機(jī)可以選擇 didMoveToParentViewController
澜建、viewDidDisappear
,View
中可以選擇 removeFromSuperview
等蝌以,但這種方案并一定是正確可行的炕舵。
比如在注冊(cè)頁(yè)面中加了一個(gè)倒計(jì)時(shí),如果在 viewDidDisappear
中銷毀了 _timer
跟畅,當(dāng)用戶點(diǎn)擊跳轉(zhuǎn)到用戶協(xié)議頁(yè)面時(shí)咽筋,倒計(jì)時(shí)就會(huì)被提前銷毀,這是不合邏輯的碍彭。因此需要結(jié)合具體業(yè)務(wù)的需求場(chǎng)景來(lái)考慮晤硕。
使用 GCD 的定時(shí)器
GCD 不基于 runloop,可以用 GCD 的計(jì)時(shí)器代替 NSTimer 實(shí)現(xiàn)計(jì)時(shí)任務(wù)庇忌。但需要注意的是舞箍,GCD 內(nèi)部 block 中的循環(huán)引用問(wèn)題還是需要解決的。
__weak typeof(self) weakSelf = self;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
[weakSelf timeFire];
});
// 開啟計(jì)時(shí)器
dispatch_resume(_timer);
// 銷毀計(jì)時(shí)器
// dispatch_source_cancel(_timer);
借助中介者銷毀
中介者指的是用別的對(duì)象代替 target
里的 self
皆疹,中介者綁定 selector
之后疏橄,再在 dealloc
中釋放 timer
。
這里介紹兩種中介者略就,一種是 NSObject 對(duì)象捎迫,一種是 NSProxy 的子類。它們的存在是為了斷開對(duì) self
的強(qiáng)引用表牢,使之可以被釋放窄绒。
以一個(gè) NSObject 對(duì)象作為中介者
新建一個(gè) NSObject 對(duì)象 _target
,為它動(dòng)態(tài)添加一個(gè)方法崔兴,方法的地址指向 self
方法列表中的 timeFire
的 IMP彰导。這樣 _target
與 self
之間沒(méi)有直接的引用關(guān)系,又能引用 self
里的方法敲茄,就不會(huì)出現(xiàn)循環(huán)引用位谋。
_target = [NSObject new];
class_addMethod([_target class], @selector(timeFire), class_getMethodImplementation([self class], @selector(timeFire)), "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:_target selector:@selector(timeFire) userInfo:nil repeats:YES];
以 NSProxy 的子類作為中介者
創(chuàng)建一個(gè)繼承自 NSProxy
的子類 WeakProxy
,將 timer
的 target
設(shè)置為 WeakProxy
實(shí)例堰燎,利用完整的消息轉(zhuǎn)發(fā)機(jī)制實(shí)現(xiàn)執(zhí)行 self
中的計(jì)時(shí)方法掏父,解決循環(huán)引用。
// WeakProxy.h
@property (nonatomic, weak, readonly) id weakTarget;
+ (instancetype)proxyWithTarget:(id)target;
- (instancetype)initWithTarget:(id)target;
// WeakProxy.m
@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target {
return [[self alloc] initWithTarget:target];
}
- (instancetype)initWithTarget:(id)target {
_weakTarget = target;
return self;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = [invocation selector];
if ([self.weakTarget respondsToSelector:sel]) {
[invocation invokeWithTarget:self.weakTarget];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.weakTarget methodSignatureForSelector:sel];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self.weakTarget respondsToSelector:aSelector];
}
@end
然后這樣創(chuàng)建 timer
:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[WeakProxy proxyWithTarget:self] selector:@selector(timeFire) userInfo:nil repeats:YES];
這時(shí)候的循環(huán)持有鏈?zhǔn)沁@樣的:
由于 WeakProxy
與 self
之間是弱引用關(guān)系秆剪,self
最終是可以被銷毀的赊淑。
帶 block 的 timer
iOS 10 之后,Apple 提供了一種 block 的方式來(lái)解決循環(huán)引用的問(wèn)題仅讽。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
為了兼容 iOS 10 之前的方法膏燃,可以寫成 NSTimer 分類的形式,將 block 作為 SEL 傳入初始化方法中何什,統(tǒng)一以 block 的形式處理回調(diào)组哩。
// NSTimer+WeakTimer.m
#import "NSTimer+WeakTimer.h"
@implementation NSTimer (WeakTimer)
+ (NSTimer *)ht_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void(^)(void))block {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ht_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)ht_blockInvoke:(NSTimer *)timer {
void (^block)(void) = timer.userInfo;
if(block) {
block();
}
}
@end
然后在需要的類中創(chuàng)建 timer
。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer ht_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
[weakSelf timeFire];
}];
委托模式
委托模式处渣,是對(duì)象之間通信的一種設(shè)計(jì)模式伶贰。該模式的主旨是:定義一套接口,某對(duì)象若想接受另一個(gè)對(duì)象的委托罐栈,則需遵從此接口黍衙,以便成為其“委托對(duì)象”。
UITableView 的 delegate
我們常用的 tableView
與 ViewController
就是委托方和代理方的關(guān)系荠诬。
需要在控制器中加入列表時(shí)琅翻,通常我們會(huì)將 tableView
設(shè)為 ViewController
中 view
的子視圖位仁,UIViewController
的源碼是這樣定義 view
的:
@property(null_resettable, nonatomic, strong) UIView *view;
因此 ViewController
強(qiáng)引用了 tableView
。而 tableView
又要委托 ViewController
幫它實(shí)現(xiàn)幾個(gè)代理方法和數(shù)據(jù)源方法方椎。如果此時(shí) dataSource
和 delegate
屬性用 strong
來(lái)修飾聂抢,就會(huì)出現(xiàn) UITableView
與 ViewController
互相強(qiáng)引用,形成循環(huán)引用棠众。
那么看一下 UITableView
的實(shí)現(xiàn)源碼琳疏,我們會(huì)發(fā)現(xiàn)其中定義 dataSource
和 delegate
屬性時(shí)是用 weak
修飾的。
@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
所以 tableView
的 dataSource
和 delegate
只是 weak
指針闸拿,指向了 ViewController
空盼,它們之間的關(guān)系是這樣的:
這也就避免了循環(huán)引用的發(fā)生。
NSURLSession 的 delegate
那么 delegate
一定被 weak
修飾嗎新荤?
也不一定揽趾,需要看具體的場(chǎng)景。比如 NSURLSession
類中的 delegate
就是用 retain
修飾的苛骨。
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
它這么做但骨,是因?yàn)榱舜_保網(wǎng)絡(luò)請(qǐng)求回調(diào)之前,delegate
不被釋放智袭。
這也間接引起了 AFNetworking
中循環(huán)引用的出現(xiàn)奔缠。我們看 AFURLSessionManager
類中聲明的 session
是 strong
類型的。
/**
The managed session.
*/
@property (readonly, nonatomic, strong) NSURLSession *session;
在構(gòu)造 session
對(duì)象時(shí)吼野,也將 delegate
設(shè)為了 self
校哎,也就是 AFURLSessionManager
類。
- (NSURLSession *)session {
@synchronized (self) {
if (!_session) {
_session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
}
}
return _session;
}
如此三者就形成了這樣循環(huán)持有關(guān)系瞳步。
要解決這個(gè)問(wèn)題闷哆,有兩種解決思路:
方式一:將 AFHTTPSessionManager
對(duì)象設(shè)為單例
對(duì)于客戶端來(lái)說(shuō),大多數(shù)情況下都是對(duì)應(yīng)同一個(gè)后臺(tái)服務(wù)单起,所以可以將 AFHTTPSessionManager
對(duì)象設(shè)為單例來(lái)處理抱怔。
- (AFHTTPSessionManager *)sharedManager {
static dispatch_once_t onceToken;
static AFHTTPSessionManager *_manager = nil;
dispatch_once(&onceToken, ^{
_manager = [AFHTTPSessionManager manager];
_manager.requestSerializer = [AFHTTPRequestSerializer serializer];
_manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/html",@"text/json", @"text/plain", @"text/javascript",@"text/xml", nil];
_manager.responseSerializer = [AFHTTPResponseSerializer serializer];
});
return _manager;
}
如果要設(shè)定固定請(qǐng)求頭, 以這種 key-value
形式加入到 dispatch_once
中嘀倒。
[_manager.requestSerializer setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"];
缺點(diǎn):因?yàn)檎?qǐng)求的 header
是由 AFHTTPSessionManager
的 requestSerializer.mutableHTTPRequestHeaders
字典持有的屈留,所以這種單例模式會(huì)導(dǎo)致全局共享一個(gè) header
,如果要處理不同自定義 header
的請(qǐng)求就會(huì)變得很麻煩测蘑。
方式二:在請(qǐng)求結(jié)束時(shí)灌危,手動(dòng)銷毀 session
對(duì)象
由于 session
對(duì)象對(duì) delegate
強(qiáng)持有,要打破循環(huán)引用碳胳,需要在請(qǐng)求結(jié)束后手動(dòng)調(diào)用 AFHTTPSessionManager
對(duì)象銷毀的方法勇蝙。
- (AFHTTPSessionManager *)getSessionManager{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/html",@"text/json", @"text/plain", @"text/javascript",@"text/xml", nil];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
return manager;
}
- (void)sendRequest{
AFHTTPSessionManager *manager = [self getSessionManager];
__weak typeof(manager)weakManager = manager;
[manager GET:@"https://blog.fiteen.top" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
__strong typeof (weakManager)strongManager = weakManager;
NSLog(@"success 回調(diào)");
[strongManager invalidateSessionCancelingTasks:YES];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
__strong typeof (weakManager)strongManager = weakManager;
NSLog(@"error 回調(diào)");
[strongManager invalidateSessionCancelingTasks:YES];
}];
}
非 OC 對(duì)象內(nèi)存處理
雖然現(xiàn)在已經(jīng)普及了 ARC 模式,但它僅對(duì) OC 對(duì)象進(jìn)行自動(dòng)內(nèi)存管理挨约。對(duì)于非 OC 對(duì)象味混,比如 CoreFoundation
框架下的 CI
产雹、CG
、CF
等開頭的類的對(duì)象翁锡,在使用完畢后仍需我們手動(dòng)釋放蔓挖。
比如這段獲取 UUID 的代碼:
CFUUIDRef puuid = CFUUIDCreate( kCFAllocatorDefault );
CFStringRef uuidString = CFUUIDCreateString( kCFAllocatorDefault, puuid );
NSString *uuid = [(NSString *)CFBridgingRelease(CFStringCreateCopy(NULL, uuidString)) uppercaseString];
// 使用完后釋放 puuid 和 uuidString 對(duì)象
CFRelease(puuid);
CFRelease(uuidString);
還有 C 語(yǔ)言中,如果用 malloc
動(dòng)態(tài)分配內(nèi)存后盗誊,需要用 free
去釋放,否則會(huì)出現(xiàn)內(nèi)存泄漏隘弊。比如:
person *p = (person *)malloc(sizeof(person));
strcpy(p->name,"fiteen");
p->age = 18;
// 使用完釋放內(nèi)存
free(p);
// 防止野指針
p = NULL;
循環(huán)加載引起內(nèi)存峰值
先看下面這段代碼哈踱,看似沒(méi)有內(nèi)存泄漏的問(wèn)題,但是在實(shí)際運(yùn)行時(shí)梨熙,for 循環(huán)內(nèi)部產(chǎn)生了大量的臨時(shí)對(duì)象开镣,會(huì)出現(xiàn) CPU 暴增。
for (int i = 0; i < 1000000; i++) {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
}
這是因?yàn)檠h(huán)內(nèi)產(chǎn)生大量的臨時(shí)對(duì)象咽扇,直至循環(huán)結(jié)束才釋放邪财,可能導(dǎo)致內(nèi)存泄漏。
解決方案:
在循環(huán)中創(chuàng)建自己的 autoreleasepool
质欲,及時(shí)釋放占用內(nèi)存大的臨時(shí)變量树埠,減少內(nèi)存占用峰值。
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
}
}
在沒(méi)有手加自動(dòng)釋放池的情況下嘶伟,autorelease
對(duì)象是在當(dāng)前的 runloop 迭代結(jié)束時(shí)釋放的怎憋,而它能夠釋放的原因是系統(tǒng)在每個(gè) runloop 迭代中都會(huì)先銷毀并重新創(chuàng)建自動(dòng)釋放池。
下面舉個(gè)特殊的例子九昧,使用容器 block 版本的枚舉器時(shí)绊袋,內(nèi)部會(huì)自動(dòng)添加一個(gè)自動(dòng)釋放池,比如:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這里被一個(gè)局部 @autoreleasepool 包圍著
}];
野指針與僵尸對(duì)象
指針指向的對(duì)象已經(jīng)被釋放/回收铸鹰,這個(gè)指針就叫做野指針癌别。這個(gè)被釋放的對(duì)象就是僵尸對(duì)象。
如果用野指針去訪問(wèn)僵尸對(duì)象蹋笼,或者說(shuō)向野指針發(fā)送消息展姐,會(huì)發(fā)生 EXC_BAD_ACCESS
崩潰,出現(xiàn)內(nèi)存泄漏剖毯。
// MRC 下
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
[stu setAge:18];
[stu release]; // stu 在 release 之后诞仓,內(nèi)存空間被釋放并回收,stu 變成野指針
// [stu setAge:20]; // set 再調(diào)用 setAge 就會(huì)崩潰
}
return 0;
}
解決方案:當(dāng)對(duì)象釋放后速兔,應(yīng)該將其置為 nil
墅拭。
內(nèi)存泄漏檢查工具
Instruments
Instruments 是 Xcode 自帶的工具集合,為開發(fā)者提供強(qiáng)大的程序性能分析和測(cè)試能力涣狗。
它打開方式為:Xcode → Open Developer Tool → Instruments
谍婉。其中的 Allocations 和 Leaks 功能可以協(xié)助我們進(jìn)行內(nèi)存泄漏檢查舒憾。
Leaks:動(dòng)態(tài)檢查泄漏的內(nèi)存,如果檢查過(guò)程時(shí)出現(xiàn)了紅色叉叉穗熬,就說(shuō)明存在內(nèi)存泄漏镀迂,可以定位到泄漏的位置,去解決問(wèn)題唤蔗。此外探遵,Xcode 中還提供靜態(tài)監(jiān)測(cè)方法 Analyze,可以直接通過(guò)
Product → Analyze
打開妓柜,如果出現(xiàn)泄漏箱季,會(huì)出現(xiàn)“藍(lán)色分支圖標(biāo)”提示。Allocations:用來(lái)檢查內(nèi)存使用/分配情況棍掐。比如出現(xiàn)“循環(huán)加載引起內(nèi)存峰值”的情況藏雏,就可以通過(guò)這個(gè)工具檢查出來(lái)。
Zombies:檢查是否訪問(wèn)了僵尸對(duì)象作煌。
Instruments 的使用相對(duì)來(lái)說(shuō)比較復(fù)雜掘殴,你也可以通過(guò)在工程中引入一些第三方框架進(jìn)行檢測(cè)。
MLeaksFinder
MLeaksFinder 是 WeRead 團(tuán)隊(duì)開源的 iOS 內(nèi)存泄漏檢測(cè)工具粟誓。
它的使用非常簡(jiǎn)單奏寨,只要在工程引入框架,就可以在 App 運(yùn)行過(guò)程中監(jiān)測(cè)到內(nèi)存泄漏的對(duì)象并立即提醒鹰服。MLeaksFinder 也不具備侵入性服爷,使用時(shí)無(wú)需在 release 版本移除,因?yàn)樗粫?huì)在 debug 版本生效获诈。
不過(guò) MLeaksFinder 的只能定位到內(nèi)存泄漏的對(duì)象仍源,如果你想要檢查該對(duì)象是否存在循環(huán)引用。就結(jié)合 FBRetainCycleDetector 一起使用舔涎。
FBRetainCycleDetector
FBRetainCycleDetector 是 Facebook 開源的一個(gè)循環(huán)引用檢測(cè)工具笼踩。它會(huì)遞歸遍歷傳入內(nèi)存的 OC 對(duì)象的所有強(qiáng)引用的對(duì)象,檢測(cè)以該對(duì)象為根結(jié)點(diǎn)的強(qiáng)引用樹有沒(méi)有出現(xiàn)循環(huán)引用亡嫌。