From ea1f624ced8037931ac1dcf66dc05d78f315e2e7 Mon Sep 17 00:00:00 2001 From: Andrew Druk Date: Sat, 12 Apr 2025 16:33:23 +0300 Subject: [PATCH] Fix memory leak in NSAttributedString --- Sources/CoreFoundation/CFRunArray.c | 8 +++++ Sources/CoreFoundation/include/CFRunArray.h | 2 ++ Sources/Foundation/NSAttributedString.swift | 9 +++-- Tests/Foundation/TestNSAttributedString.swift | 36 +++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Sources/CoreFoundation/CFRunArray.c b/Sources/CoreFoundation/CFRunArray.c index 03fb1cdc8d..7a79ea1356 100644 --- a/Sources/CoreFoundation/CFRunArray.c +++ b/Sources/CoreFoundation/CFRunArray.c @@ -200,6 +200,14 @@ CFRunArrayRef CFRunArrayCreate(CFAllocatorRef allocator) { return array; } +CFRunArrayRef CFRunArrayRetain(CFRunArrayRef array) { + return COPY(array); +} + +void CFRunArrayRelease(CFRunArrayRef array) { + FREE(array); +} + CFIndex CFRunArrayGetCount(CFRunArrayRef array) { return array->guts->length; } diff --git a/Sources/CoreFoundation/include/CFRunArray.h b/Sources/CoreFoundation/include/CFRunArray.h index 6d966cff87..f46386ca0c 100644 --- a/Sources/CoreFoundation/include/CFRunArray.h +++ b/Sources/CoreFoundation/include/CFRunArray.h @@ -34,6 +34,8 @@ Returns the type identifier of all CFAttributedString instances. CF_EXPORT CFTypeID CFRunArrayGetTypeID(void); CF_EXPORT CFRunArrayRef CFRunArrayCreate(CFAllocatorRef allocator); +CF_EXPORT CFRunArrayRef CFRunArrayRetain(CFRunArrayRef array); +CF_EXPORT void CFRunArrayRelease(CFRunArrayRef array); CF_EXPORT CFIndex CFRunArrayGetCount(CFRunArrayRef array); CF_EXPORT CFTypeRef CFRunArrayGetValueAtIndex(CFRunArrayRef array, CFIndex loc, CFRange *effectiveRange, CFIndex *runArrayIndexPtr); diff --git a/Sources/Foundation/NSAttributedString.swift b/Sources/Foundation/NSAttributedString.swift index c83818d720..fe8bfb932b 100644 --- a/Sources/Foundation/NSAttributedString.swift +++ b/Sources/Foundation/NSAttributedString.swift @@ -63,7 +63,7 @@ open class NSAttributedString: NSObject, NSCopying, NSMutableCopying, NSSecureCo // use the resulting _string and _attributeArray to initialize a new instance, just like init _string = mutableAttributedString._string - _attributeArray = mutableAttributedString._attributeArray + _attributeArray = CFRunArrayRetain(mutableAttributedString._attributeArray) } open func encode(with aCoder: NSCoder) { @@ -223,7 +223,12 @@ open class NSAttributedString: NSObject, NSCopying, NSMutableCopying, NSSecureCo // use the resulting _string and _attributeArray to initialize a new instance _string = mutableAttributedString._string - _attributeArray = mutableAttributedString._attributeArray + _attributeArray = CFRunArrayRetain(mutableAttributedString._attributeArray) + } + + deinit { + // Release the CFRunArray created in init methods + CFRunArrayRelease(_attributeArray) } /// Executes the block for each attribute in the range. diff --git a/Tests/Foundation/TestNSAttributedString.swift b/Tests/Foundation/TestNSAttributedString.swift index 18e1ff2a3f..1f6e6f17d6 100644 --- a/Tests/Foundation/TestNSAttributedString.swift +++ b/Tests/Foundation/TestNSAttributedString.swift @@ -300,6 +300,42 @@ class TestNSAttributedString : XCTestCase { XCTAssertEqual(string, unarchived, "Object loaded from \(variant) didn't match fixture.") } } + + func test_attributeValueDeallocation() throws { + + class AttributeValueTracker { + let id: UUID + let deinitExpectation: XCTestExpectation + + init(expectation: XCTestExpectation) { + self.id = UUID() + self.deinitExpectation = expectation + } + + deinit { + deinitExpectation.fulfill() + } + } + + let deinitExpectation = self.expectation(description: "AttributeValueTracker should be deallocated") + var trackedObject: AttributeValueTracker? = AttributeValueTracker(expectation: deinitExpectation) + weak var weakTrackedObject = trackedObject + + // Use a 'do' block for scoping instead of 'autoreleasepool' + // This ensures 'attributedString' goes out of scope at the end of the block + do { + let strongTrackedObject = try XCTUnwrap(trackedObject) + let attributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key(rawValue: "KEY"): strongTrackedObject + ] + let attributedString = NSAttributedString(string: "Test string for lifecycle", attributes: attributes) + _ = attributedString.length + } // <-- attributedString scope ends here, ARC should release it + + trackedObject = nil + wait(for: [deinitExpectation], timeout: 1.0) + XCTAssertNil(weakTrackedObject, "Weak reference should be nil after object deallocation.") + } } fileprivate extension TestNSAttributedString {