- _imageSize reset on image switch (wrong swap threshold on nav) - downsample caps both dims (tall images no longer full-res decode) - single _readImageSize instead of leaking loadingBuilder listeners - evict old downsample provider on full-res swap - remove imageCache.clear() that dropped live image (gallery flash)
6.2 KiB
Kod İnceleme + Performans Analizi
Tarih: 2026-08-14
Kapsam: Tüm lib/ (13 dosya, 1799 satır)
Bağlam: Downsampling + full-res swap + RAM leak fix (v1.2.0) sonrası genel inceleme
1. Tespit Edilen Hatalar
HATA-1 (Önemli): _imageSize resim değişiminde sıfırlanmıyor
lib/widgets/image_canvas.dart
_imageSize, loadingBuilder içinde _imageSize.isEmpty koşuluyla yalnızca bir kez
dolduruluyor. Ama _resetView() ve didUpdateWidget() bu alanı sıfırlamıyor.
Sonuç: ikinci resme geçince _imageSize eski resmin boyutunu tutar, yeni resmin
boyutu hiç okunmaz. _checkFullResSwap() eski boyutla stretch hesabı yapar →
swap yanlış zamanda tetiklenir (ya hiç, ya da gereksiz erken).
Örnek: 2000px bir resimden 12000px panoramaya geçince, stretch hesabı hâlâ 2000px üzerinden yapılır; swap ya geç tetiklenir ya da downsample 2048px'te takılı kalır (zoom'da kalite düşer). Ters yönde ise gereksiz erken full-res yükleme olur.
Düzeltme: _resetView() içine _imageSize = Size.zero; ekle.
HATA-2 (Önemli): Downsample yalnızca genişliğe uygulanıyor
_buildImage() içinde ResizeImage.resizeIfNeeded(2048, null, ...) çağrısı
cacheHeight = null geçiyor. Bu, yalnızca genişlik kısıtı demek.
Sonuç: Uzun dikey resimler (ör. 800×20000 ekran görüntüsü) genişliği 2048'in altında olduğu için hiç küçültülmez, tam çözünürlükte (20000px) decode edilir. Tek resim yüzlerce MB RAM yer — downsampling'in amacı boşa çıkar. Aynı sorun panoramik 20000×800 resimlerde genişlik kısıtıyla zaten yakalanır, ama dikeyde kaçar.
Düzeltme: ResizeImage.resizeIfNeeded(2048, 2048, ...) (iki boyut da sınırla).
Flutter codec aspect oranını koruyarak sığdırır (fit).
HATA-3 (Performans): loadingBuilder içinde listener sızıntısı
loadingBuilder her frame'de çağrılabilir. _imageSize.isEmpty olduğu sürece her
çağrıda provider.resolve() + addListener() yapılıyor; listener hiç
remove edilmiyor. _imageSize bir kez dolunca duruyor ama dolana kadar (ya da
HATA-1 yüzünden hiç dolmazsa) her rebuild'de yeni listener birikir.
Ayrıca resolve() ikinci bir decode stream'i açar — Image widget'ı zaten kendi
decode'ini yaparken boyut öğrenmek için ikinci kez çözmek gereksiz iş.
Düzeltme: _imageSize'i loadingBuilder yerine initState/didUpdateWidget'ta
tek sefer resolve() ile oku ve ImageStreamListener'ı ImageStream.removeListener
ile kaldır. Ya da Image widget'ının ImageInfo'sini imageStream üzerinden
tek listener ile dinle.
HATA-4 (Küçük): Full-res swap'ta eski downsample provider evict edilmiyor
_checkFullResSwap() _cachedProvider = _cachedFileImage yapıyor ama eski
ResizeImage provider'ı ImageCache'ten evict etmiyor. Downsamlı ui.Image
(~16MB) cache'te gereksiz kalır. maximumSizeBytes = 256MB altında LRU temizler
ama tamamen gereksiz bir tutma.
HATA-5 (Yorum yanlışlığı): imageCache.clear() canlı görseli de siler
image_manager.dart:73 — PaintingBinding.instance.imageCache.clear() tüm cache'i
boşaltır, ekrandaki mevcut görsel de dahil. Üstündeki yorum "canlı görsel korunur"
diyor ama bu doğru değil. clear() o an render edilen image'ı da atar → galeri
kurulumunda kısa bir flaş/boşluk riski.
Düzeltme: Yalnızca eski klasöre ait dosyaları evict et (tek tek) ya da bu satırı
kaldırıp didUpdateWidget evict mantığına bırak.
2. RAM Modeli (mevcut davranış)
- Downsample 2048px → ~16MB/resim (RGBA 2048×2048×4)
- Full-res swap sonrası → 100MB+ (büyük resimlerde)
ImageCache:maximumSize = 16entry,maximumSizeBytes = 256MB
Kullanıcının "50 resim = 5GB" endişesi gerçekleşmez. maximumSizeBytes = 256MB
LRU evict ile toplamı ~256MB–1.6GB arasında sınırlar:
- Tüm resimler downsample ise: 16 entry × ~16MB ≈ 256MB (tam limit)
- Full-res resimler varsa: 2–3 tane tutulur, gerisi LRU ile atılır
Yani ölçülen "her resim ~100MB, artmıyor" davranışı doğru ve beklenen.
Kalan risk: maximumSize = 16 entry limiti, maximumSizeBytes'tan bağımsız
işler. Full-res ağırlıklı kullanımda 16 × 100MB = 1.6GB'a kadar tutulabilir.
maximumSizeBytes'ı düşürmek (ör. 128MB) ya da entry limitini küçültmek bu tavanı
düşürür.
3. Performans İyileştirme Önerileri
ÖN-1: dir.listSync() → async/isolate
image_manager.dart:43 — klasör listSync() ile senkron taranıyor. Büyük klasörde
(10k+ dosya) UI thread bloklanır, pencere donar. dir.list() (async) ya da
Isolate.run ile arka planda tarama önerilir. Sonuç notifyListeners ile gelir.
ÖN-2: _printImage tam dosyayı belleğe alıyor
viewer_screen.dart:173 — File(path).readAsBytes() tüm dosyayı RAM'e yükler.
12000px resimde print sırasında +200MB spike. Nadir işlem ama downsample edilmiş
kopya üzerinden print etmek düşünülebilir.
ÖN-3: _buildImage içindeki _imageKey + ResizeImage memo cache
HATA-1 çözülünce _imageSize doğru çalışır; ayrıca _checkFullResSwap'ın
setState çağrısı scroll handler içinde — bu zaten _markRenderDirty ile çift
rebuild üretebilir. _checkFullResSwap'ın setState yerine _markRenderDirty
kullanması yeterli (ValueListenableBuilder zaten tick'i dinliyor).
ÖN-4: Warmup katmanı her açılışta
_startWarmup shader derlemesini her resim açılışında yapıyor; uygulama ömründe
bir kez yapılması yeterli olabilir (global static flag). Küçük kazanç.
4. Doğrulama Matrisi
| Madde | Durum |
|---|---|
flutter analyze |
0 error (mevcut info/deprecated uyarıları hariç) |
| Release build | ✓ |
| RAM stabilitesi (kullanıcı testi) | ✓ "her resim ~100MB, artmıyor" |
HATA-1 (_imageSize reset) |
✅ düzeltildi |
| HATA-2 (dikey downsample) | ✅ düzeltildi |
| HATA-3 (listener sızıntısı) | ✅ düzeltildi |
| HATA-4 (eski provider evict) | ✅ düzeltildi |
HATA-5 (clear() canlı görsel) |
✅ düzeltildi |
5. Öncelik Sırası
- HATA-1 + HATA-2 — doğruluk + RAM (uzun dikey resimler hâlâ patlayabilir)
- HATA-3 — listener sızıntısı (uzun oturumlarda)
- ÖN-1 — büyük klasörde UI donması
- HATA-4, HATA-5 — kozmetik/ince ayar