เมื่อใดจึงจะปลอดภัยในการใช้คลาสภายใน (ไม่ระบุชื่อ)


324

ผมได้อ่านบทความบางอย่างเกี่ยวกับการรั่วไหลของหน่วยความจำใน Android และดูวิดีโอที่น่าสนใจนี้จาก Google I / O ในเรื่อง

แต่ฉันก็ยังไม่เข้าใจแนวคิดและโดยเฉพาะอย่างยิ่งเมื่อมันมีความปลอดภัยหรือเป็นอันตรายต่อผู้ใช้เรียนภายในภายในกิจกรรม

นี่คือสิ่งที่ฉันเข้าใจ:

หน่วยความจำรั่วจะเกิดขึ้นหากอินสแตนซ์ของคลาสภายในมีชีวิตอยู่นานกว่าคลาสภายนอก (กิจกรรม) -> สิ่งนี้สามารถเกิดขึ้นได้ในสถานการณ์ใด

ในตัวอย่างนี้ฉันคิดว่าไม่มีความเสี่ยงต่อการรั่วไหลเนื่องจากไม่มีวิธีใดที่การขยายคลาสแบบไม่ระบุชื่อOnClickListenerจะใช้งานได้นานกว่ากิจกรรมใช่ไหม

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

ตอนนี้เป็นตัวอย่างที่อันตรายและทำไม

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

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

ฉันมีข้อสงสัยเกี่ยวกับความจริงที่ว่าการทำความเข้าใจหัวข้อนี้เกี่ยวข้องกับการทำความเข้าใจในรายละเอียดสิ่งที่ถูกเก็บไว้เมื่อกิจกรรมถูกทำลายและสร้างขึ้นใหม่

ใช่ไหม?

สมมติว่าฉันเพิ่งเปลี่ยนการวางแนวของอุปกรณ์ (ซึ่งเป็นสาเหตุที่พบบ่อยที่สุดของการรั่วไหล) เมื่อsuper.onCreate(savedInstanceState)ใดจะได้รับการเรียกในของฉันonCreate()สิ่งนี้จะคืนค่าของฟิลด์ (เหมือนเดิมก่อนการเปลี่ยนทิศทาง) หรือไม่ สิ่งนี้จะทำให้สถานะของชนชั้นภายในกลับมาเหมือนเดิมหรือไม่

ฉันรู้ว่าคำถามของฉันไม่แม่นยำ แต่ฉันขอขอบคุณคำอธิบายใด ๆ ที่สามารถทำให้สิ่งต่าง ๆ ชัดเจนขึ้น


14
โพสต์บล็อกนี้และโพสต์บล็อกนี้มีข้อมูลที่ดีเกี่ยวกับหน่วยความจำรั่วและคลาสภายใน :)
Alex Lockwood

คำตอบ:


651

สิ่งที่คุณถามเป็นคำถามที่ค่อนข้างยาก แม้ว่าคุณจะคิดว่ามันเป็นเพียงคำถามเดียว แต่จริงๆแล้วคุณกำลังถามคำถามหลายข้อพร้อมกัน ฉันจะทำให้ดีที่สุดด้วยความรู้ที่ฉันต้องครอบคลุมและหวังว่าคนอื่นจะเข้าร่วมเพื่อปกปิดสิ่งที่ฉันอาจพลาด

คลาสที่ซ้อนกัน: บทนำ

เนื่องจากฉันไม่แน่ใจว่าคุณมีความสะดวกสบายเพียงใดกับ OOP ใน Java สิ่งนี้จะเป็นพื้นฐานสองสามอย่าง คลาสที่ซ้อนกันคือเมื่อนิยามคลาสมีอยู่ภายในคลาสอื่น โดยพื้นฐานแล้วมีสองประเภท: คลาสที่ซ้อนกันแบบคงที่และชั้นใน ความแตกต่างที่แท้จริงระหว่างสิ่งเหล่านี้คือ:

  • คลาสที่ซ้อนกันแบบคงที่:
    • ถือว่าเป็น "ระดับสูงสุด"
    • ไม่ต้องการอินสแตนซ์ของคลาสที่ประกอบด้วยเพื่อสร้าง
    • อาจไม่อ้างอิงสมาชิกชั้นเรียนที่มีโดยไม่มีการอ้างอิงที่ชัดเจน
    • มีชีวิตของตัวเอง
  • คลาสซ้อนภายใน:
    • ต้องการอินสแตนซ์ของคลาสที่ประกอบด้วยเพื่อสร้างเสมอ
    • มีการอ้างอิงโดยนัยไปยังอินสแตนซ์ที่มีอยู่โดยอัตโนมัติ
    • อาจเข้าถึงสมาชิกคลาสของคอนเทนเนอร์โดยไม่มีการอ้างอิง
    • อายุการใช้งานควรจะไม่นานกว่าของคอนเทนเนอร์

การรวบรวมขยะและชั้นใน

การรวบรวมขยะเป็นไปโดยอัตโนมัติ แต่พยายามที่จะลบวัตถุตามที่คิดว่ามันถูกใช้งานอยู่ The Garbage Collector นั้นค่อนข้างฉลาด แต่ไม่ไร้ที่ติ มันสามารถตัดสินได้ว่ามีบางสิ่งถูกใช้โดยมีการอ้างอิงวัตถุที่ใช้งานอยู่หรือไม่

ปัญหาที่แท้จริงของที่นี่คือเมื่อชั้นในถูกทำให้มีอายุการใช้งานนานกว่าตู้คอนเทนเนอร์ นี่เป็นเพราะการอ้างอิงโดยนัยถึงคลาสที่มีอยู่ วิธีเดียวที่สามารถเกิดขึ้นได้คือถ้าวัตถุภายนอกคลาสที่มีการเก็บรักษาการอ้างอิงถึงวัตถุภายในโดยไม่คำนึงถึงวัตถุที่ประกอบด้วย

สิ่งนี้สามารถนำไปสู่สถานการณ์ที่วัตถุภายในยังมีชีวิตอยู่ (ผ่านการอ้างอิง) แต่การอ้างอิงไปยังวัตถุที่บรรจุนั้นได้ถูกลบออกจากวัตถุอื่นทั้งหมดแล้ว วัตถุภายในจึงทำให้วัตถุที่มีชีวิตอยู่เพราะมันจะเสมอมีการอ้างอิงถึงมัน ปัญหานี้คือถ้าไม่มีการตั้งโปรแกรมไว้ไม่มีวิธีใดที่จะกลับไปที่วัตถุที่มีอยู่เพื่อตรวจสอบว่ามีชีวิตอยู่หรือไม่

สิ่งสำคัญที่สุดในการทำให้เกิดความเข้าใจนี้คือมันไม่ได้สร้างความแตกต่างไม่ว่าจะเป็นในกิจกรรมหรือเป็นสิ่งที่สามารถดึงได้ คุณจะต้องมีระเบียบเสมอเมื่อใช้คลาสภายในและตรวจสอบให้แน่ใจว่าพวกเขาไม่เคยอยู่ได้นานกว่าวัตถุของภาชนะ โชคดีที่ถ้าไม่ใช่วัตถุหลักของรหัสของคุณการรั่วไหลอาจน้อยเมื่อเปรียบเทียบ น่าเสียดายที่สิ่งเหล่านี้เป็นรอยรั่วที่ยากที่สุดในการค้นหาเพราะพวกเขามีแนวโน้มที่จะไม่มีใครสังเกตจนกว่าพวกเขาจะรั่วไหลออกมา

โซลูชั่น: ชั้นใน

  • รับการอ้างอิงชั่วคราวจากวัตถุที่มี
  • อนุญาตให้วัตถุที่มีอยู่เป็นวัตถุเดียวที่จะเก็บการอ้างอิงระยะยาวกับวัตถุภายใน
  • ใช้รูปแบบที่กำหนดเช่นโรงงาน
  • หากคลาสภายในไม่ต้องการการเข้าถึงสมาชิกคลาสที่มีอยู่ให้พิจารณาเปลี่ยนเป็นคลาสแบบสแตติก
  • ใช้ด้วยความระมัดระวังไม่ว่าจะอยู่ในกิจกรรมหรือไม่ก็ตาม

กิจกรรมและมุมมอง: บทนำ

กิจกรรมมีข้อมูลจำนวนมากที่สามารถเรียกใช้และแสดงผลได้ กิจกรรมถูกกำหนดโดยคุณลักษณะที่ต้องมีมุมมอง พวกเขายังมีตัวจัดการอัตโนมัติบางอย่าง ไม่ว่าคุณจะระบุหรือไม่กิจกรรมนั้นมีการอ้างอิงโดยนัยถึงมุมมองที่มี

เพื่อให้สามารถสร้างมุมมองได้จะต้องทราบว่าจะสร้างมุมมองนั้นและมีลูก ๆ หรือไม่เพื่อให้สามารถแสดงได้ ซึ่งหมายความว่าทุกมุมมองมีการอ้างอิงถึงกิจกรรม (ผ่านgetContext()) ยิ่งกว่านั้นทุกมุมมองยังคงอ้างอิงถึงลูก ๆ ของมัน (เช่นgetChildAt()) สุดท้ายมุมมองแต่ละอันจะเก็บการอ้างอิงไปยังบิตแมปที่แสดงผลซึ่งแสดงถึงการแสดงผลของมัน

เมื่อใดก็ตามที่คุณมีการอ้างอิงถึงกิจกรรม (หรือบริบทของกิจกรรม) หมายความว่าคุณสามารถทำตามโซ่ทั้งหมดลงในลำดับชั้นของเค้าโครง นี่คือเหตุผลที่หน่วยความจำรั่วไหลเกี่ยวกับกิจกรรมหรือมุมมองเป็นเรื่องใหญ่ มันอาจจะเป็นตันของหน่วยความจำการรั่วไหลทั้งหมดในครั้งเดียว

กิจกรรมมุมมองและชั้นใน

จากข้อมูลข้างต้นเกี่ยวกับ Inner Classes สิ่งเหล่านี้เป็นความจำรั่วทั่วไป แต่ก็หลีกเลี่ยงได้บ่อยที่สุด ในขณะที่เป็นที่ต้องการให้คลาสภายในมีการเข้าถึงโดยตรงกับสมาชิกคลาสของกิจกรรม แต่หลายคนก็เต็มใจที่จะทำให้พวกเขาคงที่เพื่อหลีกเลี่ยงปัญหาที่อาจเกิดขึ้น ปัญหาของกิจกรรมและมุมมองนั้นลึกซึ้งยิ่งกว่านั้น

กิจกรรมที่รั่วไหล, การดูและบริบทของกิจกรรม

ทุกอย่างลงมาที่บริบทและวงจรชีวิต มีเหตุการณ์บางอย่าง (เช่นการวางแนว) ที่จะฆ่าบริบทของกิจกรรม เนื่องจากคลาสและเมธอดจำนวนมากต้องการบริบทดังนั้นบางครั้งนักพัฒนาจะพยายามบันทึกโค้ดบางส่วนโดยการอ้างอิงถึงบริบทและกดค้างไว้ มันเกิดขึ้นเมื่อวัตถุหลายอย่างที่เราต้องสร้างเพื่อเรียกใช้กิจกรรมของเราต้องมีอยู่นอก ActivityCycleCycle เพื่อให้กิจกรรมสามารถทำสิ่งที่ต้องทำ หากวัตถุใด ๆ ของคุณมีการอ้างอิงถึงกิจกรรมบริบทหรือมุมมองใด ๆ ของมันเมื่อมันถูกทำลายคุณเพิ่งรั่วไหลกิจกรรมและต้นไม้ทั้งหมดของมุมมอง

โซลูชัน: กิจกรรมและมุมมอง

  • หลีกเลี่ยงค่าใช้จ่ายทั้งหมดโดยอ้างอิงจากมุมมองหรือกิจกรรม
  • การอ้างอิงถึงบริบทของกิจกรรมทั้งหมดควรมีอายุสั้น (ระยะเวลาของฟังก์ชัน)
  • หากคุณต้องการบริบทที่ยาวนานให้ใช้บริบทแอปพลิเคชัน ( getBaseContext()หรือgetApplicationContext()) สิ่งเหล่านี้ไม่ได้เก็บการอ้างอิงโดยนัย
  • หรือคุณอาจ จำกัด การทำลายกิจกรรมโดยการแทนที่การเปลี่ยนแปลงการกำหนดค่า อย่างไรก็ตามสิ่งนี้จะไม่หยุดเหตุการณ์ที่อาจเกิดขึ้นอื่น ๆ จากการทำลายกิจกรรม ในขณะที่คุณสามารถทำสิ่งนี้ได้คุณอาจต้องการอ้างอิงแนวทางปฏิบัติข้างต้น

Runnables: บทนำ

Runnables ไม่เลวจริง ๆ ฉันหมายถึงพวกมันอาจเป็นได้ แต่จริงๆแล้วเราได้โจมตีพื้นที่อันตรายส่วนใหญ่แล้ว Runnable เป็นการดำเนินการแบบอะซิงโครนัสที่ดำเนินงานอิสระจากเธรดที่สร้างขึ้น Runnables ส่วนใหญ่นั้นอินสแตนซ์จากเธรด UI โดยพื้นฐานแล้วการใช้ Runnable กำลังสร้างเธรดอื่นซึ่งมีการจัดการเพียงเล็กน้อยเท่านั้น หากคุณคลาส Runnable เหมือนคลาสมาตรฐานและปฏิบัติตามแนวทางข้างต้นคุณควรพบปัญหาเล็กน้อย ความจริงก็คือนักพัฒนาหลายคนไม่ทำเช่นนี้

จากความง่ายความสามารถในการอ่านและการไหลของโปรแกรมแบบลอจิคัลนักพัฒนาหลายคนใช้ Anonymous Inner Classes เพื่อกำหนด Runnables เช่นตัวอย่างที่คุณสร้างไว้ด้านบน ผลลัพธ์นี้เป็นตัวอย่างเช่นที่คุณพิมพ์ด้านบน ระดับชั้นในแบบไม่ระบุชื่อนั้นเป็นชั้นในแบบไม่ต่อเนื่อง คุณไม่จำเป็นต้องสร้างคำจำกัดความใหม่ทั้งหมดและเพียงแทนที่วิธีที่เหมาะสม ในทุกแง่มุมอื่น ๆ มันเป็นชั้นในซึ่งหมายความว่ามันทำให้การอ้างอิงโดยนัยไปยังภาชนะของมัน

Runnables และกิจกรรม / มุมมอง

เย้! ส่วนนี้อาจสั้น! เนื่องจากความจริงที่ว่า Runnables ทำงานนอกเธรดปัจจุบันอันตรายของสิ่งเหล่านี้มาจากการดำเนินการแบบอะซิงโครนัสที่ใช้เวลานาน หาก runnable ถูกกำหนดไว้ในกิจกรรมหรือมุมมองเป็น Class Inner ที่ไม่ระบุชื่อหรือชั้นในที่ซ้อนกันมีอันตรายที่ร้ายแรงมาก นี้เป็นเพราะตามที่ระบุไว้ก่อนหน้านี้ก็มีจะรู้ว่าใครเป็นภาชนะที่ใช้บรรจุ ป้อนการเปลี่ยนแปลงการวางแนว (หรือการฆ่าระบบ) ตอนนี้กลับไปดูหัวข้อก่อนหน้าเพื่อทำความเข้าใจกับสิ่งที่เกิดขึ้น ใช่ตัวอย่างของคุณค่อนข้างอันตราย

โซลูชั่น: Runnables

  • ลองและขยาย Runnable ถ้ามันไม่ทำลายตรรกะของรหัสของคุณ
  • ทำอย่างดีที่สุดเพื่อให้ Runnables แบบคงที่เพิ่มเติมถ้าพวกเขาจะต้องเรียนซ้อน
  • หากคุณต้องใช้ Runnables แบบไม่ระบุชื่อหลีกเลี่ยงการสร้างสิ่งเหล่านั้นในวัตถุใด ๆที่มีการอ้างอิงถึงกิจกรรมหรือมุมมองที่ใช้งานมานาน
  • Runnables มากมายอาจเป็น AsyncTasks ได้อย่างง่ายดาย พิจารณาใช้ AsyncTask เนื่องจาก VM จัดการโดยค่าเริ่มต้น

ตอบคำถามสุดท้ายทันที เพื่อตอบคำถามที่ไม่ได้รับการแก้ไขโดยตรงจากส่วนอื่น ๆ ของโพสต์นี้ คุณถามว่า "เมื่อใดที่วัตถุภายในชั้นในจะอยู่รอดได้นานกว่าชั้นนอก" ก่อนที่เราจะไปถึงสิ่งนี้ขอให้ฉันเน้นย้ำถึง: คุณมีสิทธิ์ที่จะกังวลเกี่ยวกับสิ่งนี้ในกิจกรรม แต่มันสามารถทำให้เกิดการรั่วไหลได้ทุกที่ ฉันจะให้ตัวอย่างง่ายๆ (โดยไม่ต้องใช้กิจกรรม) เพียงเพื่อแสดงให้เห็น

ด้านล่างเป็นตัวอย่างทั่วไปของโรงงานพื้นฐาน (ไม่มีรหัส)

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

นี่ไม่ใช่ตัวอย่างทั่วไป แต่ง่ายพอที่จะสาธิต ที่สำคัญนี่คือตัวสร้าง ...

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

ตอนนี้เรามีรอยรั่ว แต่ไม่มีโรงงาน แม้ว่าเราจะเปิดตัว Factory มันจะยังคงอยู่ในความทรงจำเพราะทุก Leak มีการอ้างอิงถึงมัน มันไม่สำคัญว่าชั้นนอกจะไม่มีข้อมูล สิ่งนี้เกิดขึ้นบ่อยกว่าที่คิด เราไม่ต้องการผู้สร้างเพียงแค่การสร้างสรรค์ ดังนั้นเราจึงสร้างขึ้นชั่วคราว แต่ใช้การสร้างอย่างไม่มีกำหนด

ลองจินตนาการว่าเกิดอะไรขึ้นเมื่อเราเปลี่ยน Constructor เพียงเล็กน้อย

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

ตอนนี้ทุก LeakFactories ใหม่เหล่านั้นเพิ่งรั่วไหลออกมา คุณคิดอย่างไรกับเรื่องนี้? เหล่านี้เป็นสองตัวอย่างทั่วไปของวิธีการที่ชั้นในสามารถอยู่ได้นานกว่าชั้นนอกของประเภทใด ๆ ถ้าชั้นนอกนั้นเป็นกิจกรรมลองคิดดูว่ามันจะแย่ขนาดไหน

ข้อสรุป

รายการเหล่านี้เป็นอันตรายที่รู้จักกันดีของการใช้วัตถุเหล่านี้อย่างไม่เหมาะสม โดยทั่วไปโพสต์นี้ควรครอบคลุมคำถามส่วนใหญ่ของคุณ แต่ฉันเข้าใจว่าเป็นโพสต์ของ loooong ดังนั้นหากคุณต้องการคำชี้แจงเพียงแค่แจ้งให้เราทราบ ตราบใดที่คุณปฏิบัติตามแนวทางข้างต้นคุณจะมีความกังวลเล็กน้อยเกี่ยวกับการรั่วไหล


3
ขอบคุณมากสำหรับคำตอบที่ชัดเจนและมีรายละเอียดนี้ ฉันไม่เข้าใจสิ่งที่คุณหมายถึงโดย "นักพัฒนาหลายคนใช้ประโยชน์จากการปิดเพื่อกำหนด Runnables ของพวกเขา"
Sébastien

1
การปิดใน Java เป็นคลาสภายในแบบไม่ระบุชื่อเช่น Runnable ที่คุณอธิบาย มันเป็นวิธีที่จะใช้คลาส (เกือบขยาย) โดยไม่ต้องเขียน Class ที่กำหนดไว้ซึ่งขยาย Runnable มันเรียกว่าการปิดเพราะมันเป็น "คำจำกัดความของคลาสที่ปิด" เนื่องจากมันมีพื้นที่หน่วยความจำปิดของตัวเองภายในวัตถุที่มีอยู่จริง
Fuzzical Logic

26
Enlightening บทความ! หนึ่งข้อสังเกตเกี่ยวกับคำศัพท์: ไม่มีสิ่งเช่นคลาสภายในคงที่ใน Java ( เอกสาร ) คลาสที่ซ้อนกันอาจเป็นแบบคงที่หรือภายในแต่ไม่สามารถเป็นทั้งสองอย่างในเวลาเดียวกัน
jenzz

2
ในขณะที่ถูกต้องทางเทคนิค Java ช่วยให้คุณสามารถกำหนดคลาสคงที่ภายในคลาสคงที่ คำศัพท์ไม่ใช่เพื่อประโยชน์ของฉัน แต่เพื่อประโยชน์ของผู้อื่นที่ไม่เข้าใจความหมายทางเทคนิค นี่คือสาเหตุที่มีการกล่าวถึงครั้งแรกว่า "ระดับสูงสุด" เอกสารนักพัฒนาซอฟต์แวร์ Android ใช้คำศัพท์นี้และนี่สำหรับคนที่มองการพัฒนา Android ดังนั้นฉันคิดว่ามันดีกว่าที่จะรักษาความมั่นคง
Fuzzical Logic

13
โพสต์ยอดเยี่ยมหนึ่งในดีที่สุดที่ StackOverflow โดยเฉพาะสำหรับ Android
StackOverflow
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.