如何使用 autoreleasepool 泄漏测试 NSString?

How to test NSString with autoreleasepool leak?

正在尝试修复 300MB 内存泄漏,并在找到泄漏原因后;

(Which was calls to NSString's stringFromUTF8String:, from C++ thread (without @autoreleasepool-block wrapper))

我编辑了代码,以强制执行引用计数(而不是自动释放),如下所示:

public func withNSString(
    _ chars: UnsafePointer<Int8>,
    _ callback: (NSString) -> Void
) {
    let result: NSString = NSString(utf8String: chars)!;
    callback(result);
}

作为个人政策,带有单元测试,例如:

import Foundation
import XCTest
@testable import MyApp

class AppTest: XCTestCase {
    func testWithNSString_hasNoMemoryLeak() {
        weak var weakRef: NSString? = nil
        autoreleasepool {
            let chars = ("some data" as NSString).utf8String!;
            withNSString(chars, { strongRef in
                weakRef = strongRef;
                XCTAssertNotNil(weakRef);
            })
            // Checks if reference-counting is used.
            XCTAssertNil(weakRef); // Fails, so no reference-counting. 
        }
        // Checks if autoreleased.
        XCTAssertNil(weakRef); // Fails, OMG! what is this?
    }
}

但是现在,似乎连自动释放都不起作用了(-_- )
为什么上次 XCTAssertNil 调用失败?
(换句话说,我该如何修复内存泄漏?)

问题是您使用的字符串非常短。它被内联到堆栈中,因此直到整个堆栈帧超出范围时它才会被释放。如果您使字符串稍微长一点(长 2 个字符),这将按您预期的方式运行。当然,这是一个实现细节,可能会因不同版本的编译器、不同版本的 OS、不同的优化设置或不同的架构而发生变化。

请记住,使用任何类型的静态字符串测试此类内容可能很棘手,因为静态字符串已放入二进制文件中。因此,如果编译器注意到您间接创建了一个指向静态字符串的指针,那么它可能会优化间接寻址而不释放它。

但在这些情况中 none 存在内存泄漏。您的内存泄漏更有可能发生在 withNSString 调用代码 中。我主要怀疑您没有正确处理作为 chars 传递的字节。我们需要更多地了解您认为存在泄漏的原因才能对其进行评估。 (Foundation 也有一些小泄漏,而 Instruments 对泄漏有误报,所以如果你正在追逐一个小于 50 字节的分配并且不会在每个操作中重现,你可能正在追逐幽灵。)


注意这有点危险:

let chars = ("some data" as NSString).utf8String!
withNSString(chars, { strongRef in

utf8String 内部指针未承诺比 NSString 更长寿,并且 Swift 可以在对象最后一次引用后(可能在它们超出范围之前)自由销毁对象。正如文档所述:

This C string is a pointer to a structure inside the string object, which may have a lifetime shorter than the string object and will certainly not have a longer lifetime. Therefore, you should copy the C string if it needs to be stored outside of the memory context in which you use this property.

在这种情况下对象是一个常量字符串,它在二进制文件中并且不能被销毁。但在更一般的情况下,这是崩溃的典型原因。我强烈建议远离 NSString 接口并使用 String。它提供 utf8CString,其中 returns 一个适当的 ContinguousArray,这更安全。

let chars = "some data".utf8CString
chars.withUnsafeBufferPointer { buffer in
    withNSString(buffer.baseAddress!, { strongRef in
        weakRef = strongRef;
        XCTAssertNotNil(weakRef);
    })
}

withUnsafeBufferPointer 确保 chars 在块完成之前不会被销毁。

如果需要,您还可以确保字符串的生命周期(这对于修复您不想以更安全的方式重写的旧代码非常有用):

let string = "some data"

withExtendedLifetime(string) {
    let chars = string.utf8CString
    chars.withUnsafeBufferPointer { buffer in
        withNSString(buffer.baseAddress!, { strongRef in
            weakRef = strongRef;
            XCTAssertNotNil(weakRef);
        })
    }
}