Finding and Fixing Memory Leaks in iOS and Android Apps
The app crashes on iOS after 20 map screen switches — heap shows 240 MB from MapViewController alone. The crash log contains Terminated due to memory pressure. Instruments → Allocations shows ~12 MB growth per open, and those 12 MB are never released. This is not a guess but a precise memory leak.
Each such freeze reduces user LTV: according to App Annie, memory leaks increase uninstall rate by 15-20% in the first week. Fixing leaks not only saves the app from crashes but also saves budget: the cost of fixing one leak is an order of magnitude lower than losses from user churn. Our engineers with 7+ years of experience have completed over 30 memory optimization projects. We help find and eliminate leaks with guaranteed results.
Why Memory Leaks Are Critical for Performance
Memory leaks cause gradual RAM growth, leading to CPU throttling, frequent GC pauses on Android, and increased response time. Eventually — crash or freeze. On iOS, at 80% RAM load the system starts killing background processes, at 90% it kills the app. On Android — OutOfMemoryError. Users delete such apps within days. Statistics show 68% of users have encountered memory issues, and 25% uninstall immediately after two crashes.
Common Types of Memory Leaks on iOS and Android
Retain Cycles on iOS
ARC counts strong references. If A holds B and B holds A — neither will reach zero count and never be released. Common patterns:
Closure without [weak self]:
// LEAK viewModel.onDataLoaded = { self.tableView.reloadData() } // FIX viewModel.onDataLoaded = { [weak self] in self?.tableView.reloadData() } Timer:
// LEAK — Timer holds target strongly timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true) When the ViewController is closed, the timer continues working and holds the ViewController. Solution — Timer.scheduledTimer(withTimeInterval:repeats:block:) with [weak self] and timer.invalidate() in deinit.
Delegate without weak:
// LEAK protocol DataDelegate: AnyObject { func didLoad() } class DataService { var delegate: DataDelegate? // should be weak! } Use weak var delegate: DataDelegate?.
Leaks on Android
Context leak is the most common: Activity Context stored in a singleton. Activity is not released as long as the Repository lives. Always use applicationContext.
Anonymous inner class + Handler:
private val handler = Handler(Looper.getMainLooper()) handler.postDelayed({ updateUI() }, 5000) // LEAK Solution: WeakReference<MyActivity> or lifecycleScope.launch { delay(5000); updateUI() }.
LiveData observers without removeObserver: Subscribing with liveData.observe(this, ...) where this is Fragment instead of viewLifecycleOwner leads to accumulating observers on every View recreation. Always use viewLifecycleOwner.
Tools for Detecting Memory Leaks
| Tool | Platform | Leak Types | Features |
|---|---|---|---|
| LeakCanary | Android | Activity, Fragment, View | Automatic monitoring, retain tree |
| Instruments Leaks | iOS | Retain cycles | Object graph, integration with Allocations |
| Memory Profiler | Android | All objects | Heap dump, path to GC root |
| Instruments Alloc | iOS | Logical leaks | Generations analysis, heap growth |
| Method | Complexity | Effectiveness | Implementation time |
|---|---|---|---|
| Manual code review | Medium | 60% | 2-3 days |
| LeakCanary/Instruments | Low | 95% | 1 day |
| CI checks | High | 99% | 3-5 days |
Instruments Leaks detects retain cycles 5 times faster than manual code review. LeakCanary finds leaks 3 times faster than manual heap dump analysis.
Details: LeakCanary and Instruments.
Case Study: RxJava Disposable Leak
Recently a client came to us — a Flutter developer who moved to Android. His app had a leak via Observable.interval. The subscription was created in onCreate, the Disposable was never saved. On every screen rotation, a new Observer was created, while the old one kept running. After 10 rotations — 10 active threads. LeakCanary found it in 2 minutes: retained Activity via Observable → Observer → Activity reference. Solution: use CompositeDisposable, add all subscriptions, call disposables.clear() in onStop() or onDestroy().
Our Process and Timelines
- Code base audit (1-2 days)
- Set up LeakCanary / Instruments (0.5 day)
- Run test scenarios (0.5 day)
- Analyze heap dumps (1 day)
- Fix leaks (2-5 days depending on complexity)
- Add logging in deinit/onDestroy (0.5 day)
- Regression testing (1 day)
Timelines: diagnosis — 1-3 days, fixing — 2-7 days.
What's Included in the Service
We provide a full report with heap dump diagrams, instructions for implementing defensive patterns, and architecture recommendations to prevent future leaks. If needed, we set up CI checks (LeakCanary in debug, strict retain cycle checks). As a result, you get a stable application with a typical 30-50% reduction in RAM usage.
Typical Mistakes When Searching for Leaks on Your Own
- Using Instruments Leaks for only one scenario — leaks may only appear under a specific sequence of actions.
- Ignoring leaks in third-party libraries — the problem is often not in your code but in dependencies.
- Forgetting to check deinit/onDestroy in subclasses of ViewController/Activity — the error surfaces a month later.
Memory leaks are not fatal, but they require a systematic approach. We offer a full cycle from diagnosis to implementing protective measures. Contact us for a consultation on your project. Order a memory audit today.







