- 内存溢出 (out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
- 内存泄露( memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
一般易造成泄漏的点:
- Retain Cycle,Block强引用
- NSTimer释放不当
- 第三方提供方法造成的内存泄漏
- CoreFoundation方式申请的内存,忘记释放
eg:
block循环:
[cell setSelectTagCityBlock:^(NSIndexPath *indexPath, NSInteger index){
[self tableView:_tableViewCityList didSelectRowAtIndexPath:indexPath index:index];
}];
一般用weak打破保留环
@WeakObj(self)
[cell setSelectTagCityBlock:^(NSIndexPath *indexPath, NSInteger index){
if (selfWeak)
{
[selfWeak tableView:selfWeak.tableViewCityList didSelectRowAtIndexPath:indexPath index:index];
}
}];
AFNetWorking上的经典代码,防止循环引用的
//创建__weak弱引用,防止强引用互相持有
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
//创建局部__strong强引用,防止多线程情况下weakSelf被析构
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
weak 本身是可以避免循环引用的问题的,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 strong 的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题
block 本身无法避免循环引用的问题,但是我们可以通过在 block 内部手动把 blockObj 赋值为 nil 的方式来避免循环引用的问题。另外一点就是 block 修饰的变量在 block 内外都是唯一的,要注意这个特性可能带来的隐患。
_timer = [NSTimer timerWithTimeInterval:[refreshTime integerValue]
target:self
selector:@selector(doFSearchDoubleBackNumberRequest:)
userInfo:searchResult
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
Timer 添加到 Runloop 的时候,会被 Runloop 强引用。
Timer 又会有一个对 Target 的强引用。
所以说如果不对Timer进行释放,Timer的targer(self)也一直不会被释放。
有时候我们我们对某个Timer的targer设置了nil。但没设置[timer invalidate]。
其实这个对象还是没被释放的。timer对应的执行方法也一直会在线程中执行。容易造成内存泄露。
注:repeats:NO不会强引用
一、排查方法
我们知道,iOS开发中对内存管理的要求非常严格,一旦存在内存泄漏,后果是非常严重的,会导致程序非常容易崩溃。尽管目前iOS开发基本上都是采用的ARC方式进行内存管理,但是一不小心就会存在内存泄漏的问题。
首先,我们需要定位内存泄漏的问题,目前比较常用的内存泄漏的排查方法有两种,都在xcode中可以直接使用:静态分析方法(Analyze)和动态分析方法(Instrument的leak)。
1.1 静态内存泄漏分析方法 (command + shift + b)
主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、Api调用错误:未包含使用的库和框架。
通过xcode打开项目,然后点击product-->Analyze ,如下图左侧的图所示,这样就开始对项目进行静态内存泄漏分析,分析结果如下图右侧的图所示。根据分析结果进行休整之后在进行分析就好了。
静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,如果需要,我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments中的Leaks方法进行排查。
1.2 动态内存泄漏分析方法
分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。具体操作是通过xcode打开项目,然后点击product-->profile,如下图所示。
按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择Leaks选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:
点击左上角的红色圆点,这时项目开始启动了,由于leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call Tree 和Hide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。
- Separate by Thread:按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
- Invert Call Tree:反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
- Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
- Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条
二、内存泄漏的原因分析
在目前主要以ARC进行内存管理的开发模式,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,这就会导致dealloc()方法无法被调用。主要原因大概有一下几种类型。
2.1 ViewController中存在NSTimer
如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];
时的target:self
就增加了ViewController的return count
,如果你不将这个timer invalidate
,将别想调用dealloc
。
2.2 ViewController中的代理delegate
一个比较隐秘的因素,你去找找与这个类有关的代理,有没有强引用属性?如果你这个VC需要外部传某个Delegate进来,来通过Delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量assign
或者weak
,否则你的VC会持续持有这个delegate,直到它自身被释放。
2.3 ViewController中Block
这个可能就是经常容易犯的一个问题了,Block体内使用实例变量也会造成循环引用,使得拥有这个实例的对象不能释放。因为该block本来就是当前viewcontroller的一部分,现在盖子部门又强引用self,导致循环引用无法释放。 例如你这个类叫OneViewController,有个属性是NSString *name; 如果你在block体中使用了self.name,或者_name,那样子的话这个类就没法释放。 要解决这个问题其实很简单,就是在block之前申明当前的self引用为弱引用
即可。
//MRC下代码如下
__block Viewcontroller *weakSelf = self;
//ARC下代码如下
__weak Viewcontroller *weakSelf = self;
2.4 ViewController的子视图对self的持有
这个问题也是我的项目中内存泄漏的问题所在。我们有时候需要在子视图或者某个cell中点击跳转等操作,需要在子视图或cell中持有当前的ViewController对象,这样跳转之后的back键才能直接返回该页面,同时也不销毁当前ViewController。此时,你就要注意在子视图或者cell中对当前页面的持有对象不能是强引用,尽量assign或者weak,否则会造成循环引用,内存无法释放。
问题1:
关于"User-facing text should use localized string macro”警告的解决
使用静态分析,发现工程中有1000多个“User-facing text should use localized string macro”警告,如下图:
给label赋值的时候,提示
面向用户的文本应该使用本地化的字符串宏
此为代码中配置了本地化,面向用户的应该用字符串宏,而我们直接赋值为汉字.
解决方法:
问题2:
NSMutableArray *tempMutArr = [NSMutableArray arrayWithCapacity:0];
if ([self.clickedButtonTpye isEqualToString:KClickedButtonTypeLast]) {
tempMutArr = self.lastDataSourceArr;
}else{
tempMutArr = self.hotDataSourceArr;
}
用Analyze检测后报出了下面的错误
Value stored to 'dataArr' during its initialization is never read
仔细看了代码后才发现代码存在一个细节上的问题,也正是这个细节导致的内存泄漏。在项目里我的self.lastDataSourceArr和self.hotDataSourceArr都是已经初始化过的可变数组,但是在if里我又重新初始化了一个新的可变数组,并且把之前已经初始化过的可变数组赋值给了它,这就是内训泄漏的问题所在,看似代码没有大的问题,但是其实已经造成了内存泄漏。原因是:因为self.lastDataSourceArr和self.hotDataSourceArr是已经创建并且分配过内存的可变数组了,
但是我把这些数组又赋值给了重新创建并分配了内存的可变数组tempMutArr,所以这样就出现了一个数据源却申请了两块内存的情况,那么就存在一块内存空闲了,所以就存在了内存泄漏。
其实本意很简单,就是想把两个可变数组分情况赋值给可变数组tempMutArr,那么我们完全可以这样做:
NSMutableArray *tempMutArr;
if ([self.clickedButtonTpye isEqualToString:KClickedButtonTypeLast]) {
tempMutArr = self.lastDataSourceArr;
}else{
tempMutArr = self.hotDataSourceArr;
}
这样只做了一个可变数组tempMutArr的声明,不需要给它分配实际内存,就可以赋值了,其实只是声明了这样一个对象,使其持有对原有数据对象的引用即可