iOS 内存泄露监测 144 做者 谢谢生活 已关注 2017.05.19 17:38* 字数 4235 阅读 209评论 0喜欢 6 iOS可能存在的内存泄露: block 循环引用。当一个对象有一个block属性,而block属性又引用这个对象自己那么要形成循环引用。这个时候就用___weak声明下对象,用对象的弱引用指针。 头文件相互包含。那么先在.h文件用前向引用声明,@class(类名);而后在.m文件导入#import " AHMessageCell"(类头文件) 移除通知 [[NSNotificationCenter defaultCenter]removeObserver:self];、 移除NSTimer [_timer invalidate]; _timer = nil; 移除观察者 //添加观察者 [self addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>] //移除观察者 [self removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>]; timer,观察者,通知的移除。通常的开发者都是放到dealloc中,可是这样不能保证必定可以移除成功。能够更加实际状况移除,能够在viewWillAppear中添加,viewWillDisappear中移除,也能够强制移除。 iOS内存泄露测试:能够用xcode自带instrument工具,如:leaks、Analyze、allocation,也能够用第三方工具。 一: leaks 打开Xcode7自带的Instruments  打开Instruments 按上面操做,build成功后跳出Instruments工具,选择Leaks选项 选择以后界面以下图: 打开leaks 到这里以后,咱们前期的准备工做作完啦,下面开始正式的测试! 1.选中Xcode先把程序(command + R)运行起来 2.再选中Xcode,按快捷键(command + control + i)运行起来,此时Leaks已经跑起来了 3.因为Leaks是动态监测,因此咱们须要手动操做APP,一边操做,一边观察Leaks的变化,当出现红色叉时,就监测到了内存泄露,点击右上角的第二个,进行暂停检测(也可继续检测,当多个时暂停,一次处理了多个).如图所示: 4.下面就是定位修改了,此时选中有红色柱子的Leaks,下面有个"田"字方格,点开,选中Call Tree显示以下图界面 找到内存泄露位置 5.下面就是最关键的一步,在这个界面的右下角有若干选框,选中Invert Call Tree 和Hide System Libraries,(红圈范围内)显示以下: 监测回调函数 到这里就算基本完成啦,这里显示的就是内存泄露代码部分,那么如今还差一步:定位! 6.选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,如图所示  查看回调函数 7.找到了内存泄露的地方,那么咱们就能够修改便可。 二:Analyze—静态分析 顾名思义,静态分析不须要运行程序,就能检查到存在内存泄露的地方。 使用方法:打开Xcode,command + shift + B;或者Xcode - Product - Analyze; 常见的三种泄露情形: (1)建立了一个对象,可是并无使用。Xcode提示信息: Value Stored to 'number' is never read 。翻译一下:存储在'number'里的值从未被读取过。 (2)建立了一个(指针可变的)对象,且初始化了,可是初始化的值一直没读取过。Xcode提示信息: Value Stored to 'str' during its initialization is never read (3)调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息: Potential leak of an object stored into 'subImageRef' 。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。 贴上Demo代码: /** * 情 形 一:建立了一个对象,可是并无使用。 * 提示信息:Value Stored to 'number' is never read * 翻译一下:存储在'number'里的值从未被读取过, */ - (void)leakOne { NSString *str1 = [NSString string]; NSNumber *number; number = @(str1.length); /* 说咱们没有读取过它,那就读取一下,好比打开下面这句代码,对它发送class消息,就再也不会有这个提示了。 固然最好的方法仍是将有关number的代码都删掉,由于,你只对number赋值,又不使用,那干吗建立出来呢。 这是一个比较常见和典型的错误,也很容易检查出来 */ // [number class]; } /** * 情 形 二:建立了一个(指针可变的)对象,且初始化了,可是初始化的值一直没读取过。 * 提示信息:Value Stored to 'str' during its initialization is never read */ - (void)leakTwo { NSString *str = [NSString string]; // 建立并初始化str,此时已经有一个内存单元保存str初始化的值 // NSString *str; // 这样就内存不泄露,由于str是可变的,只须要先声明就行。 // printf("str前 = %p\n",str); str = @"ceshi"; // str被改变了,指向了"ceshi"所在的地址,指针改变了,但以前保存初始化值的内存空间还未释放,保存str初始化值的内存单元泄露了。 // printf("str后 = %p\n",str); // 指针改变了 [str class]; // 再举两个例子,同理 NSArray *arr = [NSArray array]; // printf("arr前 = %p\n",arr); // NSArray *arr; // 这样就内存不泄露 arr = @[@"1",@"2"]; // printf("arr后 = %p\n",arr); // 指针改变了 [arr class]; CGRect rect = self.view.frame; // CGRect rect = CGRectZero; // 这样就内存不泄露 rect = CGRectMake(0, 0, 0, 0); NSLog(@"rect = %@",NSStringFromCGRect(rect)); } /** * 情 形 三:调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。 * 提示信息:Potential leak of an object stored into 'subImageRef' * 翻译一下:subImageRef对象的内存单元有潜在的泄露风险 */ - (void)leakThree { CGRect rect = CGRectMake(0, 0, 50, 50); UIImage *image; CGImageRef subImageRef = CGImageCreateWithImageInRect(image.CGImage, rect); // subImageRef 引用计数 + 1; UIImage* smallImage = [UIImage imageWithCGImage:subImageRef]; // 应该调用对应的函数,让subImageRef的引用计数减1,就不会泄露了 // CGImageRelease(subImageRef); [smallImage class]; UIGraphicsEndImageContext(); } 监测结果: 可能存在内存泄露的地方 三:allocation使用 这个时候咱们经过Allocation能够进行内存分析,将Xcode切换为Release状态,经过Product→Profile(Cmd+i)找到Allocations:  代开allocation 1.红色的按钮是表示中止和启动应用程序,不要理解成了暂停,Objective-C全部的对象都是在堆上分配的,记得勾选一下All Heap Allocations:  开始监测 2.点击All Heap Allocation,勾选Call Tree,同时不查看系统的函数库:  监测回调函数 3.具体方法占用的内存,能够逐级点开,效果以下:  内存占用 以上是常规的Allocations使用,关于第二张图的有框中的几个选项能够解释一下: Separate by Thread: 每一个线程应该分开考虑,考虑到应用程序中GCD的存在; Invert Call Tree: 从上倒下跟踪堆栈,这意味着你看到的表中的方法,将已从第0帧开始取样,利用栈的先进后出的特性,咱们能够在栈顶看到最近调用的函数; Hide System Libraries: 勾选此项会显示app的代码,这是很是有用的; Flatten Recursion: 递归函数, 每一个堆栈跟踪一个条目; 左侧有几个比较有用的选项: All Objects Created Created & Still Living Created & Destroyed  内存监测 4.Allocation 分析技巧 经过以上方法能够对应用的总体内存使用状况有所了解,但内存不合理使用致使的内存警告每每是部分代码或视图致使的,咱们每每要关注于某段时间或操做过程当中内存的分配和使用状况,Allocation提供了这种功能。 好比在进入一个视图前或操做前,咱们在Allocation面板左侧点击Mark Generation,这时候会产生Generation A节点,显示内存当前的状况:  比较测出内存泄露点 咱们能够在进入视图后再点一次Mark Generation,在视图退出后再点一次Mark,这样三次产生的 Generation分别记录了进入前、进入后、关闭后,再最后一个Generation应该内存被合理释放,不然就表明了在这个视图或操做中有泄漏或不合理的地方。 以上只是Allocation的基本运用,设计出一套使用Allocation来合理测试的方案是比较复杂的,后续慢慢介绍。 四:MLeaksFinder MLeaksFinder 提供了内存泄露检测更好的解决方案。只须要引入 MLeaksFinder,就能够自动在 App 运行过程检测到内存泄露的对象并当即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操做。MLeaksFinder 目前能自动检测 UIViewController 和 UIView 对象的内存泄露,并且也能够扩展以检测其它类型的对象。 MLeaksFinder 的使用很简单,参照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目录下的文件添加到你的项目中,就能够在运行时(debug 模式下)帮助你检测项目里的内存泄露了,无需修改任何业务逻辑代码,并且只在 debug 下开启,彻底不影响你的 release 包。 当发生内存泄露时,MLeaksFinder 会中断言,并准确的告诉你哪一个对象泄露了。这里设计为中断言而不是打日志让程序继续跑,是由于不少人不会去看日志,断言则能强制开发者注意到并去修改,而不是犯拖延症。 中断言时,控制台会有以下提示,View-ViewController stack 从上往下看,该 stack 告诉你,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 没被释放。并且,这里咱们能够确定的是 MyTableViewController,UITableView,UITableViewWrapperView 这三个已经成功释放了。 * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Possibly Memory Leak.In case that MyTableViewCell should not be dealloced, override -willDealloc in MyTableViewCell by returning NO.View-ViewController stack: ( MyTableViewController, UITableView, UITableViewWrapperView, MyTableViewCell)' 从 MLeaksFinder 的使用方法能够看出,MLeaksFinder 具有如下优势: 使用简单,不侵入业务逻辑代码,不用打开 Instrument 不须要额外的操做,你只需开发你的业务逻辑,在你运行调试时就能帮你检测 内存泄露发现及时,更改完代码后一运行即能发现(这点很重要,你立刻就能意识到哪里写错了) 精准,能准确地告诉你哪一个对象没被释放 原理(http://wereadteam.github.io/2016/02/22/MLeaksFinder/?from=singlemessage&isappinstalled=0#u539F_u7406) MLeaksFinder 一开始从 UIViewController 入手。咱们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但通常不多这样作)。因而,咱们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。 具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的做用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,经过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要做用是直接中断言。 - (BOOL)willDealloc { __weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf assertNotDealloc]; }); return YES;}- (void)assertNotDealloc { NSAssert(NO, @“”);} 这样,当咱们认为某个对象应该要被释放了,在释放前调用这个方法,若是3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,若是它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(咱们认为它应该要被释放了),咱们遍历该 UIViewController 上的全部 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。 在这里,有几个问题须要解决: 不入侵开发代码 这里使用了 AOP 技术,hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,关于如何 hook,请参考 Method Swizzling。 遍历相关对象 在实际项目中,咱们发现有时候一个 UIViewController 被释放了,但它的 view 没被释放,或者一个 UIView 被释放了,但它的某个 subview 没被释放。这种内存泄露的状况很常见,所以,咱们有必要遍历基于 UIViewController 的整棵 View-ViewController 树。咱们经过 UIViewController 的 presentedViewController 和 view 属性,UIView 的 subviews 属性等递归遍历。对于某些 ViewController,如 UINavigationController,UISplitViewController 等,咱们还须要遍历 viewControllers 属性。 构建堆栈信息 须要构建 View-ViewController stack 信息以告诉开发者是哪一个对象没被释放。在递归遍历 View-ViewController 树时,子节点的 stack 信息由父节点的 stack 信息加上子结点信息便可。 例外机制 对于有些 ViewController,在被 pop 或 dismiss 后,不会被释放(好比单例),所以须要提供机制让开发者指定哪一个对象不会被释放,这里能够经过重载上面的 -willDealloc 方法,直接 return NO 便可。 特殊状况 对于某些特殊状况,释放的时机不大同样(好比系统手势返回时,在划到一半时 hold 住,虽然已被 pop,但这时还不会被释放,ViewController 要等到彻底 disappear 后才释放),须要作特殊处理,具体的特殊处理视具体状况而定。 系统View 某些系统的私有 View,不会被释放(多是系统 bug 或者是系统出于某些缘由故意这样作的,这里就不去深究了),所以须要创建白名单 手动扩展 MLeaksFinder目前只检测 ViewController 跟 View 对象。为此,MLeaksFinder 提供了一个手动扩展的机制,你能够从 UIViewController 跟 UIView 出发,去检测其它类型的对象的内存泄露。以下所示,咱们能够检测 UIViewController 底下的 View、Model: - (BOOL)willDealloc { if (![super willDealloc]) { return NO; } MLCheck(self.viewModel); return YES;} 这里的原理跟上面的是同样的,宏 MLCheck() 作的事就是为传进来的对象创建 View-ViewController stack 信息,并对传进来的对象调用 -willDealloc 方法。 五:faceBook提供的内存泄露自动化测试: FBRetainCycleDetector、FBAllocationTracker、FBMemoryProfiler。 让这工具真正闪光的是,在工程师内部构建的时候,它会连续的、自动的运行。 客户端部分自动化是简单的。咱们在定时器上运行循环引用检测器,按期扫描内存去寻找循环引用,虽然这不是彻底没有问题。当咱们第一次运行分析器的时候,咱们意识到它不足以很快的扫描整个内存空间。当它开始检测的时候,咱们须要给它提供一组候选对象。 为了更有效的解决这个问题,咱们开发了FBAllocationTracker。这个工具会主动跟踪NSObject 子类的建立和释放。它能够以一个很小的性能开销来获取任何类的任何实例。 对于客户端的自动化,只要在NSTimer 上使用FBRetainCycleDetector,再用FBAllocationTracker来抓取实例来配合跟踪就行。 如今,让咱们来仔细看看后台会发生什么。 循环引用能够包含任何数量的对象。一个坏的链接会致使不少环的时候,这就复杂了。 在环中,A→B是一个坏链接,建立了两个环:A-B-C-D 和 A-B-C-E。 这有两个问题: 咱们不想给一个坏链接致使的两个循环引用分别标记。 咱们不想给可能表明两个问题的两个循环引用一块儿标记,即便它们共享一个链接。 因此咱们须要给循环引用定义簇组(clusters),鉴于这些启发,咱们写了个算法来找到这些问题。 在给定的时间收集全部的环。 对于每个环,提取Facebook特定的类名。 对于每个环,找到包含在环内的被报告的最小的环。 依据上面的最小环,将环添加到组中。 只报告最小环。 最后一部分是找出谁第一时间偶然引入了循环引用。咱们能够经过环中的”git/hg责任”的部分代码来猜想最近的变化所致使的问题。最后一个接触这个代码的人将会收到修复代码的任务。 整个系统以下: 手动性能分析 虽然自动化有助于简化发现循环引用的过程,下降人员的消耗,手动性能分析依然有它的用武之地。咱们建立的另外一个工具容许任何人查看内存使用,甚至不须要把他的手机插到电脑上。 FBMemoryProfiler能够很容易的添加到任何应用程序,可让你手动配置构建文件,可让你在应用程序内运行循环应用检测。它会借用FBAllocationTracker和FBRetainCycleDetector来实现此功能。 生成(Generations) FBMemoryProfiler的一个很伟大的特性是“生成追踪(generation tracking)”,相似于苹果的Instruments的生成追踪。生成只是简单的在两次标记之间拍摄全部仍然活着的对象的快照。 使用FBMemoryProfiler的界面,咱们能够标记生成,例如,分配三个对象。而后咱们标记另外一个生成,以后继续分配对象。第一个生成包含咱们一开始的三个对象。若是任意一个对象被释放了,它会从咱们第二个生成中移除。 当咱们有一个重复的任务,咱们认为可能会内存泄露的时候,生成追踪是颇有用的,例如,导航View Controller的进出。在每次开始咱们的任务的时候,咱们标记一个生成,而后,对以后的每一个生成进行调查。若是一个对象不该该活这么长时间,咱们能够在FBMemoryProfiler界面清楚地看到。 Check Out 不管你的应用程序是大是小,功能是可能是少,好的工程师都应有好的内存管理。在这些工具的帮助之下,咱们能够更简单的找到并修复这些内存泄露,因此咱们能够花费更少的时间去手动处理,这样就能够有更多的时间去编写更好的代码。咱们也但愿你能够发现它们是有用的。在Github上check out下来吧。FBRetainCycleDetector, FBAllocationTracker 和 FBMemoryProfiler。