Skip to main content

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​

  1. Understand ownership: Know who owns what and when objects are deallocated
  2. Break retain cycles: Avoid strong reference cycles in closures/callbacks
  3. Cancel long-running tasks: Clean up async operations when no longer needed
  4. 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 weak when the captured reference might become nil
  • Use unowned when 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:

  1. Break retain cycles with weak/unowned references
  2. Cancel async operations when no longer needed
  3. Use lifecycle-aware APIs (viewModelScope, LaunchedEffect)
  4. Dispose controllers, streams, and subscriptions
  5. Prefer value types over reference types when possible
  6. Never hold Context/BuildContext beyond immediate use
  7. Profile regularly to catch leaks early
PlatformPrimary ConcernKey APIsTools
iOSRetain cycles[weak self], .task, deinitInstruments, Memory Graph
AndroidContext leaksviewModelScope, repeatOnLifecycleProfiler, LeakCanary
FlutterUndisposed resources.dispose(), BlocDevTools, Observatory