I've been writing an application for a couple months now and just started checking for memory leaks - it turns out I have a lot of them - 25 unique types (purple exclamation point) and over 500 total if I navigate through my app enough.
The Memory Graph in Xcode is pointing mostly to 1.) different elements of UIDiffableDataSource, 2.) "CFString"/"CFString (Storage)", and 3.) "Malloc Blocks". The Leaks Instrument is giving me similar feedback, saying about 90% of my memory leaks are these "Malloc Blocks" and also says the Responsible Frame is either newJSONString or newJSONValue. In many of the leaks there is a "4-node-cycle" involved which contains a Swift Closure Context, My UIDiffableDataSource<Item, Section> object, and __UIDiffableDataSource. The CFString ones just have one object - CFString, and nothing else. I'll try to add images showing the 2 examples, but StackO is restricting my ability to add them.
This leads me to believe I'm creating some type of memory leak within my custom dataSource closure for the UICollectionViewDiffableDataSource. I've tried making the DataSource a weak var & I've tried using weak self and unowned self in each of my closures -- especially the closure creating my datasource, but it's had no impact.
any additional info or additional resources would be super helpful. So far I've found iOS Academy & Mark Moeykens YouTube videos helpful in understanding the basics, but having trouble applying it to my app. I can provide code blocks if that helps, but there is a lot of code that could be causing it and not really sure what all to dump in here.
4 node cycle (diffableDataSource)
I was able to find some additional info after posting this. Based on this post I was able to use the Backtrace pane and 95% of the memory leaks are pointing back to my createDataSource() and my applySnapshotUsing(sectionIDs, itemsBySection) methods [dropped those below].
I feel like I've figured out WHERE the leaks are originating, but still stumped on HOW or IF I should fix the leaks... I've tried making closures 'weak self' and any possible variables as weak, to no avail. Any help would be appreciated :)
func applySnapshotUsing(sectionIDs: [SectionIdentifierType], itemsBySection: [SectionIdentifierType: [ItemIdentifierType]],animatingDifferences: Bool, sectionsRetainedIfEmpty: Set<SectionIdentifierType> = Set<SectionIdentifierType>()) {
var snapshot = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>()
for sectionID in sectionIDs {
guard let sectionItems = itemsBySection[sectionID], sectionItems.count > 0 || sectionsRetainedIfEmpty.contains(sectionID) else { continue }
snapshot.appendSections([sectionID])
snapshot.appendItems(sectionItems, toSection: sectionID)
}
self.apply(snapshot, animatingDifferences: animatingDifferences)
}
func createDataSource() -> DataSourceType {
//use DataSourceType closure provided by UICollectionViewDiffableDataSource class to setup each collectionViewCell
let dataSource = DataSourceType(collectionView: collectionView) { [weak self] (collectionView, indexPath, item) in
//loops through each 'Item' in itemsBySection (provided to the DataSource snapshot) and expects a UICollectionView cell to be returned
//unwrap self
guard let self = self else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SeeMore", for: indexPath)
return cell
}
//figure out what type of ItemIdentifier we're dealing with
switch item {
case .placeBox(let place):
//most common cell -> will display an image and Place name in a square box
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Restaurant", for: indexPath) as! RestaurantBoxCollectionViewCell
//Place object stored in each instance of ViewModel.Item and can be used to configure the cell
cell.placeID = place.ID
cell.segment = self.model.segment
//fetch image with cells fetchImage function
//activity indicator stopped once the image is returned
cell.imageRequestTask?.cancel()
cell.imageRequestTask = nil
cell.restaurantPic.image = nil
cell.fetchImage(imageURL: place.imageURL)
//image task will take time so animate an activity indicator to show activity in progress
cell.activityIndicator.isHidden = false
cell.activityIndicator.startAnimating()
cell.restaurantNameLabel.text = place.name
//setup long press gesture recognizer - currently contains 1 context menu item (<3 restaurant)
let lpgr : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress))
lpgr.minimumPressDuration = 0.5
lpgr.delegate = cell as? UIGestureRecognizerDelegate
lpgr.delaysTouchesBegan = true
self.collectionView?.addGestureRecognizer(lpgr)
//return cell for each item
return cell
case .header:
//top cell displaying the city's header image
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Header", for: indexPath) as! CityHeaderCollectionViewCell
self.imageRequestTask = Task {
if let image = try? await ImageRequest(path: self.city.headerImageURL).send() {
cell.cityHeaderImage.image = image
}
self.imageRequestTask = nil
}
return cell
}
}
dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) in
//Setup Section headders
let header = collectionView.dequeueReusableSupplementaryView(ofKind: "SectionHeader", withReuseIdentifier: "HeaderView", for: indexPath) as! NamedSectionHeaderView
//get section from IndexPath
let section = dataSource.snapshot().sectionIdentifiers[indexPath.section]
//find the section we're currently in & set the section header label text according
switch section {
case .genreBox(let genre):
header.nameLabel.text = genre
case .neighborhoodBox(let neighborhood):
header.nameLabel.text = "The best of \(neighborhood)"
case .genreList(let genre):
header.nameLabel.text = genre
case .neighborhoodList(let neighborhood):
header.nameLabel.text = "The best of \(neighborhood)"
default:
print(indexPath)
header.nameLabel.text = "favorites"
}
return header
}
return dataSource
}
After months and months of debugging I think I finally found the issue. For some reason I don't totally understand the section headers in my collection view (supplementaryViewProvider) were causing many of my objects to be caught in memory and not get deallocated.
Adding this line to my CollectionViewController's deinit{} completely eliminated the memory leaks in my app (validated with Memory Graph Debugger & Leaks Instrument):
deinit { dataSource.supplementaryViewProvider = nil }
To reach this conclusion I rebuilt the relevant parts of my app piece by piece and tested each time I added new code to identify where the leaks were being introduced.
Any additional clarity on why this caused leaks would be great too.