iOS 內(nèi)存泄漏場(chǎng)景與解決方案

歡迎訪問(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();

blockself 的屬性浅悉,因此 self 強(qiáng)引用了 block趟据,而 block 內(nèi)部又調(diào)用了 self,因此 block 也強(qiáng)引用了 self术健。要解決這個(gè)循環(huán)引用的問(wèn)題汹碱,有兩種思路。

使用 Weak-Strong Dance

先用 __weakself 置為弱引用荞估,打破“循環(huán)”關(guān)系咳促,但是 weakSelfblock 中可能被提前釋放,因此還需要在 block 內(nèi)部勘伺,用 __strong 對(duì) weakSelf 進(jìn)行強(qiáng)引用跪腹,這樣可以確保 strongSelfblock 結(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千康,并且 targetself享幽,就算不是循環(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澜建、viewDidDisappearView 中可以選擇 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彰导。這樣 _targetself 之間沒(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,將 timertarget 設(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)沁@樣的:

image

由于 WeakProxyself 之間是弱引用關(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

我們常用的 tableViewViewController 就是委托方代理方的關(guān)系荠诬。

需要在控制器中加入列表時(shí)琅翻,通常我們會(huì)將 tableView 設(shè)為 ViewControllerview 的子視圖位仁,UIViewController 的源碼是這樣定義 view 的:

@property(null_resettable, nonatomic, strong) UIView *view;

因此 ViewController 強(qiáng)引用了 tableView。而 tableView 又要委托 ViewController 幫它實(shí)現(xiàn)幾個(gè)代理方法和數(shù)據(jù)源方法方椎。如果此時(shí) dataSourcedelegate 屬性用 strong 來(lái)修飾聂抢,就會(huì)出現(xiàn) UITableViewViewController 互相強(qiáng)引用,形成循環(huán)引用棠众。

那么看一下 UITableView 的實(shí)現(xiàn)源碼琳疏,我們會(huì)發(fā)現(xiàn)其中定義 dataSourcedelegate 屬性時(shí)是用 weak 修飾的。

@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;

所以 tableViewdataSourcedelegate 只是 weak 指針闸拿,指向了 ViewController空盼,它們之間的關(guān)系是這樣的:

image

這也就避免了循環(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 類中聲明的 sessionstrong 類型的。

/**
 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)系瞳步。

image

要解決這個(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 是由 AFHTTPSessionManagerrequestSerializer.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产雹、CGCF 等開頭的類的對(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)引用亡嫌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嚎于,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挟冠,更是在濱河造成了極大的恐慌于购,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件知染,死亡現(xiàn)場(chǎng)離奇詭異肋僧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門嫌吠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)止潘,“玉大人,你說(shuō)我怎么就攤上這事辫诅∑敬鳎” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵炕矮,是天一觀的道長(zhǎng)么夫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)肤视,這世上最難降的妖魔是什么档痪? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮钢颂,結(jié)果婚禮上钞它,老公的妹妹穿的比我還像新娘拜银。我一直安慰自己殊鞭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布尼桶。 她就那樣靜靜地躺著操灿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泵督。 梳的紋絲不亂的頭發(fā)上趾盐,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音小腊,去河邊找鬼救鲤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛秩冈,可吹牛的內(nèi)容都是我干的本缠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼入问,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丹锹!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起芬失,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤楣黍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后棱烂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體租漂,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窜锯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片张肾。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锚扎,靈堂內(nèi)的尸體忽然破棺而出吞瞪,到底是詐尸還是另有隱情,我是刑警寧澤驾孔,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布芍秆,位于F島的核電站,受9級(jí)特大地震影響翠勉,放射性物質(zhì)發(fā)生泄漏妖啥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一对碌、第九天 我趴在偏房一處隱蔽的房頂上張望荆虱。 院中可真熱鬧,春花似錦朽们、人聲如沸怀读。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)菜枷。三九已至,卻和暖如春叁丧,著一層夾襖步出監(jiān)牢的瞬間啤誊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工拥娄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚊锹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓稚瘾,卻偏偏與公主長(zhǎng)得像牡昆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孟抗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容