iOS - 内存管理十全大补丸——内存泄露检查实战总结

3 篇文章 0 订阅

  前段时间被分配查应用的内存泄露问题,然后搜集了一大波内存管理相关的资料,并且在整个过程中狠狠的实践了一波。下面就把这段时间内碰到的坑(从别的地方也搜刮到很多坑),以及搜集到的一些理论知识(简洁版)总结一发。

  背景:一个复杂的iOS应用可能会混合着MRR和ARC两种内存管理方式,就是有一部分文件MRR实现,而另一部分则是ARC实现(原因目测是因为复用MRR实现的旧代码)。所以就存在一种现象,如果后来的开发者没注意该文件是否为ARC模式时,则有可能会忘记对对象进行release等操作从而导致内存泄露的发生。

  除了项目可能是ARC和MRR混合模式,有时候还有很多底层库是使用C/C++实现的,所以对于C/C++的内存管理也是需要有所了解的。

一、应用内存管理

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

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

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

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

  其中 Leaked memoryAbandoned memory 都属于应该释放而没释放的内存,某种意义上来说都可以算内存泄露。而 Leaks工具只负责检测Leaked memory,而无法检测 Abandoned memory(这个是不是就是传说中的循环引用问题)相关的问题。

  在 MRR 模式下 Leaked memory是比较常见,因为很容易忘了发送 release消息。而在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memoryLeaks工具是无法直接检测出这类内存泄露问题的,可以借助一些别的工具(例如静态分析Static Analyze)。(引用出处【传送门】)

1.1 内存管理原理

  iOS平台内存管理采用的是引用计数机制,主要提供了MRR(Manual Retain-Release)和ARC(Automatic Reference Counting)两种内存管理方式,同时还提供了自动释放池这种半自动释放机制。

PS:这里的MRR就是传说中的MRR。只是我查iOS Developer文档时发现好像没有MRR这个叫法,也不知道这个叫法是怎么来的…

这里写图片描述

  无论是MRR还是ARC,其本质都是引用计数。在MRR模式中,通过发送alloc,new, copy等方法可以创建一个对象,刚创建的对象引用计数值都为1。然后通过发送retain方法可以拥有该对象,同时会使得该对象的引用计数+1。最后可以向对象发送release消息放弃该对象所有权,同时会使得该对象的引用计数-1。当一个对象的引用计数为0时,则会被系统回收释放。

  ARC实际上是一种编译器特性。编译器会在编译时自动在适当的位置插入retain, release消息来进行对象内存管理。而对于实际开发,只需要谨记:当一个对象被任何强指针引用时,则会被系统回收释放(可以理解为有几个强引用指向对象,则该对象引用计数为x,当没有强指针引用对象,则其引用计数为0)。

1.2 MRR内存管理准则

  • 谁创建,谁释放。
  • 谁retain,谁release/autorelease。
  • 不要向不拥有的对象发送release。

二、检测工具介绍

2.1 Instrument — Leaks,Allocations,Analyze

  我用到的检测内存泄露的工具主要是Xcode中集成的Leaks组件,这个组件的检测准确率比较高,可以查看到很多比如说是泄露大小,泄露产生的地方及其堆栈信息等。

  这里的“泄露产生的地方”并不一定可以定位到具体发生泄漏的某一句代码,而是会标出发生泄漏的对象初始化分配内存的地方,然后需要具体去分析和跟踪该对象的内存管理来查找泄漏的原因。

  Allocations工具是一个跟踪由应用程序分配的对象内存的工具。可以用来在疑似内存泄露的地方,通过反复操作,查看某些对象内存是否有被正常的释放,从而得知是否发生内存泄露。

  Analyze是一款静态分析代码的工具。它可以发现一些逻辑错误,内存泄漏和声明错误(未使用变量)等。这个组件还可以检测出一些内存泄漏问题,比如一些比较明显的循环引用,CF库对象未release等相对简单的问题,通常是在进行其他方式检测之前就使用的方式,把一些简单的问题先发现并处理了。

参考资料:
1. 使用Instruments定位iOS应用的Memory Leaks
2. Leaks Instrument
3. iOS性能优化:Instruments 工具的救命三招
2. IOS性能调优系列:使用Allocation动态分析内存使用情况
1. IOS性能调优系列:Analyze静态分析
2. IPhone开发工具篇-利用xcode profile和analyze进行性能优化

2.2 内存检测组件

  此外还有一些“植入”项目中的内存检测组件,比如说Facebook iOS 内存检测三剑客(FBAllocationTracker/FBMemoryProfiler/FBRetainCycleDetector),MSLeakHunter,MLeaksFinderPLeakSniffer等等。

2.2.1 内存泄露检测组件原理简介

  这些组件的其中大部分的实现原理都是类似的。主要就是灵活运用了OC中的Rumtime机制,以及各种OC对象生命周期管理相关的特性。这些组件为了实现对OC对象的内存监控,其本质就是在这些对象被分配和释放的时机进行监测。

  比如说需要监测一个UIViewController类型的对象,就可以联想到iOS中VC的生命周期和UINavigationController有很大关系,因为后者在iOS应用中常常被用来管理大量VC的跳转控制。所以就可以考虑通过监控UINavigationControllernavigation stack来达到检测VC是否发生内存泄露的目的。(以下这些方法都会被hook)

这里写图片描述

  再比如说常见的NSObject对象,其allocdealloc方法就是对象生命周期中很重要的两个方法,分别是分配内存资源和释放内存资源时会被调用的方法。那么就可以考虑通过method swizzing方法替换allocdealloc这两个方法的实现,从而获得对象内存分配和释放的相关信息。

这里写图片描述

检测UIViewController内存泄漏的原理

(1)如何判断VC是否还在内存驻留?
答:利用ARC中weak指针指向的对象在对象释放时会自动置为nil的特性来检测VC是否在内存驻留。

(2) 在什么时机检测VC是否发生内存泄露?
答:通过监控UINavigationControllernavigation stack,可以判断一个VC的生命周期的开始和结束。就是当VC从navigation stack移除且VC的viewDidDisappear方法执行时,可以粗略的认为一个VC的生命周期即将结束。这时候就可以创建一个指向该VC的weak指针,并初始化一个定时器对VC进行延时扫描,最后通过(1)中的方法判断VC是否还驻留在内存从而得出VC是否发生内存泄露的结论。

三、实践

3.1 对象内存管理

  1. 在MRR模式下,通过new, copy, alloc方式创建的对象,记得release。一般在delloc中进行释放操作。当然局部内产生的也要在局部内进行释放。

点评:呵呵,在实践中发现最多的问题就是这个。尤其是在ARC和MRR都有的项目中。= =。我猜测原因之一可能是后面的代码修改者没意识到当前修改的文件是MRR模式的,所以在新增一些属性或成员变量后,没有在dealloc方法或对象使用完毕后及时的释放资源。


2. 在MRR模式下,发送了retain消息,记得也要发送release消息。并且在一个对象发送retain消息之前,也要考虑是否要release原来的对象。

碰到的一个栗子:

@interface classA{
    NSString *_str;
}

- (void)functionA{
    //正确的方式是这里要有: [_str release];
    _str = [[NSString stringWithFormat:@"%d", @(213)] retain];

    //后续代码...
}

- (void)functionB{
    //正确的方式是这里要有: [_str release];
    _str = [[NSString stringWithFormat:@"%d", @(213)] retain];
//后续代码...
}

@end

Tips:这里存在一个问题就是functionA中对一个对象发送了retain消息,如果这时候又调用了functionB方法,str变量被重新赋值。此时如果没有先对str发送release消息的话,则会导致functionA中引用的对象发生内存泄露。
对于一般情况下使用的局部变量都会记得发送retain后发送release,然而在栗子中那种情况下,成员变量可能在不同方法中被重新赋值的时候,就要注意了!


3. 不论是MRR还是ARC情况下,使用Core Foundation框架(C语言实现的框架,其可以和Cocoa Foundation库中的对象进行类型转换)创建的对象需要手动进行内存管理。即需要手动调用CFRetain和CFRelease来管理对象内存。

Tips:这种情况没啥好说的了,就是记得CFRetain、CFRelease和retain、release一样要成对出现~

  再多说一点就是Core Foundation框架和Cocoa Foundation对象指针转换的内容。Cocoa Foundation指针与Core Foundation指针转换,需要考虑的是所指向对象所有权的归属。ARC提供了3个修饰符来管理。【参考资料: IOS之Core Foundation框架和Cocoa Foundation框架区别 Core Foundation Framework Reference

  1. __bridge,什么也不做,仅仅是转换。此种情况下:
    (1). 从Cocoa转换到Core,需要人工CFRetain,否则,Cocoa指针释放后, 传出去的指针则无效。

    (2). 从Core转换到Cocoa,需要人工CFRelease,否则,Cocoa指针释放后,对象引用计数仍为1,不会被销毁。

  2. __bridge_retained,转换后自动调用CFRetain,即帮助自动解决上述(1)的情形。

  3. __bridge_transfer,转换后自动调用CFRelease,即帮助自动解决上述(2)的情形。


4. 使用NSAutoreleasePool创建的自动释放池,一定要确保其发送drain或release消息。这样创建的自动释放池对象才会被释放,同时被加入自动释放池的对象才能收到release消息,避免内存泄露。

碰到的栗子:

- (void)functionA{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString *str = [[NSString alloc] initWithFormat:@"%d", @(213)];
    [str release];
    //执行各种代码...

    if (...){
        //执行各种代码...
        //问题就在这里:return 之前没有释放自动释放池!!!
        //正确的做法,加上: [pool release];
        return;
    }

    [pool release];
}

Tips:栗子中的案例虽然看起来是个很逗比的错误,不过在实战中已经发现两处了…所以如果是MRR方式下这样使用自动释放池时,记得也要对自动释放池发送drain或release操作。如果是使用ARC的话,则不推荐栗子中使用自动释放池的方式,而是下面这种方式了。

@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

  既然说到自动释放池,那就顺便简单了解一下其实现原理,使用场景和一些注意事项吧。上面也有提到NSAutoreleasePool有两个方法drain和release,关于这两者的区别可以参考这些资料:【NSAutoReleasePool使用中drain和release的区别】【NSAutoreleasePool】。此外,还发现了一篇讲解AutoReleasePool的比较好的文章,里面也有解释了AutoReleasePool释放时间,原理等等:【黑幕背后的Autorelease】。


5. 函数返回的对象,是否加入自动释放池(延迟释放)。从内存管理的规范上来讲,如果一个函数需要返回一个对象,这个对象应该加入自动释放池中(”谁创建,谁释放”)?虽然说从某种角度来说,不加进自动释放池,而是由函数调用者负责该对象的释放也是可行的。如果函数返回的对象没有加入自动释放池,而函数调用者在外部又没有释放该对象,则就有可能造成内存泄露的现象。

(1)OC中有一些对象有多种创建的方法,比如说NSString, NSArray, NSDictionary之类的(还有它们的可变类型)。这些类都提供了两种类型的创建方式,一种是成员函数initWithXXX,另一种则是类函数stringWithXXX, arrayWithXXX(或array), dictionaryWithXXX(或dictionary)这些。
这些方法都是有区别的,第一种方式产生的对象需要手动release来释放内存,第二种方式产生的对象已经被加到autoreleasepool中,不需要手动release来释放内存。所以在项目中也要注意这些对象使用不同创建方式时所采用的不同的对象管理方法,针对这两种对象生成方式,也有很多讨论,大家自己看看吧哈哈哈哈哈。

参考资料:
1. stringWithFormat vs. initWithFormat on NSString
2. objective-C: NSString应该用initWithFormat? 还是 stringWithFormat?
3.Difference between [NSMutableArray array] vs [[NSMutableArray alloc] init]

(2)其中就碰到过Runtime方法中的class_copyIvarListclass_copyMethodList这些方法返回的对象没有被手动释放导致的内存泄漏。因为这些是C实现的函数,是需要手动对函数返回值进行free的,不然则会导致内存泄露。= =。这里也顺便提醒平时需要注意对于C/C++的实现,当见到malloc/new分配的对象,就应该检查该对象有没有对应的free/delete操作,这些地方往往也是内存泄漏产生的地方。

3.2 引用循环

  这是无论在MRR还是ARC下都存在的一种导致内存泄露的情况,尤其是在ARC中,如果发生内存泄漏,其一般都会是罪魁祸首。尤其是在使用block的时候,更要注意适当处理以避免强引用循环的发生。

后续有待更新。。。。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值