onmyway133 / blog

🍁 What you don't know is what you haven't learned
https://onmyway133.com/
MIT License
679 stars 33 forks source link

How to use NSFetchedResultsController memory wise in Core Data #982

Open onmyway133 opened 2 months ago

onmyway133 commented 2 months ago

If you’re using NSFetchedResultsController in Core Data, it might take up a lot of memory, especially when working with large datasets. To keep your app running smoothly, it’s important to manage memory efficiently

Use Fetch Limits and Batch Fetching

If your dataset is large, limit the number of objects fetched at a time. You can achieve this by setting a fetchBatchSize and a fetchLimit on your NSFetchRequest.

fetchBatchSize: This controls how many objects Core Data will fetch in memory at a time. Instead of loading everything, it loads a small batch on demand. fetchLimit: This sets the maximum number of records to fetch.

let fetchRequest = NSFetchRequest<YourEntity>(entityName: "YourEntity")

// Limit the fetch to 20 objects at a time (batch size)
fetchRequest.fetchBatchSize = 20

// Optionally set a maximum number of objects to fetch
fetchRequest.fetchLimit = 100

Avoid Faulting Entire Object Graph

By default, Core Data loads objects as faults (placeholders), which only load data when accessed. However, accessing related objects (e.g., through relationships) can trigger Core Data to load entire object graphs into memory. To avoid unnecessary memory consumption, carefully control faulting behavior:

Use fetchRequest.includesPropertyValues = false to avoid pre-fetching attribute values when you don’t need them. Be cautious when accessing relationships or large attributes (like images or large text fields).

fetchRequest.includesPropertyValues = false // Only fetch object IDs without actual data
fetchRequest.includesSubentities = false    // Prevent fetching related objects unless needed

Use Lightweight Fetching with propertiesToFetch

If you only need specific properties from Core Data objects (not the full object), use propertiesToFetch to reduce memory consumption. This avoids fetching the entire object and its relationships.

fetchRequest.propertiesToFetch = ["name", "id"] // Only fetch specific attributes
fetchRequest.resultType = .dictionaryResultType  // Return a dictionary result instead of full objects

Enable NSFetchedResultsController's Section Caching

If your fetch results don’t change often, you can enable caching with NSFetchedResultsController. Caching reduces the need for Core Data to re-fetch objects from the database every time you interact with the data, improving performance and reducing memory spikes.

let fetchController = NSFetchedResultsController(
    fetchRequest: fetchRequest,
    managedObjectContext: context,
    sectionNameKeyPath: "section",
    cacheName: "MyCache"  // Set a cache name to enable caching
)

Limit Relationship Fetching with relationshipKeyPathsForPrefetching

If you must fetch relationships, specify the exact relationships to prefetch using relationshipKeyPathsForPrefetching. This prevents Core Data from loading unnecessary relationships into memory.

fetchRequest.relationshipKeyPathsForPrefetching = ["relatedEntity"]

Fault Objects after Use

When you finish using certain objects and want to free up memory, you can convert them back to faults using managedObjectContext.refresh(_:mergeChanges:). This will release the memory used by the object's attributes while keeping its NSManagedObjectID in memory.

// Faulting objects manually to free memory
for object in fetchedResultsController.fetchedObjects ?? [] {
    context.refresh(object, mergeChanges: false)  // Turn the object back into a fault
}

Fetch in Background to Avoid Main Thread Blockage

Perform large fetches in the background to avoid blocking the main thread and causing memory spikes due to heavy UI interactions. Use performBackgroundTask(_:) with NSPersistentContainer to do this safely.

persistentContainer.performBackgroundTask { context in
    let fetchRequest = NSFetchRequest<YourEntity>(entityName: "YourEntity")
    // Fetch and process data in the background
}

Reduce the Number of Fetched Objects (Use Efficient Predicates)

Make sure your NSFetchedResultsController only retrieves the necessary objects by using appropriate predicates to filter the data, so only relevant objects are loaded into memory.

fetchRequest.predicate = NSPredicate(format: "age > %@", 25)