• 7.09.2026 13:17:54
  • Admin Admin

Flutter mobil uygulama geliştirme projelerinde bellek sızıntısını DevTools heap snapshot, retaining path ve native bellek ölçümleriyle teşhis edin. Controller, abonelik, görsel önbellek ve provider yaşam döngülerini ölçülebilir biçimde düzeltin.

Flutter Mobil Uygulama Geliştirmede DevTools ile Bellek Sızıntısı Avı

Flutter mobil uygulama geliştirmede ölçülebilir bellek profili

Bellek sızıntısı incelemesini debug modda değil, profile derlemede yapın. Debug moddaki assert'ler, service extension'lar ve JIT davranışı heap dağılımını değiştirir. Uygulamayı flutter run --profile ile başlatın, DevTools Memory sekmesinde bir başlangıç heap snapshot alın, hedef ekrana 10 kez girip çıkın, GC çalıştırın ve ikinci snapshot'ı ilk snapshot ile karşılaştırın. Test senaryosunu sabitlemek için aynı kullanıcı, aynı API yanıtı ve aynı görsel listesi kullanılmalıdır.

flutter run --profile
flutter pub global run devtools
adb shell am force-stop com.example.app
adb shell monkey -p com.example.app 1

DevTools'ta yalnızca toplam heap grafiğine bakmak yeterli değildir. Snapshot diff görünümünde artan sınıfları filtreleyin: örneğin her navigasyondan sonra State, ScrollController, StreamSubscription veya uygulamanıza ait ViewModel instance sayısı yükseliyorsa ilgili sınıfı seçip Retaining Path açın. Retaining path'te bir static cache, event bus listener'ı veya closure zinciri görmeniz, nesnenin neden GC tarafından toplanamadığını doğrudan gösterir. Bir dart programlama eğitimi içinde bu ayrım özellikle önemlidir: instance sayısındaki geçici artış ile GC sonrası kalan referans zinciri aynı problem değildir.

Önce-sonra karşılaştırmasını sayısallaştırın. Örneğin test protokolünüz 'ürün listesine gir, 50 öğe kaydır, detay aç, geri dön' akışını 10 kez çalıştırmak olsun. Düzeltmeden önce ve sonra aynı noktada alınan snapshot'larda canlı ProductDetailsState sayısını, Dart heap farkını ve External memory farkını kaydedin. Başarılı kriter, sadece grafiğin daha düşük görünmesi değil, GC sonrasında beklenmeyen sınıfların sayısının başlangıç seviyesine dönmesidir.

Controller ve aboneliklerin yaşam döngüsünü kapatın

mounted kontrolü, sızıntı önleme mekanizması değildir. Aşağıdaki örnekte if (!mounted) return, dispose edilmiş State üzerinde setState hatasını engeller; fakat StreamSubscription iptal edilmezse stream, callback closure aracılığıyla State nesnesini referanslamaya devam edebilir. Timer ve ScrollController için de benzer şekilde açık bir kapatma gerekir.

import 'dart:async';
import 'package:flutter/material.dart';

class LiveQuotesPage extends StatefulWidget {
  const LiveQuotesPage({super.key, required this.quotes});
  final Stream<double> quotes;

  @override
  State<LiveQuotesPage> createState() => _LiveQuotesPageState();
}

class _LiveQuotesPageState extends State<LiveQuotesPage> {
  final _scrollController = ScrollController();
  StreamSubscription<double>? _quoteSubscription;
  Timer? _refreshTimer;
  double? _latestQuote;

  @override
  void initState() {
    super.initState();
    _quoteSubscription = widget.quotes.listen((quote) {
      if (!mounted) return;
      setState(() => _latestQuote = quote);
    });
    _refreshTimer = Timer.periodic(const Duration(minutes: 1), (_) {
      // Yenileme isteği burada tetiklenir.
    });
  }

  @override
  void dispose() {
    _refreshTimer?.cancel();
    unawaited(_quoteSubscription?.cancel() ?? Future.value());
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Text('${_latestQuote ?? '-'}');
}

Flutter Inspector ile widget ağacından bir sayfanın ekrandan kalktığını doğruladıktan sonra DevTools'ta _LiveQuotesPageState için retaining path kontrolü yapın. Sık görülen edge case, bir repository'nin broadcast StreamController'ına listen ekleyip yalnızca widget dispose olduğunda controller'ı kapatmaktır. Controller uygulama ömrü boyunca yaşayacaksa kapatılması yanlış olabilir; doğru işlem, o sayfaya ait StreamSubscription'ı cancel etmektir. flutter eğitimi veya flutter kursu materyallerinde sık atlanan bu fark, özellikle WebSocket ve event bus kullanan ekranlarda kalıcı State birikimini açıklar.

Flutter state management içinde family cache sınırları

flutter state management katmanında sızıntı çoğu zaman klasik bir dispose unutulmasından değil, sınırsız anahtar uzayından gelir. Arama sorgusunu provider family anahtarı yaparsanız 'a', 'ap', 'app', 'appl' gibi her ara değer bağımsız state oluşturabilir. Riverpod kullanıyorsanız autoDispose ile dinleyicisi kalmayan provider'ları bırakın; gerçekten gerekli kısa süreli geri dönüş önbelleği için keepAlive süresini bilinçli ve sınırlı tanımlayın.

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final searchResultsProvider =
    FutureProvider.autoDispose.family<List<SearchResult>, String>(
  (ref, rawQuery) async {
    final query = rawQuery.trim().toLowerCase();
    if (query.length < 2) return const [];

    final link = ref.keepAlive();
    final ttl = Timer(const Duration(seconds: 30), link.close);
    ref.onDispose(ttl.cancel);

    return ref.read(searchApiProvider).search(query);
  },
);

Bu kodda keepAlive, sonuçları yalnızca 30 saniye saklar; link.close çağrılmazsa family anahtarları oturum boyunca birikebilir. Ayrıca UI katmanında debounce uygulayın ve normalize edilmiş sorguyu provider'a verin. Örneğin 250 ms debounce ile yalnızca son sorguyu çalıştırmak, hem oluşturulan family anahtarı sayısını hem de iptal edilmemiş HTTP isteği sayısını azaltır. DevTools snapshot diff'te provider container tarafından tutulan state nesnelerini aramak yerine, uygulamanızdaki SearchResult listelerinin ve family anahtarlarına bağlı controller'ların navigasyon sonrası kalıp kalmadığını inceleyin.

Cross platform mobil uygulama geliştirmede görsel belleği izleme

Dart heap sabit kalırken uygulamanın bellek kullanımı artıyorsa decoded image buffer'lar External memory tarafında birikiyor olabilir. 4000 x 3000 piksel RGBA bir görsel yaklaşık 45.8 MB alan kaplar: 4000 x 3000 x 4 byte. Ekranda 360 x 240 piksel gösterilmesi, kaynak görselin varsayılan olarak tam çözünürlükte decode edilmeyeceği anlamına gelmez. Liste ve grid hücrelerinde hedef decode boyutunu cacheWidth ve cacheHeight ile verin.

Image.network(
  product.thumbnailUrl,
  width: 180,
  height: 120,
  fit: BoxFit.cover,
  cacheWidth: 360,
  cacheHeight: 240,
  errorBuilder: (context, error, stackTrace) =>
      const ColoredBox(color: Color(0xFFE0E0E0)),
)

Uygulama başlangıcında ImageCache için cihazınıza uygun açık bir byte bütçesi belirleyin ve etkisini aynı kaydırma senaryosunda ölçün. Bu değer evrensel değildir: 64 MB bütçe düşük bellekli cihazda agresif olabilir, yüksek çözünürlüklü katalogda ise sürekli yeniden decode üretir. DevTools Memory grafiğindeki External memory ile Android'de adb shell dumpsys meminfo paket.adiniz çıktısını birlikte kaydedin. iOS tarafında aynı akışı Instruments Allocations ile çalıştırarak VM heap yerine native allocation artışını kontrol edin; cross platform mobil uygulama geliştirme sürecinde yalnızca Dart snapshot'ına bakmak platforma özgü görüntü buffer maliyetini gizler.

void configureImageCache() {
  final cache = PaintingBinding.instance.imageCache;
  cache.maximumSize = 120;
  cache.maximumSizeBytes = 64 << 20; // 64 MiB
}

Bu ayarı ekledikten sonra önce 100 kartlık listeyi iki kez sonuna kadar kaydırın, GC sonrası External memory değerini not edin, sonra aynı testi bütçe değişikliğiyle tekrarlayın. Eğer ağ isteği sayısı yükseliyorsa sorun ImageCache değil, HTTP cache politikası veya benzersiz URL parametreleri olabilir. URL'ye her render'da değişen timestamp eklemek, ImageProvider anahtarını da değiştirir ve ImageCache'in aynı görseli tekrar kullanmasını engeller.

İlgili Eğitim

Flutter Eğitimi

Sık Sorulan Sorular

Flutter eğitimi sırasında DevTools ile bellek sızıntısı nasıl bulunur?

Uygulamayı flutter run --profile ile çalıştırın. Aynı navigasyon akışından önce ve sonra DevTools Memory snapshot alın, GC sonrası diff görünümünde artan sınıfı seçin ve Retaining Path zincirini inceleyin. State nesnesini bir StreamSubscription veya static koleksiyon tutuyorsa düzeltme referansın bulunduğu noktadadır.

Flutter state management kullanırken autoDispose neden bellek sorununu tek başına çözmez?

autoDispose, dinleyici kalmadığında provider state'ini bırakır; ancak ref.keepAlive ile açılan bağlantı kapatılmazsa veya family anahtarları sınırsız üretilirse state yaşamaya devam eder. keepAlive link'i için Timer ile TTL tanımlayın, ref.onDispose içinde timer.cancel çağırın ve arama gibi akışlarda normalize edilmiş anahtar kullanın.

Flutter mobil uygulama geliştirmede Image.network bellek tüketimi nasıl düşürülür?

Widget'ın ekrandaki fiziksel hedef boyutuna göre cacheWidth ve cacheHeight verin. Ardından ImageCache.maximumSizeBytes ayarını değiştirip aynı kaydırma senaryosunda DevTools External memory ve adb shell dumpsys meminfo çıktısını önce-sonra karşılaştırın. Kaynak görselin piksel sayısı, ekrandaki widget boyutundan daha belirleyicidir.

Dart programlama eğitimi için mounted kontrolü abonelik sızıntısını engeller mi?

Hayır. mounted yalnızca dispose sonrası setState çağrısını atlar. Stream callback'i State'i referanslamayı sürdürüyorsa nesne toplanamaz. StreamSubscription.cancel, Timer.cancel, AnimationController.dispose ve ScrollController.dispose işlemlerini State.dispose içinde açıkça çağırın.

AI / LLM Discovery

Bu makale Opendart Akademi Flutter eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt