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

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

悬挂指针问题

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

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

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

[cpp] view plaincopy
  1. static NSString *const kNameCellIdentifier = @"NameCell";
  2. @interface NameListViewController ()
  3. #pragma mark - Model
  4. @property (strong, nonatomic) NSArray *nameList;
  5. #pragma mark - Data source
  6. @property (assign, nonatomic) ArrayDataSource *dataSource;
  7. @end
  8. @implementation NameListViewController
  9. - (void)viewDidLoad {
  10. [super viewDidLoad];
  11. self.tableView.dataSource = self.dataSource;
  12. }
  13. #pragma mark - Lazy initialization
  14. - (NSArray *)nameList
  15. {
  16. if (!_nameList) {
  17. _nameList = @[@"Sam", @"Mike", @"John", @"Paul", @"Jason"];
  18. }
  19. return _nameList;
  20. }
  21. - (ArrayDataSource *)dataSource
  22. {
  23. if (!_dataSource) {
  24. _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
  25. cellIdentifier:kNameCellIdentifier
  26. tableViewStyle:UITableViewCellStyleDefault
  27. configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
  28. cell.textLabel.text = item;
  29. }];
  30. }
  31. return _dataSource;
  32. }
  33. @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:

[cpp] view plaincopy
  1. #pragma mark - Data source
  2. @property (assign, nonatomic) ArrayDataSource *dataSource;

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

因此,在viewDidLoad方法中:

[cpp] view plaincopy
  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.tableView.dataSource = self.dataSource;
  4. /*  由于dataSource是被assign修饰,self.dataSource赋值后,它对象的对象就马上释放,
  5. *  而self.tableView.dataSource也不是strong,而是weak,此时仍然使用,所有会导致程序crash
  6. */
  7. }

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

内存泄露问题

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

静态分析

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

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

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

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

[cpp] view plaincopy
  1. @implementation StaticAnalysisViewController
  2. #pragma mark - Lifecycle
  3. - (void)viewDidLoad
  4. {
  5. [super viewDidLoad];
  6. [self memoryLeakBug];
  7. [self resoureLeakBug];
  8. [self parameterNotNullCheckedBlockBug:nil];
  9. [self npeInArrayLiteralBug];
  10. [self prematureNilTerminationArgumentBug];
  11. }
  12. #pragma mark - Test methods from facebook infer iOS Hello examples
  13. - (void)memoryLeakBug
  14. {
  15. CGPathRef shadowPath = CGPathCreateWithRect(self.inputView.bounds, NULL);
  16. }
  17. - (void)resoureLeakBug
  18. {
  19. FILE *fp;
  20. fp=fopen("info.plist", "r");
  21. }
  22. -(void) parameterNotNullCheckedBlockBug:(void (^)())callback {
  23. callback();
  24. }
  25. -(NSArray*) npeInArrayLiteralBug {
  26. NSString *str = nil;
  27. return @[@"horse", str, @"dolphin"];
  28. }
  29. -(NSArray*) prematureNilTerminationArgumentBug {
  30. NSString *str = nil;
  31. return [NSArray arrayWithObjects: @"horse", str, @"dolphin", nil];
  32. }
  33. @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工具后,它会在程序运行时记录内存分配信息和检查是否发生内存泄露。当你点击引用循环进去那个页面后,再返回到主页,就会发生内存泄露:

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

定位内存泄露

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

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

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

分析内存泄露原因

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

难以检测Block引用循环

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

[cpp] view plaincopy
  1. - (ArrayDataSource *)dataSource
  2. {
  3. if (!_dataSource) {
  4. _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
  5. cellIdentifier:kNameCellIdentifier
  6. tableViewStyle:UITableViewCellStyleDefault
  7. configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
  8. cell.textLabel.text = item;
  9. [self configureCell];
  10. }];
  11. }
  12. return _dataSource;
  13. }
  14. - (void)configureCell
  15. {
  16. NSLog(@"Just for test");
  17. }
  18. - (void)dealloc
  19. {
  20. NSLog(@"release BlockLeakViewController");
  21. }

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

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

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

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

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

[cpp] view plaincopy
  1. - (void)dealloc
  2. {
  3. NSLog(@"release BlockLeakViewController");
  4. }

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

总结

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

作者简介:刘耀柱(@Sam_Lau_Dev),iOS Developer兼业余Designer,参与开发技术前线iOS项目翻译,个人博客:http://www.jianshu.com/users/256fb15baf75/latest_articles,GitHub:https://github.com/samlaudev。

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

  1. android内存池,两种常见的内存管理方法:堆和内存池

    描述 本文导读 在程序运行过程中,可能产生一些数据,例如,串口接收的数据,ADC采集的数据.若需将数据存储在内存中,以便进一步运算.处理,则应为其分配合适的内存空间,数据处理完毕后,再释放相应的内存空 ...

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

    摘要 内存管理对于长期运行的程序,例如服务器守护程序,是相当重要的影响:因此,理解PHP是如何分配与释放内存的对于创建这类程序极为重要.本文将重点探讨PHP的内存管理问题. 一. 内存 在PHP中,填 ...

  3. iPhone开发资料之内存管理 ,循环引用导致的内存问题

    iPhone开发资料之内存管理 ,循环引用导致的内存问题 https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual ...

  4. 两种常见的内存管理方法:堆和内存池

    在程序运行过程中,可能产生一些数据,例如,串口接收的数据,ADC采集的数据.若需将数据存储在内存中,以便进一步运算.处理,则应为其分配合适的内存空间,数据处理完毕后,再释放相应的内存空间.为了便于内存 ...

  5. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  6. 【Linux 内核 内存管理】Linux 内核堆内存管理 ① ( 堆内存管理 | 内存描述符 mm_struct 结构体 | mm_struct 结构体中的 start_brk、brk 成员 )

    文章目录 一.堆内存管理 二.内存描述符 mm_struct 结构体 三.mm_struct 结构体中的 start_brk.brk 成员 一.堆内存管理 Linux 操作系统中的 " 堆内 ...

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

    本文已经迁移到: http://cpp.winxgui.com/cn:peculiar-ideas-of-memory-management-autofreealloc-s-typical-appli ...

  8. java内存管理的一些基础,内存溢出的解决方案

    Java的内存组成:      Java的内存主要有两种:栈内存(stack)和堆内存(heap) 栈内存的优势是存取速度快,在栈中存放的变量都是在编译期就可确定其值.生命周期的,栈内存最大的一个特点 ...

  9. Linux Unix内存管理,简述:Unix/Linux内存管理

    一.底层结构 采用三层结构,实际使用中可以方便映射到两层或者三层结构,以适用不同的硬件结构.最下层的申请内存函数get_free_page.之上有三种类型的内存分配函数: 1.kmalloc类型.内核 ...

最新文章

  1. 网易云课堂计算机体系,计算机系统结构 (三) CPU及其结构分析
  2. (仿头条APP项目)6.点击过的新闻列表文字变灰和下拉刷新与滚动加载新闻数据
  3. python是一种动态语言、这意味着_【python编程的优点是什么?难怪选择python的人越来越多了】- 环球网校...
  4. c语言实验报告5数组,c语言实验报告五一维数组.doc
  5. 饱和气压与温度的关系_气压和钓鱼的关系,冬天钓鱼还需要看气压吗?很多钓友都错了...
  6. Idea不能显示类的继承关系,pom文件的右键属性中也没有Diagrams选项(已解决)
  7. 【CCCC】L3-013 非常弹的球 (30分)物理计算
  8. Android插件框架VirtualAPK学习和使用
  9. java8 32位和64位资源分享 Windows 版本:8u311
  10. c语言对fpga编程,利用C语言对FPGA计算解决方案进行编程方法介绍
  11. linux下磁盘坏道修复,linux磁盘坏道修复记录
  12. Spring Boot+Vue+阿里云OOS实现图片上传
  13. python爬虫捕鱼网站_古法捕鱼,千年绝技
  14. 拼多多如何撤销退店?万顿思教育
  15. 清风数学建模学习之TOPSIS法
  16. MQ 消息丢失、重复、积压问题,如何解决?
  17. 利用python做词频统计
  18. 刨根问底:Kafka 到底会不会丢数据?
  19. 天翼云赋能工业行业 安全上云更有保障
  20. 我与python第一次亲密接触

热门文章

  1. 修复OutLook2007 pst 文件
  2. R studio caret package 安装
  3. Python之路【第七篇】:常用模块
  4. 毕业设计之基于协同过滤算法的电影推荐系统设计(一) - 项目简介
  5. MATLAB中的特殊图像显示
  6. Android美化menu的小技巧-item菜单项添加标题
  7. MySQL(3):可视化数据库管理工具
  8. 华农计算机科学转专业,转专业门槛有多高? 每8名新生就有一个想转专业
  9. Simulink学习之Combinatorial Logic模块
  10. ausu-fx80-efi黑苹果10.15.7