İç sınıfları (anonim) kullanmak tam olarak ne zaman sızdırıyor?


324

Android'de bellek sızıntıları ile ilgili bazı makaleler okudum ve konuyla ilgili Google I / O'dan bu ilginç videoyu izledim .

Yine de, kavramı tam olarak anlamıyorum ve özellikle bir Faaliyet içindeki kullanıcı iç sınıflarının güvenli veya tehlikeli olduğu durumlarda .

Anladığım bu:

Bir iç sınıf örneği, kendi dış sınıfından (bir Etkinlik) daha uzun süre hayatta kalırsa, bellek sızıntısı meydana gelir. -> Bu hangi durumlarda olabilir?

Bu örnekte, sanırım sızıntı riski yoktur, çünkü anonim sınıfın genişletilmesinin OnClickListeneretkinlikten daha uzun yaşayacağı bir yol yoktur , değil mi?

    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();

Şimdi, bu örnek tehlikeli mi ve neden?

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

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

Bu konuyu anlamanın, bir faaliyet yok edildiğinde ve yeniden yaratıldığında nelerin saklandığını ayrıntılı olarak anlamakla ilgili olduğu konusunda bir kuşkum var.

Bu mu?

Diyelim ki cihazın yönünü yeni değiştirdim (en yaygın sızıntı nedeni). İçimde super.onCreate(savedInstanceState)ne zaman çağrılacak onCreate(), bu alanların değerlerini geri yükleyecek mi (yönelim değişikliğinden önceki gibi)? Bu aynı zamanda iç sınıfların durumlarını da eski haline getirecek mi?

Sorumun çok kesin olmadığını anlıyorum, ancak işleri daha net hale getirebilecek herhangi bir açıklamayı gerçekten takdir ediyorum.


14
Bu blog yazısı ve bu blog yazısı , bellek sızıntıları ve iç sınıflar hakkında bazı iyi bilgilere sahiptir. :)
Alex Lockwood

Yanıtlar:


651

Sorduğunuz şey oldukça zor bir soru. Bunun sadece bir soru olduğunu düşünebilirsiniz, ancak aslında aynı anda birkaç soru soruyorsunuz. Bunu kapsamak zorunda olduğum bilgisiyle elimden geleni yapacağım ve umarım bazıları kaçırabileceğim şeyi kapsayacak şekilde katılacak.

İç İçe Sınıflar: Giriş

Java'da OOP ile ne kadar rahat olduğunuzdan emin olmadığım için, bu birkaç temel konuyu vuracak. İç içe sınıf, başka bir sınıfta sınıf tanımının bulunduğu zamandır. Temel olarak iki tür vardır: Statik İç İçe Sınıflar ve İç Sınıflar. Bunlar arasındaki gerçek fark:

  • Statik İç İçe Sınıflar:
    • "Üst düzey" olarak kabul edilir.
    • Kapsayıcı sınıfın bir örneğinin oluşturulmasını gerektirmez.
    • Açık bir referans olmadan içeren sınıf üyelerine başvuramaz.
    • Kendi ömürleri var.
  • İç Yuvalanmış Sınıflar:
    • Her zaman içeren sınıfın bir örneğinin oluşturulmasını gerektirir.
    • İçeren örneğe otomatik olarak örtük başvuru var.
    • Referans olmadan kabın sınıf üyelerine erişebilir.
    • Ömür boyu olan sözde artık kaba göre daha olmaktır.

Çöp Toplama ve İç Sınıflar

Çöp Toplama otomatiktir ancak kullanıldığını düşünüp düşünmediğine göre nesneleri kaldırmaya çalışır. Çöp Toplayıcı oldukça akıllıdır, ancak kusursuz değildir. Yalnızca nesneye etkin bir başvuru olup olmadığına göre bir şeyin kullanılıp kullanılmadığını belirleyebilir.

Burada asıl mesele, bir iç sınıfın konteynerinden daha uzun süre canlı tutulmasıdır. Bunun nedeni, kapsayıcı sınıfa örtük başvurudır. Bunun olabilmesinin tek yolu, içeren sınıfın dışındaki bir nesnenin, içerdiği nesneye bakılmaksızın iç nesneye bir referans tutmasıdır.

Bu, iç nesnenin canlı olduğu (başvuru yoluyla) bir duruma neden olabilir, ancak içeren nesneye yapılan referanslar diğer tüm nesnelerden zaten kaldırılmıştır. Bu nedenle iç nesne, içerdiği nesneyi canlı tutmaktır, çünkü her zaman bir referansı olacaktır. Bununla ilgili sorun, programlanmadığı sürece, içeride olup olmadığını bile kontrol etmek için içeren nesneye geri dönmenin bir yolu olmamasıdır.

Bu gerçekleşmenin en önemli yönü, bir Faaliyette mi yoksa çekilebilir mi olduğu konusunda hiçbir fark yaratmamasıdır. Sen olacak hep iç sınıfları kullanarak ve kabın onlar asla outlive nesneler emin olduğunda metodik olmak zorunda. Neyse ki, kodunuzun temel bir nesnesi değilse, sızıntılar karşılaştırıldığında küçük olabilir. Ne yazık ki, bunlar bulmak için en zor sızıntılardan bazılarıdır, çünkü birçoğu sızana kadar fark edilmeyeceklerdir.

Çözümler: İç Sınıflar

  • İçeren nesneden geçici referanslar alın.
  • İç nesneler için uzun ömürlü referanslar tutabilen tek nesne olmasını sağlayın.
  • Fabrika gibi yerleşik kalıpları kullanın.
  • İç sınıf, kapsayıcı sınıf üyelerine erişim gerektirmiyorsa, bunu statik bir sınıfa dönüştürmeyi düşünün.
  • Bir Faaliyette olup olmadığına bakılmaksızın dikkatli kullanın.

Faaliyetler ve Görüşler: Giriş

Faaliyetler, çalıştırıp görüntüleyebilmek için birçok bilgi içerir. Faaliyetler, bir Görüşe sahip olmaları gereken özelliklerle tanımlanır. Ayrıca belirli otomatik işleyicileri var. Belirtip belirtmediğinizde, Etkinliğin içerdiği Görünüme dolaylı bir referansı vardır.

Bir Görünümün oluşturulabilmesi için, nerede oluşturulacağını ve görüntülenebilmesi için alt öğesi olup olmadığını bilmesi gerekir. Bu, her Görünümün Etkinliğe (üzerinden getContext()) bir referansı olduğu anlamına gelir . Dahası, her Görüş çocuklarına atıfta bulunur (yani getChildAt()). Son olarak, her Görünüm, görüntüsünü temsil eden oluşturulan Bitmap'e bir referans tutar.

Bir Etkinliğe (veya Etkinlik Bağlamına) başvurunuz olduğunda, düzen hiyerarşisinde ENTIRE zincirini takip edebileceğiniz anlamına gelir. Bu nedenle Etkinlikler veya Görüşlerle ilgili bellek sızıntıları çok büyüktür. Bir kerede bir ton bellek sızdırabilir.

Etkinlikler, Görüşler ve İç Sınıflar

İç Sınıflar hakkında yukarıdaki bilgiler göz önüne alındığında, bunlar en yaygın bellek sızıntılarıdır, ancak aynı zamanda en yaygın şekilde kaçınılır. Bir iç sınıfın bir Activities sınıfı üyelerine doğrudan erişimi olması arzu edilirken, birçoğu potansiyel sorunları önlemek için onları statik hale getirmeye isteklidir. Etkinlikler ve Görüşlerle ilgili sorun bundan çok daha derin.

Sızan Etkinlikler, Görüşler ve Etkinlik Bağlamları

Her şey Bağlam ve Yaşam Döngüsüne gelir. Etkinlik Bağlamını öldürecek belirli olaylar (yönlendirme gibi) vardır. Pek çok sınıf ve yöntem bir Bağlam gerektirdiğinden, geliştiriciler bazen bir Bağlama referans alarak ve üzerine tutarak bazı kodları kaydetmeye çalışırlar. Faaliyetimizi yürütmek için yaratmamız gereken birçok nesnenin, Faaliyetin yapması gerekenleri yapmasına izin vermek için Etkinlik Yaşam Döngüsü dışında olması gerekir. Nesnelerinizden herhangi birinin bir Etkinliğe, İçeriğine veya yok edildiğinde Görünümlerinden herhangi birine referans olması durumunda, bu Etkinliği ve tüm Görünüm ağacını sızdırmışsınız demektir.

Çözümler: Etkinlikler ve Görüşler

  • Her ne pahasına olursa olsun, bir Görünüme veya Etkinliğe Statik bir referans yapmaktan kaçının.
  • Etkinlik Bağlamlarına yapılan tüm referanslar kısa ömürlü olmalıdır (işlevin süresi)
  • Uzun ömürlü bir bağlama ihtiyacınız varsa, Uygulama Bağlamını ( getBaseContext()veya getApplicationContext()) kullanın. Bunlar referansları dolaylı olarak tutmaz.
  • Alternatif olarak, Yapılandırma Değişikliklerini geçersiz kılarak bir Etkinliğin yok edilmesini sınırlayabilirsiniz. Ancak bu, diğer potansiyel olayların Etkinliği yok etmesini engellemez. Bunu yapabilirsiniz , ancak yine de yukarıdaki uygulamalara başvurmak isteyebilirsiniz.

Runnables: Giriş

Runnables aslında o kadar da kötü değil. Yani, olabilirler , ama gerçekten zaten tehlikeli bölgelerin çoğunu vurduk. Çalıştırılabilir, oluşturulduğu iş parçacığından bağımsız bir görev gerçekleştiren eşzamansız bir işlemdir. Çoğu çalıştırılabilir kablo UI iş parçacığından başlatılır. Özünde, bir Runnable kullanmak, biraz daha yönetilen başka bir iş parçacığı oluşturmaktır. Runnable'ı standart bir sınıf gibi sınıflandırır ve yukarıdaki yönergeleri izlerseniz, birkaç sorunla karşılaşmanız gerekir. Gerçek şu ki, birçok geliştirici bunu yapmıyor.

Kolaylık, okunabilirlik ve mantıksal program akışı dışında, birçok geliştirici Anonim İç Sınıfları kullanarak yukarıda oluşturduğunuz örnek gibi Runnable'larını tanımlar. Bu, yukarıda yazdığınız gibi bir örnekle sonuçlanır. Anonim İç Sınıf temelde ayrık bir İç Sınıftır. Tamamen yeni bir tanım oluşturmak ve sadece uygun yöntemleri geçersiz kılmak zorunda değilsiniz. Diğer tüm açılardan, bir İç Sınıftır, yani kabına örtük bir referans tutar.

Çalıştırılabilirler ve Etkinlikler / Görünümler

Yaşasın! Bu bölüm kısa olabilir! Runnable'ların mevcut iş parçacığının dışında çalışması nedeniyle, bunlarla ilgili tehlike uzun süreli asenkron işlemlere neden olur. Runnable bir Etkinlik veya Görünümde Anonim İç Sınıf VEYA iç içe İç Sınıf olarak tanımlanmışsa, bazı çok ciddi tehlikeler vardır. Daha önce belirtildiği gibi, bu Bunun nedeni vardır kabını kim olduğunu bilmek. Yön değişikliğini (veya sistem ölümünü) girin. Şimdi ne olduğunu anlamak için önceki bölümlere bakın. Evet, örneğiniz oldukça tehlikelidir.

Çözümler: Çalıştırılabilirler

  • Kodunuzun mantığını bozmazsa Runnable'ı genişletmeyi deneyin.
  • İç içe geçmiş sınıflar gerekiyorsa, genişletilmiş Runnable'ları statik yapmak için elinizden geleni yapın.
  • Anonim Runnable'ları kullanmanız gerekiyorsa, bunları herhangi bir olan bir Etkinlik veya Görünüm için uzun ömürlü bir referansı olan nesnede .
  • Birçok Runnable, AsyncTasks kadar kolay olabilirdi. Varsayılan olarak VM tarafından yönetilen AsyncTask kullanmayı düşünün.

Son Soruyu Cevaplama Şimdi doğrudan olmayan soruları cevaplamak için bu yazının diğer bölümleri tarafından ele . "Bir iç sınıftaki bir nesne dış sınıfından ne zaman daha uzun süre hayatta kalabilir?" Bunu yapmadan önce, yeniden vurgulayayım: Etkinlikler'de bu konuda endişelenmeye haklı olmanıza rağmen, her yerde bir sızıntıya neden olabilir. Sadece göstermek için basit bir örnek vereceğim (Etkinlik kullanmadan).

Aşağıda temel bir fabrikaya (kod eksik) ortak bir örnek verilmiştir.

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;
    }
}

Bu yaygın bir örnek değil, gösterilebilecek kadar basit. Burada anahtar yapıcı ...

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...
    }
}

Şimdi, Sızıntılarımız var, ama Fabrika yok. Fabrikayı serbest bırakmamıza rağmen, bellekte kalacak çünkü her bir Sızıntı'nın bir referansı var. Dış sınıfın veri içermesi bile önemli değil. Bu, birinin düşündüğünden çok daha sık olur. Yaratıcıya ihtiyacımız yok, sadece yaratımlarına. Bu yüzden geçici olarak bir tane yaratıyoruz, ancak kreasyonları süresiz olarak kullanıyoruz.

Yapıcıyı biraz değiştirdiğimizde ne olacağını hayal edin.

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

Şimdi, bu yeni LeakFactories'in her biri sızdırılmış durumda. Bunun hakkında ne düşünüyorsun? Bunlar, bir iç sınıfın herhangi bir türden bir dış sınıftan nasıl daha uzun yaşayabileceğine dair çok yaygın iki örnektir. Bu dış sınıf bir Aktivite olsaydı, ne kadar kötü olacağını hayal edin.

Sonuç

Bunlar, bu nesneleri uygun olmayan şekilde kullanmanın bilinen tehlikelerini listeler. Genel olarak, bu yazı sorularınızın çoğunu kapsamalıdır, ancak bunun bir loooong yazısı olduğunu anlıyorum, bu yüzden açıklığa ihtiyacınız varsa, bana bildirin. Yukarıdaki uygulamaları takip ettiğiniz sürece, sızıntı konusunda çok az endişe duyacaksınız.


3
Bu açık ve ayrıntılı cevap için çok teşekkürler. Ne demek istediğini anlamıyorum "birçok geliştirici Runnable'larını tanımlamak için kapanışları kullanıyor"
Sébastien

1
Java'daki kapaklar, tanımladığınız Runnable gibi Anonim İç Sınıflardır. Runnable'ı genişleten tanımlı bir Sınıf yazmadan bir sınıfı (neredeyse genişletmek) kullanmanın bir yolu. Buna "kapalı sınıf tanımı" denir, çünkü asıl içeren nesne içinde kendi kapalı bellek alanına sahiptir.
Fuzzical Logic

26
Aydınlatıcı yazı! Terminoloji ile ilgili bir açıklama: Java'da statik bir iç sınıf diye bir şey yoktur . ( Dokümanlar ). Yuvalanmış bir sınıf statik veya içseldir , ancak her ikisi de aynı anda olamaz.
jenzz

2
Bu teknik olarak doğru olsa da, Java statik sınıflar içindeki statik sınıfları tanımlamanıza izin verir. Terminoloji benim yararım için değil, teknik anlambilimi anlamayan başkalarının yararınadır. Bu yüzden ilk önce "üst düzey" oldukları belirtiliyor. Android geliştirici dokümanları da bu terminolojiyi kullanıyor ve bu Android geliştirmeye bakan insanlar için, bu yüzden tutarlılığı korumanın daha iyi olduğunu düşündüm.
Fuzzical Logic

13
Büyük yazı, StackOverflow, Android için esp en iyi biri.
StackOverflowed
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.