iOS自從引入ARC機(jī)制后暇番,一般的內(nèi)存管理就可以不用我們碼農(nóng)來負(fù)責(zé)了崩哩,但是一些操作如果不注意,還是會引起內(nèi)存泄漏戏自。
本文主要介紹一下內(nèi)存泄漏的原理邦投、常規(guī)的檢測方法以及出現(xiàn)的常用場景和修改方法。
1擅笔、 內(nèi)存泄漏原理
內(nèi)存泄漏的在百度上的解釋就是“程序中已動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放尼摹,造成系統(tǒng)內(nèi)存的浪費,導(dǎo)致程序運行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果”剂娄。
在我的理解里就是蠢涝,公司給一個入職的員工分配了一個工位,但是這個員工離職后阅懦,這個工位卻不能分配給下一位入職的員工使用和二,造成了大量的資源浪費。
2耳胎、 常規(guī)的檢測方法
2.1惯吕、Analyze靜態(tài)分析 (command + shift + b)。
2.2怕午、動態(tài)分析方法(Instrument工具庫里的Leaks)废登,product->profile ->leaks 打開可以工具主窗口,具體使用方法可以參考這篇文章郁惜。
3堡距、 內(nèi)存泄漏的場景和分析:
3.1、代理的屬性關(guān)鍵字設(shè)置為strong造成的內(nèi)存泄漏
請看下面這段代碼:
@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];
造成的后果就是控制器得不到釋放兆蕉,原因是控制器對視圖進(jìn)行了強(qiáng)引用羽戒,而控制器又是視圖的代理,視圖對代理進(jìn)行了強(qiáng)引用虎韵,導(dǎo)致了控制器和視圖的循環(huán)引用易稠。
解決方法也很簡單,strong改成weak就行:
@property (nonatomic, weak) id<MFMemoryLeakViewDelegate> delegate;
3.2包蓝、CoreGraphics框架里申請的內(nèi)存忘記釋放
請看下面這段代碼:
- (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)"這行代碼缺失驶社,就會引起內(nèi)存泄漏,使用靜態(tài)分析可以輕易發(fā)現(xiàn)测萎。
需要注意的是:只有當(dāng)CGImageRef使用create或retain后才要手動release亡电,沒有就不需要手動處理了,系統(tǒng)會進(jìn)行自動的釋放绳泉。上面的imageRef對象就是這樣逊抡,如果進(jìn)行了手動release,會引起不確定性的崩潰零酪。
為什么是不確定性的崩潰呢冒嫡,目前我支持的一種說法是:CFRelease的對象不能是NULL,若是NULL的話四苇,會引起runtime的錯誤并且程序要崩潰孝凌,本來imageRef的管理者是會在某個時刻調(diào)用release的,但是因為這里已經(jīng)release過了月腋,已經(jīng)成了NULL蟀架,所以當(dāng)這個調(diào)用時期到來的時候就crash掉了。
關(guān)于這個問題榆骚,大家可以使用我的demo進(jìn)行嘗試片拍,打開后圖中注釋的代碼后運行,先進(jìn)入內(nèi)存泄漏的頁面妓肢,然后返回上級捌省,再進(jìn)入這個頁面,程序崩潰碉钠,demo地址見底部纲缓。
3.3、 CoreFoundation框架里申請的內(nèi)存忘記釋放
請看下面這段代碼:
- (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)"這行代碼缺失喊废,就會引起內(nèi)存泄漏祝高,使用靜態(tài)分析可以輕易發(fā)現(xiàn)。
需要注意的是:“ __bridge”是將CoreFoundation框架的對象所有權(quán)交給Foundation框架來使用污筷,但是Foundation框架中的對象并不能管理該對象的內(nèi)存工闺。“ __bridge_transfer”是將CoreFoundation框架的對象所有權(quán)交給Foundation來管理瓣蛀,如果Foundation中對象銷毀斤寂,那么我們之前的對象(CoreFoundation)會一起銷毀。
所以__bridge_transfer這種橋接方式揪惦,以后就不用再自己手動管理內(nèi)存了遍搞。如果上面代碼里的“CFRelease(uuid_string_ref)”的注釋,uuid就會被銷毀器腋,程序運行到reurn 就崩潰溪猿。
3.4、NSTimer 不正確使用造成的內(nèi)存泄漏
3.4.1纫塌、NSTimer重復(fù)設(shè)置為NO的時候诊县,不會引起內(nèi)存泄漏
3.4.2、NSTimer重復(fù)設(shè)置為YES的時候措左,有執(zhí)行invalidate就不會內(nèi)存泄漏依痊,沒有執(zhí)行invalidate就會內(nèi)存泄漏,在 timer的執(zhí)行方法里調(diào)用invalidate也可以。
3.4.3胸嘁、中間target:控制器無法釋放瓶摆,是因為timer對控制器進(jìn)行了強(qiáng)引用,使用類方法創(chuàng)建的timer默認(rèn)加入了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 轉(zhuǎn)發(fā)給_target 去響應(yīng)
- (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的方法還是會在不斷的調(diào)用,如果對性能要求不那么嚴(yán)謹(jǐn)?shù)慕褪梗梢允褂眠@種方法荐吉,具體代碼見demo。
3.4.4口渔、重寫NSTimer:結(jié)合上面中間target的思路样屠,在timer內(nèi)部進(jìn)行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"
//不判斷是否響應(yīng),是為了不實現(xiàn)定時器的方法就報錯
[self.target performSelector:self.selector withObject:timer];
#pragma clang diagnostic pop
}else {
[self.timer invalidate];
self.timer = nil;
}
}
@end
3.4.5瞧哟、使用block創(chuàng)建定時器,需要正確使用block枪向,要執(zhí)行invalidate勤揩,否則也會內(nèi)存泄漏。這里涉及到block的內(nèi)存泄漏問題秘蛔,我會在下篇中一起講解陨亡。
其他內(nèi)存泄漏如通知和KVO、block循環(huán)引用 深员、NSThread造成的內(nèi)存泄漏請見下篇负蠕。
歡迎大家來我的小窩做客啊,里面記錄下了我進(jìn)步的點點滴滴倦畅,一切逆境只是前進(jìn)的理由遮糖,與君共勉。