We ended the previous episode with some bad news. We discovered that the current implementation of the ImageTableViewCell class is leaking DispatchWorkItem instances. Before we implement a solution, I have more bad news. The problem is more complex than it appears. There are several memory issues we need to address. Let's tackle them one by one.
Weak References
To understand the first problem, we need to remove the references to the DispatchWorkItem instance in the closure that is passed to the initializer. Comment out those lines of code.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
// if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
// }
}
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
Run the application and scroll the table view up and down. Wait a few moments until the table view is populated with images. Click the Debug Memory Graph button in the debug bar at the bottom to inspect the memory graph. Use the text field at the bottom of the Debug Navigator to only show DispatchWorkItem instances.

More than a dozen DispatchWorkItem instances are in memory. This isn't what we want or expect. Once a dispatch work item has finished executing, it should be deallocated. The root cause becomes clear if we select one of the objects in the Debug Navigator.

The selected dispatch work item is referenced by an ImageTableViewCell instance. Remember that we defined a property in the ImageTableViewCell class for keeping a reference to the dispatch work item that fetches the data for the remote resource. The dispatch work item can't be deallocated as long as the ImageTableViewCell instance holds a strong reference to it. The solution is simple. We need to turn the strong reference into a weak reference by applying the weak keyword to the fetchDataWorkItem property.
import UIKit
final class ImageTableViewCell: UITableViewCell {
...
private weak var fetchDataWorkItem: DispatchWorkItem?
...
}
If you're not familiar with strong and weak references, then I recommend reading What Is the Difference Between Strong, Weak, and Unowned References. Run the application and scroll the table view up and down. Click the Debug Memory Graph button in the debug bar at the bottom when the table view is populated with images.

The Debug Navigator confirms that we resolved the first memory issue. The DispatchWorkItem instances are deallocated as soon as they're finished executing.
Manual Memory Management
The closure that is passed to the initializer of the DispatchWorkItem class keeps a strong reference to the DispatchWorkItem instance. That is the root cause of the memory leak we discovered in the previous episode. How do we resolve this memory leak? That is a bit more complicated. Start by uncommenting the references to the DispatchWorkItem instance in the closure.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
Can we use the same technique? Can we declare the DispatchWorkItem instance as weak by using the weak keyword? The answer is no and the compiler immediately warns us if we do.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
weak var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}

Because there's no other object keeping a reference to the DispatchWorkItem instance, it is immediately deallocated. Print the value of the workItem variable immediately after the initialization of the DispatchWorkItem instance and run the application.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
weak var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
})
print(workItem)
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}

The application doesn't show any images because the DispatchWorkItem instances are not submitted to the global dispatch queue. They're already deallocated. The output in the console confirms this.
nil
nil
nil
nil
nil
nil
nil
nil
nil
nil
nil
nil
nil
nil
nil
Remove the weak keyword and the print statement. Weakly referencing the DispatchWorkItem instance isn't the solution. Can we use a capture list to weakly reference the DispatchWorkItem instance in the closure that is passed to the initializer? The answer is no. Because the dispatch work item is weakly referenced, it is deallocated by the time the closure is executed.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: { [weak workItem] in
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
The closure that is passed to the initializer is executed. That isn't the problem. The moment the closure of the dispatch work item is executed, the DispatchWorkItem instance is deallocated because there's no object keeping a strong reference to it. Run the application to confirm this.

Click the Debug Memory Graph button in the debug bar at the bottom to inspect the memory graph. The Debug Navigator confirms that no DispatchWorkItem instances are in memory.

What other options do we have left? There is one solution that isn't immediately obvious. We can set the workItem variable to nil in the closure that is passed to the initializer. By setting the workItem variable to nil, the strong reference to the DispatchWorkItem instance is broken, deallocating the dispatch work item.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
workItem = nil
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0, execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
From the moment workItem is set to nil, no other object keeps a strong reference to the DispatchWorkItem instance and it is deallocated as a result. Run the application, but don't scroll the table view.

Click the Debug Memory Graph button in the debug bar at the bottom to inspect the memory graph. The Debug Navigator confirms that no DispatchWorkItem instances are in memory.

There's a caveat, though. There's a reason why I asked you not to scroll the table view. Run the application one more time and scroll the table view up and down. Click the Debug Memory Graph button in the debug bar at the bottom to inspect the memory graph.

It seems we're back to square one. The Debug Navigator shows that dozens of DispatchWorkItem instances are being leaked. How is that possible? Select a dispatch work item and inspect the memory graph on the right.

The memory graph shows us a retain cycle. This isn't surprising. We know that the closure of the DispatchWorkItem instance retains the dispatch work item. That is why we set the workItem variable to nil in the closure.
Let's take a step back to understand why dispatch work items are being leaked. What happens if the closure isn't executed? Take a moment and try to answer that question. If the closure isn't executed, then the DispatchWorkItem instance is not deallocated, resulting in a memory leak. That is what's happening and the memory graph debugger confirms this.
Let me explain the problem. We submit the dispatch work item to a global dispatch queue. We invoke the asyncAfter(deadline:execute:) method to execute the dispatch work item with a delay. I showed you in the previous episode that the delay gives us a performance improvement.
How does that work? The dispatch work item is cancelled in the prepareForReuse() method. There are two possible cancellation scenarios. The dispatch work item is cancelled before execution or during execution. What happens if the dispatch work item is cancelled before execution? As I explained in the previous episode, in that scenario Grand Central Dispatch doesn't execute the dispatch work item. Do you see the problem?
Grand Central Dispatch discards the dispatch work item if it's cancelled before execution. But the closure of the dispatch work item still keeps a strong reference to the dispatch work item. That is causing the problem. We can verify this by removing the delay. Replace the asyncAfter(deadline:execute:) method with the async(execute:) method.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
workItem = nil
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().async(execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
Run the application and scroll the table view up and down several times. Wait until the images are displayed in the table view and click the Debug Memory Graph button in the debug bar at the bottom to inspect the memory graph. The Debug Navigator confirms that no DispatchWorkItem instances are in memory.

Have we resolved the problem? Yes and no. Yes because the dispatch work items are no longer being leaked and no because the application's performance isn't what it should be. Do you know why that is? Run the application, scroll the table view up and down a few times, and click the pause button in the debug bar at the bottom.

The Debug Navigator shows that dozens of blocks are running or pending. How is that possible? If a dispatch work item is submitted to a dispatch queue without a delay, then it's scheduled for execution. There's no way back. This means that the data for the remote resource is fetched even if the dispatch work item is cancelled.
Because a dispatch queue is a FIFO queue, the dispatch queue suffers from a significant delay between the moment of submission and the moment of execution. That is why we applied a delay earlier. That gives the application the opportunity to cancel the dispatch work item before its execution.
A Workaround
The problem we are facing is often overlooked by developers, resulting in memory leaks. I hope this episode has shown you the risks as well as a number of solutions.
I'd like to end this episode with a workaround that allows us to reintroduce a delay. This should improve the performance of the table view. We won't be using the asyncAfter(deadline:execute:) method. We use the sleep() function in the closure of the dispatch work item to interrupt the execution of the background thread for one second.
After the delay, we ask the dispatch work item whether it has been cancelled. If it has been cancelled, then we exit early and set the workItem variable to nil.
func configure(title: String, url: URL?) {
// Configure Title Label
titleLabel.text = title
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
if let url = url {
var workItem: DispatchWorkItem?
// Initialize Dispatch Work Item
workItem = DispatchWorkItem(block: {
sleep(1)
guard let dispatchWorkItem = workItem, !dispatchWorkItem.isCancelled else {
workItem = nil
return
}
// Load Data
if let data = try? Data(contentsOf: url) {
// Initialize Image
let image = UIImage(data: data)
if let workItem = workItem, !workItem.isCancelled {
DispatchQueue.main.async {
// Configure Thumbnail Image View
self.thumbnailImageView.image = image
}
}
}
workItem = nil
})
if let workItem = workItem {
// Submit Dispatch Work Item to Dispatch Queue
DispatchQueue.global().async(execute: workItem)
// Update Fetch Data Work Item
fetchDataWorkItem = workItem
}
}
}
Run the application to see the result. The table view is performant and there are no DispatchWorkItem instances leaking. While the workaround isn't very elegant, it is, as far as I know, the only viable solution if you want to use the DispatchWorkItem class in this scenario.

What's Next?
Memory management isn't complicated, but it requires your attention. This episode illustrates how easy it is to overlook memory issues. The memory graph debugger is very helpful in finding memory issues. You can learn more about the memory graph debugger in Debugging Applications With Xcode.