内存泄漏的相关定义
OC当中内存管理方式:ARC/MRC
ARC:自动引用计数(系统自动管理内存),由开发人员开辟内存空间,但是不需要释放该内存空间,系统会自动释放该空间;本质上还是基于MRC的,只不过是系统自动添加了释放内存的方法;
MRC:手动引用计数(手动管理内存),由开发人员开辟内存空间,在使用完对象之后,释放该对象所占用的内存空间;释放时机由开发人员自己释放;
内存泄露:如果程序运行时一直分配内存而不及时释放无用的内存,程序占用的内存越来越大,直到把系统分配给该APP的内存消耗殚尽,程序因无内存可用导致崩溃,这样的情况我们称之为内存泄漏。
内存泄漏的常见场景
1.ARC只负责管理OC对象的内存,CoreGraphics、CoreFoundation等C语言框架下的对象需要手动管理内存。注意以creat,copy,retain作为关键字的函数都是需要释放内存的,注意配对使用。比如:CGColorCreate<-->CGColorRelease
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGColorSpaceRetain(rgbColorSpace);
解决方法
CGColorSpaceRelease(rgbColorSpace);
CGColorSpaceRelease(rgbColorSpace);
- block造成的循环引用
当前类持有的block中又使用了self或者实例变量都会造成循环引用;单例类持有的block中使用了self或者实例变量
self.myBlock = ^{
[self testAction];
//_myTitle = @"MemoryLeak";
};
[[NSNotificationCenter defaultCenter] addObserverForName:@"" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
[self testAction];
}];
解决方法(注意在对象持有的block内直接使用实例变量也会循环引用,一定要用弱化过的weakSelf)
__weak typeof(self)weakSelf = self;
self.myBlock = ^{
[weakSelf testAction];
};
delegate造成的循环引用,一般情况下设置代理属性时使用weak修饰即可
NSTimer & CADisplayLink
NSTimer会造成循环引用,timer会强引用target即self,在加入runloop的操作中,又引用了timer,所以在timer被invalidate之前,self也就不会被释放。
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
常见解决方法:
1.适当时机调用 invalidate方法
2.使用YYKit框架下的YYWeakProxy
YYWeakProxy原理:生成一个临时对象,让 timer 强引用这个临时对象,在这个临时对象中弱引用 self
[NSTimer timerWithTimeInterval:5 target:[YYWeakProxy proxyWithTarget:self] selector:@selector(timerAction1:) userInfo:nil repeats:YES];
vcA pushTo vcB, vcA 与 vcB 相互强引用,则vcA和vcB都不能正常释放,弱化其中一方即可打破循环引用,如果此时vcB的某个属性被vcA强引用,则pop后,vcB释放,但该属性不会被正常释放。
若项目中使用地图相关类,一定要检测内存情况,因为地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil,注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等。
这条是我从别的地方看的,自己以前做地图功能时也没在意这个大次数循环内存暴涨问题
该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *string = @"Abc";
string = [string lowercaseString];
NSLog(@"%@", string);
}
}
检测内存泄漏的方法
xcode自带的静态分析 Xcode -> Product -> Analyze
可以检测到如下这些问题:
(1) 逻辑缺陷,例如访问未初始化的变量;
(2) 内存管理缺陷,如内存泄露;
(3) 无用存储缺陷(永不会被访问的变量);
(4) 因未遵从项目用到的框架(frameworks)或类库(libraries)所规范的而导致的API使用缺陷;xcdoe自带工具 Instrument -> Leaks
先看看 Leaks,从苹果的开发者文档里可以看到,一个 app 的内存分三类:
Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
Abandoned memory: Memory still referenced by your application that has no useful purpose.
Cached memory: Memory still referenced by your application that might be used again for better performance.
其中 Leaked memory 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。
这个工具的使用不难,网上很多教程,我这里不再详述,我的感觉是效果不怎么样,没检测出什么有用的
xcdoe自带工具 Instrument -> Allocation
对于 Abandoned memory,可以用 Instrument 的 Allocations 检测出来。检测方法是用 Mark Generation 的方式,当你每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。举一个最简单的例子:
我们可以不断重复 push 和 pop 同一个 UIViewController,理论上来说,push 之前跟 pop 之后,app 会回到相同的状态。因此,在 push 过程中新分配的内存,在 pop 之后应该被 dealloc 掉,除了前几次 push 可能有预热数据和 cache 数据的情况。如果在数次 push 跟 pop 之后,内存还不断增长,则有内存泄露。因此,我们在每回 push 之前跟 pop 之后,都 Mark Generation 一下,以此观察内存是不是无限制增长。这个方法在 WWDC 的视频里:Session 311 - Advanced Memory Analysis with Instruments,以及苹果的开发者文档:Finding Abandoned Memory 里有介绍。
用这种方法来发现内存泄露还是很不方便的:
首先,你得打开 Allocations
其次,你得一个个场景去重复的操作
无法及时得知泄露,得专门做一遍上述操作,十分繁琐
Allocation使用细节见该链接: http://www.cnblogs.com/lxlx1798/p/6933195.html第三方检测工具MLeaksFinder
特点:
1.使用非常方便。用pod直接导入即可(pod 'MLeaksFinder'),对代码没有任何侵入。按照正常测试流程操作,如果有泄漏,在界面 pop/dismiss 2秒后会有alert弹框提示。
2.默认自动检测继承自 UIViewController 和 UIView 对象的内存泄露,而且也可以扩展以检测其它类型的对象
3.能准确显示出哪个对象没有被释放,并可以查看循环引用的关系(依赖Facebook开源的一个循环引用检测工具 FBRetainCycleDetector)
4.只在 debug 下开启,完全不影响 release 包,当然也可以打包时删除这个库减小包大小
原理:
我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。
具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是弹出alert弹框。这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会弹框,如果它没被释放(泄露了),-assertNotDealloc 就会被调用。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若2秒后没被释放,就会alert。
MLeaksFinder githup地址: https://github.com/Tencent/MLeaksFinder
facebook自动化检测内存泄漏: http://www.cocoachina.com/ios/20170102/18490.html
日常的一些内存使用优化
优化加载本地图片的缓存
使用imageNamed加载图片会有缓存,较大的图片和不常使用的本地图片可以用[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"btn_download_n@2x" ofType:@"png"]]加载,注意:使用该方法加载,如果图片名字包含@2x或@3x,一定也要加上,否则无法加载出来优化加载网络图片的缓存
适时地清理缓存。可以在适当的时机调用清除的方法,或者适当时机查询当前的缓存的大小(SDWebImage中提供了接口),超过一定缓存量再清除,这个看自己的需求了
[[SDImageCache sharedImageCache] clearMemory];
[[SDImageCache sharedImageCache] clearDiskOnCompletion:nil];
- 优化加载H5页面的缓存
用UIWebView加载网页,退出控制器内存不减,每次加载持续增涨
解决方法:
如果系统版本最低支持iOS8,直接将UIWebView替换成WKWebView即可。
UIWebView可在加载完成的代理方法中加上清除web缓存的代码
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//防止内存暴增
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:@"WebKitCacheModelPreferenceKey"];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"WebKitDiskImageCacheEnabled"];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"WebKitOfflineWebApplicationCacheEnabled"];
[[NSUserDefaults standardUserDefaults] synchronize];
}