java.lang.OutOfMemoryError: ukuran bitmap melebihi anggaran VM - Android

159

Saya mengembangkan aplikasi yang menggunakan banyak gambar di Android.

Aplikasi ini berjalan sekali, mengisi informasi pada layar ( Layouts, Listviews, Textviews, ImageViews, dll) dan user membaca informasi.

Tidak ada animasi, tidak ada efek khusus atau apa pun yang dapat mengisi memori. Terkadang drawable dapat berubah. Beberapa sumber daya android dan beberapa file disimpan dalam folder di SDCARD.

Kemudian pengguna berhenti ( onDestroymetode ini dieksekusi dan aplikasi tetap berada di memori oleh VM) dan kemudian di beberapa titik pengguna memasukkan lagi.

Setiap kali pengguna masuk ke aplikasi, saya bisa melihat memori semakin bertambah sampai pengguna mendapatkan java.lang.OutOfMemoryError.

Jadi apa cara terbaik / yang benar untuk menangani banyak gambar?

Haruskah saya menempatkan mereka dalam metode statis sehingga tidak dimuat sepanjang waktu? Apakah saya harus membersihkan tata letak atau gambar yang digunakan dalam tata letak dengan cara khusus?

Daniel Benedykt
sumber
4
Ini dapat membantu jika Anda memiliki banyak drawable drawings yang berubah. Ini bekerja sejak saya membuat program sendiri :) androidactivity.wordpress.com/2011/09/24/…

Jawaban:

72

Sepertinya Anda memiliki kebocoran memori. Masalahnya tidak menangani banyak gambar, itu adalah bahwa gambar Anda tidak mendapatkan alokasi saat aktivitas Anda dihancurkan.

Sulit untuk mengatakan mengapa ini tanpa melihat kode Anda. Namun, artikel ini memiliki beberapa kiat yang mungkin membantu:

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

Secara khusus, menggunakan variabel statis cenderung membuat segalanya lebih buruk, bukan lebih baik. Anda mungkin perlu menambahkan kode yang menghapus panggilan balik ketika aplikasi Anda digambar ulang - tetapi sekali lagi, tidak ada cukup informasi di sini untuk mengatakan dengan pasti.

Trevor Johns
sumber
8
ini benar dan juga memiliki banyak gambar (dan bukan kebocoran memori) dapat menyebabkan Memori keluar karena berbagai alasan seperti banyak aplikasi lain sedang berjalan, fragmentasi memori, dll. Saya belajar bahwa ketika bekerja dengan banyak gambar jika Anda mendapatkan memori keluar sistematis, seperti selalu melakukan hal yang sama, maka itu kebocoran. Jika Anda mendapatkannya sekali sehari atau sesuatu, itu karena Anda terlalu dekat dengan batas. Saran saya dalam hal ini adalah mencoba untuk menghapus beberapa hal dan coba lagi. Memori gambar juga berada di luar memori tumpukan, jadi perhatikan keduanya.
Daniel Benedykt
15
Jika Anda mencurigai adanya kebocoran memori, cara cepat untuk memantau penggunaan heap adalah melalui perintah meminfo dumpsys adb shell. Jika Anda melihat tumpukan meningkat selama beberapa siklus gc (GC_ * baris log di logcat) Anda bisa yakin Anda memiliki kebocoran. Kemudian buat heap dump (atau beberapa kali pada waktu yang berbeda) melalui adb atau DDMS dan analisa melalui tool tree dominator pada Eclipse MAT. Anda harus segera menemukan objek yang memiliki tumpukan retain yang tumbuh seiring waktu. Itu kebocoran ingatan Anda. Saya menulis sebuah artikel dengan beberapa perincian lebih lanjut di sini: macgyverdev.blogspot.com/2011/11/…
Johan Norén
98

Salah satu kesalahan paling umum yang saya temukan dalam mengembangkan Aplikasi Android adalah kesalahan "java.lang.OutOfMemoryError: Bitmap Size Exceed VM Budget". Saya menemukan kesalahan ini sering pada kegiatan menggunakan banyak bitmap setelah mengubah orientasi: Kegiatan dihancurkan, dibuat lagi dan tata letak "meningkat" dari XML memakan memori VM yang tersedia untuk bitmap.

Bitmap pada tata letak aktivitas sebelumnya tidak dialokasikan dengan benar oleh pengumpul sampah karena mereka telah melewati referensi ke aktivitas mereka. Setelah banyak percobaan saya menemukan solusi yang cukup bagus untuk masalah ini.

Pertama, atur atribut "id" pada tampilan induk dari tata letak XML Anda:

    <?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"
     >
     ...

Kemudian, pada onDestroy() metode Aktivitas Anda, panggil unbindDrawables()metode yang meneruskan referensi ke Tampilan induk dan kemudian lakukan a 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();
        }
    }

unbindDrawables()Metode ini mengeksplorasi pohon tampilan secara rekursif dan:

  1. Menghapus panggilan balik pada semua drawables latar belakang
  2. Menghapus anak-anak di setiap viewgroup
hp.android
sumber
10
Kecuali ... java.lang.UnsupportedOperationException: removeAllViews () tidak didukung di AdapterView
Terima kasih ini membantu mengurangi ukuran tumpukan saya secara signifikan karena saya memiliki banyak gambar ... @GJTorikian untuk itu coba saja menangkapnya di removeAllViews () sebagian besar subkelas ViewGroup bekerja dengan baik.
Eric Chen
5
Atau ubah saja kondisinya: if (lihat instance dari ViewGroup &&! (Lihat instanceof AdapterView))
Anke
2
@ Adam Varhegyi, Anda dapat secara eksplisit memanggil fungsi yang sama untuk tampilan galeri di onDestroy. Suka unbindDrawables (galleryView); Semoga ini berhasil untuk Anda ...
hp.android
1
hati-hati terhadap kedua gotcha ini stackoverflow.com/questions/8405475/… AND stackoverflow.com/questions/4486034/…
max4ever
11

Untuk menghindari masalah ini Anda dapat menggunakan metode asli Bitmap.recycle()sebelum null-ing Bitmapobjek (atau pengaturan nilai lain). Contoh:

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

Dan selanjutnya Anda dapat mengubah tanpa myBitmappanggilan System.gc()seperti:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
ddmytrenko
sumber
1
ini tidak akan berhasil jika mencoba menambahkan elemen-elemen itu ke dalam tampilan daftar. apakah Anda punya saran tentang itu?
Rafael Sanches
@ ddmytrenko Anda mendaur ulang gambar sebelum menetapkannya. Bukankah itu mencegah pikselnya dari rendering?
IgorGanapolsky
8

Saya telah mengalami masalah yang tepat ini. Tumpukannya cukup kecil sehingga gambar-gambar ini bisa lepas kendali agak cepat dalam hal memori. Salah satu caranya adalah memberi pengumpul sampah petunjuk untuk mengumpulkan memori pada bitmap dengan memanggil metode daur ulangnya .

Juga, metode onDestroy tidak dijamin untuk dipanggil. Anda mungkin ingin memindahkan logika ini / membersihkan ke aktivitas onPause. Lihatlah diagram / tabel Siklus Hidup Aktivitas di halaman ini untuk informasi lebih lanjut.

Donn Felker
sumber
Terimakasih atas infonya. Saya mengelola semua Drawables dan bukan Bitmap, jadi saya tidak bisa memanggil Recycle. Ada saran lain untuk drawables? Terima kasih Daniel
Daniel Benedykt
Terima kasih untuk onPausesarannya. Saya menggunakan 4 tab dengan Bitmapgambar di dalamnya semua, jadi menghancurkan aktivitas mungkin tidak terjadi terlalu cepat (jika pengguna menelusuri semua tab).
Azurespot
7

Penjelasan ini mungkin membantu: http://code.google.com/p/android/issues/detail?id=8488#c80

"Kiat Cepat:

1) PERNAH panggil System.gc () sendiri. Ini telah disebarkan sebagai perbaikan di sini, dan itu tidak berhasil. Jangan lakukan itu. Jika Anda perhatikan dalam penjelasan saya, sebelum mendapatkan OutOfMemoryError, JVM sudah menjalankan pengumpulan sampah sehingga tidak ada alasan untuk melakukannya lagi (ini memperlambat program Anda). Melakukan satu di akhir aktivitas Anda hanya menutupi masalahnya. Ini dapat menyebabkan bitmap ditempatkan pada antrian finalizer lebih cepat, tetapi tidak ada alasan Anda tidak bisa hanya memanggil daur ulang pada setiap bitmap saja.

2) Selalu panggil daur ulang () pada bitmap yang tidak Anda perlukan lagi. Paling tidak, di onDestroy aktivitas Anda, lewati dan daur ulang semua bitmap yang Anda gunakan. Juga, jika Anda ingin instance bitmap dikumpulkan dari tumpukan dalvik lebih cepat, tidak ada salahnya untuk menghapus referensi apa pun ke bitmap.

3) Memanggil daur ulang () dan kemudian System.gc () masih mungkin tidak menghapus bitmap dari tumpukan Dalvik. JANGAN PERNAH khawatir tentang hal ini. recycle () melakukan tugasnya dan membebaskan memori asli, itu hanya akan mengambil beberapa waktu untuk melalui langkah-langkah yang saya uraikan sebelumnya untuk benar-benar menghapus bitmap dari tumpukan Dalvik. Ini BUKAN masalah besar karena sebagian besar memori asli sudah gratis!

4) Selalu menganggap ada bug dalam kerangka terakhir. Dalvik melakukan apa yang seharusnya dilakukan. Mungkin bukan apa yang Anda harapkan atau apa yang Anda inginkan, tetapi cara kerjanya. "

Aron
sumber
5

Saya memiliki masalah yang sama persis. Setelah beberapa pengujian saya menemukan bahwa kesalahan ini muncul untuk penskalaan gambar besar. Saya mengurangi skala gambar dan masalahnya hilang.

PS Awalnya saya mencoba mengurangi ukuran gambar tanpa menurunkan gambar. Itu tidak menghentikan kesalahan.

GSree
sumber
4
Anda dapat memposting kode tentang bagaimana Anda melakukan penskalaan gambar, saya menghadapi masalah yang sama dan saya pikir ini mungkin menyelesaikannya. Terima kasih!
Gligor
@ Gix - Saya percaya maksudnya dia harus mengurangi ukuran gambar sebelum membawanya ke sumber daya proyek, sehingga menurunkan jejak memori drawable. Untuk ini, Anda harus menggunakan editor foto pilihan Anda. Saya suka pixlr.com
Kyle Clegg
5

Poin-poin berikut sangat membantu saya. Mungkin ada poin lain juga, tetapi ini sangat penting:

  1. Gunakan konteks aplikasi (alih-alih aktivitas. Ini) sedapat mungkin.
  2. Hentikan dan lepaskan utas Anda di metode aktivitas onPause ()
  3. Lepaskan pandangan / panggilan balik Anda dalam metode aktivitas onDestroy ()

sumber
4

Saya menyarankan cara yang mudah untuk menyelesaikan masalah ini. Cukup berikan nilai atribut "android: configChanges" seperti yang diikuti di Mainfest.xml untuk aktivitas Anda yang salah. seperti ini:

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

solusi pertama yang saya berikan benar-benar mengurangi frekuensi kesalahan OOM ke level yang rendah. Tapi, itu tidak menyelesaikan masalah sama sekali. Dan kemudian saya akan memberikan solusi ke-2:

Sebagai rincian OOM, saya telah menggunakan memori runtime terlalu banyak. Jadi, saya mengurangi ukuran gambar di ~ / res / drawable proyek saya. Seperti gambar yang terlalu berkualitas yang memiliki resolusi 128X128, bisa diubah ukurannya menjadi 64x64 yang juga cocok untuk aplikasi saya. Dan setelah saya melakukannya dengan tumpukan gambar, kesalahan OOM tidak terjadi lagi.

Ranger Way
sumber
1
Ini berfungsi karena Anda menghindari restart aplikasi Anda. Jadi ini bukan solusi, melainkan penghindaran. Tetapi terkadang hanya itu yang Anda butuhkan. Dan metode onConfigurationChanged () default di Aktivitas akan membalikkan orientasi Anda untuk Anda, jadi jika Anda tidak memerlukan perubahan UI untuk benar-benar beradaptasi dengan orientasi yang berbeda, Anda mungkin sangat senang dengan hasilnya. Hati-hati terhadap hal-hal lain yang dapat memulai kembali Anda. Anda mungkin ingin menggunakan ini: android: configChanges = "keyboardHidden | orientasi | keyboard | lokal | mcc | mnc | touchscreen | screenLayout | fontScale"
Carl
3

Saya juga frustrasi dengan bug memori. Dan ya, saya juga menemukan bahwa kesalahan ini banyak muncul saat menskala gambar. Awalnya saya mencoba membuat ukuran gambar untuk semua kepadatan, tetapi saya menemukan ini secara substansial meningkatkan ukuran aplikasi saya. Jadi saya sekarang hanya menggunakan satu gambar untuk semua kepadatan dan meningkatkan skala gambar saya.

Aplikasi saya akan melempar kesalahan memori luar setiap kali pengguna berpindah dari satu aktivitas ke aktivitas lainnya. Mengatur drawables saya ke null dan memanggil System.gc () tidak berfungsi, begitu pula daur ulang bitmapDrawables saya dengan getBitMap (). Recycle (). Android akan terus membuang kesalahan memori dengan pendekatan pertama, dan itu akan melempar pesan kesalahan kanvas setiap kali mencoba menggunakan bitmap daur ulang dengan pendekatan kedua.

Saya mengambil pendekatan yang ketiga. Saya mengatur semua tampilan ke nol dan latar belakang menjadi hitam. Saya melakukan pembersihan ini pada metode onStop () saya. Ini adalah metode yang dipanggil segera setelah aktivitas tidak lagi terlihat. Jika Anda melakukannya dalam metode onPause (), pengguna akan melihat latar belakang hitam. Tidak ideal Adapun melakukannya dalam metode onDestroy (), tidak ada jaminan bahwa itu akan dipanggil.

Untuk mencegah layar hitam terjadi jika pengguna menekan tombol kembali pada perangkat, saya memuat kembali aktivitas dalam metode onRestart () dengan memanggil metode startActivity (getIntent ()) dan kemudian menyelesaikan ().

Catatan: tidak perlu mengubah latar belakang menjadi hitam.

bersuka ria
sumber
1

Metode * BitmapFactory.decode, dibahas dalam pelajaran Memuat Bitmap Besar Secara efisien , tidak boleh dieksekusi pada utas UI utama jika data sumber dibaca dari disk atau lokasi jaringan (atau benar-benar sumber selain memori). Waktu yang diperlukan untuk memuat data ini tidak dapat diprediksi dan tergantung pada berbagai faktor (kecepatan membaca dari disk atau jaringan, ukuran gambar, kekuatan CPU, dll.). Jika salah satu dari tugas ini memblokir utas UI, sistem menandai aplikasi Anda sebagai tidak responsif dan pengguna memiliki opsi untuk menutupnya (lihat Merancang untuk Responsif untuk informasi lebih lanjut).

Li3ro
sumber
0

Yah saya sudah mencoba semua yang saya temukan di internet dan tidak ada yang bekerja. Calling System.gc () hanya menurunkan kecepatan aplikasi. Mendaur ulang bitmap di onDestroy juga tidak berhasil untuk saya.

Satu-satunya hal yang berfungsi sekarang adalah memiliki daftar statis semua bitmap sehingga bitmap bertahan setelah restart. Dan cukup gunakan bitmap yang disimpan daripada membuat yang baru setiap kali aktivitas jika dimulai kembali.

Dalam kasus saya, kode ini terlihat seperti ini:

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;
    }
}
HarryHao
sumber
tidakkah ini akan membocorkan konteks Anda? (data aktivitas)
Rafael Sanches
0

Saya memiliki masalah yang sama hanya dengan mengganti gambar latar belakang dengan ukuran yang masuk akal. Saya mendapat hasil yang lebih baik dengan mengatur ImageView ke nol sebelum memasukkan gambar baru.

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

FWIW, inilah bitmap-cache ringan yang saya kodekan dan telah digunakan selama beberapa bulan. Ini bukan all-the-bells-and-whistles, jadi baca kodenya sebelum Anda menggunakannya.

/**
 * 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();
    }

}
Guy Smith
sumber