内存泄漏以及内存排查技术分享

内存泄漏

区分两个基本概念:

· 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。
一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。(因此,开发中我们要尽量避免内存泄漏的出现)

·  内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用。
通俗理解就是内存不够用了,通常在运行大型应用或游戏时,应用或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。最终导致机器重启或者程序crash。

 

内存泄漏的原因分析

目前,在ARC环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致dealloc()方法无法被调用。主要原因大概有一下几种类型:

 

一、AFNetWorking的NSURLSession不能释放

在封装网络请求类时需注意的是需要将请求队列管理者AFHTTPSessionManager声明为单例创建形式。在相同配置下保证AFHTTPSessionManager只有一个,进行全局管理,因此我们可以通过单例形式进行解决.

 

二、Block循环引用

防止Block循环引用就是要防止对象之间引用的闭环出现.

 

1. 那么请问:什么时候在block 里面用 self,不需要使用 weak self?

答案

当block 本身不被 self 持有,而被别的对象持有,同时不产生循环引用的时候,就不需要使用 weak self 了。最常见的代码就是 UIView 的动画代码,我们在使用 UIView 的 animateWithDuration:animations 方法 做动画的时候,并不需要使用 weak self,因为引用持有关系是:

UIView 的某个负责动画的对象持有了 block 
block 持有了 self 
因为self 并不持有 block,所以就没有循环引用产生,因为就不需要使用 weak self 了。

[UIView animateWithDuration:0.2 animations:^{

    self.alpha = 1;

}];

当动画结束时,UIView 会结束持有这个 block,如果没有别的对象持有 block 的话,block 对象就会释放掉,从而 block 会释放掉对于 self 的持有。整个内存引用关系被解除。

 

weakSelf 与 strongSelf

2.为什么block 里面还需要写一个 strong self,如果不写会怎么样? 

 

在block 中先写一个 strong self,其实是为了避免在 block 的执行过程中,突然出现 self 被释放的尴尬情况。通常情况下,如果不这么做的话,还是很容易出现一些奇怪的逻辑,甚至闪退。

__weak typeof(self) weakSelf = self; 

[self doSomeBackgroundJob:^{ 

__strong typeof(weakSelf) strongSelf = weakSelf;

 if (strongSelf) {

 ... 

}];

3.需要不使用weak self 的场景是:你需要构造一个循环引用,以便保证引用双方都存在。比如你有一个后台的任务,希望任务执行完后,通知另外一个实例.

在开源的YTKNetwork 网络库的源码中,就有这样的场景。

在YTKNetwork 库中,我们的每一个网络请求 API 会持有回调的 block,回调的 block 会持有 self,而如果 self 也持有网络请求 API 的话,我们就构造了一个循环引用。虽然我们构造出了循环引用,但是因为在网络请求结束时,网络请求 API 会主动释放对 block 的持有,因此,整个循环链条被解开,循环引用就被打破了,所以不会有内存泄漏问题。代码其实很简单,如下所示:

- (void)clearCompletionBlock {

    // nil out to break the retain cycle.

    self.successCompletionBlock = nil;

    self.failureCompletionBlock = nil;

}

 

解决循环引用问题主要有两个办法:

第一个办法是「事前避免」,我们在会产生循环引用的地方使用weak 弱引用,以避免产生循环引用。 
第二个办法是「事后补救」,我们明确知道会存在循环引用,但是我们在合理的位置主动断开环中的一个引用,使得对象得以回收。 

 

三、delegate循环引用问题

delegate循环引用问题比较基础,只需注意将代理属性修饰为weak即可

使用weak修饰就是为了防止ViewController和UITableView相互强引用内存无法释放的问题

 

四、NSNotification移除

通知NSNotification在注册者被回收时需要手动移除,是一直以来的使用准则。原因是在MRC时代,通知中心持有的是注册者的unsafe_unretained指针,在注册者被回收时若不对通知进行手动移除,则指针指向被回收的内存区域,成为野指针。这时再发送通知,便会造成crash。而在iOS 9以后,通知中心持有的是注册者的weak指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。我们知道,向空指针发送消息是不会有问题的。
但是有一个例外。如果用

- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));这个API来注册通知,可以直接传入block类型参数。使用这个API会导致注册者被系统retain,因此仍然需要像以前一样手动移除通知,同时这个block类型参数也需注意避免循环引用。

 

问:

如果

viewWillAppear中添加通知
viewWillDisappear中移除通知

会有什么问题?为什么?

 

正常操作:

viewDidLoad中添加通知

dealloc中移除通知

 

五、NSTimer循环引用

如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用

[NSTimer scheduledTimerWithTimeInterval:1.0 

                                 target:self 

                               selector:@selector(updateTime:) 

                               userInfo:nil 

                                repeats:YES];

时的 target:self 就增加了ViewController的return count,如果你不将这个timer invalidate,将别想调用dealloc。

 检测报告->概况中定时器未释放,导致内存泄漏的原因及解决方案。

解决方案:

1. viewWillDisapper 中移除定时器,但是这样会导致新的问题,push到下一个页面后定时器失效,导致倒计时时间停滞。

2. 采用弱引用的方式打断循环,定时器不能释放的原因就是,self持有timer,timer又持有self,导致不能释放,那么在其中以弱引用形式打断循环引用,就不会出现内存泄漏的问题。

 

六、非OC对象内存处理

非OC对象需要手动释放

对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意

解决:非OC对象需要手动释放

 

七、地图类处理

  若项目中使用地图相关类,一定要检测内存情况,因为地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil,注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等。

- (void)clearMapView{

    self.mapView = nil;

    self.mapView.delegate =nil;

    self.mapView.showsUserLocation = NO;

    [self.mapView removeAnnotations:self.annotations];

    [self.mapView removeOverlays:self.overlays];

    [self.mapView setCompassImage:nil];

}

八、UIWebView、WKWebView引起的内存泄漏

咱们只说WKWebview造成的内存泄漏

在OC与JS互相调用时,
-(void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
如果不remove掉会使得此VC不走dealloc,导致内存泄漏。

项目中也有遇到,处理方法如下:  

这里userContentController持有了self ,然后 userContentController 又被configuration持有,最终呗webview持有,然后webview是self的一个私有变量,所以self也持有self,所以,这个时候有循环引用的问题存在,导致界面被pop或者dismiss之后依然会存在内存中。不会被释放.

正常操作:

1. js注册方法放在viewDidLoad中,移除方法放在dealloc中。会出现循环引用情况,需要改为弱引用

2. js注册方法放在viewWillAppear,移除方法放在viewWillDisappear。

项目中采取:js注册方法放在viewWillAppear,移除方法放在viewWillDisappear。同时弱引用

 

九、ViewController的子视图对self的持有

与block产生循环引用的原因一样

如上面的userContentController中产生循环引用的问题

 

十、大次数循环内存暴涨问题

for (int i = 0; i < 100000; i++) {

        NSString *string = @"Abc";

        string = [string lowercaseString];

        string = [string stringByAppendingString:@"xyz"];

        NSLog(@"%@", string);

}

该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

for (int i = 0; i < 100000; i++) {

        @autoreleasepool {

            NSString *string = @"Abc";

            string = [string lowercaseString];

            string = [string stringByAppendingString:@"xyz"];

            NSLog(@"%@", string);

        }

}

注意:即便临时对象在调用完方法后就不再使用了,它们也依然处于存活状态,因为目前它们都在自动释放池里,等待系统稍后进行回收。但自动释放池却要等到该线程执行下一次事件循环时才会清空,这就意味着在执行for循环时,会有持续不断的新的临时对象被创建出来,并加入自动释放池。要等到结束for循环才会释放。在for循环中内存用量会持续上涨,而等到结束循环后,内存用量又会突然下降。

而如果把循环内的代码包裹在“自动释放池”中,那么在循环中自动释放的对象就会放在这个池,而不是在线程的主池里面。

新增的自动释放池可以减少内存用量,因为系统会在块的末尾把这些对象回收掉。而上述这些临时对象,正在回收之列。

自动释放池的机制就像“栈”。系统创建好池之后,将其压入栈中,而清空自动释放池相当于将池从栈中弹出。在对象上执行自动释放操作,就等于将其放入位于栈顶的那个池。

实验验证

我们可以通过实验进行验证。新建工程加入上述代码,并关闭ARC(不然是看不到区别的)。

在未添加autoreleasepool时,我们的堆内存实时分配情况如下图:



大家可以看到Persistent Bytes不断增加,到达10W次的创建峰值后(出for循环)开始逐步释放。因此图像是一个向上凸的曲线。

而在加入autoreleasepool后,我们看到如下的曲线:

可以发现尽管字符串在不断地创建,但由于得到了及时的释放,堆内存始终保持在一个很低的水平。

@autoreleasepool小结

自动释放池排布在栈中,对象受到autorelease消息后,系统将其放入栈顶的池里。

合理运用自动释放池,可以降低程序的内存峰值。

 

十一.MRC文件

如项目中XLCycleScrollView,因轮播图的计数label没有手动释放,导致的内存泄漏

在ARC机制的项目下使用MRC机制的文件,需要设置对应文件的Compiler Flags为-fno-objc-arc。

在MRC机制的项目下使用ARC机制的文件,需要设置对应文件的Compiler Flags为-fobjc-arc。

需要手动释放,因_cycleCountLabel没有释放导致内存泄漏

如何区分项目是否为ARC:

十二、分类中直接添加属性,没有进行属性管理。

如项目中加载组建UIView+LoadingNormalView 

因在分类中直接添加属性,没有进行属性管理。不受VC管理,隐藏操作后没有置nil,导致内存泄漏

分类添加属性要用动态添加

 

排查方法

一、静态内存泄漏分析方法

通过xcode打开项目,然后点击product-->Analyze,这样就开始对项目进行静态内存泄漏分析,分析结果如下图右侧的图所示。根据分析结果进行休整之后在进行分析就好了。静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,如果需要,我们需要将对项目进行更为完善的内存泄漏分析和排查。

如上面非oc对象没有手动释放导致的内存泄漏,就可以通过静态分析检查出来。

 

二、动态内存泄漏分析方法

分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。具体操作是通过xcode打开项目,然后点击product-->profile,

选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call Tree 和Hide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。

三、通过工具MLeaksFinder

pod 'MLeaksFinder'

项目在Debug 模式下直接运行就好,这款库在上架Release 模式下不用删除也不影响项目

Memory Leak

(

    MyTableViewController,

    UITableView,

    UITableViewWrapperView,

    MyTableViewCell

)

FBRetainCycleDetector 检测该对象有没有循环引用即可。

循环引用的输出信息如下:

1

2

3

4

(

    "-> MyTableViewCell ",

    "-> _callback -> __NSMallocBlock__ "

)

上面的信息表示,MyTableViewCell 有一个强引用的成员变量 _callback,该变量的类型是 __NSMallocBlock__,在 _callback 里,又强引用了 MyTableViewCell 造成循环引用。

 

内存泄漏问题,原因排查难度颇大,但是项目中又不能产生内存泄漏问题,所以最好写代码的时候多多注意这方面的问题,然后每个功能完成的时候,多去测试一下,解决项目中内存泄漏的问题。

内存泄漏问题导致的影响极大,注意性能优化,为用户提供更好的体验感。

任重道远。

仅以记录此次内存管理的技术分享。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值