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,主要显示多个名字的信息。它的实现文件如下:

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

#pragma mark - Data source
@property (assign, nonatomic) ArrayDataSource *dataSource;

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

因此,在viewDidLoad方法中

- (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:

@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

- (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信息:

- (void)dealloc
{
    NSLog(@"release BlockLeakViewController");
}

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

总结

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

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

推荐阅读更多精彩内容