内存泄露 iOS 内存分区 高地址 栈区 临时变量,参数等 系统管理,可读可写 FILO 先进后出 连续的内存地址 地址 0x7开头 堆区 alloc,malloc 申请分配地址 手动管理内存,可读可写 不连续的内存地址 链表结构 先进先出 FIFO 内存空间分配灵活,不过也会导致内存碎片 用户管理内存,可能导致内存泄露 内存分配:动态, 速度比栈等都慢 地址 常以0x6 开头 全局/静态区 未分配全局变量区 .bss 已分配全局变量区 .data 以 0x1 开头 退出app时销毁,可读可写 运行中对象内存一直在 常量区 编译时加载,退出app时销毁 代码中的常量申请内存 只读 代码区 二进制代码数据 只读 编译时加载,退出app时销毁 保留区 低地址 内存泄露检测工具:MLeaksFinder + FBRetainCycleDetector MLeaksFinder 腾讯开源,把库引入项目就可以了。无需设置 存在泄露时会弹出提示框 主要检测 UIViewController 和 UIView 对象的内存泄露 其它对象类型检测需要自己扩展 默认在debug模式下,不影响线上环境 MLeaksFinder 依赖 FBRetainCycleDetector(脸书 开源循环检测) FBRetainCycleDetector 依赖 fishhook 原理 步骤1: UINavigationController+MemoryLeak Navi 通过hook 导航的pop 和 push 方法: pushViewController, popToViewController 再通过 对象绑定方法 记录控制器 入栈,出栈状态: extern const void *const kHasBeenPoppedKey; objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN); 通过标记控制器是否被pop,根据这个判断是否需要检测控制器里对象, 否则不 检查。 步骤2: UIViewController+MemoryLeak VC hook UIViewController 的 显示和消失方法: viewDidDisappear: viewWillAppear: dismissViewControllerAnimated:completion: 对子控制器和父控制器 都进行释放状态判断: [self willReleaseChildren:self.childViewControllers]; [self willReleaseChild:self.presentedViewController]; 结合 导航文件中对控制器状态的标记,获取: objc_getAssociatedObject([NSObject class], kDelegateKey); 判断是否是 出栈状态,如果是,继续往下走 控制器 被 pop了 && (dismiss 或 disappear) 表示需要检测,默认3秒后判断 获取VC 的实例变量: unsigned int count; Ivar *ivars = class_copyIvarList([self class], &count); 同时还要获取实例变量的标记状态: id<MLeaksFinderDelegate> delegate = objc_getAssociatedObject([NSObject class], kDelegateKey); 判断标记状态是需要检测循环引用的话,再走下一步: 调用 [self willReleaseChild:ivarObject]; 方法, 其内部调用: willDealloc 方法 willDealloc里使用 MLeakedObjectProxy 管理类的一个方法(判断释放泄 露): isAnyObjectLeakedAtPtrs: 这个判断方法使用的就是FBRetainCycleDetector 框架 FBRetainCycleDetector 是Facebook 开源的检测对象内存泄露的 步骤3 最终把泄露对象及响应链向底层对象 通过弹框形式弹出 流程 文件 项目主要是一些分类文件 NSObject+MemoryLeak UIViewController+MemoryLeak UIView+MemoryLeak UINavigationController+MemoryLeak UITouch+MemoryLeak UITabBarController+MemoryLeak UIApplication+MemoryLeak 等等 为基类 NSObject 添加一个方法 -willDealloc 方法 在一小段时间(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(), ^{ // 延迟3秒后,如果这个对象还没有释放,这个方法就会被调用 [weakSelf assertNotDealloc]; }); return YES; } - (void)assertNotDealloc { NSAssert(NO, @“”); // 处理异常等 } 遍历相关对象,获取泄露对象 当控制器走到 -willDealloc 方法后,对VC,UIView,及其subViews等等进行遍 历,并调用 -willDealloc 方法 例如 UINavigationController willDealloc 时就会调用 [self willReleaseChildren:self.viewControllers]; UIView (MemoryLeak) @implementation UIView (MemoryLeak) - (BOOL)willDealloc { if (![super willDealloc]) { return NO; } [self willReleaseChildren:self.subviews]; return YES; } @end 构建堆栈信息 对应泄露信息弹框,尽量带上 对象对父节点信息等,方便定位问题 特殊情况 例外机制 对于有些 ViewController,在被 pop 或 dismiss 后,不会被释放(比如单 例),因此需要提供机制让开发者指定哪个对象不会被释放,这里可以通过重载 上面的 -willDealloc 方法,直接 return NO 即可 特殊情况 对于某些特殊情况,释放的时机不大一样(比如系统手势返回时,在划到一半时 hold 住,虽然已被 pop,但这时还不会被释放,ViewController 要等到完全 disappear 后才释放),需要做特殊处理,具体的特殊处理视具体情况而定。 白名单 某些系统的私有 View,不会被释放(可能是系统 bug 或者是系统出于某些原因 故意这样做的,这里就不去深究了),因此需要建立白名单 扩展对象支持 对一些model等想要支持的类,可以通过支持下面方法: - (BOOL)willDealloc { if (![super willDealloc]) { return NO; } MLCheck(self.viewModel); return YES; } 如何检测循环引用的,看下面的FBRetainCycleDetector 实现 FBRetainCycleDetector 是如何实现内存泄露检测的呢? 分三种处理逻辑: NSTimer ,block,其它object 简单理解就是,将对象的强引用关系,形成树,被检测obj封装成根结点,对树进 行深度遍历(默认深度10): 根结点入栈,开始深度遍历, 只要有子结点,就判断栈里这条路径是否已经存在这个子节点,没有就入栈,如 果有就说明存在循环。 当一个结点没有子结点时,出栈,继续栈顶结点相同操作,直到有环,或者结点 全部出栈。 所以,关键就是: 1.获取对象强引用列表, 2.深度递归,默认最大深度10, 使用栈记录,判断遍历链中是否存在相同对象地址,存在就形成环 其中,深度遍历 比较好理解,主要就是获取强引用对象列表(当然还有很多复杂 逻辑,这里不讨论) 疑问? 如何获取对象对强引用对象列表 这个比较复杂了,先看返回强引用列表片段: NSMutableArray *array = [NSMutableArray array]; for (auto &key: *refs) { id value = objc_getAssociatedObject(object, key); if (value) { [array addObject:value]; } } return array; 强引用 只有三种编码字符串存在强引用关系: 【@ 】表示 obj 【@?】block 【 { 】Structrue - (FBType)_convertEncodingToType:(const char *)typeEncoding { if (typeEncoding[0] == '{') return FBStructType; if (typeEncoding[0] == '@') { if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType; return FBObjectType; } return FBUnknownType; } 1. 常规对象 获取强引用成员变量 class_getIvarLayout @interface XXObject: NSObject @property(nonatomic, strong) id first; @property(nonatomic, weak) id second; @property(nonatomic, strong) id third; @property(nonatomic, strong) id forth; @property(nonatomic, weak) id fifth; @property(nonatomic, strong) id sixth; @end class_getIvarLayout 返回的布局字符串为\x01\x12\x11, 如何解析这个字符串? \x01\x12\x11 = \x0(no)1(yes) \x1(no)2(yes) \x1(no)1(yes) 也就是左边数字表示非强引用,右边表示强引用 一共 1+2+1 = 4个强引用成员变量 ivar_getOffset ivar_getOffset可以获取成员变量在类结构中的偏移地址,通过偏移地址找到成 员变量 将对应顺序对成员变量记录到列表,最后返回 2. 关联属性 获取强引用成员变量 FBRetainCycleDetector在对关联对象进行追踪时,通过fishhook第三方库hook 了关联对象的两个C函数,objc_setAssociatedObject 和 objc_removeAssociatedObjects,然后重新实现了关联对象模块,将通过 OBJC_ASSOCIATION_RETAIN和OBJC_ASSOCIATION_RETAIN_NONATOMIC 策略,保存起来,只追踪强引用的属性。 3. block 获取强引用成员变量 对于block持有的强引用变量的获取,依据block引用的对象总是基于block地址 偏移整个结构体的size,并且被持有的对象按照强引用在前,弱引用在后的顺序排 列,因为block强引用的对象都会进行copy到堆上和release对象引用的操作,因 此可以通过接收类FBBlockStrongRelationDetector构造detector对象,然后用 block的dispose_helper方法调用,判断如果detector对象调用release方法,就 说明当前对象是强引用对象,然后获取block持有的所有强引用变量的集合 4. NSTimer 强引用 timer比较特殊,还需要获取 target 关系: _timer 会持有target,如果target也持有_timer,会造成引用循环