Debugging and Fixing Memory Leaks in iOS: My Experience with the Fare Estimate Screen in Rapido 🕵️♂🕵️
Introduction
As an iOS developer 💻, effective memory management is crucial for ensuring smooth performance and a seamless user experience in your apps. Recently, I encountered a memory leak in the new Fare Estimate screen of the Rapido app. This article chronicles my journey of identifying and fixing the leak, sharing insights and techniques that might help fellow developers facing similar issues. These issues often arise with new iOS APIs like UITableViewDiffableDataSource
and Swift concurrency continuation APIs.
The Challenge
Upon introducing the new Fare Estimate screen, we noticed increased memory usage leading to performance degradation. Additionally, the fare estimate events were being triggered unexpectedly. Identifying and resolving memory leaks can be challenging, especially when the issue isn’t immediately apparent.
Tools and Techniques
- Leaks Instrument
- The Leaks Instrument in Xcode is a powerful tool for detecting memory leaks. However, pinpointing the exact location and cause of the leak can be complex.
- Experience: While the Leaks Instrument helped identify that a leak existed, it was difficult to determine exactly where the issue originated. This is often the case with more subtle memory management problems.
2. Memory Debugger
- The Memory Debugger in Xcode provides a visual representation of your app’s memory usage, including objects that have strong reference cycles.
- Experience: Using the Memory Debugger, I was able to get hints about what might be causing the issue. The primary problem turned out to be related to strong references ♻️ where weak references should have been used. Specifically, updating closures to use
[weak self]
resolved part of the issue. - Learning: Sometimes you might omit
self
when passing a closure, which implicitly capturesself
, making it harder to reason about. While the code looks cleaner with syntactic sugar, it can hide memory leaks.
Class FareEstimateVC {
lazy var someProperty = {
let view = SomeViewClass(onCancelButton)
return view
}()
func onCancelButton() {
....
}
}
Before you guessed it right, we need to capture self
as weak
or unowned
to break the cycle.
Class FareEstimateVC {
lazy var someProperty = {
let view = SomeViewClass() { [weak self] in
onCancelButton()
}
return view
}()
func onCancelButton() {
....
}
}
Same kind issue with capturing self in cellProvider while using diffable api
class MyViewController: UIViewController {
var dataSource: UITableViewDiffableDataSource<RequestRapidoBottomSectionType, AnyHashable>!
override func viewDidLoad() {
super.viewDidLoad()
dataSource = UITableViewDiffableDataSource<RequestRapidoBottomSectionType, AnyHashable>(
tableView: tableView,
cellProvider: { [self] tableView, indexPath, item in
// Make it weak self to solve the leak here as well
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
// Configure the cell
cell.onUpdate = self.onUpdate()
return cell
}
)
}
}
3. Manual Code Review and Commenting
After fixing these leaks, we noticed additional issues causing leaks in the screen. We manually added print statements in deinit
and started commenting out code to identify the problem. Eventually, we found the culprit: new Swift concurrency continuation APIs.
import UIKit
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
}
func fetchData() {
async {
let data = await loadData()
process(data)
}
}
func loadData() async -> String {
await withCheckedContinuation { continuation in
// Simulate a network call or long-running task
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// Forgot to call continuation.resume(returning:)
// continuation.resume(returning: "Some data")
}
self.process("data")
}
}
func process(_ data: String) {
print("Processing data: \(data)")
}
}
Fixing the Memory Leak
Properly ending the continuation ensures that the closure and any captured objects are released.
import UIKit
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
}
func fetchData() {
Task {
let data = await loadData()
process(data)
}
}
func loadData() async -> String {
await withCheckedContinuation { continuation in
// Simulate a network call or long-running task
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
continuation.resume(returning: "Some data")
}
}
}
func process(_ data: String) {
print("Processing data: \(data)")
}
}
Memory Management with Continuations
Continuations need to be carefully managed because they hold strong references to the closure and any objects captured by the closure. This means:
- Continuation Holds the Closure:
withCheckedContinuation
creates a continuation and captures the closure passed to it. - Captured Objects: Any objects captured by this closure (e.g.,
self
, if referenced) are also retained. - Memory Leak: Because
continuation.resume(returning:)
is never called, the continuation and its closure remain in memory 🚰. This meansself
is also retained, preventing the view controller from being deallocated🐛.
The Solution
After thorough investigation and debugging, the key issues contributing to the memory leak were:
- Strong Reference Cycles: Using strong references in closures where weak references were more appropriate.
- Retain Cycles: Objects retaining each other, preventing proper deallocation.
By addressing these issues through the techniques mentioned above, the memory leak was resolved, and the Fare Estimate screen now functions smoothly.
Conclusion
Debugging memory leaks requires patience and a methodical approach. While tools like the Leaks Instrument and Memory Debugger are incredibly useful, sometimes manual techniques such as code review and commenting can be just as effective. Sharing experiences and solutions within the developer community helps us all build more efficient and robust applications.
Feedback
I’d love to hear your thoughts and experiences on debugging memory leaks. Have you encountered similar issues? What tools and techniques have you found most effective? Share your feedback and let’s learn together! 🚀