【问题标题】:Unit tests for memory management in Cocoa/Objective-CCocoa/Objective-C 中内存管理的单元测试
【发布时间】:2011-09-28 09:53:19
【问题描述】:

您将如何编写单元测试(例如,使用 OCUnit)以确保对象在 Cocoa/Objective-C 中被正确释放/保留?

一种天真的方法是检查retainCount 的值,当然you should never use retainCount。你能简单地检查一个对象的引用是否被赋值为nil 以表明它已经被释放了吗?另外,您对实际释放对象的时间有什么保证?

我希望只有几行代码的简洁解决方案,因为我可能会广泛使用它。实际上可能有两个答案:一个使用自动释放池,另一个不使用。

澄清一下,我并不是在寻找一种方法来全面测试我创建的每个对象。不可能对任何行为进行全面的单元测试,更不用说内存管理了.不过,至少,最好检查已发布对象的行为以进行回归测试(并确保相同的与内存相关的错误不会发生两次)。

关于答案

我接受了BJ Homeranswer,因为我发现它是完成我的想法的最简单、最简洁的方法,因为需要注意的是Automatic Reference Counting 提供的弱指针在截至 2011 年 7 月 23 日的 XCode 生产版本(4.2 之前?)。得知这一点我也印象深刻

ARC 可以在每个文件的基础上启用;它不需要您的 整个项目都使用它。您可以使用 ARC 和 将您的主要项目留在手动保留释放上,此测试将 还在工作。

话虽如此,为了更详细地探索 Objective-C 中单元测试内存管理所涉及的潜在问题,我强烈推荐 Peter Hoseyin-depth response

【问题讨论】:

  • 我赞成这个不是因为我认为这是一件好事,而是因为我认为它会引起一个很好的答案,我猜这基本上是 “不要".

标签: objective-c cocoa unit-testing memory-management ocunit


【解决方案1】:

你能简单地检查一个对象的引用是否被赋值为nil来表明它已经被释放了吗?

不,因为向对象发送release 消息和将nil 分配给变量是两个不同且不相关的事情。

您可以获得的最接近的结果是将任何内容分配给强/保留或复制属性,这会转换为访问器消息,导致该属性的先前值被释放(由设置器完成)。即便如此,观察属性的值——比如使用 KVO——并不意味着你会知道对象什么时候被释放;最特别的是,当拥有对象被释放时,当它直接向拥有的对象发送release 时,您不会收到通知。您还将在控制台中收到一条警告消息(因为拥有对象在您观察它时死亡),并且您不希望来自单元测试的嘈杂警告消息。此外,您必须专门观察 每个对象的每个属性 才能实现这一点——错过一个,您可能会错过一个错误。

发给对象的release 消息对指向该对象的任何变量都没有影响。释放也不行。

这在ARC 下略有变化:当引用的对象消失时,弱引用变量将自动分配nil。不过,这对您没有多大帮助,因为根据定义,强烈引用变量不会:如果有对对象的强引用,则对象不会 (嗯,不应该)消失,因为强引用将(应该)让它保持活力。一个物体在它应该之前死亡是您正在寻找的问题之一,而不是您想要用作工具的东西。

可以理论上为您创建的每个对象创建一个弱引用,但您必须专门引用每个对象,在您的代码中手动为其创建一个变量。可想而知,一种巨大的痛苦,肯定会错过物体。

另外,您对实际释放对象的时间有什么保证?

通过向对象发送release 消息来释放对象,因此对象在收到该消息时被释放。

也许您的意思是“解除分配”。释放只是让它更接近那个点。一个对象可以被多次释放,并且如果每次释放仅仅平衡了之前的保留,那么它仍然有很长的生命周期。

一个对象在最后一次释放时被释放。这会立即发生。臭名昭著的retainCount 甚至不会降到 0,正如许多试图写 while ([obj retainCount] > 0) [obj release]; 的聪明人所发现的那样。

实际上可能有两个答案:一个使用自动释放池,另一个不使用。

使用自动释放池的解决方案仅适用于自动释放的对象;根据定义,未自动释放的对象不会进入池中。永远不要自动释放某些对象(尤其是您创建的成千上万个对象)是完全有效的,有时也是可取的。此外,您无法查看池中的内容和没有的内容,或者尝试戳每个对象以查看其是否已死。

您将如何编写单元测试(例如使用 OCUnit)以确保在 Cocoa/Objective-C 中正确释放/保留对象?

您可以做的最好的事情是在setUp 中将NSZombieEnabled 设置为YES,并在tearDown 中恢复其先前的值。这将捕获过度释放/保留不足,但不会捕获任何类型的泄漏。

即使您可以编写一个彻底测试内存管理的单元测试,它仍然是不完美的,因为它只能测试可测试的代码——模型对象,也许还有某些控制器。视图代码、nib-borne 引用和某些选项(想到“Release When Closed”)等原因仍然可能导致您的应用程序出现泄漏和崩溃。

您无法编写任何应用程序外测试来确保您的应用程序没有内存错误。

也就是说,像您想象的那样的测试,如果它是独立的和自动的,即使它无法测试所有内容,它也会非常酷。所以我希望我错了,有办法。

【讨论】:

  • 我羡慕你的写作毅力。 :)
  • 分析仪器泄漏测试并非易事,这很痛苦。
【解决方案2】:

如果您可以使用新引入的自动引用计数(在 Xcode 的生产版本中尚不可用,但 documented here),那么您可以使用弱指针来测试是否有任何内容被过度保留。

- (void)testMemory {
    __weak id testingPointer = nil;
    id someObject = // some object with a 'foo' property

    @autoreleasepool {
        // Point the weak pointer to the thing we expect to be dealloc'd
        // when we're done.
        id theFoo = [someObject theFoo];
        testingPointer = theFoo;

        [someObject setTheFoo:somethingElse];

        // At this point, we still have a reference to 'theFoo',
        // so 'testingPointer' is still valid. We need to nil it out.
        STAssertNotNil(testingPointer, @"This will never happen, since we're still holding it.")

        theFoo = nil;
    }


    // Now the last strong reference to 'theFoo' should be gone, so 'testingPointer' will revert to nil
    STAssertNil(testingPointer, @"Something didn't release %@ when it should have", testingPointer);
}

请注意,由于语言语义发生了这种变化,这在 ARC 下有效:

可保留对象指针要么是空指针,要么是指向有效对象的指针。

因此,设置一个指向 nil 的指针的行为保证释放它指向的对象,并且没有办法(在 ARC 下)在不删除指向它的指针的情况下释放一个对象。

需要注意的一点是 ARC 可以在每个文件的基础上启用;它不需要您的整个项目都使用它。你可以用 ARC 编译你的单元测试,然后让你的主项目手动保留发布,这个测试仍然可以工作。

上面没有检测到过度释放,但无论如何NSZombieEnabled 很容易捕捉到。

如果 ARC 根本不是一个选项,您可以使用 Mike Ash 的MAZeroingWeakRef 做类似的事情。我用的不多,但它似乎以向后兼容的方式提供了与 __weak 指针类似的功能。

【讨论】:

  • 这确实不适用于 ARC,因为不允许您分配自动释放池。请改用@autorelease 关键字。 :-)
  • 是的,我考虑过。但我不确定@autoreleasepool 是否已公开记录。原来是这样。 :)
  • 为什么我们需要 theFoo = nil;不应该在releasepool耗尽后自动释放吗?
  • 是的,theFoo = nil 不是必需的。我要么试图明确地指出我写它时发生的事情(9 年前),要么我当时并没有完全了解语义。
【解决方案3】:

这可能不是您想要的,但作为一个思想实验,我想知道这是否可能会做一些接近您想要的事情:如果您创建一个机制来跟踪您想要的特定对象的保留/释放行为怎么办去测试。像这样工作:

  1. 创建 NSObject dealloc 的覆盖
  2. 创建一个CFMutableSetRef 并设置一个自定义保留/释放函数以什么都不做
  3. 制作像registerForRRTracking: (id) object这样的单元测试例程
  4. 制作一个像clearRRTrackingReportingLeaks: (BOOL) report 这样的单元测试例程,它将及时报告集合中的任何对象。
  5. 在单元测试开始时调用[tracker clearRRTrackignReportingLeaks: NO];
  6. 在单元测试中为要跟踪的每个对象调用 register 方法,它会在 dealloc 时自动删除。
  7. 在测试结束时调用[tracker clearRRTrackingReportingLeaks: YES];,它会列出所有未正确处理的对象。

您也可以覆盖NSObject alloc 并只跟踪所有内容,但我想您的集合会变得过大(!!!)。

最好将CFMutableSetRef 放在一个单独的进程中,这样它就不会过多地影响您的程序运行时内存占用。虽然增加了进程间通信的复杂性和运行时影响。可以使用私有堆(或区域 - 那些还存在吗?)将其隔离到较小的程度。

【讨论】:

  • 有趣!来自 C++ 背景,我什至没有想过(甚至不知道你可以安全地)覆盖NSObjectdealloc。您能否用一些示例代码阐明第 1-4 步?
  • 不是今天,被淹没了。不过没那么难;准备一些文档应该可以帮助您到达那里。用于交换方法实现的 ObjectiveC 运行时例程 (#1),#2 的 CoreFoundation 参考。 #3 & #4 只是描述了我放在 MemoryTracker 类实现中的几个方法(它也将 CFMutableSetRef 作为实例变量)。如果您深入研究并陷入困境,请在此处发表另一条评论,我将在下周进行宣传。
  • 所以基本上,在 test -setup swizzle (replace) NSObject 的 -init 和 dealloc 方法中分别从全局数组中添加和删除对象。在 -teardown 结束时,全局数组应该是空的(您还需要将测试包装在 releasepool 中)。你的旅费可能会改变。我实际上使用过类似的东西来清理特别棘手的保留周期。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-28
  • 1970-01-01
  • 2020-01-27
相关资源
最近更新 更多