Downsample used ResizeImagePolicy.exact which forced a 2048×2048 square and stretched non-square images. Switch to ResizeImagePolicy.fit to preserve aspect ratio while still capping both dimensions.
136 lines
4.8 KiB
Markdown
136 lines
4.8 KiB
Markdown
# Analiz: Aspect Ratio Bozulması + RAM Büyümesi
|
||
|
||
Tarih: 2026-08-14
|
||
Sürüm: v1.3.0 sonrası regresyon
|
||
|
||
---
|
||
|
||
## 1. Aspect Ratio Bug'ı (kök neden bulundu)
|
||
|
||
### Belirti
|
||
- Next/prev ile resim değişince görüntü **en/boy oranı bozulmuş** (gerilmiş/sıkışmış) geliyor.
|
||
- Fazla zoom yapınca aspect düzeliyor.
|
||
|
||
### Kök Neden — `ResizeImagePolicy.exact`
|
||
|
||
`lib/widgets/image_canvas.dart:429`:
|
||
|
||
```dart
|
||
ResizeImage.resizeIfNeeded(effectiveWidth, effectiveWidth, _cachedFileImage!)
|
||
```
|
||
|
||
Bu çağrı Flutter SDK içinde şuna dönüşür (`image_provider.dart:1302-1310`):
|
||
|
||
```dart
|
||
static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, provider) {
|
||
if (cacheWidth != null || cacheHeight != null) {
|
||
return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
|
||
}
|
||
return provider;
|
||
}
|
||
```
|
||
|
||
Ve `ResizeImage` constructor'ı (`image_provider.dart:1262-1268`):
|
||
|
||
```dart
|
||
const ResizeImage(this.imageProvider, { this.width, this.height,
|
||
this.policy = ResizeImagePolicy.exact, // ← VARSAYILAN exact
|
||
this.allowUpscaling = false,
|
||
});
|
||
```
|
||
|
||
**`ResizeImagePolicy.exact`**: genişlik ve yüksekliği **bağımsız** ölçekler, aspect ratio'yu
|
||
**koruMAZ**. Yani `width: 2048, height: 2048` demek → her resmi **2048×2048 kareye
|
||
zorla**. 4000×2000 bir resim 2048×2048'e gerilir (2:1 → 1:1), görüntü bozulur.
|
||
|
||
Zoom yapınca `_checkFullResSwap()` → `_usingFullRes = true` → `_cachedProvider =
|
||
_cachedFileImage` (orijinal boyutlu `FileImage`, aspect doğru) → görüntü düzelir.
|
||
Bu, belirtiyle birebir örtüşüyor.
|
||
|
||
### Düzeltme
|
||
|
||
`resizeIfNeeded` yerine doğrudan `ResizeImage` + `policy: ResizeImagePolicy.fit`:
|
||
|
||
```dart
|
||
_cachedProvider = effectiveWidth != null
|
||
? ResizeImage(
|
||
_cachedFileImage!,
|
||
width: effectiveWidth,
|
||
height: effectiveWidth,
|
||
policy: ResizeImagePolicy.fit,
|
||
)
|
||
: _cachedFileImage;
|
||
```
|
||
|
||
`fit` policy'si (SDK `image_provider.dart:1378-1409`) aspect ratio'yu koruyarak
|
||
2048×2048 kutusuna **sığdırır**:
|
||
- 4000×2000 → 2048×1024 (2:1 korunur)
|
||
- 800×20000 dikey → 82×2048 (HATA-2'nin istediği dikey sınırlama da korunur)
|
||
- 20000×800 panorama → 2048×82
|
||
|
||
> Not: `resizeIfNeeded` helper'ı `policy` parametresi **almaz**, bu yüzden `fit`
|
||
> için `ResizeImage`'ı doğrudan kurmak zorunlu.
|
||
|
||
---
|
||
|
||
## 2. RAM Büyümesi Analizi
|
||
|
||
### Gözlem
|
||
RAM 200MB'a kadar büyüdü. "Daha büyür mü?" — Evet, aşağıdaki modele göre büyüyebilir.
|
||
|
||
### RAM modeli (mevcut)
|
||
|
||
| Kaynak | Boyut |
|
||
|---|---|
|
||
| Downsample (fit 2048×2048, RGBA) | en fazla 2048×2048×4 = **16.7MB** |
|
||
| Full-res swap (zoom ≥ 1.5x) | resme bağlı: 12000px → **576MB**, 4000px → 64MB |
|
||
| `ImageCache.maximumSize` | 16 entry |
|
||
| `ImageCache.maximumSizeBytes` | 256MB |
|
||
|
||
### Neden 200MB'a büyüdü?
|
||
|
||
1. Kullanıcı zoom yaptı → `_checkFullResSwap` threshold 1.5 aşıldı → tam çözünürlük
|
||
decode edildi → RAM'e ~100MB+ girdi.
|
||
2. Full-res resim **canlı** olduğu için `maximumSizeBytes = 256MB` sınırı onu
|
||
**atamaz** (Flutter aktif kullanımdaki image'ı evict etmez).
|
||
|
||
### Daha büyür mü? — Evet, iki senaryo
|
||
|
||
- **Daha büyük resim:** 12000px panorama full-res'e geçince tek resim ~576MB.
|
||
`maximumSizeBytes = 256MB` canlı resmi korur, sınır aşılır.
|
||
- **Çok sayıda full-res swap:** her resme zoom yapıp geçince, `didUpdateWidget`
|
||
eski provider'ı `evict` eder ama evict **asenkron/gecikmeli** olabilir; ardışık
|
||
hızlı geçişte 2-3 full-res aynı anda RAM'de kalabilir.
|
||
|
||
### Yapılacak iyileştirmeler
|
||
|
||
1. **Full-res swap'ı kademeli yap** (şu an binary: 2048 → full). Orta seviye
|
||
(ör. 4096px) ekleyerek RAM'i sınırla. Ama bu kullanıcı isteği değilse yapma.
|
||
2. **`_checkFullResSwap` stretch eşiğini yükselt** (1.5 → 2.0) → daha az full-res
|
||
swap, daha az RAM. Trade-off: zoom'da biraz daha geç keskinleşme.
|
||
3. **`maximumSizeBytes`'ı düşür** (256MB → 128MB) → LRU daha agresif. Ama canlı
|
||
resmi yine korur, etkisi sınırlı.
|
||
4. **Full-res swap'ı dispose etme zamanlamasını netleştir** — şu an `didUpdateWidget`
|
||
evict yapıyor; `dispose()` içine `_cachedProvider?.evict()` de eklenebilir.
|
||
|
||
### Karar önerisi
|
||
|
||
Aspect bug'ı **kesin düzeltilmeli** (görsel regresyon, her resimde). RAM büyümesi
|
||
ise full-res swap'ın **beklenen davranışı** — 200MB makul, ama 576MB'lık dev
|
||
panoramalarda tek resimde patlayabilir. `fit` policy'ye geçmek downsample'ı da
|
||
düzelteceği için ortalama RAM'i de bir miktar düşürür (exact kare yerine doğru
|
||
orantılı küçük resim).
|
||
|
||
---
|
||
|
||
## 3. Öncelik Sırası
|
||
|
||
1. **Aspect bug fix** — `ResizeImagePolicy.fit` (tek satır, kritik görsel düzeltme)
|
||
2. **RAM** — şimdilik izle; 2. eşik/kademeli swap istersen ayrı yaparız.
|
||
|
||
## 4. Doğrulama
|
||
|
||
- `flutter analyze`: 0 error
|
||
- Release build: ✓
|
||
- Manuel test: kullanıcı (aspect düzelmesi + RAM gözlemi)
|