27

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

一种天真的方法是检查 的值retainCount,但当然你不应该使用retainCount. 你能简单地检查一个对象的引用是否被分配了一个值nil来表明它已经被释放了吗?此外,您对实际释放对象的时间有什么保证?

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

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

关于答案

我接受了BJ Homer回答,因为我发现它是完成我的想法的最简单、最简洁的方法,因为需要注意的是自动引用计数提供的弱指针在 XCode 的生产版本中不可用(之前到 4.2?)截至 2011 年 7 月 23 日。得知这一点我也印象深刻

ARC 可以在每个文件的基础上启用;它不需要您的整个项目都使用它。您可以使用 ARC 编译您的单元测试并将您的主项目保留在手动保留发布上,并且该测试仍然可以工作。

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

4

3 回答 3

17

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

也就是说,像你想象的那样的测试,如果它是独立的和自动的,即使它不能测试所有东西,它也会很酷。所以我希望我错了,有办法。

于 2011-07-05T09:56:05.253 回答
14

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

- (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 指针类似的功能。

于 2011-07-05T16:07:23.617 回答
1

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

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

于 2011-07-15T17:18:15.087 回答