iOS自從引入ARC機制後,通常的內存管理就能夠不用咱們碼農來負責了,可是一些操做若是不注意,仍是會引發內存泄漏。git
本文主要介紹一下內存泄漏的原理、常規的檢測方法以及出現的經常使用場景和修改方法。github
一、 內存泄漏原理bash
內存泄漏的在百度上的解釋就是「程序中已動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果」。框架
在個人理解裏就是,公司給一個入職的員工分配了一個工位,可是這個員工離職後,這個工位卻不能分配給下一位入職的員工使用,形成了大量的資源浪費。工具
二、 常規的檢測方法oop
2.一、Analyze靜態分析 (command + shift + b)。性能
2.二、動態分析方法(Instrument工具庫裏的Leaks),product->profile ->leaks 打開能夠工具主窗口,具體使用方法能夠參考這篇文章。ui
三、 內存泄漏的場景和分析:atom
3.一、代理的屬性關鍵字設置爲strong形成的內存泄漏 請看下面這段代碼:spa
@protocol MFMemoryLeakViewDelegate <NSObject>
@end
@interface MFMemoryLeakView : UIView
@property (nonatomic, strong) id<MFMemoryLeakViewDelegate> delegate;
@end
複製代碼
MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds];
view.delegate = self;
[self.view addSubview:view];
複製代碼
形成的後果就是控制器得不到釋放,緣由是控制器對視圖進行了強引用,而控制器又是視圖的代理,視圖對代理進行了強引用,致使了控制器和視圖的循環引用。 解決方法也很簡單,strong改爲weak就行:
@property (nonatomic, weak) id<MFMemoryLeakViewDelegate> delegate;
複製代碼
3.二、CoreGraphics框架裏申請的內存忘記釋放
請看下面這段代碼:
- (UIImage *)coreGraphicsMemoryLeak{
CGRect myImageRect = self.view.bounds;
CGImageRef imageRef = [UIImage imageNamed:@"MemoryLeakTip.jpeg"].CGImage;
CGImageRef subImageRef = CGImageCreateWithImageInRect(imageRef, myImageRect);
UIGraphicsBeginImageContext(myImageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, myImageRect, subImageRef);
UIImage *newImage = [UIImage imageWithCGImage:subImageRef];
CGImageRelease(subImageRef);
// CGImageRelease(imageRef);
UIGraphicsEndImageContext();
return newImage;
}
複製代碼
若是"CGImageRelease(subImageRef)"這行代碼缺失,就會引發內存泄漏,使用靜態分析能夠輕易發現。
須要注意的是:只有當CGImageRef使用create或retain後纔要手動release,沒有就不須要手動處理了,系統會進行自動的釋放。上面的imageRef對象就是這樣,若是進行了手動release,會引發不肯定性的崩潰。
爲何是不肯定性的崩潰呢,目前我支持的一種說法是:CFRelease的對象不能是NULL,如果NULL的話,會引發runtime的錯誤而且程序要崩潰,原本imageRef的管理者是會在某個時刻調用release的,可是由於這裏已經release過了,已經成了NULL,因此當這個調用時期到來的時候就crash掉了。
關於這個問題,你們可使用個人demo進行嘗試,打開後圖中註釋的代碼後運行,先進入內存泄漏的頁面,而後返回上級,再進入這個頁面,程序崩潰,demo地址見底部。
3.三、 CoreFoundation框架裏申請的內存忘記釋放
請看下面這段代碼:
- (NSString *)coreFoundationMemoryLeak{
CFUUIDRef uuid_ref = CFUUIDCreate(NULL);
CFStringRef uuid_string_ref= CFUUIDCreateString(NULL, uuid_ref);
// NSString *uuid = (__bridge NSString *)uuid_string_ref;
NSString *uuid = (__bridge_transfer NSString *)uuid_string_ref;
CFRelease(uuid_ref);
// CFRelease(uuid_string_ref);
return uuid;
}
複製代碼
若是"CFRelease(uuid_ref)"這行代碼缺失,就會引發內存泄漏,使用靜態分析能夠輕易發現。
須要注意的是:「 __bridge」是將CoreFoundation框架的對象全部權交給Foundation框架來使用,可是Foundation框架中的對象並不能管理該對象的內存。「 __bridge_transfer」是將CoreFoundation框架的對象全部權交給Foundation來管理,若是Foundation中對象銷燬,那麼咱們以前的對象(CoreFoundation)會一塊兒銷燬。
因此__bridge_transfer這種橋接方式,之後就不用再本身手動管理內存了。若是上面代碼裏的「CFRelease(uuid_string_ref)」的註釋,uuid就會被銷燬,程序運行到reurn 就崩潰。
3.四、NSTimer 不正確使用形成的內存泄漏
3.4.一、NSTimer重複設置爲NO的時候,不會引發內存泄漏
3.4.二、NSTimer重複設置爲YES的時候,有執行invalidate就不會內存泄漏,沒有執行invalidate就會內存泄漏,在 timer的執行方法裏調用invalidate也能夠。
3.4.三、中間target:控制器沒法釋放,是由於timer對控制器進行了強引用,使用類方法建立的timer默認加入了runloop,因此,timer只要不持有控制器,控制器就能釋放了。
[NSTimer scheduledTimerWithTimeInterval:1 target:[MFTarget target:self] selector:@selector(timerActionOtherTarget:) userInfo:nil repeats:YES];
複製代碼
#import "MFTarget.h"
@implementation MFTarget
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)target:(id)target {
return [[MFTarget alloc] initWithTarget:target];
}
//這裏將selector 轉發給_target 去響應
- (id)forwardingTargetForSelector:(SEL)selector {
if ([_target respondsToSelector:selector]) {
return _target;
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
複製代碼
這樣控制器的確是釋放了,可是timer的方法仍是會在不斷的調用,若是對性能要求不那麼嚴謹的,可使用這種方法,具體代碼見demo。
3.4.四、重寫NSTimer:結合上面中間target的思路,在timer內部進行invalidate操做,請看一下代碼。
@interface MFTimer : NSObject
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
@end
複製代碼
#import "MFTimer.h"
@interface MFTimer ()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation MFTimer
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
MFTimer *mfTimer = [[MFTimer alloc] init];
mfTimer.timer = [NSTimer timerWithTimeInterval:ti target:mfTimer selector:@selector(timerAction:) userInfo:userInfo repeats:yesOrNo];
mfTimer.target = aTarget;
mfTimer.selector = aSelector;
return mfTimer.timer;
}
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
MFTimer *mfTimer = [[MFTimer alloc] init];
mfTimer.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:mfTimer selector:@selector(timerAction:) userInfo:userInfo repeats:yesOrNo];
mfTimer.target = aTarget;
mfTimer.selector = aSelector;
return mfTimer.timer;
}
- (void)timerAction:(NSTimer *)timer {
if (self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//不判斷是否響應,是爲了避免實現定時器的方法就報錯
[self.target performSelector:self.selector withObject:timer];
#pragma clang diagnostic pop
}else {
[self.timer invalidate];
self.timer = nil;
}
}
@end
複製代碼
3.4.五、使用block建立定時器,須要正確使用block,要執行invalidate,不然也會內存泄漏。這裏涉及到block的內存泄漏問題,我會在下篇中一塊兒講解。
其餘內存泄漏如通知和KVO、block循環引用 、NSThread形成的內存泄漏請見下篇。