Kapan tepatnya bocor untuk menggunakan kelas dalam (anonim)?

324

Saya telah membaca beberapa artikel tentang kebocoran memori di Android dan menonton video menarik ini dari Google I / O pada subjek .

Namun, saya tidak sepenuhnya memahami konsep ini, dan terutama ketika aman atau berbahaya bagi pengguna kelas batin di dalam suatu Kegiatan .

Inilah yang saya mengerti:

Kebocoran memori akan terjadi jika instance kelas batin bertahan lebih lama dari kelas luarnya (Aktivitas). -> Dalam situasi apa ini bisa terjadi?

Dalam contoh ini, saya kira tidak ada risiko bocor, karena tidak mungkin ekstensi kelas anonim OnClickListenerakan hidup lebih lama dari aktivitas, bukan?

    final Dialog dialog = new Dialog(this);
    dialog.setContentView(R.layout.dialog_generic);
    Button okButton = (Button) dialog.findViewById(R.id.dialog_button_ok);
    TextView titleTv = (TextView) dialog.findViewById(R.id.dialog_generic_title);

    // *** Handle button click
    okButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            dialog.dismiss();
        }
    });

    titleTv.setText("dialog title");
    dialog.show();

Sekarang, apakah contoh ini berbahaya, dan mengapa?

// We are still inside an Activity
_handlerToDelayDroidMove = new Handler();
_handlerToDelayDroidMove.postDelayed(_droidPlayRunnable, 10000);

private Runnable _droidPlayRunnable = new Runnable() { 
    public void run() {
        _someFieldOfTheActivity.performLongCalculation();
    }
};

Saya ragu mengenai fakta bahwa memahami topik ini ada hubungannya dengan memahami secara terperinci apa yang disimpan ketika suatu kegiatan dihancurkan dan diciptakan kembali.

Apakah itu?

Katakanlah saya baru saja mengubah orientasi perangkat (yang merupakan penyebab paling umum dari kebocoran). Kapan super.onCreate(savedInstanceState)akan dipanggil di saya onCreate(), akankah ini mengembalikan nilai bidang (seperti sebelum orientasi berubah)? Apakah ini juga akan mengembalikan keadaan kelas batin?

Saya menyadari pertanyaan saya tidak terlalu tepat, tetapi saya sangat menghargai penjelasan apa pun yang dapat membuat semuanya lebih jelas.

Sébastien
sumber
14
Posting blog ini dan posting blog ini memiliki beberapa informasi bagus tentang kebocoran memori dan kelas dalam. :)
Alex Lockwood

Jawaban:

651

Apa yang Anda tanyakan adalah pertanyaan yang cukup sulit. Meskipun Anda mungkin berpikir itu hanya satu pertanyaan, Anda sebenarnya mengajukan beberapa pertanyaan sekaligus. Saya akan melakukan yang terbaik dengan pengetahuan bahwa saya harus menutupinya dan, mudah-mudahan, beberapa orang lain akan bergabung untuk membahas apa yang mungkin saya lewatkan.

Kelas Bersarang: Pendahuluan

Karena saya tidak yakin seberapa nyaman Anda dengan OOP di Jawa, ini akan menjadi dasar beberapa. Kelas bersarang adalah ketika definisi kelas terkandung dalam kelas lain. Pada dasarnya ada dua jenis: Kelas Bersarang Statis dan Kelas Dalam. Perbedaan nyata antara ini adalah:

  • Kelas Bersarang Statis:
    • Dianggap "tingkat atas".
    • Tidak memerlukan instance kelas yang mengandung untuk dibangun.
    • Mungkin tidak merujuk anggota kelas yang mengandung tanpa referensi eksplisit.
    • Memiliki masa hidup mereka sendiri.
  • Kelas Bersarang Bagian Dalam:
    • Selalu membutuhkan instance kelas yang mengandung untuk dibangun.
    • Secara otomatis memiliki referensi implisit ke instance yang berisi.
    • Dapat mengakses anggota kelas penampung tanpa referensi.
    • Seumur hidup seharusnya tidak lebih dari wadah.

Pengumpulan Sampah dan Kelas Dalam

Pengumpulan Sampah bersifat otomatis tetapi mencoba untuk menghapus objek berdasarkan apakah mereka berpikir sedang digunakan. Pengumpul Sampah cukup pintar, tetapi tidak sempurna. Itu hanya dapat menentukan apakah sesuatu sedang digunakan oleh apakah ada referensi aktif ke objek.

Masalah sebenarnya di sini adalah ketika kelas batin telah dipertahankan lebih lama dari wadahnya. Ini karena referensi implisit ke kelas yang mengandung. Satu-satunya cara ini dapat terjadi adalah jika suatu objek di luar kelas yang berisi menyimpan referensi ke objek batin, tanpa memperhatikan objek yang mengandung.

Ini dapat mengarah pada situasi di mana objek dalam hidup (melalui referensi) tetapi referensi ke objek yang mengandung telah dihapus dari semua objek lainnya. Objek batin adalah, oleh karena itu, menjaga objek yang mengandung tetap hidup karena akan selalu memiliki referensi untuk itu. Masalah dengan ini adalah bahwa kecuali jika diprogram, tidak ada cara untuk kembali ke objek yang berisi untuk memeriksa apakah itu masih hidup.

Aspek yang paling penting untuk realisasi ini adalah tidak ada bedanya apakah itu dalam suatu Kegiatan atau dapat digambar. Anda harus selalu metodis saat menggunakan kelas dalam dan memastikan bahwa mereka tidak pernah hidup lebih lama dari objek wadah. Untungnya, jika itu bukan objek inti dari kode Anda, kebocorannya mungkin kecil dibandingkan. Sayangnya, ini adalah beberapa kebocoran yang paling sulit ditemukan, karena kemungkinan besar tidak diketahui sampai banyak dari mereka bocor.

Solusi: Kelas Dalam

  • Dapatkan referensi sementara dari objek yang mengandung.
  • Biarkan objek yang berisi menjadi satu-satunya yang menyimpan referensi berumur panjang ke objek dalam.
  • Gunakan pola yang sudah ada seperti Pabrik.
  • Jika kelas dalam tidak memerlukan akses ke anggota kelas yang mengandung, pertimbangkan untuk mengubahnya menjadi kelas statis.
  • Gunakan dengan hati-hati, terlepas dari apakah itu dalam suatu Kegiatan atau tidak.

Kegiatan dan Pandangan: Pendahuluan

Kegiatan mengandung banyak informasi untuk dapat dijalankan dan ditampilkan. Aktivitas ditentukan oleh karakteristik bahwa mereka harus memiliki Tampilan. Mereka juga memiliki penangan otomatis tertentu. Apakah Anda menentukannya atau tidak, Kegiatan memiliki referensi implisit ke Lihat yang dikandungnya.

Agar Tampilan dapat dibuat, ia harus tahu di mana membuatnya dan apakah memiliki anak sehingga dapat ditampilkan. Ini berarti bahwa setiap Tampilan memiliki referensi ke Aktivitas (via getContext()). Selain itu, setiap View menyimpan referensi untuk anak-anaknya (yaitu getChildAt()). Akhirnya, setiap Tampilan menyimpan referensi ke Bitmap yang disajikan yang mewakili tampilannya.

Setiap kali Anda memiliki referensi ke suatu Kegiatan (atau Konteks Kegiatan), ini berarti bahwa Anda dapat mengikuti rantai SELURUH ke bawah hierarki tata letak. Inilah sebabnya mengapa kebocoran memori tentang Kegiatan atau Tampilan adalah masalah besar. Bisa jadi satu ton memori bocor sekaligus.

Kegiatan, Tampilan, dan Kelas Batin

Mengingat informasi di atas tentang Kelas Batin, ini adalah kebocoran memori yang paling umum, tetapi juga yang paling umum dihindari. Sementara itu diinginkan untuk memiliki kelas batin memiliki akses langsung ke anggota kelas Kegiatan, banyak yang bersedia untuk membuat mereka statis untuk menghindari masalah potensial. Masalah dengan Aktivitas dan Tampilan jauh lebih dalam dari itu.

Aktivitas yang Kebocoran, Tampilan dan Konteks Kegiatan

Semuanya bermuara pada Konteks dan Siklus Hidup. Ada peristiwa tertentu (seperti orientasi) yang akan membunuh Konteks Aktivitas. Karena begitu banyak kelas dan metode membutuhkan suatu Konteks, pengembang terkadang akan mencoba untuk menyimpan beberapa kode dengan mengambil referensi ke suatu Konteks dan menahannya. Kebetulan bahwa banyak objek yang harus kita buat untuk menjalankan Aktivitas kita harus ada di luar Activity LifeCycle untuk memungkinkan Aktivitas melakukan apa yang perlu dilakukan. Jika ada objek Anda yang memiliki referensi ke suatu Aktivitas, Konteksnya, atau salah satu Tampilannya saat dihancurkan, Anda baru saja membocorkan Kegiatan itu dan seluruh pohon View-nya.

Solusi: Aktivitas dan Tampilan

  • Hindari, bagaimanapun caranya, membuat referensi Statis ke Tampilan atau Aktivitas.
  • Semua referensi untuk Konteks Kegiatan harus berumur pendek (durasi fungsi)
  • Jika Anda membutuhkan Konteks yang berumur panjang, gunakan Konteks Aplikasi ( getBaseContext()atau getApplicationContext()). Ini tidak menyimpan referensi secara implisit.
  • Atau, Anda dapat membatasi penghancuran suatu Kegiatan dengan mengesampingkan Perubahan Konfigurasi. Namun, ini tidak menghentikan potensi peristiwa lain untuk menghancurkan Aktivitas. Meskipun Anda bisa melakukan ini, Anda mungkin masih ingin merujuk pada praktik di atas.

Runnables: Pendahuluan

Runnables sebenarnya tidak terlalu buruk. Maksud saya, mereka mungkin saja, tetapi sebenarnya kita sudah mencapai sebagian besar zona bahaya. Runnable adalah operasi asinkron yang menjalankan tugas secara independen dari utas yang dibuatnya. Sebagian besar runnables dibuat dari utas UI. Intinya, menggunakan Runnable adalah membuat utas lain, hanya sedikit lebih terkelola. Jika Anda mengklasifikasikan Runnable seperti kelas standar dan mengikuti panduan di atas, Anda akan mengalami beberapa masalah. Kenyataannya adalah bahwa banyak pengembang tidak melakukan ini.

Karena mudah, mudah dibaca, dan alur program logis, banyak pengembang memanfaatkan Kelas Batin Anonim untuk menentukan Runnables mereka, seperti contoh yang Anda buat di atas. Ini menghasilkan contoh seperti yang Anda ketikkan di atas. Anonim Inner Class pada dasarnya adalah Inner Class diskrit. Anda tidak perlu membuat definisi yang sama sekali baru dan cukup mengganti metode yang sesuai. Dalam semua hal lain itu adalah Kelas Batin, yang berarti bahwa ia menyimpan referensi implisit ke wadahnya.

Runnables dan Kegiatan / Tampilan

Yay! Bagian ini bisa pendek! Karena kenyataan bahwa Runnables berjalan di luar utas saat ini, bahaya dengan ini datang ke operasi asinkron berjalan lama. Jika runnable didefinisikan dalam Kegiatan atau Lihat sebagai Kelas Batin Anonim ATAU Kelas Batin bersarang, ada beberapa bahaya yang sangat serius. Ini karena, seperti yang dinyatakan sebelumnya, ia harus tahu siapa wadahnya. Masukkan perubahan orientasi (atau system kill). Sekarang cukup merujuk kembali ke bagian sebelumnya untuk memahami apa yang baru saja terjadi. Ya, teladan Anda cukup berbahaya.

Solusi: Runnables

  • Coba dan rentangkan Runnable, jika itu tidak merusak logika kode Anda.
  • Lakukan yang terbaik untuk membuat Runnables yang diperluas menjadi statis, jika harus bersarang kelas.
  • Jika Anda harus menggunakan Runnables Anonim, hindari membuatnya di objek apa pun yang memiliki referensi jangka panjang untuk suatu Aktivitas atau Tampilan yang sedang digunakan.
  • Banyak Runnables dapat dengan mudah menjadi AsyncTasks. Pertimbangkan untuk menggunakan AsyncTask karena itu adalah VM yang Dikelola secara default.

Menjawab Pertanyaan Akhir Sekarang untuk menjawab pertanyaan-pertanyaan yang tidak secara langsung ditangani oleh bagian lain dari posting ini. Anda bertanya "Kapan objek kelas dalam bisa bertahan lebih lama dari kelas luarnya?" Sebelum kita membahas hal ini, izinkan saya menekankan kembali: meskipun Anda benar khawatir tentang hal ini dalam Kegiatan, ini dapat menyebabkan kebocoran di mana saja. Saya akan memberikan contoh sederhana (tanpa menggunakan Aktivitas) hanya untuk menunjukkan.

Di bawah ini adalah contoh umum dari pabrik dasar (hilang kode).

public class LeakFactory
{//Just so that we have some data to leak
    int myID = 0;
// Necessary because our Leak class is an Inner class
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

Ini bukan contoh yang umum, tetapi cukup sederhana untuk diperagakan. Kuncinya di sini adalah konstruktor ...

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Gotta have a Factory to make my holes
        LeakFactory _holeDriller = new LeakFactory()
    // Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//Store them in the class member
            myHoles[i] = _holeDriller.createLeak();
        }

    // Yay! We're done! 

    // Buh-bye LeakFactory. I don't need you anymore...
    }
}

Sekarang, kami memiliki Kebocoran, tetapi tidak ada Pabrik. Meskipun kami merilis Factory, itu akan tetap di memori karena setiap Leak memiliki referensi untuk itu. Bahkan tidak masalah bahwa kelas luar tidak memiliki data. Ini terjadi jauh lebih sering daripada yang diperkirakan. Kami tidak membutuhkan pencipta, hanya ciptaannya. Jadi kami membuat sementara, tetapi gunakan kreasi tanpa batas.

Bayangkan apa yang terjadi ketika kita mengubah konstruktor sedikit.

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//WOW! I don't even have to create a Factory... 
        // This is SOOOO much prettier....
            myHoles[i] = new LeakFactory().createLeak();
        }
    }
}

Sekarang, setiap LeakFactories baru itu baru saja bocor. Apa yang kamu pikirkan tentang itu? Itu adalah dua contoh yang sangat umum tentang bagaimana kelas batin dapat hidup lebih lama dari kelas luar dari jenis apa pun. Jika kelas luar itu adalah sebuah Kegiatan, bayangkan betapa buruknya itu.

Kesimpulan

Ini daftar bahaya yang diketahui terutama menggunakan benda-benda ini secara tidak tepat. Secara umum, pos ini seharusnya mencakup sebagian besar pertanyaan Anda, tapi saya mengerti itu adalah posting yang terlalu panjang, jadi jika Anda perlu klarifikasi, beri tahu saya. Selama Anda mengikuti praktik-praktik di atas, Anda akan sangat khawatir akan kebocoran.

Fuzzical Logic
sumber
3
Terima kasih banyak atas jawaban yang jelas dan terperinci ini. Saya hanya tidak mengerti apa yang Anda maksud dengan "banyak pengembang menggunakan penutupan untuk mendefinisikan Runnables mereka"
Sébastien
1
Penutupan di Jawa adalah Kelas Batin Anonim, seperti Runnable yang Anda jelaskan. Ini cara untuk menggunakan kelas (hampir memperluasnya) tanpa menulis Kelas yang didefinisikan yang memperluas Runnable. Ini disebut penutupan karena itu adalah "definisi kelas tertutup" karena memiliki ruang memori tertutup sendiri di dalam objek yang berisi sebenarnya.
Logika Fuzzical
26
Artikel yang mencerahkan! Satu komentar tentang terminologi: Tidak ada yang namanya kelas batin statis di Jawa. ( Documents ). Kelas bersarang adalah statis atau batin , tetapi tidak bisa keduanya sekaligus.
jenzz
2
Sementara itu secara teknis benar, Java memungkinkan Anda untuk mendefinisikan kelas statis di dalam kelas statis. Terminologi ini bukan untuk keuntungan saya, tetapi untuk kepentingan orang lain yang tidak mengerti teknis semantik. Inilah sebabnya mengapa pertama kali disebutkan bahwa mereka "top-level". Dokumen pengembang Android juga menggunakan terminologi ini, dan ini untuk orang yang melihat pengembangan Android, jadi saya pikir lebih baik untuk menjaga konsistensi.
Logika Fuzzical
13
Pos hebat, salah satu yang terbaik di StackOverflow, esp untuk Android.
StackOverflowed