Uygulama bellek bütçelerini ayarlama

Uygulama bellek bütçeleri, uygulamaların kendileri için bir bellek bütçesi beyan etmesine olanak tanır. Bu bütçe, uygulama belirlenen bütçesinden daha fazla bellek kullandığında sisteme bellek kullanımını azaltmasını söyler. Bu, özellikle sistem uygulamaları ve paket uygulamalar ya da belleği kısıtlı cihazları hedefleyen uygulamalar için kullanışlıdır. Bu durumlarda geliştirici, beklenen bellek çalışma grubunu (uygulamanın kullanıcının o anda yaptığı işlem için aktif olarak ihtiyaç duyduğu bellek) bilir ve uygulamasının sistemin paylaşılan RAM kaynaklarını çok fazla kullanmamasını ister.

Bütçe, son zamanlarda kullanılmayan bellek sayfalarını kaldırmak için bellek çıkarma ve takas kullanılarak dengede tutulur. Böylece uygulamanın bellekte kapladığı yer, mevcut çalışma grubuna odaklanır. Bir uygulama beyan edilen bütçesini aştığında işletim sistemi, özellikle bu uygulamayı hedefleyerek geri alma işlemi gerçekleştirir:

  1. Temiz dosya destekli sayfalar (ör. etkin olmayan kod ve eşlenen öğeler), gerektiğinde depolama alanından yeniden okunabildikleri için ilk olarak çıkarılır.
  2. Kirli dosya destekli sayfalar depolama alanına geri yazılır ve çıkarılır.
  3. Anonim bellek sayfaları (ör. yığın ayırmalar) sıkıştırılır ve zRAM'e aktarılır.

Çalışma grubu bütçeyi aşmadığı sürece uygulama, bütçesinden daha fazla bellek kullanmadan iyi çalışır. İşletim sistemi, kullanılmayan belleği çıkarır ve etkin olmayan yığın sayfalarını takas alanına sıkıştırır. Böylece bellek ayırmaları, işlem sonlandırılmadan sınırlı kalır.

Bir uygulama bütçesini aştığında ne olur?

Bellek bütçesinin aşılması, sistemin uygulamanızı sonlandırmasına veya kilitlenmesine neden olmaz. Bunun yerine, bir uygulama bütçesinin izin verdiğinden daha fazla bellek kullandığında işletim sistemi, uygulamanın bir süredir kullanmadığı belleği bulur ve tekrar ihtiyaç duyulana kadar güvenli bir şekilde saklar. Bu sayede yeni uygulama bellek ayırmaları için yer açılır ve uygulamanın genel bellek kullanımı bütçe dahilinde tutulur.

Uygulamanın belirli bir zamanda ihtiyaç duyduğu bellek (çalışma kümesi) ile belirlenen bütçe arasında boşluk olduğu sürece uygulama normal şekilde çalışmaya devam eder. Bir uygulama, çalışma grubundan daha küçük bir bütçe belirlerse uygulamanın çalışma zamanındaki performansı yavaşlayabilir.

İşletim sisteminin belleği nasıl izlediğini ve geri aldığını öğrenmek için bellek mimarisi kılavuzuna bakın:

Android manifest dosyasında bütçeleri bildirme

Bellek bütçelerinizi AndroidManifest.xml içinde tanımlamak, bütçe tanımlamak için birincil ve önerilen yöntemdir. Çalışma zamanı kodu gerektirmez, işlem başlatıldığında hemen geçerli olur ve işletim sistemi için net bir sözleşme sağlar.

<memory-budget> beyanları, Android 17 QPR2 (API düzeyi 37.2) ve sonraki sürümleri çalıştıran cihazlarda geçerli olur. Daha eski Android sürümlerinde platform manifest ayrıştırıcısı, tanınmayan XML öğelerini güvenli bir şekilde yoksayar. Böylece geriye dönük uyumluluğu etkilemeden <memory-budget> öğesini kullanabilirsiniz.

Referans bütçe belirleme

Çoğu uygulamada, uygulama için tek bir bütçe tanımlamak yeterlidir. Doğrudan <application> etiketinin içinde bir <memory-budget> öğesi tanımlayın:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Baseline budget for the application -->
        <memory-budget android:maxMb="256" />

    </application>
</manifest>

Bu, paketin tüm işlemleri ve durumları için 256 MB'lık bir yerleşik bellek bütçesi belirler. Uygulamanın bellekte kapladığı yer 256 MB'ı aştığında işletim sistemi, çıkarma ve takas yöntemlerini kullanarak etkin olmayan bellek sayfalarını kırpar.

Bütçeleri işlem durumuna göre değiştirme

Bir uygulamanın ihtiyaç duyduğu bellek miktarı, kullanıcı görünürlüğüne bağlı olarak değişir:

  • Ön plan: İşlem, kullanıcıyla etkileşimde bulunan görünür bir etkinliği barındırıyor. Bu durum, etkin kullanıcı arayüzü ve grafikler nedeniyle genellikle en büyük ayak izine sahiptir.
  • Algılanabilir: İşlem, kullanıcı tarafından algılanabilir ancak görünür bir pencere barındırmaz (ör. medya oynatma ön plan hizmeti, etkin bir arka plan indirme işlemi, adım adım rota veya etkin bir giriş yöntemi barındırma).
  • Arka plan: İşlem, arka plan işlerini, alıcıları veya veri senkronizasyonlarını çalıştırıyor. Minimum düzeyde yer kaplaması beklenir.

Bu durumlarla eşleşecek birden fazla <memory-budget> ifadesi bildirebilirsiniz:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Default budget for visible foreground UI -->
        <memory-budget android:maxMb="200" />

        <!-- Tighter budget when playing audio in background -->
        <memory-budget
            android:maxMb="120"
            android:state="perceptible" />

        <!-- Minimal budget when fully in background -->
        <memory-budget
            android:maxMb="48"
            android:state="background" />

    </application>
</manifest>

Ön plan veya atlanan durum yan tümcesi gerekli değildir. Yalnızca perceptible veya background yan tümcelerini bildirirseniz foreground gibi daha yüksek öneme sahip durumlar, uygulama bütçesiyle kısıtlanmaz. android:state ifadesinin kullanılmaması, varsayılan olarak foreground değerinin kullanılmasına neden olur. Bu değer, daha sonraki bir yan tümce bu durumları geçersiz kılmadığı sürece perceptible ve background değerlerine de uygulanır. Yan tümce sıralamasıyla ilgili ayrıntılar için Çözüm kuralı: Geçerli olan son yan tümce yürürlüğe girer başlıklı makaleyi inceleyin.

Her durum için bütçe boyutunu belirlerken, en yüksek yerleşik bellek (RSS üst sınır) yerine o durumun etkin çalışma grubunu hedefleyin:

  • Sorunsuz kare oluşturma için foreground bütçelerini boyutlandırın: Ön plan kullanıcı arayüzüne, etkin çalışma grubu için yeterli alan verin. Böylece eşzamanlı geri alma, kare oluşturmayı geciktirmez. Uygulamanızda yoğun bellek kullanan bağımsız bir iş akışı (ör. yüksek çözünürlüklü resim veya video düzenleyici) varsa bu etkinliği özel bir işleme (Çok işlemli uygulamalar bölümüne bakın) taşımayı düşünebilirsiniz. Böylece, tahsisatları bağımsız olarak bütçelendirilir ve kullanıcı çıktığında kaldırılır.
  • Başsız çalışma için bütçeleri sıkı bir şekilde background boyutlandırın: Saha telemetrisindeki yüksek arka plan RSS'si, genellikle ön plandan ayrıldıktan sonra korunan kullanıcı arayüzü tahsislerini (Bellek yoğun bir kullanıcı arayüzüyle birlikte konumlandırılan bağlama hizmeti ve Etkinliklere yanıt olarak belleği serbest bırakma bölümlerine bakın) veya arka plan dizine ekleme ve senkronizasyon sırasında tek seferlik dosya okumalarını yansıtır. Başsız arka plan çalışmasının kare son tarihleri olmadığından, arka plan senkronizasyonu bütçesini kısa süreliğine aştığında tek seferlik dosya önbelleği sayfalarının çıkarılması ve etkin olmayan yığın sayfalarının zRAM ile değiştirilmesi, kullanıcı tarafından görülebilen bir takılmaya neden olmaz ve arka plandaki ani artışların kullanıcının ön planda çalışan uygulamasını çıkarmasını önler.

Adım adım iş akışı için Çalışma kümenizi ölçme ve bütçeleri kalibre etme başlıklı makaleyi inceleyin.

Bütçe durumlarının işlem durumlarıyla eşleşme şekli

Platform, manifest bütçe durumlarını çalışma zamanı işlem durumlarıyla eşler. Bu eşleme, Activity Manager'ın hem işlemin kendi etkin bileşenlerinden hem de diğer uygulamalardan gelen hizmet bağlamaları veya içerik sağlayıcı sorgularından hesapladığı RunningAppProcessInfo.importance değerine göre yapılır:

  • foreground: Devam ettirilen bir etkinliği barındırma, ekran kapandığında en üstteki uygulama durumunu koruma veya en üstteki ön planda çalışan uygulama tarafından etkin bir şekilde bağlı ya da sorgulanan bir hizmeti veya içerik sağlayıcıyı barındırma gibi etkin, görünür kullanıcı etkileşimleri.
  • perceptible: Görünür bir pencere olmadan kullanıcı tarafından algılanabilen iş yükleri. Örneğin, etkin medya oynatma, adım adım rota, kamera veya mikrofonla kayıt, etkin arka plan indirmeleri, veri senkronizasyonu ön plan hizmetleri ya da BIND_NOT_FOREGROUND gibi işaretler kullanılarak ön plan istemcisine bağlı hizmetler (BIND işaretleriyle devralmayı kontrol etme bölümüne bakın). Dahili platform metrikleri bu iş yüklerinden bazılarını PROCESS_STATE_IMPORTANT_FOREGROUND olarak etiketleyebilir ancak bu dahili sabit, görünür bir pencereyi ifade etmez ve perceptible bütçesi tarafından yönetilir.
  • background: Arka plan işleri, alarmlar ve yayın alıcıları gibi kullanıcı tarafından hemen fark edilemeyen işlemler.

Aşağıdaki tabloda, çalışma zamanı önem düzeylerinin manifest durumlarıyla nasıl eşleştiği gösterilmektedir:

Manifest android:state Çalışma zamanı önemi (RunningAppProcessInfo) Tipik bileşenler
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Devam ettirilen görünür etkinlik, ekranı kilitli olan en üstteki uygulama, en üstteki uygulamaya bağlı hizmet veya içerik sağlayıcı
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Etkin medya oynatma, navigasyon, indirme veya senkronizasyon ön plan hizmetleri; ön plan hizmeti veya görünür işaretlerle bağlı hizmet
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
IMPORTANCE_CACHED
Arka plan işleri, alıcılar, arka plan senkronizasyonu ve önbelleğe alınmış işlemler

İstemci bağlamalarının bir hizmet veya sağlayıcı ana makine işlemini nasıl yükselttiği ve istemci bağlamayı kaldırdıktan veya ekrandan çıktıktan sonra işlemin nasıl tekrar background ya da cached durumuna geçtiği hakkında ayrıntılı bilgi için Hizmet bağlamaları ve işlem durumları başlıklı makaleyi inceleyin.

Bir işlem, etkin bileşenleri olmadan önbelleğe alınmış duruma (IMPORTANCE_CACHED) girdiğinde platform, önbelleğe alınmış işlem dondurma ve geri alma işlemlerinin yanı sıra background bellek bütçesini uygulamaya devam eder. Böylece, dondurma veya sıkıştırma gerçekleşmese bile işlem sınırlı kalır.

Uygulamanızın işlem durumunu ve bütçesini inceleme

Geliştirme sırasında uygulamanızın etkin işlem durumunu ve önemini incelemenin bir yolu, ADB'yi kullanarak Etkinlik Yöneticisi'ni sorgulamaktır:

adb shell dumpsys activity processes <package-name>

Aşağıdaki kısaltılmış örnekte, ön plan hizmeti çalıştıran bir uygulamanın işlem kaydı ve OOM kontrol girişleri gösterilmektedir:

ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
  All known processes:
  *APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
    pid=3919
    oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
    curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
    hasStartedServices=true
    mHasForegroundServices=true forcingToImportant=null
...
  Process OOM control (48 total):
    Proc #22: prcp  F/S/FGS  ---NFU-TI  t: 0 3919:com.example.app/u0a123 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS  lastRss=0.00 lastCachedRss=0.00

Bu çıkışta:

  • curProcState=4 ve state: cur=FGS, işlemin ön plan hizmeti durumunda olduğunu gösterir. Uygulamanız etkin bir görünür etkinliğe ev sahipliği yapıyorsa bu, TOP olarak görünür. Önemli bir ön plan bileşeni (ör. indirme veya senkronizasyon) çalıştırıyorsa IMPF olarak görünür.
  • İşlem OOM kontrol tablosunda prcp, işlemin perceptible bütçesine karşılık gelen algılanabilir öncelik katmanı altında değerlendirildiğini gösterir.

Uygulamanızın mevcut yerleşik bellek kullanımını incelemek için:

adb shell dumpsys meminfo <package-name>

Çok işlemli uygulamalar

Uygulamanız işini birden fazla işleme bölüyorsa <processes> içindeki <process> etiketini kullanarak özel işlem bütçeleri yapılandırın.

Örneğin, bir müzik yayını uygulaması (com.example.radio) düşünün:

  1. Ana işlem: Görünür kullanıcı arayüzünü ve ses çalma motorunu barındırır (mediaPlayback ön plan hizmetiyle MediaSessionService). Görünür olduğunda işlem, 180 MB ön plan bütçesiyle çalışır. Kullanıcı, müzik çalmaya devam ederken uygulamadan ayrıldığında işlem perceptible durumuna girer. Bu durumda, oynatma motoru ve ses arabelleği için 64 MB bütçe yeterlidir.
  2. Senkronizasyon işlemi (:sync): Arka planda meta veri senkronizasyonu ve indirme dizine ekleme işlemlerini çalıştıran özel işlem. Bu işlem yalnızca arka planda etkin olduğundan state="background" değerini açıkça belirtmeniz gerekmez. Tek bir bütçe geçerlidir.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.radio">

    <application
        android:label="@string/app_name">

        <!-- Package baseline: main process with UI and audio playback -->
        <memory-budget android:maxMb="180" />

        <!-- Tighter budget when audio plays in the background -->
        <memory-budget
            android:maxMb="64"
            android:state="perceptible" />

        <processes>
            <!-- Dedicated background sync process -->
            <process android:process=":sync">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <service
            android:name=".playback.AudioPlayerService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false" />

        <service
            android:name=".sync.PlaylistSyncService"
            android:process=":sync"
            android:exported="false" />

    </application>
</manifest>

Manifest dosyanızda bir <processes> bloğu beyan ettiğinizde, yalnızca <memory-budget> beyan eden işlemler için <process> girişlerini listeleyin. Uygulamanın geri kalanını cihazın sistem sınırında bırakırken yalnızca belirli alt süreçleri bütçelemek istiyorsanız doğrudan <application> altındaki <memory-budget> öğelerini atlayabilirsiniz.

Herhangi bir alt işlemdeki bellek kullanımı, hem kendi işlem bütçesine hem de kapsayıcı paket bütçesine dahil edilir. Bir işlem, çalışma zamanında işlem bütçesini veya paket bütçesini (hangisine önce ulaşılırsa) aşarsa bellek baskısı yaşar.

Yüksek yoğunluklu ekranlar için bütçeleri ölçeklendirme

Bellekte kapladığı yer, tek seferde çizilecek piksel sayısına göre önemli ölçüde değişen uygulamalar (ör. pencere boyutunda bit eşlemleri önbelleğe alan bir fotoğraf galerisi uygulaması) için Android, bütçeleri ekran özelliklerine göre dinamik olarak ölçeklendiren iki alternatif mekanizma sunar:

  • Ekran yoğunluğu grubuna göre ölçeklendirme (android:additionalMbPerDensity): Ekranın mdpi (1,0x / 160 dpi) değerine göre yoğunluk oranının karesiyle orantılı olarak megabayt ekler.Bunun nedeni, 2D raster çizilebilir öğelerin ve kullanıcı arayüzü öğelerinin hem genişlik hem de yükseklik açısından ölçeklendirilmesidir:

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    mdpi ekranda (1,0x) bütçe 180 + 16 × 12 = 196 MB'tır. xxhdpi ekranda (3,0x), bütçe 180 + 16 × 32 = 324 MB'a ölçeklenir.

  • Pencere piksel sayısına göre ölçeklendir (android:additionalBytesPerDisplayPixel): Etkin pencere sınırlarında (WindowMetrics.getBounds genişlik × yükseklik) piksel başına bayt ekler. Bu, tam pencere grafik yüzeyleri, oluşturma arabellekleri veya yüksek çözünürlüklü fotoğraf önbellekleri ayıran uygulamalar için uygundur. Bu uygulamalarda bellek tüketimi, kullanıcı arayüzü yoğunluk grupları yerine doğrudan ham piksel sayısıyla ölçeklenir:

    <!-- Baseline 128MB + 16 bytes per window pixel -->
    <!-- For example, a 4-byte RGBA full-window buffer with double or quadruple
         buffering -->
    <memory-budget
        android:maxMb="128"
        android:additionalBytesPerDisplayPixel="16" />
    

    Tam ekran 1080p pencerede (1080 × 2400 &approx; 2,59 milyon piksel), bu, temel bütçeye 41.472.000 bayt (&approx; 39,6 MB, burada 1 MB = 1.048.576 bayt) ekler. 1440p pencerede (1440 × 3120 &approx; 4,49 milyon piksel) 71.884.800 bayt (&approx; 68,6 MB) ekler.

Bu iki özellik birbirinin alternatifidir. Uygulamanızın birincil ölçeklendirme faktörüyle eşleşen özelliği seçin ve her ikisini aynı maddede birleştirmekten kaçının.

Cihaz form faktörleri için özelleştirme

Telefonlar, tabletler ve Wear OS için APK gönderirken farklı donanım hedeflerine yönelik bütçeleri ayarlamak için android:feature özelliğini kullanın.

Wear OS kol saatlerinde RAM sınırlıdır ve uygulamanın kullanıcı arayüzü ile özellik seti çok daha basittir. watch özelliği için özel olarak daha sıkı bir bütçe belirleyebilirsiniz:

<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />

<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
    android:maxMb="48"
    android:feature="watch" />

Çözüm kuralı: Geçerli olan son madde uygulanır.

Sistem, bir uygulama veya işlem için birden fazla <memory-budget> öğesi tanımlarken bunları manifest dosyasında göründükleri sırayla değerlendirir ve geçerli olan son bütçe maddesini uygular.

Atlanan bir android:state, varsayılan olarak foreground değerini alır. Bu değer, daha sonraki bir ifadeyle geçersiz kılınmadığı sürece perceptible ve background için de geçerli olur. Sıra yan tümceleri foreground (veya atlanan android:state) ile perceptible ile background arasında olmalıdır ve android:feature geçersiz kılmaları eşleşen eyalet grubuna yerleştirilmelidir. Böylece, sondaki atlanan eyalet geçersiz kılma işlemi önceki perceptible veya background yan tümcelerini gölgelemez.

XML özelliği referansı

Tüm megabayt (MB) özellikleri ikili birimler kullanır (1 MB = 1.048.576 bayt) ve Linux cgroup memory.current ücretiyle eşlenir (Zygote'tan paylaşılan bellek hariç tutulur).

Özellik Biçim Varsayılan Açıklama
android:maxMb Tam sayı (> 0) Zorunlu MB cinsinden temel yerleşik bellek bütçesi sınırı.
android:state Enum foreground Bu bütçenin geçerli olduğu işlem durumu: foreground, perceptible veya background. Atlanırsa varsayılan olarak foreground olur ve daha sonraki bir yan tümce tarafından geçersiz kılınmadığı sürece perceptible ve background değerlerine basamaklanır.
android:additionalMbPerDensity Tam sayı (≥ 0) 0 Ek megabaytlar, mdpi (1,0x) ile karşılaştırıldığında ekran yoğunluğu oranının karesiyle çarpılır.
android:additionalBytesPerDisplayPixel Tam sayı (≥ 0) 0 Pencere pikseli başına ayrılan ek bayt sayısı (WindowMetrics.getBounds genişlik × yükseklik). Yüzey arabellekleri ve bit eşlemler için kullanışlıdır.
android:feature Dize Tümü Yan tümceyi belirli donanım özelliklerini bildiren cihazlarla kısıtlar: watch, automotive veya leanback.

Çalışma setinizi ölçme ve bütçeleri kalibre etme

İyi bir bütçe, uygulamanızın belirli bir durumdaki çalışma grubunun hemen üzerinde yer alır, kısıtlanmamış zirvesinde değil. Bu noktayı bulmak için gerçekçi bir kullanıcı işlemleri akışı çalıştırın, bütçeyi adım adım azaltın ve uygulamanızın bütçeyi karşılamaya başladığı anı izleyin.

Çalışma kümesi ile yerleşik bellek arasındaki farkı anlama

Bütçe olmadan uygulamanızın yerleşik ayak izi (memory.current) iki tür bellek barındırır:

  • Çalışma grubu: Java, Kotlin ve yerel yığın ile mevcut kullanıcı işlemleri akışının tekrar tekrar dokunduğu, dosya destekli etkin sayfalar (DEX kodu, yerel kitaplıklar ve öğeler)
  • Soğuk boşluk: Önceki ekranlardan bitmap önbellekleri, tek seferlik başlangıç arabellekleri, bir ayırma patlamasından sonra kalan yığın boşluğu ve bir kez okunan ve bir daha dokunulmayan dosya sayfaları

Kısıtlanmamış zirve noktanızdaki bir bütçe, soğuk boşluğu asla geri almaz. Çalışma kümenizin altındaki bir bütçe, çekirdeğin yığın sayfalarını değiştirmesine ve uygulamanızın hemen geri döndürdüğü kod sayfalarını çıkarmasına neden olur. Bu çalışma kümesi yeniden hataları, CPU süresi ve pil tüketimine neden olur. Kullanıcı arayüzü iş parçacığına ulaştıklarında kare kaybına da yol açarlar.

Aradığınız şey büküm noktası: Geri alma işleminin yalnızca soğuk boşluğu kaldırdığı en düşük bütçe. Böylece yeniden hata, CPU süresi ve kare süreleri bütçe olmadan olduğu yerde kalır.

Test ortamınızı hazırlama

Üretim gibi davranan tekrarlanabilir bir test oluşturun:

  • Gönderdiğiniz R8 ile optimize edilmiş, hata ayıklanamayan yayınlanan derlemeyi kalibre edin. Hata ayıklama derlemeleri daha fazla bellek kullandığından, hata ayıklama derlemesine göre ayarlanmış bir bütçe, yayın için çok yüksektir.
  • Android 17 QPR2 (API düzeyi 37.2) veya sonraki sürümleri çalıştıran bir cihazda ya da emülatörde test edin. Google Play içermeyen bir userdebug derlemesi veya emülatör görüntüsü kullanmak en iyisidir. Bazı cihazlar, aşağıdaki bölümlerde yer alan cgroup dosyalarını yalnızca adb root komutunu çalıştırdıktan sonra okumanıza izin verir.
  • Bütçe ayırmayı planladığınız her durum için bir kez Jetpack Macrobenchmark, UI Automator veya adb shell input ile 30 ila 60 saniyelik bir kullanıcı işlemleri akışı için komut dosyası oluşturun. Boşta olan bir uygulama, çalışma kümesi hakkında bilgi vermez.
    • foreground: Yoğun listeleri ve medya ızgaralarını kaydırın, ana hedefleriniz arasında geçiş yapın, menüleri ve iletişim kutularını açın ve animasyonları çalıştırın.
    • perceptible: Algılanabilir çalışma (ör. ses çalma veya indirme) devam ederken görünür pencereden çıkın, 10-15 saniye çalışmasına izin verin ve ardından uygulamaya dönün.
    • background: Uygulamanın görünür bir kullanıcı arayüzü yokken senkronizasyon, WorkManager işi (ör. adb shell cmd jobscheduler run -f <package-name> <job-id> ile), başlatılmış bir hizmet veya yayın alıcısı tetikleme

background bütçesini, işlem önbelleğe alınmış durumda (IMPORTANCE_CACHED) boştayken değil, arka plan çalışması etkin bir şekilde yürütülürken ölçün. background bütçesi önbelleğe alınmış durumdayken geçerli olsa da boşta olan önbelleğe alınmış bir işlem, arka plan çalışması yürütmez ve arka plan işlerinizin veya alıcılarınızın çalışma grubunu yansıtmaz.

Referans değerinizi ölçme

Normalin nasıl göründüğünü anlamak için bütçesi olmayan çalıştırmalarla başlayın. <memory-budget> beyanları içermeyen bir derleme kullanın ve her çalıştırmayı yeni bir işlemle (adb shell am force-stop <package-name>) başlatın.

Her uygulama işleminin kendi cgroup v2 bellek denetleyicisi vardır. Cgroup düzeni cihazlar arasında farklılık gösterebileceğinden, yolu kendiniz oluşturmak yerine /proc/<PID>/cgroup dosyasının 0:: satırından yolu bulun. Cihaz kabuğunun tırnak işaretlerini görebilmesi için her cihaz komutunu adb shell aracına tek tırnak içine alınmış bir dize olarak iletin:

PID=$(adb shell pidof -s <package-name>)
CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")
adb shell "cat $CG/memory.current $CG/memory.swap.current"
adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

Bu dosyaları okumak kolaydır ve uygulamanızı rahatsız etmez. Bu nedenle, çalışma sırasında her saniye örnekleme yapabilirsiniz:

  • memory.current: İşleme yüklenen yerleşik bellek (bayt cinsinden). Bu, <memory-budget> sınırının sayısıdır.
  • memory.swap.current: zRAM takasına sıkıştırılmış bellek (bayt cinsinden)
  • anon and file in memory.stat: uygulamasında yerleşik anonim ve dosya destekli bellek (bayt cinsinden)
  • workingset_refault_anon ve workingset_refault_file, memory.stat: Uygulamanızın tekrar ihtiyaç duyduğu, takas edilen veya çıkarılan sayfaların çalışan sayıları. Bu sayaçlar, bütçenin çok kısıtlı olduğunun en önemli göstergesidir.

İzin hatası alırsanız adb root komutunu çalıştırıp tekrar deneyin. Kök erişimi elde edemezseniz adb shell dumpsys meminfo <package-name> size daha kaba bir görünüm sunar. App Summary, Java Heap, Native Heap ve Stack çoğunlukla anonim bellektir, Code ise dosya desteklidir. Yazılım bit eşlemleri Native Heap ve bütçeniz için hesaba katılırken donanım bit eşlemleri ve Graphics hesaba katılmaz. Kare süresini ölçerken dumpsys meminfo komutunu döngü içinde çalıştırmayın. Uygulamanızda çöp toplamayı zorunlu kılar ve tüm bellek eşlemelerini inceler. Bu da ölçmeye çalıştığınız şeyi değiştirir.

Aday bütçeleri tarama

Uygulamanızı yeniden derlemeden aday bütçeleri deneyebilirsiniz. Çalışan bir işlem için bütçe belirlemek üzere am memory-limiter manual, bütçeyi kaldırmak için ise none komutunu kullanın:

adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none

Manuel geçersiz kılma, işlem durumu değişiklikleri boyunca o işlem örneğinin kullanım ömrü boyunca devam eder. Bu nedenle, devam etmeden önce bu ayarı temizleyin veya işlemi yeniden başlatın. Ayrıca işlemin katı sınırını (memory.max) aynı değere ayarlar. Böylece, uygulamanızın çalışma grubunun altındaki bir aday bütçe, işlemi yavaşlatmak yerine sonlandırabilir. Bu bütçeyi başarısız olarak değerlendirin.

Ayrıca, bir araç bağımsız değişkeninden aday değer okuyup MemoryBudgetManager.processBudgetBytes değerini ayarlayarak karşılaştırma derlemesinden tarama yapabilirsiniz. Çalışma zamanı bütçesi yalnızca manifest bütçesini daraltabilir. Bu nedenle, <memory-budget> beyanları olmadan derleme üzerinde tarama yapın. Ayrıca, clearProcessBudget yöntemini çağırana kadar işlem durumu değişiklikleri boyunca geçerli kalır.

Referans değerinizden en yüksek memory.current ile başlayın ve her seferinde 10 ila 15 MB azaltın. Her bir aday bütçe için:

  1. Yeni bir süreç başlatın, süreci hedef duruma getirin ve bütçeyi uygulayın.
  2. Kullanıcı işlemleri akışını bir kez çalıştırarak ısınma sağlayın. Bütçe uygulandığında sıfırdan başlatma sayfaları bir kez çıkarılır. Bu geçiş yerine sabit durumu ölçmek istersiniz.
  3. Kare istatistiklerini sıfırlayın ve başlangıçtaki yeniden hata sayaçlarını ve işlem CPU süresini (saat tikleri cinsinden utime ve stime) kaydedin:

    adb shell dumpsys gfxinfo <package-name> reset
    adb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"
    adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"
  4. Kullanıcı işlemleri akışınızın ölçülen geçişini çalıştırın.

  5. memory.current, anon, file, yeniden hata sayaçları ve CPU süresini tekrar kaydedin ve kare istatistiklerini toplayın. dumpsys gfxinfo aynı istatistikleri birden fazla kez yazdırabilir. Bu nedenle, uygulamanın tamamını kapsayan ilk eşleşmeyi alın:

    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'
    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'

Her adayı en az üç kez tekrarlayın ve bir adayın tüm tekrarlarını arka arkaya çalıştırmak yerine adayları aralıklı olarak çalıştırın. Her sonucu tek bir çalıştırmayla değil, temel çalıştırmalarınızın dağılımıyla karşılaştırın.

Sonuçları okuma

Bütçe azaldıkça uygulamanız üç aşamadan geçer:

  • Diz üstü: memory.current, soğuk file sayfaları ve boş yığınları atarak bütçeyi azaltır. anon düz kalır, yeniden varsayılan değerler referans değerine yakın kalır ve CPU süresi ile kare süreleri değişmez. Bu, olmak istediğiniz yerdir.
  • Diz altı: Soğuk sayfalar kaybolur. Bu nedenle çekirdek, uygulamanızın hâlâ kullandığı yığın sayfalarını değiştirmeye başlar. Yeniden hata sayısı her çalıştırmada yüzlerce veya binlerce sayfa artar ve CPU süresi uzar. Kare süreleri, boşta olan bir test cihazında neredeyse hiç değişmeyebilir ancak uygulamanız artık kendi çalışma grubunu sıkıştırıp açmak için CPU süresi ve pil harcar.
  • Diz çizgisinin çok altında: Geri alma ve takas hataları, kullanıcı arayüzü iş parçacığına ve RenderThread'ye düşer. Takılma, kare süreleri ve sıcak devam süreleri artar.

Kısıtlanmamış referansınızla karşılaştırıldığında, aşağıdaki koşulların tümünü karşılayan bir aday bütçe kabul edilir. Bu koşullar makul bir başlangıç noktasıdır:

  • İşlem, çalıştırma sırasında devam eder.
  • Yeniden hata farkları, referans değere yakın kalır
  • İşlem CPU süresi %5'ten fazla artmaz.
  • Takılan kareler en fazla 1 yüzde puanı artar
  • P95 kare süresi en fazla 3 ms artar
  • Sıcak devam ettirme süresi (adb shell am start -W üzerinden TotalTime), perceptible ve background bütçeleri için en fazla %15 artar.

Kırılma noktası, kabul edilen en düşük aday bütçedir. Örneğin, 138 MB'lık kısıtlanmamış zirveye sahip varsayımsal bir uygulama için ön plan taraması aşağıda verilmiştir:

Aday bütçe memory.current anon file Yeniden hata (sayfa) P95 kare süresi Rejim
Yok 138 MB 75 MB 63 MB 0 10,5 ms Temel
125 MB 121 MB 74 MB 47 MB 10 10,5 ms Diz üstü
110 MB 110 MB 75 MB 35 MB 15 10,8 ms Diz
95 MB 95 MB 74 MB 21 MB 176 11,2 ms Diz altı
85 MB 85 MB 68 MB 17 MB 2.050 12,8 ms Diz altı
70 MB 70 MB 54 MB 16 MB 8.771 16,5 ms Dizin çok altında

Bütçenizi seçin

android:maxMb değerini, dizin %10 ila %20 üzerinde olacak şekilde ayarlayın. Böylece kullanıcı verileri, yerel ayarlar ve tahsisat artışlarındaki farklılıklar uygulamanızı bu değerin altına düşürmez. Önceki örnekte, diz 110 MB'ta olduğundan 125 MB iyi bir seçimdir.

Ardından, yeni <memory-budget> beyanlarını içeren bir yayınlanan derleme yükleyin, kullanıcı işlemleri akışınızı tekrar çalıştırın ve geçiş ölçütlerinin hâlâ geçerli olup olmadığını kontrol edin.

Ön planda olmayan bütçeleri sıcak dönüşle doğrulama

Uygulamanız bu durumdayken sıkı bir perceptible veya background bütçesi iyi görünebilir ancak kullanıcı geri döndüğünde size maliyet çıkarabilir. Dönüş yolculuğunu da test edin:

  1. Ön plan akışını çalıştırın, uygulamayı hedef duruma taşıyın ve am memory-limiter manual ile aday bütçeyi uygulayın.
  2. Algılanabilir veya arka plan çalışmasının 10 ila 15 saniye boyunca çalışmasına izin verin.
  3. Geçersiz kılmayı temizleyin (adb shell am memory-limiter manual <PID> none), ardından uygulamayı hemen adb shell am start -W ile geri getirin ve TotalTime değerini kaydedin. Adayı MemoryBudgetManager üzerinden ayarlarsanız uygulama döndürülmeden önce clearProcessBudget yöntemini çağırın.
  4. Kısa bir ön plan etkileşimi çalıştırın ve yeniden hata sayısını ve kare istatistiklerini geçiş ölçütleriyle karşılaştırın.

Uygulama bu durumdayken ve sıcak dönüşte her ikisini de geçen en düşük bütçeyi seçin ve aynı %10 ila %20'lik ek payı ekleyin.

Ekran ölçeklendirmesini kalibre etme

android:additionalBytesPerDisplayPixel kullanıyorsanız ön plan dizini, 1080 × 2400 (&approx; 2,59 milyon piksel) ve 1440 × 3120 (&approx; 4,49 milyon piksel) gibi farklı çözünürlüklere sahip iki ekranda bulun. android:additionalBytesPerDisplayPixel değerini elde etmek için iki kırılma noktası arasındaki farkı (bayt cinsinden) piksel sayısındaki farka bölün. Çözünürlükten bağımsız tabanı elde etmek için daha küçük ekranın dizinden piksel başına miktarını çıkarın ve android:maxMb elde etmek için tavan boşluğu ekleyin.

Çalışma zamanı API'leri (ikincil dinamik seçenek)

Bütçeleri AndroidManifest.xml içinde statik olarak bildirmek, neredeyse tüm uygulamalar için tercih edilen çözümdür. Ancak Android, dinamik iş yükleri olan uygulamalar veya çalışma zamanı denemeleri için ikincil bir seçenek olarak çalışma zamanı SDK API'si sunar.

Çalışma zamanı API'si şunları yapmanıza olanak tanır:

  • Mevcut bellek kullanımını ve geçerli bütçeleri sorgulama
  • İşlem bütçenizi dinamik olarak azaltın.
  • İşletim sistemi doğrudan geri alma işlemini tetiklemeden önce önbellekleri proaktif olarak kırpmak için bütçe aşımı etkinliklerini dinleyin.

Android SDK API (MemoryBudgetManager)

MemoryBudgetManager sistem hizmeti, Android 17 QPR2'den (küçük SDK sürümü, API düzeyi 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2) itibaren Kotlin ve Java dillerinde yazılmış uygulamalarda kullanılabilir.

Hizmeti alma

MemoryBudgetManager özelliğine erişmeden önce SDK_INT_FULL değerini kontrol ederek cihazın Android 17 QPR2 veya sonraki bir sürümü çalıştırdığından emin olun. Uygulamanızın minSdk değeri 36'dan düşükse öncelikle SDK_INT değerini kontrol edin. Böylece, Android 15 (API düzeyi 35) veya daha eski sürümleri çalıştıran cihazlar SDK_INT_FULL değerini okurken NoSuchFieldError hatası vermez:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
    Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

Sorgu kullanımı ve bütçeler

// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes

// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

Bütçeleri dinamik olarak ayarlama veya temizleme

Hafif görevler sırasında belleği kısıtlamak için çalışma zamanında daha sıkı bir bütçe belirleyebilir veya görev tamamlandığında bütçeyi temizleyebilirsiniz:

// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()

İşlem foreground, perceptible ve background durumları arasında geçiş yaptığında bütçeleri otomatik olarak değiştiren <memory-budget> manifest yan tümcelerinin aksine, MemoryBudgetManager aracılığıyla ayarlanan dinamik bütçe, kodunuz güncellenene veya temizlenene kadar tek bir tavan uygular. Saha denemeleri yapmak veya duruma özel sınırları yönetmek için çalışma zamanı API'sini kullanıyorsanız yaşam döngüsü geçişleri genelinde dinamik bütçeyi güncelleyin veya temizleyin (ör. ProcessLifecycleOwner veya etkinlik yaşam döngüsü geri çağırma yöntemlerini kullanarak, Uyarlanabilir resim düzenleyici örneğinde gösterildiği gibi).

Performans ve eş kuvvetlilik

Çalışma zamanında MemoryBudgetManager ile çalışırken aşağıdaki özellikleri göz önünde bulundurun:

  • Idempotency: Tüm bütçe mutasyon API'leri idempotenttir. clearProcessBudget() veya clearPackageBudget() işlevini tekrar tekrar ya da etkin bir dinamik bütçe yokken çağırmak güvenli bir işlem yapmama durumudur. Benzer şekilde, setProcessBudgetBytes() veya setPackageBudgetBytes() işlevini art arda aynı değere ayarlamak, gereksiz sistem yeniden yapılandırmalarını tetiklemez.
  • Çağrı sıklığı: Bütçe ayarlama, güncelleme veya temizleme işlemleri, işlemin işletim sistemi bellek denetleyicisini güncellemek için sistem hizmetine çapraz işlem çağrısı yapar. Hafif olsa da tamamen ücretsiz değildir. Bu API'leri ayrı yaşam döngüsü geçişleri ve görev aşamaları sırasında (ör. bir etkinliğe girerken veya etkinlikten çıkarken, arka plan işlemeyi başlatırken veya tamamlarken ya da konuşma isteklerini işlerken) çağırın ve bunları dar döngülerde, ses işleme iş parçacıklarında veya kare başına oluşturma rutinlerinde çağırmaktan kaçının.
  • Sorgu sıklığı: processCurrentUsageBytes ve packageCurrentUsageBytes sorgu özellikleri, sistem hizmetinden canlı bellek kullanımını getirir. Sorgular hızlı olsa da sürekli döngüler halinde sorgulanmak yerine isteğe bağlı olarak (ör. görev geçişleri sırasında veya OnOverBudgetListener geri çağırmalarında) çağrılmalıdır.

Bütçe aşımı baskısı geri çağırmalarını dinleme

Uygulamalar, bellek kullanımı bütçe eşiğini aştığında bildirim almak için bir dinleyici kaydedebilir. Bu sayede uygulama, işletim sistemi işlemi veya paketi bütçesi dahilinde tutmak için belleği geri alırken uygulama düzeyinde proaktif temizleme (bellek içi bitmap önbelleklerini temizleme gibi) gerçekleştirebilir:

val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict thread-safe caches on the background Looper
    imageTileCache.evictAll()
}

// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)

// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Bütçe aşımı geri aramalarıyla ilgili en iyi uygulamalar:

  • Arka plan kullanın Looper: Mümkün olduğunda registerProcessOverBudgetListener veya registerPackageOverBudgetListener için HandlerThread Looper arka planı iletin. Önbellek çıkarma veya dosya temizleme işlemlerini arka plan iş parçacığında çalıştırmak, işlem bellek baskısı altındayken kullanıcı arayüzü iş parçacığında kare oluşturma ile çakışmayı önler. Looper.getMainLooper, yalnızca ana iş parçacığına bağlı kullanıcı arayüzü durumuna dokunan dinleyiciler için ayrılmıştır.
  • Tekrarlanan geri çağırmalar bekleyin: Bütçe aşımı geri çağırmaları tek seferlik bildirimler değildir. Kullanım bütçeyi aştığında işletim sistemi önce soğuk sayfaları geri alır. Baskı devam ederse sistem, belleği geri almaya ve dinleyicinizi saniyede en fazla bir kez çağırmaya devam eder. Geri çağırma işlevinizi eş güçlü yapın. Uygulamanızın yalnızca tek bir bildirime ihtiyacı varsa veya temizleme işlemleri arasında daha uzun bir bekleme süresi istiyorsa geri çağırma içinde unregisterProcessOverBudgetListener (veya unregisterPackageOverBudgetListener) yöntemini çağırın ya da temizleme işleminizi bir bekleme süresi zaman damgası veya AtomicBoolean ile koruyun.
  • Bütçeyi daraltmadan önce kaydolun: setProcessBudgetBytes veya setPackageBudgetBytes yöntemini çağırdığınızda mevcut bellek kullanımı yeni bütçeyi zaten aşıyorsa yeni bütçe uygulanır uygulanmaz bütçe aşımı etkinliği tetiklenir. Bütçeyi ayarlamadan önce dinleyicinizi kaydedin. Böylece uygulamanız, bütçe aşımıyla ilgili ilk bildirimi kaçırmaz.
  • Hızlı olun: Geri kazanım işlemleri anında rahatlama sağlamalıdır. Yük altındayken yapılan karmaşık hesaplamalar performansı düşürür.
  • Tahsislerden kaçının: Geri çağırma içinde yeni nesneler tahsis etmeyin veya yeni iş parçacıkları başlatmayın. Aksi takdirde, işletim sistemi doğrudan geri alma işlemini hemen tetikleyebilir.
  • Yüksek verimli hedeflere odaklanın: Büyük bit eşlemleri, oluşturma arabelleklerini çıkarmak veya bellekle eşlenen dosyaları kapatmak, çok sayıda küçük nesneyi serbest bırakmaktan çok daha etkilidir.

Çalışma zamanı API örneği

Aşağıdaki örnekte, Kotlin'de Android SDK API'yi kullanarak çalışma zamanı API'sinin nasıl uygulanacağı gösterilmektedir:

Android SDK örneği: Uyarlanabilir resim düzenleyici

Bu örnekte, manifest dosyasında çok katmanlı tuvalini barındırmak için 256 MB'lık bir üst sınır beyan eden bir resim düzenleme uygulaması (com.example.imageeditor) gösterilmektedir:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

Kullanıcı, basit küçük resim galerisine göz atarken uygulama, Kotlin'deki Android SDK API'sini kullanarak işlem bütçesini dinamik olarak 96 MB'a düşürür. Kullanıcı çok katmanlı düzenleme tuvalini açtığında uygulama, dinamik bütçeyi temizleyerek 256 MB'lık manifest tavanını tamamen geri yükler. Ayrıca, kullanıcı arayüzü iş parçacığını engellemeden baskı altında önbelleğe alınmış önizleme bit eşlemlerini çıkarmak için galeri bütçesini uygulamadan önce arka plan HandlerThread Looper üzerinde bir OnOverBudgetListener kaydeder.

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager
    private lateinit var cleanupThread: HandlerThread

    // Thread-safe in-memory cache for rendered preview tiles (32MB limit)
    private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
        override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
    }

    private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
        Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
        previewCache.evictAll()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        budgetManager = getSystemService(MemoryBudgetManager::class.java)
        cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
    }

    override fun onStart() {
        super.onStart()
        // Register listener on a background Looper before tightening the budget
        budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    override fun onDestroy() {
        cleanupThread.quitSafely()
        super.onDestroy()
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     * Clears the dynamic budget, restoring the full 256MB manifest ceiling.
     */
    fun enterEditingCanvas() {
        // Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
        budgetManager.clearProcessBudget()
        Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     * Re-applies the tighter dynamic budget.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        applyGalleryBudget()
    }

    private fun applyGalleryBudget() {
        try {
            // Dynamically tighten budget to 96MB for the lightweight gallery view
            budgetManager.processBudgetBytes = 96L * 1024L * 1024L
            Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}