Memory Management
Overview
Proper memory management prevents memory leaks, crashes, and performance degradation. Each platform has its own memory management approach, but the principles remain consistent.
Core Principles
- Understand ownership: Know who owns what and when objects are deallocated
- Break retain cycles: Avoid strong reference cycles in closures/callbacks
- Cancel long-running tasks: Clean up async operations when no longer needed
- Observe lifecycle events: Respond to app and view lifecycle changes
iOS Memory Management
iOS uses Automatic Reference Counting (ARC) to manage memory automatically. However, developers must be careful to avoid retain cycles.
Retain Cycles in Closures
Always use [weak self] or [unowned self] in closures that capture self.
final class ProfileViewModel: ObservableObject {
private let repository: UserRepository
private var loadTask: Task<Void, Never>?
init(repository: UserRepository) {
self.repository = repository
}
func loadProfile() {
// BAD: Creates a retain cycle (self -> loadTask -> Task closure -> self)
loadTask = Task {
let user = try await repository.getCurrentUser()
self.user = user
}
// GOOD: Break the cycle with weak self
loadTask = Task { [weak self] in
guard let self else { return }
do {
let user = try await repository.getCurrentUser()
guard !Task.isCancelled else { return }
self.user = user
} catch {
// Handle error
}
}
}
deinit {
loadTask?.cancel()
}
}
Cancelling Tasks
Always cancel tasks when they're no longer needed.
@MainActor
final class FeedViewModel: ObservableObject {
@Published private(set) var posts: [Post] = []
private var loadTask: Task<Void, Never>?
func load() {
loadTask?.cancel() // Cancel any previous task
loadTask = Task { [weak self] in
guard let self else { return }
do {
let posts = try await repository.fetchPosts()
guard !Task.isCancelled else { return }
self.posts = posts
} catch {
// Handle error
}
}
}
deinit {
loadTask?.cancel()
}
}
SwiftUI View Lifecycle
Use .task modifier for automatic cancellation.
struct FeedView: View {
@StateObject private var viewModel: FeedViewModel
var body: some View {
List(viewModel.posts) { post in
PostRow(post: post)
}
.task {
await viewModel.load() // Automatically cancelled when view disappears
}
}
}
Weak vs Unowned
- Use
weakwhen the captured reference might becomenil - Use
unownedwhen you're certain the reference will outlive the closure
// GOOD: Use weak (safe)
class ViewController {
func setupButton() {
button.addAction { [weak self] in
self?.handleTap() // Safe - checks for nil
}
}
}
// CAUTION: Use unowned (unsafe but no optional unwrapping)
class ViewController {
func setupButton() {
button.addAction { [unowned self] in
self.handleTap() // Crashes if self is deallocated
}
}
}
Combine Publishers
Always store cancellables and cancel them when done.
import Combine
final class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published private(set) var results: [Result] = []
private var cancellables = Set<AnyCancellable>()
init(searchService: SearchService) {
$searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] query in
self?.search(query)
}
.store(in: &cancellables)
}
deinit {
cancellables.removeAll() // Automatically cancelled
}
}
Value Types vs Reference Types
Prefer value types (structs) for models to avoid reference-related issues.
// GOOD: Value type (no memory management concerns)
struct User {
let id: String
let name: String
}
// CAUTION: Reference type (requires careful memory management)
class User {
let id: String
let name: String
init(id: String, name: String) {
self.id = id
self.name = name
}
}
Android Memory Management
Android uses Garbage Collection, but memory leaks can still occur through Context references and lifecycle-unaware observers.
Avoid Context Leaks in ViewModels
Never hold references to Activity or Fragment in ViewModels.
// BAD: Leaks Activity
class MainViewModel(private val activity: Activity) : ViewModel() {
// activity reference prevents garbage collection
}
// GOOD: Use Application Context or no context
@HiltViewModel
class MainViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
private val repository: Repository
) : ViewModel() {
// Application context is safe
}
Use ViewModelScope
Always use viewModelScope for coroutines in ViewModels.
@HiltViewModel
class FeedViewModel @Inject constructor(
private val repository: FeedRepository
) : ViewModel() {
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts: StateFlow<List<Post>> = _posts.asStateFlow()
fun loadPosts() {
// GOOD: viewModelScope automatically cancels on ViewModel clear
viewModelScope.launch {
val result = repository.getPosts()
_posts.value = result
}
}
}
Lifecycle-Aware Collection
Use repeatOnLifecycle or flowWithLifecycle for Flow collection.
// In Activity/Fragment
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.posts.collect { posts ->
// Update UI - automatically cancelled when stopped
}
}
}
// Or use flowWithLifecycle
lifecycleScope.launch {
viewModel.posts
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { posts ->
// Update UI
}
}
Compose - LaunchedEffect
Use LaunchedEffect for side effects in Composables.
@Composable
fun FeedScreen(viewModel: FeedViewModel = hiltViewModel()) {
val posts by viewModel.posts.collectAsState()
// GOOD: Automatically cancelled when FeedScreen leaves composition
LaunchedEffect(Unit) {
viewModel.loadPosts()
}
LazyColumn {
items(posts) { post ->
PostItem(post)
}
}
}
Dispose Resources
Always clean up resources in onCleared.
class CameraViewModel : ViewModel() {
private val cameraManager: CameraManager = ...
override fun onCleared() {
super.onCleared()
cameraManager.close() // Release camera
}
}
Weak References
Use WeakReference for callbacks that shouldn't prevent GC.
class NetworkService {
private var listenerRef: WeakReference<Listener>? = null
fun setListener(listener: Listener) {
listenerRef = WeakReference(listener)
}
private fun notifyListener(data: Data) {
listenerRef?.get()?.onData(data)
}
}
Flutter Memory Management
Flutter uses Dart's garbage collector but requires manual disposal of resources like controllers and streams.
Dispose Controllers
Always dispose controllers in dispose().
class ProfilePage extends StatefulWidget {
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
late final TextEditingController _nameController;
late final ScrollController _scrollController;
void initState() {
super.initState();
_nameController = TextEditingController();
_scrollController = ScrollController();
}
void dispose() {
// GOOD: Dispose controllers
_nameController.dispose();
_scrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return TextField(controller: _nameController);
}
}
Close Streams
Always close StreamController and StreamSubscription.
class SearchBloc {
final _queryController = StreamController<String>();
StreamSubscription? _subscription;
SearchBloc() {
// Note: debounceTime requires a reactive extension package (e.g., rxdart).
_subscription = _queryController.stream
.debounceTime(const Duration(milliseconds: 300))
.listen(_performSearch);
}
void dispose() {
// GOOD: Cancel subscription and close controller
_subscription?.cancel();
_queryController.close();
}
}
Use Bloc for Stream Management
Bloc automatically manages stream lifecycle.
// GOOD: Bloc handles cleanup automatically
class FeedBloc extends Bloc<FeedEvent, FeedState> {
final FeedRepository _repository;
FeedBloc({required FeedRepository repository})
: _repository = repository,
super(const FeedInitial()) {
on<LoadFeed>(_onLoadFeed);
}
Future<void> _onLoadFeed(
LoadFeed event,
Emitter<FeedState> emit,
) async {
emit(const FeedLoading());
try {
final posts = await _repository.getPosts();
emit(FeedLoaded(posts));
} catch (e) {
emit(FeedError(e.toString()));
}
}
// No manual cleanup needed - Bloc handles it
}
Avoid Storing BuildContext
Never store BuildContext beyond the current build.
// BAD: Storing BuildContext
class MyWidget extends StatefulWidget {
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
BuildContext? _storedContext; // BAD: Never do this
Widget build(BuildContext context) {
_storedContext = context; // BAD: Bad idea
return Container();
}
}
// GOOD: Use BuildContext immediately
class MyWidget extends StatelessWidget {
void _showSnackbar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => _showSnackbar(context, 'Hello'),
child: const Text('Show'),
);
}
}
Weak References in Dart
Use WeakReference (available in recent Dart versions) sparingly.
class CacheManager<T> {
final _cache = <String, WeakReference<T>>{};
void cache(String key, T value) {
_cache[key] = WeakReference(value);
}
T? get(String key) {
return _cache[key]?.target; // Returns null if GC'd
}
}
Timer Cleanup
Always cancel timers.
class CountdownWidget extends StatefulWidget {
State<CountdownWidget> createState() => _CountdownWidgetState();
}
class _CountdownWidgetState extends State<CountdownWidget> {
Timer? _timer;
int _seconds = 60;
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_seconds > 0) {
_seconds--;
} else {
timer.cancel();
}
});
});
}
void dispose() {
_timer?.cancel(); // Cancel timer
super.dispose();
}
Widget build(BuildContext context) {
return Text('$_seconds');
}
}
Common Patterns Across Platforms
Pattern: Automatic Cleanup Wrappers
Create wrappers that automatically clean up resources.
// iOS - Automatic task cancellation
final class TaskHolder {
private var task: Task<Void, Never>?
func run(_ operation: @escaping () async throws -> Void) {
task?.cancel()
task = Task {
try? await operation()
}
}
deinit {
task?.cancel()
}
}
// Android - Lifecycle-aware job holder
class JobHolder(private val scope: CoroutineScope) {
private var job: Job? = null
fun launch(block: suspend CoroutineScope.() -> Unit) {
job?.cancel()
job = scope.launch(block = block)
}
fun cancel() {
job?.cancel()
}
}
// Flutter - Disposable mixin
mixin Disposable {
final _disposables = <void Function()>[];
void addDisposable(void Function() dispose) {
_disposables.add(dispose);
}
void disposeAll() {
for (final dispose in _disposables) {
dispose();
}
_disposables.clear();
}
}
Pattern: Repository with Cache Invalidation
Manage cached data lifecycle properly.
// iOS
final class FeedRepository {
private var cache: [Post] = []
private var cacheTimestamp: Date?
private let cacheLifetime: TimeInterval = 300 // 5 minutes
func invalidateCacheIfNeeded() {
guard let timestamp = cacheTimestamp else { return }
if Date().timeIntervalSince(timestamp) > cacheLifetime {
cache.removeAll()
cacheTimestamp = nil
}
}
}
Memory Profiling Tools
iOS
- Instruments: Leaks, Allocations, Time Profiler
- Memory Graph Debugger: Visual retain cycle detection
- XCTest Memory Metrics: Automated memory testing
Android
- Android Profiler: Memory, CPU, Network profiling
- LeakCanary: Automatic memory leak detection
- Memory Heap Dump: Analyze heap via Android Studio
Flutter
- DevTools: Memory profiler with heap snapshots
- Observatory: Low-level Dart VM inspection
- Performance Overlay: Real-time memory usage
Summary
Memory Management Best Practices:
- Break retain cycles with weak/unowned references
- Cancel async operations when no longer needed
- Use lifecycle-aware APIs (viewModelScope, LaunchedEffect)
- Dispose controllers, streams, and subscriptions
- Prefer value types over reference types when possible
- Never hold Context/BuildContext beyond immediate use
- Profile regularly to catch leaks early
| Platform | Primary Concern | Key APIs | Tools |
|---|---|---|---|
| iOS | Retain cycles | [weak self], .task, deinit | Instruments, Memory Graph |
| Android | Context leaks | viewModelScope, repeatOnLifecycle | Profiler, LeakCanary |
| Flutter | Undisposed resources | .dispose(), Bloc | DevTools, Observatory |