http://www.ox-holdings.com

本文介绍内存方面,检测方法是用 Mark Generation 的方式

摘要腾讯今日开源了业界首创iOS自动内存泄露检测工具MLeaksFinder,MLeaksFinder 是 iOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。前言腾讯今日开源了业界首创iOS自动内存泄露检测工具MLeaksFinder,MLeaksFinder 是 iOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。MLeaksFinder简介MLeaksFinder 是 iOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。开发者无需打开 instrument 等工具,也无需为了找内存泄漏而去跑额外的流程。并且,由于开发者是在修改代码之后一跑业务逻辑就能发现内存泄漏的,这使得开发者能很快地意识到是哪里的代码写得问题。这种及时的内存泄漏的发现在很大的程度上降低了修复内存泄漏的成本。特性介绍自动检测内存泄漏和释放不及时的场景构建泄漏对象相对于 ViewContrller 的引用链以帮助开发者定位问题不侵入业务逻辑,引入即生效,无需修改任何代码或引入头文件工程主页和源码地址团队博客:

[这是第15篇]

原文:

MLeaksFinder 介绍

MLeaksFinder 提供了内存泄露检测更好的解决方案。只需要引入 MLeaksFinder,就可以自动在 App 运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操作。MLeaksFinder 目前能自动检测 UIViewController 和 UIView 对象的内存泄露,而且也可以扩展以检测其它类型的对象。
MLeaksFinder 的使用很简单,参照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目录下的文件添加到你的项目中,就可以在运行时(debug 模式下)帮助你检测项目里的内存泄露了,无需修改任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。
当发生内存泄露时,MLeaksFinder 会中断言,并准确的告诉你哪个对象泄露了。这里设计为中断言而不是打日志让程序继续跑,是因为很多人不会去看日志,断言则能强制开发者注意到并去修改,而不是犯拖延症。

腾讯开源iOS自动内存泄露检测工具MLeaksFinder
https://github.com/Tencent/MLeaksFinder
腾讯开源的数据库框架
https://github.com/Tencent/wcdb
QMUI iOS——致力于提高项目 UI 开发效率的解决方案
https://github.com/QMUI/QMUI_iOS
专注于提升H5首屏速度优化
https://github.com/Tencent/VasSonic

导语:在当前的iOS开发中,虽然ARC为开发者解决了手动内存管理时代 的许多麻烦,但是内存方面的问题依然是产生iOS Crash的元凶之一,本文介绍内存方面,有关僵尸对象、野指针、内存泄漏、废弃内存这四类问题的调试方法和代码中的注意事项。

背景

MLeaksFinder 原理

引用开发者自己的简介。

MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。
具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。
当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。

1、概述
  • 僵尸对象:已经被释放掉的对象。一般来说,访问已经释放的对象或向它发消息会引起错误。因为指针指向的内存块认为你无权访问或它无法执行该消息,这时候内核会抛出一个异常,表明你不能访问该存储区域(BAD ACCESS)。(EXC_BAD_ACCESS类型错误)

  • 调试解决该类问题一般采用NSZombieEnabled

平常我们都会用 Instrument 的 Leaks / Allocations 或其他一些开源库进行内存泄露的排查,但它们都存在各种问题和不便,我们逐个来看这些工具的使用和存在的问题。

MLeaksFinder 主头文件分析

主头文件 MLeaksFinder.h

#import "NSObject+MemoryLeak.h"

//#define MEMORY_LEAKS_FINDER_ENABLED 0

#ifdef MEMORY_LEAKS_FINDER_ENABLED
#define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED
#else
#define _INTERNAL_MLF_ENABLED DEBUG
#endif
#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 0
#ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED
#define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED
#elif COCOAPODS
#define _INTERNAL_MLF_RC_ENABLED COCOAPODS
#endif

主头文件值得关注的是_INTERNAL_MLF_ENABLED_INTERNAL_MLF_RC_ENABLED两个宏,他们会用作为条件编译的表达式条件。默认不作任何修改情况下。主头文件等价于

#define _INTERNAL_MLF_ENABLED DEBUG
#define _INTERNAL_MLF_RC_ENABLED 0

在 DEBUG模式下,

#define _INTERNAL_MLF_ENABLED 1
#define _INTERNAL_MLF_RC_ENABLED 0

_INTERNAL_MLF_ENABLED 作为条件编译的表达式判断条件,用于控制MLeaksFinder的其他文件是否参与编译,在发布环境下,_INTERNAL_MLF_ENABLED为0,那么相当于该库的功能关闭。如果需要无论是调试环境还是发布环境都关闭代码,可以解注释#define MEMORY_LEAKS_FINDER_ENABLED 0. _INTERNAL_MLF_RC_ENABLED表示是否导入FBAssociationManager来监测循环引用。默认不开启

这里可以把基础知识复习一下。

1.什么是#define MM 空
#define MM value 可以后面不给替换的值,即#define MM。这样MM就被定义了,但是无具体的值。此后#ifdef MM 就为True.但是无法执行 #if MM 因为 MM 是无值的。#define MM 空常常用于做c语言的防止头文件多次包含。总之,一定要结合。 #ifdef来使用,否则没什么意义。

2.如果在调试模式下执行,DEBUG宏的值为1.在发布环境下,DEBUG宏的值为0.
无论在哪种模式下。DEBUG宏都被定义了。对于#ifdef DEBUG都为True

3.代码设定#define MM 0#ifdef MMTrue#if MMFalse。因为 MM就被定义了。哪怕 MM值为0.但是 #if MM 实际是判断 MM不等于0为true

2、使用NSZombieEnabled
  • Xcode提供的NSZombieEnabled,通过生成僵尸对象来替换dealloc的实现,当对象引用计数为0的时候,将需要dealloc的对象转化为僵尸对象。如果之后再给这个僵尸对象发消息,则抛出异常。先选中Product -> Scheme -> Edit Scheme -> Diagnostics -> 勾选Zombie Objects 项,显示如下:

图片 1设置NSZombieEnabled.png

  • 然后在Product -> Scheme -> Edit Scheme -> Arguments设置NSZombieEnabled、MallocStackLoggingNoCompact两个变量,且值均为YES。显示如下:

图片 2设置NSZombieEnabled和MallocStackLoggingNoCompact.png

  • 仅设置Zombie Objects的话,如果Crash发生在当前调用栈,系统可以把崩溃原因定位到具体代码中;但是如果Crash不是发生在当前调用栈,系统仅仅告知崩溃地址,所以我们需要添加变量MallocStackLoggingNoCompact,让Xcode记录每个地址alloc的历史,然后通过命令将地址还原出来。

  • Xcode 6之前还可以使用gdb,可以使用info malloc-history address命令来将发生崩溃的地址还原成具体的代码行,Xcode 7之后只能使用lldb,使用命令bt来打印调用堆栈。下面是某Crash通过僵尸模式调试,使用bt查看的效果。

图片 3bt效果.png

说明:发版前要将僵尸对象检测这些设置都去掉,否则每次通过指针访问对象时,都去检查指针指向的对象是否为僵尸对象,这就影响效率了。

Leaks

3、代码中的注意事项

在ARC时代,避免访问释放掉的内存,代码需要注意的地方有:

  • 检查代码1 :不能使用assgin或 unsafe_unretained修饰指向OC对象的指针

    assgin和unsafe_unretained表示不持对象,是弱引用。如果指针指向的对象被释放了,它们就变成了野指针,很有可能发生Crash。

    建议1: assign仅用于修饰NSInteger等OC基础类型,以及short、int、double、结构体等C数据类型,不修饰对象指针;

    建议2: OC对象属性一般使用strong关键字修饰。

    建议3: 如果需要弱引用OC对象,建议使用weak关键字,因为被weak指针所引用的对象被回收后,weak指针会被赋为nil,给nil发任何消息都不会出问题。使用weak修饰代理对象属性就是很好的例子

  • 检查代码2 :Core Foundation等底层操作

    Core Foundation等底层操作它们不支持ARC,还需要手动内存管理。

建议: 注意CF对象的创建和释放。

先看看 Leaks,从苹果的开发者文档里可以看到,一个 app 的内存分三类:

1、概述
  • 野指针是指向一个已删除的对象 或 未申请访问受限内存区域的指针。而这里的野指针主要是对象释放后,指针未置空导致的野指针。该类Crash发生比较随机,找出来比较费劲,比较常见的做法是,在开发阶段,提高这类Crash的复现率,尽可能得将其发现并解决。

  • 向OC对象发出release消息,只是标记对象占用的那块内存可以被释放,系统并没有立即收回内存;如果此时还向该对象发送其他消息,可能会发生Crash,也可能没有问题。下图是 访问野指针(指向已删除对象的指针)可能发生的情况。

图片 4访问野指针可能发生的情况图.png

  • 从上图可以知道,野指针造成的Crash的随机性比较大,但是被随机填入的数据是不可访问的情况下,Crash是必现的。我们的思路是:想办法给 野指针指向的内存填写不可访问的数据,让随机的Crash变成必现的Crash。

Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

2、设置Malloc Scribble
  • Xcode提供的Malloc Scribble,可以将对象释放后在内存上填上不可访问的数据,将随机发生变成不随机发生的事情,选中Product->Scheme->Edit Scheme ->Diagnostics - >勾选 Malloc Scribble项,结果如下:

图片 5设置Malloc Scribble.png

  • 设置了Enable Scribble,在对象申请内存后在申请的内存上填0xaa,内存释放后在释放的内存上填0x55;如果内存未被初始化就被访问,或者释放后被访问,Crash必现。

说明:该方法必须连接Xcode运行代码才发现,不适合测试人员使用。可以基于fishhook ,选择hook对象释放的接口,达到和设置Enable Scribble一样的效果。详情参考如何定位Obj-C野指针随机Crash:先提高野指针Crash率、如何定位Obj-C野指针随机Crash:让非必现Crash变成必现 和如何定位Obj-C野指针随机Crash:加点黑科技让Crash自报家门

Abandoned memory: Memory still referenced by your application that has no useful purpose.

3、代码中的注意事项

检查使用assgin或 unsafe_unretained 修饰指向OC对象的指针 和 Core Foundation等底层操作。

Cached memory: Memory still referenced by your application that might be used again for better performance.

1、概述
  • 内存泄漏是指没有释放掉不再引用对象的内存。即便ARC帮我们解决很多麻烦,但是内存泄漏问题依然比较多;一般开发结束后,都要做一些基本的内存泄漏排查工作。

  • 内存泄漏排查,一般采用Analyzer + Leaks + MLeaksFinder

其中 Leaked memory 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而 Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。

2-1、排查之静态分析
  • Xcode提供的 Analyzer可以在程序没运行的时候,通过分析代码上下文的语法结构和内存情况,找出代码中潜在错误,如内存泄露、未使用函数和变量等。选中Product->Analyze(快捷键command+shift+B)可以使用了。

  • Analyzer主要分析四种问题:

    1. 逻辑错误:访问空指针或未初始化的变量等;
    2. 内存管理错误:如内存泄漏等;Core Foundation不支持ARC
    3. 声明错误:从未使用过的变量;
    4. API调用错误:未包含使用的库和框架。
  • **Analyzer执行后,常见的警告类型有: **

    1)内存警告

    eg

    - (UIImage *)clipImageWithRect:rect{ CGFloat scale = self.scale; CGImageRef clipImageRef = CGImageCreateWithImageInRect(self.CGImage, CGRectMake(rect.origin.x * scale, rect.origin.y * scale, rect.size.width * scale, rect.size.height * scale)); CGRect smallBounds = CGRectMake(0, 0, CGImageGetWidth(clipImageRef)/scale, CGImageGetHeight(clipImageRef)/scale); UIGraphicsBeginImageContextWithOptions(smallBounds.size, YES, scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(context, 0, smallBounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); CGContextDrawImage(context, CGRectMake(0, 0, smallBounds.size.width, smallBounds.size.height), clipImageRef); UIImage* clipImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); CGImageRelease(clipImageRef); //不添加,内存泄漏,会警告:Potential leak of an object stored into 'clipImageRef' return clipImage;}
    

    分析:Analyzer检查出来内存泄漏,比较常见的就是CG、CF开头的内存泄漏,内存申请,忘记释放了。还有一种是,C申请的内存,没有配对使用new delete, malloc free。

    2)无效数据警告(Dead store)

    eg

    //错误做法,Analyzer分析后会告知:Value stored to ‘dataArray’ during its initialization is never readNSMutableArray *dataArray = [[NSMutableArray alloc] init];dataArray = _otherDataArray;//正确做法NSMutableArray *dataArray = nil;dataArray = _otherDataArray;
    

    分析: dataArray已经被初始化分配了内存,然后被另一个可变数组赋值,导致一个数据源却申请了两块内存,造成了内存泄露。

    3)逻辑错误监测(Logic error)

    eg

    //错误做法,Analyzer分析后会告知:Property of mutable type ’NSMutableArray’ has ‘copy’ attribute,an immutable object will be stored instead@property (nonatomic, copy) NSMutableArray *dataArr; //正确做法@property (nonatomic, strong) NSMutableArray *dataArr; 
    

    分析: NSMutableArray是可变数据类型,应该用strong来修饰其对象。

说明: Analyzer由于是编译器根据代码进行的判断, 做出的判断不一定会准确, 因此如果遇到提示, 应该去结合代码上文检查一下;还有某些造成内存泄漏的循环引用通过Analyzer分析不出来。

Allocations

2-2、排查之内存泄漏工具
  • Xcode提供的Leak可以帮助发现运行着的程序内存泄漏的地方。通过选中Product-> Profile(快捷键command+i,唤起Instrument工具界面) -> Leaks。切换到Call Tree模式,底部选中Separate by Thread、Invert Call Tree、Hide System Libraries。最后点击红色按钮开始“录制”,效果如下图:

图片 6Leaks调试界面.png

  • Leaks调试界面上,1是Allocations 模板,显示内存分配情况;2是 Leaks 模板,这里可以查看内存泄露情况。如果红X出现, 表示有内存泄露;主框体区域则会显示泄露的对象。Call Tree选项介绍如下:
Call Tree 中选项 说明
Separate by Category 按类型分类,展开All Heap Allocations这一套显示的就是不同方法里堆内存的分配情况
Separate by Thread 按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
Invert Call Tree 反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
Hide System Libraries 隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
Flattern Recursion 拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条

对于 Abandoned memory,可以用 Instrument 的 Allocations 检测出来。检测方法是用 Mark Generation 的方式,当你每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。举一个最简单的例子:

2-3、排查之MLeaksFinder

MLeaksFinder是微信阅读团队为了简化内存泄漏排查工作,推出的第三方工具,也是我们当前项目中内存泄漏的工具之一。

  • 特点:集成简单,主要检查UI方面(UIView 和 UIViewController)的泄漏。

  • 原理:不入侵开发代码,通过hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,检查ViewController对象被 pop 或 dismiss 一小段时间后,看看该ViewController对象的 view,view 的 subviews 等等是否还存在。

  • 实现:为基类 NSObject 添加一个方法 -willDealloc 方法,利用weak指针指向自己,并在一小段时间后,再次检测该weak指针是否有效,有效则内存泄漏。

  • 集成:通过Cocoapods引入或直接把代码拖进项目,很方便。发生内存泄漏,会弹出警告框,提示发生内存泄漏的位置。

说明:详细内容请参考:MLeaksFinder:精准 iOS 内存泄露检测工具和MLeaksFinder 新特性

我们可以不断重复 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里有介绍。

3、代码中的注意事项(ARC下的循环引用是内存泄漏的主要原因)
  • 检查代码1 :Core Foundation、Core Graphics等操作

    Core Foundation、CoreGraphics等操作不支持ARC,还需要手动内存管理。

    建议: 注意CF、CG对象的创建和释放。

  • 检查代码2 :NSTimer/CADisplayLink的使用,因为NSTimer/CADisplayLink对象的target会强引用self,而self又强引用NSTimer/CADisplayLink对象。

    建议:使用扩展方法,使用blocktarget弱引用目标对象 打破保留环,具体实现参考iOS实录8:解决NSTimer/CADisplayLink的循环引用

  • 检查代码3 :block使用代码。

    建议:成对使用weakSelf和strongSelf来打破block循环引用(对于self没有引用的block是不会造成循环引用,不需要使用weakSelf和strongSelf)

    原理:在block外定义弱引用,指向的self对象;在block内捕获的是这个弱引用,保证了self不会被block所持有;在执行block内方法时,生成强引用(strongSelf),指向了弱引用所指向的对象;在block内部实际是持有了self对象,但是这个强引用(strongSelf) 的生命周期只在这个block执行的过程中,block执行执行完立刻就被释放了。

用这种方法来发现内存泄露还是很不方便的:

1、概述####
  • 废弃内存(Abandoned Memory)指,依然被引用对象的内存,但在程序逻辑中无法再被利用。

  • 排查该类问题建议使用Xcode提供的AllocationAllocation可以跟踪应用的内存分配情况。

首先,你得打开 Allocations

2、使用Allocation
  • Xcode提供的Allocation由于可以跟踪应用的内存分配情况。开发者反复操作App,查看内存基线变化;甚至还可以设置Mark Generation来对比多次Generation之间的内存增长,这部分的增长就是我们没有及时释放的内存。通过Product-> Profile(快捷键command+i,唤起Instrument工具界面) -> Allocations。最后点击红色按钮开始“录制”,效果如下图:

图片 7Allocation界面Statistics Detail 下显示.png

  • 上图是Statistics Detail Type下的界面展示,下面是一些名称的说明
Detail列名 说明
Graph 类型的选择项
Category 类型,或CF对象,或OC对象,或原始块的内存
Persistent Bytes 未释放的内存和大小
Persistent 未释放的对象个数
Transient 已经释放的对象个数
Total Bytes 总使用内存大小
Total 总使用对象个数
Transient / Total Bytes 已释放内存大小/总使用内存大小
Allocation Type 说明
All Heap & Anonymous 所有堆内存和其他内存
All Heap Allocations 所有堆内存
All Anonymous VM 所有其他内存
  • 下图是切换到Call Tree下的界面展示。

图片 8Allocation界面Call Tree下显示.png

Call Tree列名 说明
Bytes Used 已经使用的内存大小
Count 符号使用的总个数
Symbol Name 符号名称

说明:这些名词的具体解释见Instrument-Allocations

  • 间隔一段时间点击“Mark Generation”,判断几次之间Generation之间的内存增长,而这些增长可能就是未能及时释放的内存:根据内存占用的比例,找到占用比例最高的那部分,然后找到我们自己的代码,再来分析并解决问题。

图片 9Allocation界面Mark Generation下显示.png

其次,你得一个个场景去重复的操作

3、代码中的注意事项

略,与内存泄漏部分代码中的注意事项相同。

  • 参考文章

    Xcode执行Analyze静态分析使用Instruments Allocations排查内存释放不及时的问题关于Instruments-Leaks工具的归纳总结MLeaksFinder:精准 iOS 内存泄露检测工具MLeaksFinder 新特性

  • 相关文章iOS实录14:浅谈iOS Crash

  • 我是南华coder,一名北漂的初级iOS程序猿。iOS实录系列是我的一点开发心得,希望能够抛砖引玉。

无法及时得知泄露,得专门做一遍上述操作,十分繁琐

开源库

在 GitHub 上有一些内存泄露检测相关的项目,例如HeapInspector-for-iOS和MSLeakHunter。

HeapInspector-for-iOS 可以说是 Allocations 的改进。它通过 hook 掉 alloc,dealloc,retain,release 等方法,来记录对象的生命周期。具体的检测内存泄露的方法和原理,与 Instrument 的 Allocations 一致。然而它跟 Allocations 一样,存在的问题是,你需要一个个场景去重复的操作,还有检测不及时。

MSLeakHunter 就简单得多,它只检测 UIViewController 和 UIView,通过 hook 掉 UIViewController 的-viewDidDisappear:方法,并认为-viewDidDisappear:后,UIViewController 将很快被释放,如果 UIViewController 没有被释放,则打个建议日志。这种做法其实不是很好,-viewDidDisappear:被调用可能是因为又 push 进来一个新的 ViewController,把当前的 ViewController 挡住了,所以可能有很多错误的建议,需要结合你实际的操作去具体地分析日志。

MLeaksFinder

郑重声明:本文版权归新匍京a奥门-最全网站手机版app官方下载所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。