Initial commit
This commit is contained in:
536
skills/rxswift-memory-check/examples.md
Normal file
536
skills/rxswift-memory-check/examples.md
Normal file
@@ -0,0 +1,536 @@
|
||||
# RxSwift Memory Check Examples
|
||||
|
||||
Detailed examples of memory leaks, retain cycles, and proper RxSwift memory management.
|
||||
|
||||
## Critical Issue Examples
|
||||
|
||||
### Example 1: Missing Disposal (Memory Leak)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
private let paymentUC: PaymentUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func processPayment() {
|
||||
paymentUC.execute(amount: paymentAmount.value)
|
||||
.subscribe(onNext: { [weak self] result in
|
||||
self?.handleResult(result)
|
||||
})
|
||||
// ❌ MISSING: .disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Subscription never releases, accumulates in memory
|
||||
**Impact**: Memory grows over time, eventually crashes app
|
||||
**Symptoms**: Increasing memory usage, app slowdown
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
private let paymentUC: PaymentUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func processPayment() {
|
||||
paymentUC.execute(amount: paymentAmount.value)
|
||||
.subscribe(onNext: { [weak self] result in
|
||||
self?.handleResult(result)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Added
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Add `.disposed(by: disposeBag)` to every subscription
|
||||
**Result**: Subscription properly cleaned up when ViewModel deallocates
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Retain Cycle (Strong Self Reference)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class StoresViewModel: BaseViewModel<StoresState> {
|
||||
private let storesUC: StoresUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
let stores = BehaviorRelay<[Store]>(value: [])
|
||||
|
||||
func loadStores() {
|
||||
storesUC.getStores()
|
||||
.subscribe(onNext: { stores in
|
||||
// ❌ Strong self reference - retain cycle!
|
||||
self.stores.accept(stores)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Closure captures self strongly, creating retain cycle
|
||||
**Impact**: ViewModel never deallocates, memory leak
|
||||
**Symptoms**: View controllers don't deallocate, memory grows
|
||||
**Detection**: Xcode Debug Memory Graph shows cycle
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class StoresViewModel: BaseViewModel<StoresState> {
|
||||
private let storesUC: StoresUseCase
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
let stores = BehaviorRelay<[Store]>(value: [])
|
||||
|
||||
func loadStores() {
|
||||
storesUC.getStores()
|
||||
.subscribe(onNext: { [weak self] stores in
|
||||
// ✅ Weak self - no retain cycle
|
||||
self?.stores.accept(stores)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use `[weak self]` in closure capture list
|
||||
**Result**: ViewModel can deallocate properly, no memory leak
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Local DisposeBag (Early Cancellation)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||
func loadTransactions() {
|
||||
let disposeBag = DisposeBag() // ❌ Local variable!
|
||||
|
||||
transactionUC.getTransactions()
|
||||
.subscribe(onNext: { [weak self] transactions in
|
||||
self?.handleTransactions(transactions)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// ❌ disposeBag deallocates here, cancels subscription immediately!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: DisposeBag deallocates when function exits, canceling subscription
|
||||
**Impact**: Observable never completes, callbacks never fire
|
||||
**Symptoms**: Data doesn't load, UI doesn't update
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class TransactionViewModel: BaseViewModel<TransactionState> {
|
||||
private let disposeBag = DisposeBag() // ✅ Property
|
||||
|
||||
func loadTransactions() {
|
||||
transactionUC.getTransactions()
|
||||
.subscribe(onNext: { [weak self] transactions in
|
||||
self?.handleTransactions(transactions)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Uses property
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Make DisposeBag a class property, not local variable
|
||||
**Result**: Subscription lives as long as the ViewModel
|
||||
|
||||
---
|
||||
|
||||
### Example 4: Multiple DisposeBags (Anti-Pattern)
|
||||
|
||||
#### ❌ Problem Code
|
||||
```swift
|
||||
class DashboardViewModel: BaseViewModel<DashboardState> {
|
||||
private var searchDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||
private var dataDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||
private var uiDisposeBag = DisposeBag() // ❌ Anti-pattern
|
||||
|
||||
func search(query: String) {
|
||||
searchService.search(query)
|
||||
.subscribe(onNext: { results in })
|
||||
.disposed(by: searchDisposeBag)
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
dataService.loadData()
|
||||
.subscribe(onNext: { data in })
|
||||
.disposed(by: dataDisposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Unnecessary complexity, harder to manage subscriptions
|
||||
**Impact**: Confusing code, potential for errors
|
||||
|
||||
#### ✅ Fixed Code
|
||||
```swift
|
||||
class DashboardViewModel: BaseViewModel<DashboardState> {
|
||||
private let disposeBag = DisposeBag() // ✅ Single DisposeBag
|
||||
|
||||
func search(query: String) {
|
||||
searchService.search(query)
|
||||
.subscribe(onNext: { results in })
|
||||
.disposed(by: disposeBag) // ✅ Same bag
|
||||
}
|
||||
|
||||
func loadData() {
|
||||
dataService.loadData()
|
||||
.subscribe(onNext: { data in })
|
||||
.disposed(by: disposeBag) // ✅ Same bag
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use single DisposeBag per class
|
||||
**Result**: Simpler code, all subscriptions disposed together
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 5: Proper RxSwift Memory Management
|
||||
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
// Dependencies
|
||||
private let paymentUC: PaymentUseCase
|
||||
private let validationService: ValidationService
|
||||
|
||||
// DisposeBag property (not local!)
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
// State
|
||||
let paymentAmount = BehaviorRelay<String>(value: "")
|
||||
let isProcessing = BehaviorRelay<Bool>(value: false)
|
||||
|
||||
init(paymentUC: PaymentUseCase, validationService: ValidationService) {
|
||||
self.paymentUC = paymentUC
|
||||
self.validationService = validationService
|
||||
super.init()
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
private func setupBindings() {
|
||||
// ✅ Proper disposal with weak self
|
||||
paymentAmount
|
||||
.map { [weak self] amount in
|
||||
self?.validationService.validateAmount(amount) ?? false
|
||||
}
|
||||
.subscribe(onNext: { [weak self] isValid in
|
||||
self?.setState(isValid ? .valid : .invalid)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
func processPayment() {
|
||||
isProcessing.accept(true)
|
||||
|
||||
paymentUC.execute(amount: paymentAmount.value)
|
||||
.subscribeOn(ConcurrentScheduler.background)
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(
|
||||
onNext: { [weak self] result in
|
||||
self?.handleSuccess(result)
|
||||
},
|
||||
onError: { [weak self] error in
|
||||
self?.handleError(error)
|
||||
},
|
||||
onCompleted: { [weak self] in
|
||||
self?.isProcessing.accept(false)
|
||||
}
|
||||
)
|
||||
.disposed(by: disposeBag) // ✅ Always disposed
|
||||
}
|
||||
|
||||
private func handleSuccess(_ result: PaymentResult) {
|
||||
setState(.success(result))
|
||||
}
|
||||
|
||||
private func handleError(_ error: Error) {
|
||||
setState(.error(error))
|
||||
isProcessing.accept(false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ DisposeBag is a property
|
||||
- ✅ Every subscription uses `.disposed(by:)`
|
||||
- ✅ Every closure uses `[weak self]`
|
||||
- ✅ Proper error handling
|
||||
- ✅ Correct scheduler usage
|
||||
|
||||
---
|
||||
|
||||
### Example 6: ViewController with Proper Bindings
|
||||
|
||||
```swift
|
||||
class PaymentViewController: UIViewController {
|
||||
@IBOutlet weak var amountTextField: UITextField!
|
||||
@IBOutlet weak var confirmButton: UIButton!
|
||||
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
|
||||
|
||||
private let viewModel: PaymentViewModel
|
||||
private let disposeBag = DisposeBag() // ✅ Property
|
||||
|
||||
init(viewModel: PaymentViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Use dependency injection")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
bindViewModel()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
title = "Payment"
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
// Input: TextField → ViewModel
|
||||
amountTextField.rx.text
|
||||
.orEmpty
|
||||
.bind(to: viewModel.paymentAmount)
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
// Input: Button tap → ViewModel action
|
||||
confirmButton.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.viewModel.processPayment()
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
// Output: ViewModel → UI
|
||||
viewModel.isProcessing
|
||||
.bind(to: loadingIndicator.rx.isAnimating)
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
viewModel.isProcessing
|
||||
.map { !$0 }
|
||||
.bind(to: confirmButton.rx.isEnabled)
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
|
||||
// State handling
|
||||
viewModel.getState()
|
||||
.compactMap { $0 }
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] state in
|
||||
self?.handleState(state.name)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Disposed
|
||||
}
|
||||
|
||||
private func handleState(_ state: PaymentState) {
|
||||
switch state {
|
||||
case .success(let result):
|
||||
showSuccessAlert(result)
|
||||
case .error(let error):
|
||||
showErrorAlert(error)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Single DisposeBag for all bindings
|
||||
- ✅ All UI bindings properly disposed
|
||||
- ✅ Weak self in subscribe closures
|
||||
- ✅ Clean separation of concerns
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: Timer/Interval Observable
|
||||
|
||||
#### ❌ Problem
|
||||
```swift
|
||||
func startTimer() {
|
||||
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] tick in
|
||||
self?.updateTime(tick)
|
||||
})
|
||||
// ❌ No disposal - timer runs forever!
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Solution
|
||||
```swift
|
||||
class TimerViewModel {
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func startTimer() {
|
||||
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] tick in
|
||||
self?.updateTime(tick)
|
||||
})
|
||||
.disposed(by: disposeBag) // ✅ Stops when ViewModel deallocates
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Network Requests
|
||||
|
||||
#### ❌ Problem
|
||||
```swift
|
||||
func loadData() {
|
||||
networkService.fetchData()
|
||||
.subscribe(onNext: { data in
|
||||
self.data = data // ❌ Strong self
|
||||
})
|
||||
// ❌ No disposal
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Solution
|
||||
```swift
|
||||
class DataViewModel {
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func loadData() {
|
||||
networkService.fetchData()
|
||||
.subscribe(
|
||||
onNext: { [weak self] data in
|
||||
self?.data = data
|
||||
},
|
||||
onError: { [weak self] error in
|
||||
self?.handleError(error)
|
||||
}
|
||||
)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Chained Observables
|
||||
|
||||
#### ❌ Problem
|
||||
```swift
|
||||
func processData() {
|
||||
fetchData()
|
||||
.flatMap { data in
|
||||
return self.transform(data) // ❌ Strong self
|
||||
}
|
||||
.flatMap { transformed in
|
||||
return self.save(transformed) // ❌ Strong self
|
||||
}
|
||||
.subscribe(onNext: { result in
|
||||
self.handleResult(result) // ❌ Strong self
|
||||
})
|
||||
// ❌ No disposal
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Solution
|
||||
```swift
|
||||
class DataProcessor {
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
func processData() {
|
||||
fetchData()
|
||||
.flatMap { [weak self] data -> Observable<TransformedData> in
|
||||
guard let self = self else { return .empty() }
|
||||
return self.transform(data)
|
||||
}
|
||||
.flatMap { [weak self] transformed -> Observable<Result> in
|
||||
guard let self = self else { return .empty() }
|
||||
return self.save(transformed)
|
||||
}
|
||||
.subscribe(
|
||||
onNext: { [weak self] result in
|
||||
self?.handleResult(result)
|
||||
},
|
||||
onError: { [weak self] error in
|
||||
self?.handleError(error)
|
||||
}
|
||||
)
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Detection Tools
|
||||
|
||||
### Using Xcode Debug Memory Graph
|
||||
|
||||
1. Run app in Simulator/Device
|
||||
2. Navigate to screen with suspected leak
|
||||
3. Pop back
|
||||
4. Xcode → Debug → View Memory Graph
|
||||
5. Look for objects that should have deallocated
|
||||
6. Check retain cycle graph
|
||||
|
||||
### Using Instruments
|
||||
|
||||
1. Product → Profile
|
||||
2. Choose "Leaks" template
|
||||
3. Record while using app
|
||||
4. Navigate between screens
|
||||
5. Check for red leak indicators
|
||||
6. Inspect stack traces
|
||||
|
||||
### Manual Verification
|
||||
|
||||
Add `deinit` to ViewModels and ViewControllers:
|
||||
|
||||
```swift
|
||||
class PaymentViewModel: BaseViewModel<PaymentState> {
|
||||
deinit {
|
||||
print("✅ PaymentViewModel deallocated") // Should print when leaving screen
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentViewController: UIViewController {
|
||||
deinit {
|
||||
print("✅ PaymentViewController deallocated") // Should print when popping
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `deinit` doesn't print, you have a memory leak!
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Memory Management Checklist
|
||||
|
||||
```markdown
|
||||
## RxSwift Memory Check
|
||||
|
||||
For each Observable subscription:
|
||||
- [ ] Has `.disposed(by: disposeBag)`
|
||||
- [ ] Uses `[weak self]` in closures
|
||||
- [ ] DisposeBag is a property (not local)
|
||||
- [ ] No strong reference cycles
|
||||
- [ ] Error handling present
|
||||
- [ ] deinit prints when tested
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
| Pattern | Issue | Fix |
|
||||
|---------|-------|-----|
|
||||
| Missing disposal | Memory leak | Add `.disposed(by: disposeBag)` |
|
||||
| Strong self | Retain cycle | Use `[weak self]` |
|
||||
| Local DisposeBag | Early cancel | Make it a property |
|
||||
| Multiple bags | Complexity | Use single DisposeBag |
|
||||
| No error handling | Crashes | Add `onError` handler |
|
||||
Reference in New Issue
Block a user