iOS内存泄漏检测方法

什么是内存泄漏,通俗来说就是有一块内存区域被你占用了,但你又不使用这块区域也不让别人用,造成内存浪费,这就是内存泄漏,泄漏严重会造成内存吃紧,严重的会使程序崩溃;
内存泄漏对于以前MRC开发来说相当痛苦,需要耗费大量精力管理内存,引入ARC机制后,系统自动管理内存,大大减轻了开发工作量,但一些特殊情况仍然会有内存泄漏发生,需要特别注意。

一般易造成泄漏的点
  • 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不会强引用
常规的监测方法
  • Analyze静态分析 (command + shift + b)

    主要分析以下四种问题:

    1、逻辑错误:访问空指针或未初始化的变量等;

    2、内存管理错误:如内存泄漏等;

    3、声明错误:从未使用过的变量;

    4、Api调用错误:未包含使用的库和框架。

    静态分析结果会有警告提示

    image
  • Instruments中的Leak动态分析内存泄漏

    product->profile ->leaks 打开工具主窗口


    image
    image

    点击暂停,将鼠标移到叉号上面点击锁定,点击下方的“田”字格,选择callTree,

    选择中间的齿轮,选中选项中的 invert Call Tree 和Hide System Libraries

  • Separate by Thread:按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
  • Invert Call Tree:反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
  • Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
  • Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。

双击左边 Call Tree 下面的任意一行,查看内存泄漏的代码位置

image
  • Allocation工具了解内存的分配情况

    每次点击generations(是两个时间标记之间所有仍然活着的对象的快照),生成快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息,数次 push 跟 pop 之后,内存还不断增长,则有内存泄露

    image

    开源项目 HeapInspector-for-iOS 可以说是 Allocations 的改进,它通过 hook 掉 alloc,dealloc,retain,release 等方法,来记录对象的生命周期,亲测不太好用

    其他工具用途

    • Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要。
    • Cocoa Layout:观察约束变化,找出布局代码的问题所在。
    • Network:跟踪 TCP / IP和 UDP / IP 连接。
    • Automations:创建和编辑测试脚本来自动化 iOS 应用的用户界面测试
XCode8后新特性
  • Debug Memory Graph

    Debug Memory Graph, 直接以关系图的形式来告诉你各个对象的持有关系, 泄露时会有紫色的小感叹号出现,
    在开发过程中,因为语法或明显的代码错误(例如Retain Cycle),编译器可以发现并报黄色或红色警告

image

实时监测内存占用情况

image

直接选择一个对象,查看与其相关的内存关系

image
- 绿色的一般都是 UIKit 控件及其子类
- 蓝色一般 NSObject 类及其子类
- 黄色一般都是容器类型及其子类
- 灰色括号是指 block
第三方工具
  • MLeaksFinder

    原理还是很简单的, 它swizzle了NavigationController的Push和Pop相关方法来管理viewController和view的生命周期, 在你Pop掉viewController的时候, 会执行这么一段代码


- (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 assertNotDealloc]; 如果这个时候view和viewController已经释放了, 那么weakSelf应该为nil, 所以将不会触发断言, 否则将会打印日志, 触发断言.

  • 关于swizzleSEL

一种简写方式


void MethodSwizzle(Class c,SEL origSEL,SEL overrideSEL)  {
        Method origMethod = class_getInstanceMethod(c, origSEL);
        Method overrideMethod= class_getInstanceMethod(c, overrideSEL);
}

传入两个参数,原方法选择子,新方法选择子,并通过class_getInstanceMethod()拿到对应的Method

  • 有两种情况要考虑一下。第一种情况是要复写的方法(overridden)并没有在目标类中实现,而是在其父类中实现了。第二种情况是这个方法已经存在于目标类中。这两种情况要区别对待
    (它的目的是为了使用一个重写的方法替换掉原来的方法。但重写的方法可能是在父类中重写的,也可能是在子类中重写的。)
  • 对于第一种情况(重写方法不存在),应当先在目标类增加一个新的实现方法(override),然后将复写的方法替换为原先的实现
  • 对于第二情况(目标类存在重写的方法)。这时可以通过method_exchangeImplementations来完成交换.

标准方式

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
        [self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
        [self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];
    });
}


+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
#if _INTERNAL_MLF_ENABLED
    
#if _INTERNAL_MLF_RC_ENABLED
    // Just find a place to set up FBRetainCycleDetector.
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [FBAssociationManager hook];
        });
    });
#endif
    
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
    
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSEL,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSEL,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
#endif
}

class_addMethod:是相对于实现来的说的,将本来不存在于被操作的Class里的newMethod的实现添加在被操作的Class里,并使用origSel作为其选择子

如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现

1.如果返回成功:则说明被替换方法没有存在.也就是被替换的方法没有被实现,我们需要先把这个方法实现,然后再执行我们想要的效果,用我们自定义的方法去替换被替换的方法. 这里使用到的是class_replaceMethod这个方法. class_replaceMethod本身会尝试调用class_addMethod和method_setImplementation,所以直接调用class_replaceMethod就可以了)

2.如果返回失败:则说明被替换方法已经存在.直接将两个方法的实现交换即可

class_replaceMethod,addMethod成功完成后,从参数可以看出,目的是换掉method_getImplaementation(roiginMethod)的选择子,将原方法的实现的SEL换成新方法的SEL

  • FBRetainCycleDetector

    能够检测指定对象的引用情况,并把所存在的引用循环中各对象和引用在终端进行打印

    #import <FBRetainCycleDetector/FBRetainCycleDetector.h>
    
    _handlerBlock = ^{
        NSLog(@"%@", self);
    };
    
    FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
    [detector addCandidate:self];
    NSSet *retainCycles = [detector findRetainCycles];
    NSLog(@"%@", retainCycles);
    
打印结果类似于:


```

{(
    (
    "-> DetailViewController ",
    "-> _handlerBlock -> __NSMallocBlock__ "
)
)}

 
 
 
 ```

DetailViewController通过_handlerBlock实例变量引用一个Block对象,而该Block又引用了DetailViewController对象。如果不存在引用循环的话,打印的结果将是空的

原理: 循环引用可以包含任何数量的对象。一个坏的连接会导致很多环的时候,这就复杂了

image

在环中,A→B是一个坏连接,创建了两个环:A-B-C-D 和 A-B-C-E。
这有两个问题:

  • 不想给一个坏连接导致的两个循环引用分别标记。
  • 不想给可能代表两个问题的两个循环引用一起标记,即使它们共享一个连接。

所以需要给循环引用定义簇组(clusters),鉴于这些启发,我们写了个算法来找到这些问题。

  • 在给定的时间收集所有的环。
  • 对于每一个环,提取Facebook特定的类名。
  • 对于每一个环,找到包含在环内的被报告的最小的环。
  • 依据上面的最小环,将环添加到组中。
  • 只报告最小环。

通过Pod安装后,通过以下代码激活即可。

[[PLeakSniffer sharedInstance] installLeakSniffer];
[[PLeakSniffer sharedInstance] addIgnoreList:@[@"MySingletonController"]];

addIgnoreList可以添加一些特殊的忽略名单,比如单例这种无法正确预测泄漏的对象

原理: 如果Controller被释放了,但其曾经持有过的子对象如果还存在,那么这些子对象就是泄漏的可疑目标

子对象(比如view)建立一个对controller的weak引用,如果Controller被释放,这个weak引用也随之置为nil。那怎么知道子对象没有被释放呢?用一个单例对象每个一小段时间发出一个ping通知去ping这个子对象,如果子对象还活着就会一个pong通知。所以结论就是:如果子对象的controller已不存在,但还能响应这个ping通知,那么这个对象就是可疑的泄漏对象。完整的结构可以用下图表示

image

参考资料

iOS 中的 NSTimer

__weak与__block区别

iOS内存泄漏自动检测工具PLeakSniffer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 在 Java 中,内存的分配是由程序完成的,而内存的释放则是由 Garbage Collecation(GC) 完...
    Shawn_Dut阅读 5,863评论 3 28
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 心情曾一度跌到谷底。就像是生命没有了继续前行的勇气,没了方向~~就像是无意识的木偶麻木的看着周遭一切,机械...
    沉默的惜惜阅读 212评论 0 1
  • 前段时间,老爷子感觉浑身无力,坐在哪里就不想动弹,尽打瞌觉想睡觉,看情形不对,老公赶紧带他到医院去检查,原...
    夏所珍创作室阅读 733评论 0 1