Summary
GutenbergCoverUploadProcessor has a retain cycle that causes it to never be deallocated after a cover block upload.
Root Cause
The lazy var coverBlockProcessor creates a GutenbergBlockProcessor with a replacer closure that strongly captures self:
// GutenbergCoverUploadProcessor.swift
lazy var coverBlockProcessor = GutenbergBlockProcessor(for: CoverBlockKeys.name, replacer: { coverBlock in
guard let mediaID = coverBlock.attributes[CoverBlockKeys.id] as? Int,
mediaID == self.mediaUploadID else { // strong capture of self
return nil
}
...
let innerProcessor = self.isVideo(attributes) ? self.videoUploadProcessor() : self.imgUploadProcessor()
...
})
This creates a cycle:
GutenbergCoverUploadProcessor
→ lazy var coverBlockProcessor (strong)
→ GutenbergBlockProcessor.replacer closure (strong)
→ self (GutenbergCoverUploadProcessor) ← cycle
Detection
Detected using XCTestLeaks — a tool that runs leaks(1) after each XCTest case via a dylib shim.
Results from GutenbergCoverUploadProcessorTests before fix:
leaks=1 testCoverBlockProcessor
leaks=2 testCoverBlockProcessorWithOtherAttributes
leaks=3 testDeepNestedCoverBlockProcessor
leaks=4 testImageCoverInVideoCoverBlockProcessor
leaks=5 testMultipleCoverBlocksProcessor
leaks=5 testNestedCoverBlockProcessor
leaks=7 testUpdateOuterCoverBlockProcessor
leaks=7 testVideoCoverBlockProcessor
leaks=7 testVideoCoverInImageCoverBlockProcessor
After fix: all tests show leaks=0.
Fix
Use [weak self] in the replacer closure:
lazy var coverBlockProcessor = GutenbergBlockProcessor(for: CoverBlockKeys.name, replacer: { [weak self] coverBlock in
guard let self,
let mediaID = coverBlock.attributes[CoverBlockKeys.id] as? Int,
mediaID == self.mediaUploadID else {
return nil
}
...
})
Summary
GutenbergCoverUploadProcessorhas a retain cycle that causes it to never be deallocated after a cover block upload.Root Cause
The
lazy var coverBlockProcessorcreates aGutenbergBlockProcessorwith areplacerclosure that strongly capturesself:This creates a cycle:
Detection
Detected using XCTestLeaks — a tool that runs
leaks(1)after each XCTest case via a dylib shim.Results from
GutenbergCoverUploadProcessorTestsbefore fix:After fix: all tests show
leaks=0.Fix
Use
[weak self]in thereplacerclosure: