ios自從引入ARC機制後,通常的內存管理就能夠不用咱們碼農來負責了,可是一些操做若是不注意,仍是會引發內存泄漏。ios
本文主要介紹一下內存泄漏的原理、常規的檢測方法以及出現的經常使用場景和修改方法。git
一、 內存泄漏原理github
內存泄漏的在百度上的解釋就是「程序中已動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果」。框架
在個人理解裏就是,公司給一個入職的員工分配了一個工位,可是這個員工離職後,這個工位卻不能分配給下一位入職的員工使用,形成了大量的資源浪費。工具
二、 常規的檢測方法oop
2.一、Analyze靜態分析 (command + shift + b)。性能
2.二、動態分析方法(Instrument工具庫裏的Leaks),product->profile ->leaks 打開能夠工具主窗口,具體使用方法能夠參考這篇文章:https://www.jianshu.com/p/9fc2132d09c7。ui
三、 內存泄漏的場景和分析:atom
3.一、代理的屬性關鍵字設置爲strong形成的內存泄漏
請看下面這段代碼:代理
@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形成的內存泄漏請見下篇。
demo地址請點擊這裏:https://github.com/zmfflying/ZMFBlogProject