iOS自從引入ARC機制后,一般的內存管理就可以不用我們碼農來負責了,但是一些操作如果不注意,還是會引起內存泄漏。
本文主要介紹一下內存泄漏的原理、常規的檢測方法以及出現的常用場景和修改方法。
1、 內存泄漏原理
內存泄漏的在百度上的解釋就是“程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果”。
在我的理解里就是,公司給一個入職的員工分配了一個工位,但是這個員工離職后,這個工位卻不能分配給下一位入職的員工使用,造成了大量的資源浪費。
2、 常規的檢測方法
2.1、Analyze靜態分析 (command + shift + b)。
2.2、動態分析方法(Instrument工具庫里的Leaks),product->profile ->leaks 打開可以工具主窗口,具體使用方法可以參考這篇文章:https://www.jianshu.com/p/9fc2132d09c7。
3、 內存泄漏的場景和分析:
3.1、代理的屬性關鍵字設置為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.2、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.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.4、NSTimer 不正確使用造成的內存泄漏
3.4.1、NSTimer重復設置為NO的時候,不會引起內存泄漏
3.4.2、NSTimer重復設置為YES的時候,有執行invalidate就不會內存泄漏,沒有執行invalidate就會內存泄漏,在 timer的執行方法里調用invalidate也可以。
3.4.3、中間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.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.5、使用block創建定時器,需要正確使用block,要執行invalidate,否則也會內存泄漏。這里涉及到block的內存泄漏問題,我會在下篇中一起講解。
其他內存泄漏如通知和KVO、block循環引用 、NSThread造成的內存泄漏請見下篇。
demo地址請點擊這里:https://github.com/zmfflying/ZMFBlogProject