Reputation: 1730
I've come across a memory leak when I make a Core Data fetch request using Swift. However, I make an almost identical fetch request in a different part of the app, but it doesn't cause a leak. In both cases, the fetch requests are made in viewDidLoad of a view controller, and the results of the fetch request are assigned to an optional property of the view controller.
Here's the method for the fetch request that does not cause any leaks:
class LocationFilter {
//Lots of other code...
class func getAllPlacesOfRegionType<T: Region>(regionType: RegionType) -> [T] {
let fetchRequest = NSFetchRequest(entityName: regionType.rawValue)
var places: [T]
do {
places = try CoreDataStack.sharedInstance.context.executeFetchRequest(
fetchRequest) as! [T]
} catch let error as NSError {
NSLog("Fetch request failed: %@", error.localizedDescription)
places = [T]()
}
places.sortInPlace({ (firstPlace, nextPlace) -> Bool in
//Ingenious sorting code...
})
return places
}
}
This method is called in viewDidLoad of a viewController, and the result is assigned to the property var allRegions: [Region]?
without any leaks. Here's the code:
class PlacesTableViewController: UITableViewController {
var allRegions: [Region]?
@IBOutlet weak var segmentedRegions: UISegmentedControl!
@IBAction func selectRegionSegment(sender: UISegmentedControl) {
// When the segmented control is tapped, the appropriate list will be loaded.
switch sender.selectedSegmentIndex {
case 0: //Country Segment
allRegions = LocationFilter.getAllPlacesOfRegionType(RegionType.Country)
case 1: //States segment
allRegions = LocationFilter.getAllPlacesOfRegionType(RegionType.Province)
case 2: //Cities segment
allRegions = LocationFilter.getAllPlacesOfRegionType(RegionType.City)
case 3: //Addresses segment
allRegions = LocationFilter.getAllPlacesOfRegionType(RegionType.Address)
default:
break
}
// Then reload the cells with animations.
let index = NSIndexSet(index: 0)
tableView.reloadSections(index, withRowAnimation: UITableViewRowAnimation.Automatic)
}
override func viewDidLoad() {
super.viewDidLoad()
selectRegionSegment(segmentedRegions)
}
}
The following method is called in viewDidLoad of a different viewController to set the property var allDays: [Day]!
.
class DateFilter {
//Lots of other code in the class...
class func getAllDays() -> [Day] {
let fetchRequest = NSFetchRequest(entityName: "Day")
let days: [Day]
do {
days = try CoreDataStack.sharedInstance.context.executeFetchRequest(
fetchRequest) as! [Day]
} catch let error as NSError {
NSLog("Fetch request failed: %@", error.localizedDescription)
days = [Day]()
}
return days
}
}
This is where it is called:
class SearchViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var allDays: [Day]!
override func viewDidLoad() {
super.viewDidLoad()
allDays = DateFilter.getAllDays()
let backgroundView = UIView(frame: CGRectZero)
tableView.tableFooterView = backgroundView
tableView.backgroundColor = UIColor.groupTableViewBackgroundColor()
}
}
Xcode instruments detect a memory leak when this is called. Supposedly the responsible library is libswiftFoundation.dylib
, and the responsible frame is static Array<A>._forceBridgeFromObjectiveC<A>(NSArray, result:inout [A]?) -> ()
. When I look at Cycles & Roots, it shows an NSArray
at the root, with +16 list: (null)
and +24 [no ivar]: (null)
branching off.
Am I doing something wrong with how I store the results of my fetch request? Or is this a bug in how Swift interacts with Core Data?
Edit: Tidied up code in accordance with Mundi's suggestion. Edit 2: Added code that calls the fetch request functions.
Upvotes: 5
Views: 2487
Reputation: 595
Just experienced this exact same issue. I went around adding weak self to every capture list, to no avail. Turned out to be a name space collision where the system failed to infer which array (or maybe its type) I was talking about (because they shared the same name). I renamed the below 'foundCategories' from 'categories' which was a property of my view controller.
func fetchCategoriesAndNotes() {
categories = []
fetchEntity("Category", predicates: nil) { [weak self] (found) -> Void in
guard let foundCategories = found as? [Category] else { return }
for category in foundCategories {} ... } }
Memory leak is gone.
Upvotes: 1
Reputation: 1730
After trying lots of things, I'm pretty sure it's a bug in how Core Data converts NSArray to Swift Arrays when fetching my Day
entities. Perhaps it has to do with the relationships or attributes of the Day
entities. I'll continue to look into it.
For now, I found a work around. Instruments kept pointing back to the libswiftFoundation method for converting NSArray to Array, and the Cycles & Roots kept showing an NSArray with no ivar
. Based on my research this has to do with the initialization of the NSArray created by the fetch request, which is converted behind the scenes to a Swift array. Since I can't change this, I made a new Array from the fetch results:
override func viewDidLoad() {
super.viewDidLoad()
let fetchResults: [Day] = DateFilter.getAllDays()
allDays = fetchResults.map({$0})
let backgroundView = UIView(frame: CGRectZero)
tableView.tableFooterView = backgroundView
tableView.backgroundColor = UIColor.groupTableViewBackgroundColor()
}
And magically, the memory leak is gone! I'm not entirely sure why the fetchResults
array is leaky, but that seems to be the source of the problem.
Upvotes: 4
Reputation: 80271
I think your fetch code is overly verbose. In particular, I think that assigning the fetch result to another variable causes some sort of conversion from Objective-C class NSArray
(which is the result type of a fetch request) which in some way causes your leak. (I also do not fully understand why but I think it also has to do with the fact that this is a class function defining variables.)
I would suggest simplifying your code.
let request = NSFetchRequest(entityName: "Day")
do { return try context.executeFetchRequest(request) as! [Day]) }
catch { return [Day]() }
Upvotes: 0