歡迎訪問個人博客原文javascript
內存泄漏指的是程序中已動態分配的堆內存(程序員本身管理的空間)因爲某些緣由未能釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度變慢甚至系統崩潰。html
在 iOS 開發中會遇到的內存泄漏場景能夠分爲幾類:java
當對象 A 強引用對象 B,而對象 B 又強引用對象 A,或者多個對象互相強引用造成一個閉環,這就是循環引用。ios
Block 會對其內部的對象強引用,所以使用的時候須要確保不會造成循環引用。git
舉個例子,看下面這段代碼:程序員
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
強引用了 block
,而 block
內部又調用了 self
,所以 block
也強引用了 self
。要解決這個循環引用的問題,有兩種思路。github
先用 __weak
將 self
置爲弱引用,打破「循環」關係,可是 weakSelf
在 block
中可能被提早釋放,所以還須要在 block
內部,用 __strong
對 weakSelf
進行強引用,這樣能夠確保 strongSelf
在 block
結束後纔會被釋放。macos
__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();
複製代碼
使用 __block
關鍵字設置一個指針 vc
指向 self
,從新造成一個 self → block → vc → self
的循環持有鏈。在調用結束後,將 vc
置爲 nil
,就能斷開循環持有鏈,從而令 self
正常釋放。json
__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();
複製代碼
這裏還要補充一個問題,爲何要用 __block
修飾 vc
?設計模式
首先,block
自己不容許修改外部變量的值。但被 __block
修飾的變量會被存在了一個棧的結構體當中,成爲結構體指針。當這個對象被 block
持有,就將「外部變量」在棧中的內存地址放到堆中,進而能夠在 block
內部修改外部變量的值。
還有一種方式能夠斷開持有關係。就是將 self
以傳參的形式傳入 block
內部,這樣 self
就不會被 block
持用,也就不會造成循環持有鏈。
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
對象是採用 target-action
方式建立的,一般 target
就是類自己,而咱們爲了方便又常把 NSTimer
聲明爲屬性,像這樣:
// 第一種建立方式,timer 默認添加進 runloop
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
// 第二種建立方式,須要手動將 timer 添加進 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)
的循環持有鏈。只要 self
不釋放,dealloc
就不會執行,timer
就沒法在 dealloc
中銷燬,self
始終被強引用,永遠得不到釋放,循環矛盾,最終形成內存泄漏。
那麼若是隻把 timer
做爲局部變量,而不是屬性呢?
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timeFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼
self
一樣釋放不了。
由於在加入 runloop 的操做中,timer
被強引用,這就造成了一條 runloop → timer → self(target)
的持有鏈。而 timer
做爲局部變量,沒法執行 invalidate
,因此在 timer
被銷燬以前,self
也不會被釋放。
因此只要申請了 timer
,加入了 runloop,而且 target
是 self
,就算不是循環引用,也會形成內存泄漏,由於 self
沒有釋放的時機。
解決這個問題有好幾種方式,開發者能夠自行選擇。
當 NSTimer
初始化以後,加入 runloop 會致使被當前的頁面強引用,所以不會執行 dealloc
。因此須要在合適的時機銷燬 _timer
,斷開 _timer
、runloop 和當前頁面之間的強引用關係。
[_timer invalidate];
_timer = nil;
複製代碼
ViewController
中的時機能夠選擇 didMoveToParentViewController
、viewDidDisappear
,View
中能夠選擇 removeFromSuperview
等,但這種方案並必定是正確可行的。
好比在註冊頁面中加了一個倒計時,若是在 viewDidDisappear
中銷燬了 _timer
,當用戶點擊跳轉到用戶協議頁面時,倒計時就會被提早銷燬,這是不合邏輯的。所以須要結合具體業務的需求場景來考慮。
GCD 不基於 runloop,能夠用 GCD 的計時器代替 NSTimer 實現計時任務。但須要注意的是,GCD 內部 block 中的循環引用問題仍是須要解決的。
__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];
});
// 開啓計時器
dispatch_resume(_timer);
// 銷燬計時器
// dispatch_source_cancel(_timer);
複製代碼
中介者指的是用別的對象代替 target
裏的 self
,中介者綁定 selector
以後,再在 dealloc
中釋放 timer
。
這裏介紹兩種中介者,一種是 NSObject 對象,一種是 NSProxy 的子類。它們的存在是爲了斷開對 self
的強引用,使之能夠被釋放。
新建一個 NSObject 對象 _target
,爲它動態添加一個方法,方法的地址指向 self
方法列表中的 timeFire
的 IMP。這樣 _target
與 self
之間沒有直接的引用關係,又能引用 self
裏的方法,就不會出現循環引用。
_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
的子類 WeakProxy
,將 timer
的 target
設置爲 WeakProxy
實例,利用完整的消息轉發機制實現執行 self
中的計時方法,解決循環引用。
// 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
複製代碼
而後這樣建立 timer
:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[WeakProxy proxyWithTarget:self] selector:@selector(timeFire) userInfo:nil repeats:YES];
複製代碼
這時候的循環持有鏈是這樣的:
因爲 WeakProxy
與 self
之間是弱引用關係,self
最終是能夠被銷燬的。
iOS 10 以後,Apple 提供了一種 block 的方式來解決循環引用的問題。
+ (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 傳入初始化方法中,統一以 block 的形式處理回調。
// 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
複製代碼
而後在須要的類中建立 timer
。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer ht_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
[weakSelf timeFire];
}];
複製代碼
委託模式,是對象之間通訊的一種設計模式。該模式的主旨是:定義一套接口,某對象若想接受另外一個對象的委託,則需聽從此接口,以便成爲其「委託對象」。
咱們經常使用的 tableView
與 ViewController
就是委託方和代理方的關係。
須要在控制器中加入列表時,一般咱們會將 tableView
設爲 ViewController
中 view
的子視圖,UIViewController
的源碼是這樣定義 view
的:
@property(null_resettable, nonatomic, strong) UIView *view;
複製代碼
所以 ViewController
強引用了 tableView
。而 tableView
又要委託 ViewController
幫它實現幾個代理方法和數據源方法。若是此時 dataSource
和 delegate
屬性用 strong
來修飾,就會出現 UITableView
與 ViewController
互相強引用,造成循環引用。
那麼看一下 UITableView
的實現源碼,咱們會發現其中定義 dataSource
和 delegate
屬性時是用 weak
修飾的。
@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
複製代碼
因此 tableView
的 dataSource
和 delegate
只是 weak
指針,指向了 ViewController
,它們之間的關係是這樣的:
這也就避免了循環引用的發生。
那麼 delegate
必定被 weak
修飾嗎?
也不必定,須要看具體的場景。好比 NSURLSession
類中的 delegate
就是用 retain
修飾的。
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
複製代碼
它這麼作,是由於了確保網絡請求回調以前,delegate
不被釋放。
這也間接引發了 AFNetworking
中循環引用的出現。咱們看 AFURLSessionManager
類中聲明的 session
是 strong
類型的。
/** The managed session. */
@property (readonly, nonatomic, strong) NSURLSession *session;
複製代碼
在構造 session
對象時,也將 delegate
設爲了 self
,也就是 AFURLSessionManager
類。
- (NSURLSession *)session {
@synchronized (self) {
if (!_session) {
_session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
}
}
return _session;
}
複製代碼
如此三者就造成了這樣循環持有關係。
要解決這個問題,有兩種解決思路:
方式一:將 AFHTTPSessionManager
對象設爲單例
對於客戶端來講,大多數狀況下都是對應同一個後臺服務,因此能夠將 AFHTTPSessionManager
對象設爲單例來處理。
- (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;
}
複製代碼
若是要設定固定請求頭, 以這種 key-value
形式加入到 dispatch_once
中。
[_manager.requestSerializer setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"];
複製代碼
缺點:由於請求的 header
是由 AFHTTPSessionManager
的 requestSerializer.mutableHTTPRequestHeaders
字典持有的,因此這種單例模式會致使全局共享一個 header
,若是要處理不一樣自定義 header
的請求就會變得很麻煩。
方式二:在請求結束時,手動銷燬 session
對象
因爲 session
對象對 delegate
強持有,要打破循環引用,須要在請求結束後手動調用 AFHTTPSessionManager
對象銷燬的方法。
- (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 回調");
[strongManager invalidateSessionCancelingTasks:YES];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
__strong typeof (weakManager)strongManager = weakManager;
NSLog(@"error 回調");
[strongManager invalidateSessionCancelingTasks:YES];
}];
}
複製代碼
雖然如今已經普及了 ARC 模式,但它僅對 OC 對象進行自動內存管理。對於非 OC 對象,好比 CoreFoundation
框架下的 CI
、CG
、CF
等開頭的類的對象,在使用完畢後仍需咱們手動釋放。
好比這段獲取 UUID 的代碼:
CFUUIDRef puuid = CFUUIDCreate( kCFAllocatorDefault );
CFStringRef uuidString = CFUUIDCreateString( kCFAllocatorDefault, puuid );
NSString *uuid = [(NSString *)CFBridgingRelease(CFStringCreateCopy(NULL, uuidString)) uppercaseString];
// 使用完後釋放 puuid 和 uuidString 對象
CFRelease(puuid);
CFRelease(uuidString);
複製代碼
還有 C 語言中,若是用 malloc
動態分配內存後,須要用 free
去釋放,不然會出現內存泄漏。好比:
person *p = (person *)malloc(sizeof(person));
strcpy(p->name,"fiteen");
p->age = 18;
// 使用完釋放內存
free(p);
// 防止野指針
p = NULL;
複製代碼
先看下面這段代碼,看似沒有內存泄漏的問題,可是在實際運行時,for 循環內部產生了大量的臨時對象,會出現 CPU 暴增。
for (int i = 0; i < 1000000; i++) {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
}
複製代碼
這是由於循環內產生大量的臨時對象,直至循環結束才釋放,可能致使內存泄漏。
解決方案:
在循環中建立本身的 autoreleasepool
,及時釋放佔用內存大的臨時變量,減小內存佔用峯值。
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
}
}
複製代碼
在沒有手加自動釋放池的狀況下,autorelease
對象是在當前的 runloop 迭代結束時釋放的,而它可以釋放的緣由是系統在每一個 runloop 迭代中都會先銷燬並從新建立自動釋放池。
下面舉個特殊的例子,使用容器 block 版本的枚舉器時,內部會自動添加一個自動釋放池,好比:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這裏被一個局部 @autoreleasepool 包圍着
}];
複製代碼
指針指向的對象已經被釋放/回收,這個指針就叫作野指針。這個被釋放的對象就是殭屍對象。
若是用野指針去訪問殭屍對象,或者說向野指針發送消息,會發生 EXC_BAD_ACCESS
崩潰,出現內存泄漏。
// MRC 下
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
[stu setAge:18];
[stu release]; // stu 在 release 以後,內存空間被釋放並回收,stu 變成野指針
// [stu setAge:20]; // set 再調用 setAge 就會崩潰
}
return 0;
}
複製代碼
解決方案:當對象釋放後,應該將其置爲 nil
。
Instruments 是 Xcode 自帶的工具集合,爲開發者提供強大的程序性能分析和測試能力。
它打開方式爲:Xcode → Open Developer Tool → Instruments
。其中的 Allocations 和 Leaks 功能能夠協助咱們進行內存泄漏檢查。
Leaks:動態檢查泄漏的內存,若是檢查過程時出現了紅色叉叉,就說明存在內存泄漏,能夠定位到泄漏的位置,去解決問題。此外,Xcode 中還提供靜態監測方法 Analyze,能夠直接經過 Product → Analyze
打開,若是出現泄漏,會出現「藍色分支圖標」提示。
Allocations:用來檢查內存使用/分配狀況。好比出現「循環加載引發內存峯值」的狀況,就能夠經過這個工具檢查出來。
Zombies:檢查是否訪問了殭屍對象。
Instruments 的使用相對來講比較複雜,你也能夠經過在工程中引入一些第三方框架進行檢測。
MLeaksFinder 是 WeRead 團隊開源的 iOS 內存泄漏檢測工具。
它的使用很是簡單,只要在工程引入框架,就能夠在 App 運行過程當中監測到內存泄漏的對象並當即提醒。MLeaksFinder 也不具有侵入性,使用時無需在 release 版本移除,由於它只會在 debug 版本生效。
不過 MLeaksFinder 的只能定位到內存泄漏的對象,若是你想要檢查該對象是否存在循環引用。就結合 FBRetainCycleDetector 一塊兒使用。
FBRetainCycleDetector 是 Facebook 開源的一個循環引用檢測工具。它會遞歸遍歷傳入內存的 OC 對象的全部強引用的對象,檢測以該對象爲根結點的強引用樹有沒有出現循環引用。