iOS/OS X 内存管理(二):借助工具解决内存问题

上一篇博客iOS/OS X内存管理(一):基本概念与原理主要讲了iOS/OSX 内存管理中引用计数和内存管理规则,以及引入ARC新的内存管理机制之后如何选择ownership qualifiers(__strong、__weak、__unsafe_unretained和__autoreleasing)来管理内存。这篇我们主要关注在实际开发中会遇到哪些内存管理问题,以及如何使用工具来调试和解决。

在往下看之前请下载实例MemoryProblems,我们将以这个工程展开如何检查和解决内存问题。

悬挂指针问题

悬挂指针(Dangling Pointer)就是当指针指向的对象已经释放或回收后,但没有对指针做任何修改(一般来说,将它指向空指针),而是仍然指向原来已经回收的地址。如果指针指向的对象已经释放,但仍然使用,那么就会导致程序crash。

当你运行MemoryProblems后,点击悬挂指针那个选项,就会出现EXC_BAD_ACCESS崩溃信息。

我们看看这个NameListViewController是做什么的?它继承UITableViewController,主要显示多个名字的信息。它的实现文件如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

static NSString *const kNameCellIdentifier = @"NameCell";

 

@interface NameListViewController ()

 

#pragma mark - Model

@property (strong, nonatomic) NSArray *nameList;

 

#pragma mark - Data source

@property (assign, nonatomic) ArrayDataSource *dataSource;

 

@end

 

@implementation NameListViewController

 

- (void)viewDidLoad {

    [super viewDidLoad];

 

    self.tableView.dataSource = self.dataSource;

}

 

#pragma mark - Lazy initialization

- (NSArray *)nameList

{

    if (!_nameList) {

        _nameList = @[@"Sam", @"Mike", @"John", @"Paul", @"Jason"];

    }

    return _nameList;

}

 

- (ArrayDataSource *)dataSource

{

    if (!_dataSource) {

        _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList

                                              cellIdentifier:kNameCellIdentifier

                                              tableViewStyle:UITableViewCellStyleDefault

                                          configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {

            cell.textLabel.text = item;

        }];

    }

    return _dataSource;

}

 

@end

要想通过tableView显示数据,首先要实现UITableViewDataSource这个协议,为了瘦身controller和复用data source,我将它分离到一个类ArrayDataSource来实现UITableViewDataSource这个协议。然后在viewDidLoad方法里面将dataSource赋值给tableView.dataSource。

解释完NameListViewController的职责后,接下来我们需要思考出现EXC_BAD_ACCESS错误的原因和位置信息。

一般来说,出现EXC_BAD_ACCESS错误的原因都是悬挂指针导致的,但具体是哪个指针是悬挂指针还不确定,因为控制台并没有给出具体crash信息。

启用NSZombieEnabled

要想得到更多的crash信息,你需要启动NSZombieEnabled。具体步骤如下:

1.选中Edit Scheme,并点击

2.Run -> Diagnostics -> Enable Zombie Objects

设置完之后,再次运行和点击悬挂指针,虽然会再次crash,但这次控制台打印了以下有用信息:

信息message sent to deallocated instance 0x7fe19b081760大意是向一个已释放对象发送信息,也就是已释放对象还调用某个方法。现在我们大概知道什么原因导致程序会crash,但是具体哪个对象被释放还仍然使用呢?

点击上面红色框的Continue program execution按钮继续运行,截图如下:

留意上面的两个红色框,它们两个地址是一样,而且ArrayDataSource前面有个_NSZombie_修饰符,说明dataSource对象被释放还仍然使用。

再进一步看dataSource声明属性的修饰符是assign


1

2

#pragma mark - Data source

@property (assign, nonatomic) ArrayDataSource *dataSource;

而assign对应就是__unsafe_unretained,它跟__weak相似,被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址。

因此,在viewDidLoad方法中


1

2

3

4

5

6

7

8

- (void)viewDidLoad {

    [super viewDidLoad];

     

    self.tableView.dataSource = self.dataSource;    

    /*  由于dataSource是被assign修饰,self.dataSource赋值后,它对象的对象就马上释放,

     *  而self.tableView.dataSource也不是strong,而是weak,此时仍然使用,所有会导致程序crash

     */

}

分析完原因和定位错误代码后,至于如何修改,我想大家都心知肚明了,如果还不知道的话,留言给我。

内存泄露问题

还记得上一篇iOS/OS X内存管理(一):基本概念与原理的引用循环例子吗?它会导致内存泄露,上次只是文字描述,不怎么直观,这次我们尝试使用Instruments里面的子工具Leaks来检查内存泄露。

静态分析

一般来说,在程序未运行之前我们可以先通过Clang Static Analyzer(静态分析)来检查代码是否存在bug。比如,内存泄露、文件资源泄露或访问空指针的数据等。下面有个静态分析的例子来讲述如何启用静态分析以及静态分析能够查找哪些bugs。

启动程序后,点击静态分析,马上就出现crash

此时,即使启用NSZombieEnabled,控制台也不能打印出更多有关bug的信息,具体原因是什么,等下会解释。

打开StaticAnalysisViewController,里面引用Facebook Infer工具的代码例子,包含个人日常开发中会出现的bugs:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

@implementation StaticAnalysisViewController

 

#pragma mark - Lifecycle

- (void)viewDidLoad

{

    [super viewDidLoad];

 

    [self memoryLeakBug];

    [self resoureLeakBug];

    [self parameterNotNullCheckedBlockBug:nil];

    [self npeInArrayLiteralBug];

    [self prematureNilTerminationArgumentBug];

}

 

#pragma mark - Test methods from facebook infer iOS Hello examples

- (void)memoryLeakBug

{

     CGPathRef shadowPath = CGPathCreateWithRect(self.inputView.bounds, NULL);

}

 

- (void)resoureLeakBug

{

    FILE *fp;

    fp=fopen("info.plist""r");

}

 

-(void) parameterNotNullCheckedBlockBug:(void (^)())callback {

    callback();

}

 

-(NSArray*) npeInArrayLiteralBug {

    NSString *str = nil;

    return @[@"horse", str, @"dolphin"];

}

 

-(NSArray*) prematureNilTerminationArgumentBug {

    NSString *str = nil;

    return [NSArray arrayWithObjects: @"horse", str, @"dolphin", nil];

}

 

@end

下面我们通过静态分析来检查代码是否存在bugs。有两个方式:

  • 手动静态分析:每次都是通过点击菜单栏的Product -> Analyze或快捷键shift + command + b

  • 自动静态分析:在Build Settings启用Analyze During 'Build',每次编译时都会自动静态分析

静态分析结果如下:

通过静态分析结果,我们来分析一下为什么NSZombieEnabled不能定位EXC_BAD_ACCESS的错误代码位置。由于callback传入进来的是null指针,而NSZombieEnabled只能针对某个已经释放对象的地址,所以启动NSZombieEnabled是不能定位的,不过可以通过静态分析可得知。

启动Instruments

有时使用静态分析能够检查出一些内存泄露问题,但是有时只有运行时使用Instruments才能检查到,启动Instruments步骤如下:

1.点击Xcode的菜单栏的 Product -> Profile 启动Instruments

2.此时,出现Instruments的工具集,选中Leaks子工具点击

3.打开Leaks工具之后,点击红色圆点按钮启动Leaks工具,在Leaks工具启动同时,模拟器或真机也跟着启动

4.启动Leaks工具后,它会在程序运行时记录内存分配信息和检查是否发生内存泄露。当你点击引用循环进去那个页面后,再返回到主页,就会发生内存泄露

内存泄露.gif

如果发生内存泄露,我们怎么定位哪里发生和为什么会发生内存泄露?

定位内存泄露

借助Leaks能很快定位内存泄露问题,在这个例子中,步骤如下:

  • 首先点击Leak Checks时间条那个红色叉

  • 然后双击某行内存泄露调用栈,会直接跳到内存泄露代码位置

分析内存泄露原因

上面已经定位好内存泄露代码的位置,至于原因是什么?可以查看上一篇的iOS/OS X内存管理(一):基本概念与原理的循环引用例子,那里已经有详细的解释。

难以检测Block引用循环

大多数的内存问题都可以通过静态分析和Instrument Leak工具检测出来,但是有种block引用循环是难以检测的,看我们这个Block内存泄露例子,跟上面的悬挂指针例子差不多,只是在configureCellBlock里面调用一个方法configureCell。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

- (ArrayDataSource *)dataSource

{

    if (!_dataSource) {

        _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList

                                              cellIdentifier:kNameCellIdentifier

                                              tableViewStyle:UITableViewCellStyleDefault

                                          configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {

                                              cell.textLabel.text = item;

 

                                              [self configureCell];

                                          }];

    }

    return _dataSource;

}

 

- (void)configureCell

{

    NSLog(@"Just for test");

}

 

- (void)dealloc

{

    NSLog(@"release BlockLeakViewController");

}

我们首先用静态分析来看看能不能检查出内存泄露:

结果是没有任何内存泄露的提示,我们再用Instrument Leak工具在运行时看看能不能检查出:

结果跟使用静态分析一样,还是没有任何内存泄露信息的提示。

那么我们怎么知道这个BlockLeakViewController发生了内存泄露呢?还是根据iOS/OS X内存管理机制的一个基本原理:当某个对象的引用计数为0时,它就会自动调用- (void)dealloc方法。

在这个例子中,如果BlockLeakViewController被navigationController pop出去后,没有调用dealloc方法,说明它的某个属性对象仍然被持有,未被释放。而我在dealloc方法打印release BlockLeakViewController信息:


1

2

3

4

- (void)dealloc

{

    NSLog(@"release BlockLeakViewController");

}

在我点击返回按钮后,其并没有打印出来,因此这个BlockLeakViewController存在内存泄露问题的。至于如何解决block内存泄露这个问题,很多基本功扎实的同学都知道如何解决,不懂的话,自己查资料解决吧!

总结

一般来说,在创建工程的时候,我都会在Build Settings启用Analyze During 'Build',每次编译时都会自动静态分析。这样的话,写完一小段代码之后,就马上知道是否存在内存泄露或其他bug问题,并且可以修bugs。而在运行过程中,如果出现EXC_BAD_ACCESS,启用NSZombieEnabled,看出现异常后,控制台能否打印出更多的提示信息。如果想在运行时查看是否存在内存泄露,使用Instrument Leak工具。但是有些内存泄露是很难检查出来,有时只有通过手动覆盖dealloc方法,看它最终有没有调用。

时间: 2025-01-30 15:52:19

iOS/OS X 内存管理(二):借助工具解决内存问题的相关文章

解析PHP中的内存管理,PHP动态分配和释放内存

本篇文章是对PHP中的内存管理,PHP动态分配和释放内存进行了详细的分析介绍,需要的朋友参考下   摘要 内存管理对于长期运行的程序,例如服务器守护程序,是相当重要的影响:因此,理解PHP是如何分配与释放内存的对于创建这类程序极为重要.本文将重点探讨PHP的内存管理问题. 一. 内存在PHP中,填充一个字符串变量相当简单,这只需要一个语句"<?php $str = 'hello world '; ?>"即可,并且该字符串能够被自由地修改.拷贝和移动.而在C语言中,尽管你能够

解析PHP中的内存管理,PHP动态分配和释放内存_php技巧

摘要 内存管理对于长期运行的程序,例如服务器守护程序,是相当重要的影响:因此,理解PHP是如何分配与释放内存的对于创建这类程序极为重要.本文将重点探讨PHP的内存管理问题. 一. 内存在PHP中,填充一个字符串变量相当简单,这只需要一个语句"<?php $str = 'hello world '; ?>"即可,并且该字符串能够被自由地修改.拷贝和移动.而在C语言中,尽管你能够编写例如"char *str = "hello world ";&qu

操作系统概念学习笔记 16 内存管理(二) 段页

操作系统概念学习笔记 16 内存管理 (二) 分页(paging) 分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的.分页避免了将不同大小的内存块匹配到交换空间上(前面叙述的内存管理方案都有这个问题,当位于内存中的代码和数据需要换出时,必须现在备份存储上找到空间,这是问题就产生了.备份存储也有前面所述的与内存相关的碎片问题,只不过访问更慢). 传统上,分页支持一直是由硬件来处理的.最近的设计是通过将硬件和操作系统相配合来实现分页. 基本方法 实现分页的基本方法设计将物理内存分

C++内存管理变革(3):另类内存管理

最简单的C++/Java程序 最简单的Java程序: class Program { public static void main() { new int; } } 对应的C++程序: void main() { new int; } 我想没有一个Java程序员会认为上面的Java代码存在问题.但是所有严谨的C++程序员则马上指出:上面这个C++程序有问题,它存在内存泄漏.但是我今天想和大家交流的一个观念是:这个C++程序没有什么问题. DocX程序的内存管理 DocX是我开发的一个文档撰写工

.NET中的内存管理,GC机制,内存释放过程

引言 作为一个.NET程序员,我们知道托管代码的内存管理是自动的..NET可以保证我们的托管程序在结束时全部释放,这为我们编程人员省去了不少麻烦,我们可以连想都不想怎么去管理内存,反正.NET自己会保证一切.好吧,有道理,有一定的道理.问题是,当我们用到非托管资源时.NET就不能自动管理了.这是因为非托管代码不受CLR(Common Language Runtime)控制,超出CLR的管理范围.那么如何处理这些非托管资源呢,.NET又是如何管理并释放托管资源的呢? 自动内存管理和GC 在原始程序

JVM内存管理:JAVA语言的内存管理概述

引言 内存管理一直是JAVA语言自豪与骄傲的资本,它让JAVA程序员基本上可以彻底忽略与内存管理相关的细节,只专注于业务逻辑.不过世界上不存在十全十美的好事,在带来了便利的同时,也因此引入了很多令人抓狂的内存溢出和泄露的问题. 可怕的事情还不只如此,有些使用其它语言开发的程序员,给JAVA程序员扣上了一个"不懂内存"的帽子,这着实有点让人难以接受.毕竟JAVA当中没有malloc和delete.没有析构函数.没有指针,刚开始接触JAVA的程序员们又怎么可能接触内存这一部分呢,更何况有不

内存管理2对象之间的内存管理

Book.h: #import <Foundation/Foundation.h> @interface Book : NSObject @property float price; -(id)initeWithPrice:(float)price; @end Book.m: #import "Book.h" @implementation Book #pragma mark 构造方法 -(id)initeWithPrice:(float)price { if(self=[

电脑提示内存不能为read怎么解决?内存不能为read解决方法

使用软件时提示如下 原因分析 内存或虚拟内存地址使用抵触造成.  顺序的运行需要分配一定的内存地址给程序使用,当顺序结束时释放留出空间让给新的顺序使用, win是 多任务的系统, 有时前程序未结束 ,又有新的任务开始 .到底要多少内存或虚拟内存来保证我同时运行的工作任务呢 ? 也许 win 这个问题上没弄好,所以有此错误常常发生,一般运行大型软件或多媒体后出现这种情况. 解决此问题办法 1.很多时候由于系统和显卡驱动的兼容性不好,也会出现这样的错误,建议你换个其他版本的显卡驱动安装,或换个其他版

IOS有关内存管理的二三事

IOS有关内存管理的二三事 一.前引 随着移动设备的内存越来越大,程序员也已经度过了为了那一两M的内存在系统的抽丝剥茧的年代,对于JAVA的开发者,对内存更是伸手即取,并且从不关心什么时候还回去.但是,程序的掌控度对程序员来说是至关重要的,任何语言的内存管理机制的初衷也是在有限的空间里完成最精致的逻辑. 二.Xcode工程设置ARC ARC是xcode5中引入的自动引用计数,其原理与MRC是一样,只是系统帮助我们添加了retain和release.现在在xcode中新建的项目默认都是ARC的环境