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就行:
1
@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; } } @end3.4.5、使用block创建定时器,需要正确使用block,要执行invalidate,否则也会内存泄漏。这里涉及到block的内存泄漏问题,我会在下篇中一起讲解。
其他内存泄漏如通知和KVO、block循环引用 、NSThread造成的内存泄漏请见下篇。
demo地址请点击这里:https://github.com/zmfflying/ZMFBlogProject
----------------------------------------------------------------------------------------------------------------
接上篇,本篇主要讲解通知和 KVO 不移除观察者、block 循环引用 、NSThread 和 RunLoop一起使用造成的内存泄漏。
1、通知造成的内存泄漏
1.1、iOS9 以后,一般的通知,都不再需要手动移除观察者,系统会自动在dealloc 的时候调用 [[NSNotificationCenter defaultCenter]removeObserver:self]。iOS9 以前的需要手动进行移除。
原因是:iOS9 以前观察者注册时,通知中心并不会对观察者对象做 retain 操作,而是进行了 unsafe_unretained 引用,所以在观察者被回收的时候,如果不对通知进行手动移除,那么指针指向被回收的内存区域就会成为野指针,这时再发送通知,便会造成程序崩溃。
从 iOS9 开始通知中心会对观察者进行 weak 弱引用,这时即使不对通知进行手动移除,指针也会在观察者被回收后自动置空,这时再发送通知,向空指针发送消息是不会有问题的。
1.2、使用 block 方式进行监听的通知,还是需要进行处理,因为使用这个 API 会导致观察者被系统 retain。
请看下面这段代码:
[[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"11111"); }]; //发个通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];第一次进来打印一次,第二次进来打印两次,第三次打印三次。大家可以在 demo 中进行尝试,demo 地址见文章底部。
解决方法是记录下通知的接收者,并且在 dealloc 里面移除这个接收者就好了:
@property(nonatomic, strong) id observer;
self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"11111"); }]; //发个通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil]; - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self.observer name:@"notiMemoryLeak" object:nil]; NSLog(@"hi,我 dealloc 了啊"); }2、KVO 造成的内存泄漏
2.1、现在一般的使用 KVO,就算不移除观察者,也不会有问题了 请看下面这段代码:
- (void)kvoMemoryLeak { MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds]; [ self.view addSubview:view]; [view addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil]; //调用这两句主动激发kvo 具体的原理会有后期的kvo详解中解释 [view willChangeValueForKey:@"frame"]; [view didChangeValueForKey:@"frame"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"frame"]) { NSLog(@"view = %@",object); }这种情况不移除也不会有问题,我猜测是因为 view 在控制器销毁的时候也销毁了,所以 view 的 frame 不会再发生改变,不移除观察者也没问题,所以我做了一个猜想,要是观察的是一个不会销毁的对象会怎么样?当观察者已经销毁,被观察的对象还在发生改变,会有问题吗?
2.2、观察一个不会销毁的对象,不移除观察者,会发生不确定的崩溃。
接上面的猜测,首先创建一个单例对象 MFMemoryLeakObject,有一个属性title:
@interface MFMemoryLeakObject : NSObject @property (nonatomic, copy) NSString *title; + (MFMemoryLeakObject *)sharedInstance; @end #import "MFMemoryLeakObject.h" @implementation MFMemoryLeakObject + (MFMemoryLeakObject *)sharedInstance { static MFMemoryLeakObject *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; sharedInstance.title = @"1"; }); return sharedInstance; } @end然后在 MFMemoryLeakView 对 MFMemoryLeakObject 的 title 属性进行监听:
#import "MFMemoryLeakView.h" #import "MFMemoryLeakObject.h" @implementation MFMemoryLeakView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor whiteColor]; [self viewKvoMemoryLeak]; } return self; } #pragma mark - 6.KVO造成的内存泄漏 - (void)viewKvoMemoryLeak { [[MFMemoryLeakObject sharedInstance] addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"title"]) { NSLog(@"[MFMemoryLeakObject sharedInstance].title = %@",[MFMemoryLeakObject sharedInstance].title); } }最后在控制器中改变 title 的值,view 销毁前改变一次,销毁后改变一次:
//6.1、在MFMemoryLeakView监听一个单例对象 MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:view]; [MFMemoryLeakObject sharedInstance].title = @"2"; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [view removeFromSuperview]; [MFMemoryLeakObject sharedInstance].title = @"3"; });经过尝试,第一次没有问题,第二次就发生崩溃,报错野指针,具体的大家可以用 demo 做测试,demo 地址见底部。
解决方法也很简单,在view 的 dealloc 方法里移除观察者就好:
- (void)dealloc { [[MFMemoryLeakObject sharedInstance] removeObserver:self forKeyPath:@"title"]; NSLog(@"hi,我MFMemoryLeakView dealloc 了啊"); }总的来说,写代码还是规范一点,有观察就要有移除,不然项目里容易产生各种欲仙欲死的 bug。KVO 还有一个重复移除导致崩溃的问题,请参考这篇文章: https://www.cnblogs.com/wengzilin/p/4346775.html。
3、block 造成的内存泄漏 block 造成的内存泄漏一般都是循环引用,即 block 的拥有者在 block 作用域内部又引用了自己,因此导致了 block 的拥有者永远无法释放内存。
本文只讲解 block 造成内存泄漏的场景分析和解决方法,其他 block 的原理会在之后 block 的单章里进行讲解。
3.1、block 作为属性,在内部调用了 self 或者成员变量造成循环引用。
请看下面这段代码,先定义一个 block 属性:
typedef void (^BlockType)(void); @interface MFMemoryLeakViewController () @property (nonatomic, copy) BlockType block; @property (nonatomic, assign) NSInteger timerCount; @end然后进行调用:
#pragma mark - 7.block 造成的内存泄漏 - (void)blockMemoryLeak { // 7.1 正常block循环引用 self.block = ^(){ NSLog(@"MFMemoryLeakViewController = %@",self); NSLog(@"MFMemoryLeakViewController = %zd",_timerCount); }; self.block(); }这就造成了 block 和控制器的循环引用,解决方法也很简单, MRC 下使用 __block、ARC 下使用 __weak 切断闭环,成员变量使用 -> 的方式访问就可以解决了。
需要注意的是,仅用 __weak 所修饰的对象,如果被释放,那么这个对象在 block 执行的过程中就会变成 nil,这就可能会带来一些问题,比如数组和字典的插入。
所以建议在 block 内部对__weak所修饰的对象再进行一次强引用,这样在 Block 执行的过程中,这个对象就不会被置为nil,而在Block执行完毕后,ARC 下这个对象也会被自动释放,不会造成循环引用:
__weak typeof(self) weakSelf = self; self.block = ^(){ //建议加一下强引用,避免weakSelf被释放掉 __strong typeof(weakSelf) strongSelf = weakSelf; NSLog(@"MFMemoryLeakViewController = %@",strongSelf); NSLog(@"MFMemoryLeakViewController = %zd",strongSelf->_timerCount); }; self.block();3.2、NSTimer 使用 block 创建的时候,要注意循环引用
请看这段代码
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"MFMemoryLeakViewController = %@",self); }];从 block 的角度来看,这里是没有循环引用的,其实在这个类方法的内部,有一个 timer 对 self 的强引用,所以也要使用 __weak 切断闭环,另外,这种方式创建的 timer,repeats 为 YES 的时候,也需要进行invalidate 处理,不然定时器还是停不下来。
@property(nonatomic,strong) NSTimer *timer; __weak typeof(self) weakSelf = self; _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"MFMemoryLeakViewController = %@",weakSelf); }]; - (void)dealloc { [_timer invalidate]; NSLog(@"hi,我MFMemoryLeakViewController dealloc 了啊"); }4、NSThread 造成的内存泄漏
NSThread 和 RunLoop 结合使用的时候,要注意循环引用问题,请看下面代码:
- (void)threadMemoryLeak { NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil]; [thread start]; } - (void)threadRun { [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }导致问题的就是 “[[NSRunLoop currentRunLoop] run];” 这一行代码。原因是 NSRunLoop 的 run 方法是无法停止的,它专门用于开启一个永不销毁的线程,而线程创建的时候也对当前当前控制器(self)进行了强引用,所以造成了循环引用。
解决方法是创建的时候使用block方式创建:
- (void)threadMemoryLeak { NSThread *thread = [[NSThread alloc] initWithBlock:^{ [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }]; [thread start]; }这样控制器是可以得到释放了,但其实这个线程还是没有销毁,就算调用 “CFRunLoopStop(CFRunLoopGetCurrent());” 也无法停止这个线程,因为这个只能停止这一次的 RunLoop,下次循环依然可以继续进行下去。具体的解决方法我会在 RunLoop 的单章里进行讲解。
5、webView 造成的内存泄漏
目前 iOS 的 webView 有UIWebView 和 WKWebView 两种。
5.1、UIWebView 2020年4月份停止接受使用UIWebView api的APP
UIWebView 内存问题应该是众所周知了吧,Apple官方也承认了内存泄露确实存在,所以在 iOS8 推出了功能和性能都更加强大WKWebView。
大家可以看看下面这段代码:
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds]; webView.backgroundColor = [UIColor whiteColor]; NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]]; [webView loadRequest:requset]; [self.view addSubview:webView];就这么一段简单的代码,打开网页的时候,内存暴涨 200M,就算返回到上级页面,webView 销毁,内存也依然比原来高了 50M 左右,就算在控制器的 dealloc 里加载一个空的 url 也没有作用,这个大家可以用demo进行尝试。
5.2、WKWebView
总的来说,WKWebView 不管是性能还是功能,都要比 UIWebView 强大很多,本身也不存在内存泄漏问题,但是,如果开发者使用不当,还是会造成内存泄漏。请看下面这段代码:
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.userContentController = [[WKUserContentController alloc] init]; [config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"]; WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config]; wkWebView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:wkWebView]; NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]]; [wkWebView loadRequest:requset];这样看起来没有问题,但是其实 “addScriptMessageHandler” 这个操作,导致了 wkWebView 对 self 进行了强引用,然后 “addSubview”这个操作,也让 self 对 wkWebView 进行了强引用,这就造成了循环引用。
解决方法就是在合适的机会里对 “MessageHandler” 进行移除操作,比如:
@property (nonatomic, strong) WKWebView *wkWebView; - (void)webviewMemoryLeak { // 9.2 WKWebView WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.userContentController = [[WKUserContentController alloc] init]; [config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"]; _wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config]; _wkWebView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:_wkWebView]; NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]]; [_wkWebView loadRequest:requset]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [_wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"WKWebViewHandler"]; }在 demo 里我选择了在 “ viewDidDisappear”方法里进行移除操作,这样控制器就可以得到释放了。
本次的内存泄漏分析,就写到这里,因为本人水平所限,很多地方还是没能讲得足够深入,欢迎诸位进行指正。
demo地址: https://github.com/zmfflying/ZMFBlogProject.git