本文将从以下两个层面解决iOS内存泄漏问题:
- 内存泄漏排查方法(工具)
- 内存泄漏原因分析(解决方案)
在正式开始前,我们先区分两个基本概念:
- 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。
一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash
。(因此,开发中我们要尽量避免内存泄漏的出现)- 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用。
通俗理解就是内存不够用了,通常在运行大型应用或游戏时,应用或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。最终导致机器重启
或者程序crash
。
简单来说:
概念 | 区别说明 |
---|---|
内存泄漏 | 供应方(操作系统)能提供给需求方(App)的内存越来越少。 |
内存溢出 | 需求方(App)需要的内存过大,超过供应方(操作系统)负载。 |
一、排查方法
我们知道,iOS开发有“ARC机制”帮忙管理内存,但在实际开发中,如果处理不好堆空间上的内存还是会存在内存泄漏的问题。如果内存泄漏严重,最终会导致程序的崩溃。
首先,我们需要检查我们的App有没有内存泄漏,并且快速定位到内存泄漏的代码。目前比较常用的内存泄漏的排查方法有两种,都在Xcode中可以直接使用:
- 第一种:静态分析方法(
Analyze
) - 第二种:动态分析方法(
Instrument
工具库里的Leaks
)。一般推荐使用第二种。
1.1 静态内存泄漏分析方法:
-
第一步:通过Xcode打开项目,然后点击Product->Analyze,开始进入静态内存泄漏分析。
如下图所示:
第二步:等待分析结果。
第三步:根据分析的结果对可能造成内存泄漏的代码进行排查,如下图所示。
PS:静态内存泄漏分析能发现大部分问题,但只是静态分析,并且并不准确,只是有可能发生内存泄漏。一些动态内存分配的情形并没有分析。如果需要更精准一些,那就要用到下面要介绍的动态内存泄漏分析方法(Instruments工具中的
Leaks
方法)进行排查。
1.2 动态内存泄漏分析方法:
静态内存泄漏分析不能把所有的内存泄漏排查出来,因为有的内存泄漏发生在运行时,当用户做某些操作时才发生内存泄漏。这是就要使用动态内存泄漏检测方法了。
步骤如下:
- 第一步:通过Xcode打开项目,然后点击Product->Profile,如下图所示:
- 第二步:按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择
Leaks
选项,点击右下角的【choose】按钮。如下图:
- 第三步:这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:
点击左上角的红色圆点,这时项目开始启动了,由于Leaks
是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call Tree
和Hide System Libraries
,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。
PS:AFHTTPSessionManager内存泄漏是一个很常见的问题:解决方法有两种:点击这里
二、内存泄漏的原因分析
目前,在ARC环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致dealloc()方法无法被调用。主要原因大概有一下几种类型:
2.1 ViewController中存在NSTimer
如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];
- 理由:这时
target: self
,增加了ViewController的retain count
,
即self
强引用timer
,timer
强引用self
。造成循环引用。 - 解决方案:在恰当时机调用
[timer invalidate]
即可。
2.2 ViewController中的代理delegate
代理在一般情况下,需要使用weak修饰。如果你这个VC需要外部传某个delegate进来,通过delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量使用weak修饰,否则你的VC会持续持有这个delegate,直到代理自身被释放。
- 理由:如果代理用
strong
修饰,ViewController(self
)会强引用View
,View
强引用delegate
,delegate
内部强引用ViewController(self
)。造成内存泄漏。 - 解决方案:代理尽量使用
weak
修饰。
举个例子:代理一般用weak
修饰,避免循环引用。
@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>
@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;
@end
@interface QiAnimationButton : UIButton
@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;
- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画
2.3 ViewController中Block
在我们日常开发中,如果block使用不当,很容易导致内存泄漏。
- 理由:如果
block
被当前ViewController(self
)持有,这时,如果block内部再持有ViewController(self
),就会造成循环引用。 - 解决方案:在
block
外部对弱化self
,再在block内部强化已经弱化的weakSelf
For Example:
__weak typeof(self) weakSelf = self;
[self.operationQueue addOperationWithBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (completionHandler) {
KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);
completionHandler([strongSelf serialReaderWithRequest:request]);
}
}];