java.lang.OutOfMemoryError: bitmap boyutu VM bütçesini aşıyor - Android


159

Android'de çok sayıda resim kullanan bir uygulama geliştirdim.

Uygulamanın bir defa çalıştırılır, ekrandaki (hakkında bilgi doldurur Layouts, Listviews, Textviews, ImageViews, vs) ve kullanıcı bilgilerini okur.

Animasyon, özel efekt veya hafızayı doldurabilecek hiçbir şey yoktur. Bazen çekmeceler değişebilir. Bazıları android kaynakları ve bazıları SDCARD'daki bir klasöre kaydedilen dosyalardır.

Sonra kullanıcı kapanır ( onDestroyyöntem yürütülür ve uygulama VM tarafından bellekte kalır) ve bir noktada kullanıcı tekrar girer.

Kullanıcı uygulamaya her girdiğinde, kullanıcı alana kadar hafızanın daha da büyüdüğünü görebiliyorum java.lang.OutOfMemoryError.

Pek çok görüntüyü işlemenin en iyi / doğru yolu nedir?

Onları sürekli yüklenmeyecek şekilde statik yöntemlere koymalı mıyım? Düzeni veya düzende kullanılan görüntüleri özel bir şekilde temizlemem gerekir mi?


4
Çok sayıda çekmeceniz varsa, bu yardımcı olabilir. Programı kendim yaptığımdan beri çalışıyor :) androidactivity.wordpress.com/2011/09/24/…

Yanıtlar:


72

Hafıza sızıntısı varmış gibi geliyor. Sorun pek çok görüntüyü işlemiyor, etkinliğiniz yok edildiğinde görüntülerinizin yerini almıyor olması.

Kodunuza bakmadan bunun neden olduğunu söylemek zor. Ancak, bu makalede yardımcı olabilecek bazı ipuçları bulunmaktadır:

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

Özellikle, statik değişkenlerin kullanılması daha iyi değil, işleri daha da kötüleştirebilir. Uygulamanız yeniden çizildiğinde geri aramaları kaldıran kod eklemeniz gerekebilir - ancak yine de burada kesin olarak söyleyecek yeterli bilgi yok.


8
Bu doğrudur ve ayrıca çok fazla görüntüye sahip olmak (ve bellek sızıntısı değil), diğer uygulamaların birçoğu, bellek parçalanması vb.Gibi birçok nedenden ötürü outOfMemory'ye neden olabilir. sistematik olarak, her zaman aynı şeyi yapmak gibi, o zaman bir sızıntı. Günde bir kez falan alırsanız, bunun nedeni sınıra çok yakın olmanızdır. Bu durumda önerim bazı şeyleri kaldırmayı ve tekrar denemektir. Ayrıca Görüntü belleği Yığın belleğin dışında olduğundan ikisine de dikkat edin.
Daniel Benedykt

15
Bellek sızıntısından şüpheleniyorsanız, yığın kullanımını izlemenin hızlı bir yolu adb shell dumpsys meminfo komutudur. Birkaç gc döngüsünde (logcat'te GC_ * log satırı) yığın kullanımının arttığını görürseniz, bir sızıntı olduğundan emin olabilirsiniz. Daha sonra adb veya DDMS aracılığıyla bir yığın dökümü (veya farklı zamanlarda birkaç) oluşturun ve Eclipse MAT üzerindeki dominator ağacı aracıyla analiz edin. Yakında hangi nesnenin zamanla büyüyen bir istinat yığınına sahip olduğunu bulmalısınız. Bu senin bellek sızıntın. Burada daha fazla ayrıntı içeren bir makale yazdım: macgyverdev.blogspot.com/2011/11/…
Johan Norén

98

Android Uygulamaları geliştirirken bulduğum en yaygın hatalardan biri “java.lang.OutOfMemoryError: Bitmap Boyutu VM Bütçesini Aşar” hatasıdır. Yönü değiştirdikten sonra çok sayıda bitmap kullanan etkinliklerde bu hatayı sık sık buldum: Etkinlik yok edildi, tekrar oluşturuldu ve düzenler bitmap'ler için kullanılabilir VM belleğini tüketen XML'den “şişirildi”.

Önceki etkinlik düzenindeki bitmapler, etkinliklerine ilişkin referansları çaprazladıkları için çöp toplayıcı tarafından doğru şekilde ayrılmaz. Birçok deneyden sonra bu sorun için oldukça iyi bir çözüm buldum.

İlk olarak, XML düzeninizin üst görünümünde “id” özelliğini ayarlayın:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

Ardından, onDestroy() Etkinliğinizin unbindDrawables()yönteminde, üst Görünüm'e bir başvuru ileten yöntemi çağırın ve ardından bir System.gc().

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

Bu unbindDrawables()yöntem, görünüm ağacını yinelemeli olarak inceler ve:

  1. Tüm arka plan çekilebilir araçlarındaki geri çağrıları kaldırır
  2. Her görünüm grubundaki çocukları kaldırır

10
Ancak ... java.lang.UnsupportedOperationException: removeAllViews () AdapterView desteklenmemektedir

Teşekkürler Bu çekilebilir bir sürü var çünkü benim yığın boyutu önemli ölçüde azaltmak yardımcı olur ... @GJTorikian için sadece removeAllViews () yakalamak deneyin çoğu ViewGroup alt sınıflar gayet iyi çalışıyor.
Eric Chen

5
Veya sadece koşulu değiştirin: if (ViewGroup örneğini görüntüle &&! (AdapterView örneğini görüntüle))
Anke

2
@Adam Varhegyi, onDestroy'da galeri görünümü için aynı işlevi açıkça çağırabilirsiniz. UnbindDrawables (galleryView) gibi; Umarım bu sizin için çalışır ...
hp.android

1
Bu iki
gotcha'nın

11

Bu sorunu önlemek için, yerli yöntemi kullanabilirsiniz Bitmap.recycle()önce null-ing Bitmapnesneyi (veya başka bir değere ayarlanması). Misal:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

Ve sonra şu şekilde myBitmapçağrı yaparak değiştirebilirsiniz System.gc():

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
bu öğeleri bir liste görünümüne eklemeye çalışırsanız bu çalışmaz. bu konuda bir öneriniz var mı?
Rafael Sanches

@ddmytrenko Resmi atamadan önce geri dönüştürüyorsunuz. Bu, piksellerinin oluşturulmasını engellemez mi?
IgorGanapolsky

8

Bu kesin problemle karşılaştım. Öbek oldukça küçük olduğundan, bu görüntüler bellek açısından oldukça hızlı bir şekilde kontrolden çıkabilir. Bunun bir yolu, çöp toplayıcıya geri dönüşüm yöntemini çağırarak bir bitmapte bellek toplaması için bir ipucu vermektir .

Ayrıca, onDestroy yönteminin çağrılması garanti edilmez. Bu mantığı / temizlemeyi onPause etkinliğine taşımak isteyebilirsiniz. Daha fazla bilgi için bu sayfadaki Etkinlik Yaşam Döngüsü şemasına / tablosuna göz atın .


Bilgi için teşekkürler. Bitmap'leri değil tüm Drawable'ları yönetiyorum, bu yüzden Geri Dönüşüm'ü çağıramıyorum. Çekmeceler için başka bir öneriniz var mı? Thanks Daniel
Daniel Benedykt

onPauseÖneri için teşekkürler . BitmapHepsini görüntülerle 4 sekme kullanıyorum , bu nedenle etkinliği yok etmek çok erken olmayabilir (kullanıcı tüm sekmelere göz atarsa).
Azurespot

7

Bu açıklama yardımcı olabilir: http://code.google.com/p/android/issues/detail?id=8488#c80

"Hızlı İpuçları:

1) ASLA System.gc () 'yi kendiniz aramayın. Bu burada bir düzeltme olarak yayıldı ve işe yaramıyor. Bunu yapma. Açıklamamda bir OutOfMemoryError almadan önce fark ederseniz, JVM zaten bir çöp toplama çalıştırır, bu yüzden tekrar yapmak için bir neden yoktur (programınızı yavaşlatır). Etkinliğinizin sonunda bir tane yapmak sadece sorunu kapsamaktadır. Bitmap'in sonlandırıcı kuyruğuna daha hızlı yerleştirilmesine neden olabilir, ancak bunun yerine her bir bitmapte geri dönüşüm olarak adlandırmanızın bir nedeni yoktur.

2) Artık ihtiyacınız olmayan bitmap'lerde recycle () öğesini çağırın. En azından, etkinliğinizin onDestroy'unda kullandığınız tüm bitmap'leri geri dönüştürün. Ayrıca, bitmap örneklerinin dalvik yığınından daha hızlı toplanmasını istiyorsanız, bitmap'e yapılan herhangi bir referansı temizlemek zarar vermez.

3) recycle () ve ardından System.gc () öğelerinin çağrılması, bitmap'i Dalvik yığınından kaldırmayabilir. Bu konuda endişelenmeyin. recycle () işini yaptı ve yerel belleği serbest bıraktı, bitmap'i Dalvik yığınından gerçekten kaldırmak için daha önce özetlediğim adımlardan geçmek biraz zaman alacak. Yerel bellek büyük bir yığın zaten ücretsiz çünkü bu büyük bir anlaşma değil!

4) Her zaman en son çerçevede bir hata olduğunu varsayın. Dalvik tam olarak yapması gerekeni yapıyor. Beklediğiniz veya istediğiniz şey olmayabilir, ancak nasıl çalıştığı. "


5

Ben de aynı problemi yaşadım. Birkaç testten sonra bu hatanın büyük görüntü ölçeklendirmesi için ortaya çıktığını gördüm. Görüntü ölçeklendirmesini azalttım ve sorun ortadan kalktı.

PS İlk başta görüntüyü küçültmeden görüntü boyutunu küçültmeye çalıştım. Bu hatayı durdurmadı.


4
nasıl görüntü ölçekleme yaptığını kodu gönderebilir miyim, ben aynı sorunla karşı karşıya ve ben bunu çözebilir düşünüyorum. Teşekkürler!
Gligor

@Gix - İnanıyorum ki proje kaynaklarına getirmeden önce görüntülerin boyutunu küçültmek zorunda kalıyor, böylece çekilebilir malzemelerin bellek ayak izini düşürüyor. Bunun için istediğiniz fotoğraf düzenleyiciyi kullanmalısınız. I like pixlr.com
Kyle Clegg

5

Puanları takip etmek bana çok yardımcı oldu. Başka noktalar da olabilir, ancak bunlar çok önemlidir:

  1. Mümkün olan yerlerde uygulama bağlamını (activity.this yerine) kullanın.
  2. İş parçacıklarınızı onPause () etkinlik yönteminde durdurun ve bırakın
  3. Görüşlerinizi / geri aramalarınızı onDestroy () etkinlik yönteminde serbest bırakın

4

Bu sorunu çözmek için uygun bir yol öneririm. Hatalı etkinliğiniz için Mainfest.xml dosyasında "android: configChanges" değerini atamanız yeterlidir. bunun gibi:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

verdiğim ilk çözüm, OOM hatası sıklığını gerçekten düşük bir seviyeye indirmişti. Ancak, sorunu tamamen çözmedi. Ve sonra 2. çözümü vereceğim:

OOM'un detaylandırdığı gibi, çok fazla çalışma zamanı belleği kullandım. Bu yüzden, projemin ~ / res / drawable içindeki resim boyutunu küçültüyorum. 128X128 çözünürlüğe sahip aşırı nitelikli bir resim gibi, benim uygulama için de uygun olan 64x64 olarak yeniden boyutlandırılabilir. Ve bunu bir yığın resim ile yaptıktan sonra, OOM hatası tekrar oluşmaz.


1
Bu, uygulamanızın yeniden başlatılmasını önlediğiniz için işe yarar. Yani bir kaçınma olduğu kadar çok bir çözüm değil. Ama bazen tüm ihtiyacınız olan bu. Etkinlik'teki varsayılan onConfigurationChanged () yöntemi, yönünüzü sizin için çevirir, bu nedenle farklı yönlere gerçekten uyum sağlamak için herhangi bir UI değişikliğine ihtiyacınız yoksa, sonuçtan tamamen memnun olabilirsiniz. Yine de sizi yeniden başlatabilecek diğer şeylere dikkat edin. Bunu kullanmak isteyebilirsiniz: android: configChanges = "keyboardHidden | orientation | klavye | yerel ayar | mcc | mnc | dokunmatik ekran | screenLayout | fontScale"
Carl

3

Ben de outofmemory böcek tarafından sinirli. Ve evet, ben de görüntüleri ölçeklerken bu hatanın çok açıldığını gördüm. İlk başta tüm yoğunluklar için görüntü boyutları oluşturmaya çalıştım, ancak bunun uygulamamın boyutunu önemli ölçüde artırdığını buldum. Şimdi tüm yoğunluklarda tek bir görüntü kullanıyorum ve görüntülerimi ölçeklendiriyorum.

Kullanıcı bir etkinlikten diğerine gittiğinde uygulamam outofmemory hatası atacaktı. Çekilebilirlerimi null olarak ayarlamak ve System.gc () işlevini çağırmak işe yaramadı, bitmapDrawables'ımı getBitMap (). Recycle () ile geri dönüştürmek de işe yaramadı. Android, outofmemory hatasını ilk yaklaşımla atmaya devam edecek ve ikinci yaklaşımla geri dönüştürülmüş bir bitmap kullanmayı denediğinde bir tuval hata mesajı atacaktı.

Hatta üçüncü bir yaklaşım izledim. Tüm görünümleri null, arkaplan siyaha ayarladım. Bu temizlemeyi onStop () yöntemimde yapıyorum. Etkinlik artık görünmez olur olmaz çağrılan yöntem budur. Bunu onPause () yönteminde yaparsanız, kullanıcılar siyah bir arka plan görür. Uygun değil. Bunu onDestroy () yönteminde yapmaya gelince, bunun çağrılacağının garantisi yoktur.

Kullanıcı cihazdaki geri düğmesine basarsa siyah bir ekranın oluşmasını önlemek için, startActivity (getIntent ()) öğesini çağırarak ve sonra bitir () yöntemlerini kullanarak onRestart () yöntemindeki etkinliği yeniden yüklerim.

Not: arka planı siyah olarak değiştirmek gerçekten gerekli değildir.


1

Kaynak verileri diskten veya bir ağ konumundan (veya gerçekten bellek dışında herhangi bir kaynaktan) okunursa, Büyük Bitmap'leri Verimli Bir Şekilde Yükle dersinde tartışılan BitmapFactory.decode * yöntemleri ana kullanıcı arabirimi iş parçacığında yürütülmemelidir. Bu verilerin yüklenmesi için geçen süre tahmin edilemez ve çeşitli faktörlere (disk veya ağdan okuma hızı, görüntü boyutu, CPU gücü vb.) Bağlıdır. Bu görevlerden biri UI iş parçacığını engelliyorsa, sistem uygulamanızı yanıt vermiyor olarak işaretler ve kullanıcı bunu kapatma seçeneğine sahiptir (daha fazla bilgi için Yanıtlama için Tasarım konusuna bakın).


0

İnternette bulduğum her şeyi denedim ve hiçbiri işe yaramadı. System.gc () öğesini çağırmak yalnızca uygulamanın hızını azaltır. Destroy'da bitmap'leri geri dönüştürme benim için de işe yaramadı.

Şimdi çalışan tek şey, tüm bitmap'lerin statik bir listesine sahip olmaktır, böylece bitmapler bir yeniden başlatmadan sonra hayatta kalırlar. Etkinlik her yeniden başlatıldığında yenilerini oluşturmak yerine kaydedilmiş bitmap'leri kullanın.

Benim durumumda kod şöyle görünür:

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

bu bağlamınıza sızmaz mı? (etkinlik verileri)
Rafael Sanches

0

Ben sadece arka plan görüntüleri makul boyutları ile geçiş ile aynı sorunu vardı. Yeni bir resim koymadan önce ImageView'i null değerine ayarlayarak daha iyi sonuçlar aldım.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

0

FWIW, kodladığım ve birkaç aydır kullandığım hafif bir bitmap-önbellek. Hepsi çanlar ve düdükler değil, bu yüzden kullanmadan önce kodu okuyun.

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.