Khi chính xác nó bị rò rỉ an toàn để sử dụng (ẩn danh) các lớp bên trong?


324

Tôi đã đọc một số bài viết về rò rỉ bộ nhớ trong Android và xem video thú vị này từ Google I / O về chủ đề này .

Tuy nhiên, tôi không hiểu đầy đủ về khái niệm này và đặc biệt là khi nó an toàn hoặc nguy hiểm đối với các lớp bên trong của người dùng bên trong một Hoạt động .

Đây là những gì tôi hiểu:

Rò rỉ bộ nhớ sẽ xảy ra nếu một thể hiện của một lớp bên trong tồn tại lâu hơn lớp bên ngoài của nó (một Hoạt động). -> Trong tình huống nào điều này có thể xảy ra?

Trong ví dụ này, tôi cho rằng không có nguy cơ rò rỉ, bởi vì không có cách nào mà lớp ẩn danh mở rộng OnClickListenersẽ sống lâu hơn hoạt động, phải không?

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

Bây giờ, ví dụ này có nguy hiểm không, và tại sao?

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

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

Tôi có một nghi ngờ liên quan đến thực tế là việc hiểu chủ đề này có liên quan đến việc hiểu chi tiết những gì được giữ khi một hoạt động bị phá hủy và được tạo lại.

Là nó?

Giả sử tôi vừa thay đổi hướng của thiết bị (đó là nguyên nhân phổ biến nhất của rò rỉ). Khi nào super.onCreate(savedInstanceState)sẽ được gọi trong của tôi onCreate(), điều này sẽ khôi phục các giá trị của các trường (như trước khi thay đổi định hướng)? Điều này cũng sẽ khôi phục trạng thái của các lớp bên trong?

Tôi nhận ra câu hỏi của tôi không chính xác lắm, nhưng tôi thực sự đánh giá cao bất kỳ lời giải thích nào có thể làm cho mọi thứ rõ ràng hơn.


14
Bài đăng blog này bài đăng blog này có một số thông tin tốt về rò rỉ bộ nhớ và các lớp bên trong. :)
Alex Lockwood

Câu trả lời:


651

Những gì bạn đang hỏi là một câu hỏi khá khó khăn. Trong khi bạn có thể nghĩ rằng đó chỉ là một câu hỏi, bạn thực sự đang hỏi một số câu hỏi cùng một lúc. Tôi sẽ làm hết sức mình với kiến ​​thức mà tôi phải trang trải và hy vọng, một số người khác sẽ tham gia để trang trải những gì tôi có thể bỏ lỡ.

Các lớp lồng nhau: Giới thiệu

Vì tôi không chắc bạn cảm thấy thoải mái như thế nào với OOP trong Java, điều này sẽ đạt được một vài điều cơ bản. Một lớp lồng nhau là khi một định nghĩa lớp được chứa trong một lớp khác. Về cơ bản có hai loại: Lớp lồng nhau tĩnh và Lớp bên trong. Sự khác biệt thực sự giữa những điều này là:

  • Các lớp lồng nhau tĩnh:
    • Được coi là "cấp cao nhất".
    • Không yêu cầu một thể hiện của lớp chứa được xây dựng.
    • Có thể không tham chiếu các thành viên lớp chứa mà không có một tài liệu tham khảo rõ ràng.
    • Có cuộc sống riêng của họ.
  • Các lớp lồng nhau bên trong:
    • Luôn yêu cầu một thể hiện của lớp chứa được xây dựng.
    • Tự động có một tham chiếu ngầm đến thể hiện chứa.
    • Có thể truy cập các thành viên lớp của container mà không cần tham khảo.
    • Trọn đời được cho là không dài hơn container.

Bộ sưu tập rác và các lớp học bên trong

Garbage Collection là tự động nhưng cố gắng loại bỏ các đối tượng dựa trên việc nó nghĩ rằng chúng đang được sử dụng. Garbage Collector khá thông minh, nhưng không hoàn mỹ. Nó chỉ có thể xác định nếu một cái gì đó đang được sử dụng bởi liệu có một tham chiếu hoạt động đến đối tượng hay không.

Vấn đề thực sự ở đây là khi một lớp bên trong đã được giữ sống lâu hơn thùng chứa của nó. Điều này là do tham chiếu ngầm đến lớp chứa. Cách duy nhất điều này có thể xảy ra là nếu một đối tượng bên ngoài lớp chứa giữ một tham chiếu đến đối tượng bên trong, mà không liên quan đến đối tượng chứa.

Điều này có thể dẫn đến một tình huống trong đó đối tượng bên trong còn sống (thông qua tham chiếu) nhưng các tham chiếu đến đối tượng chứa đã bị xóa khỏi tất cả các đối tượng khác. Do đó, đối tượng bên trong là giữ cho đối tượng chứa còn sống vì nó sẽ luôn có một tham chiếu đến nó. Vấn đề với điều này là trừ khi nó được lập trình, không có cách nào để quay lại đối tượng chứa để kiểm tra xem nó còn sống hay không.

Khía cạnh quan trọng nhất đối với nhận thức này là nó không có sự khác biệt cho dù đó là trong Hoạt động hay là có thể rút ra được. Bạn sẽ luôn phải có phương pháp khi sử dụng các lớp bên trong và đảm bảo rằng chúng không bao giờ tồn tại lâu hơn các đối tượng của container. May mắn thay, nếu nó không phải là một đối tượng cốt lõi của mã của bạn, thì sự rò rỉ có thể nhỏ so với. Thật không may, đây là một số rò rỉ khó tìm nhất, bởi vì chúng có khả năng không được chú ý cho đến khi nhiều trong số chúng bị rò rỉ.

Giải pháp: Lớp học bên trong

  • Có được tài liệu tham khảo tạm thời từ các đối tượng có chứa.
  • Cho phép đối tượng chứa là đối tượng duy nhất giữ các tham chiếu tồn tại lâu dài cho các đối tượng bên trong.
  • Sử dụng các mẫu được thiết lập như Nhà máy.
  • Nếu lớp bên trong không yêu cầu quyền truy cập vào các thành viên của lớp chứa, hãy xem xét biến nó thành một lớp tĩnh.
  • Sử dụng thận trọng, bất kể nó có trong Hoạt động hay không.

Hoạt động và quan điểm: Giới thiệu

Các hoạt động chứa rất nhiều thông tin để có thể chạy và hiển thị. Các hoạt động được xác định bởi đặc điểm là chúng phải có Chế độ xem. Họ cũng có một số xử lý tự động. Cho dù bạn có chỉ định hay không, Hoạt động có một tham chiếu ngầm đến Chế độ xem mà nó chứa.

Để Chế độ xem được tạo, nó phải biết nơi tạo và xem nó có con nào để nó có thể hiển thị hay không. Điều này có nghĩa là mọi Chế độ xem đều có tham chiếu đến Hoạt động (thông qua getContext()). Hơn nữa, mọi View đều giữ các tham chiếu đến các phần tử con của nó (tức là getChildAt()). Cuối cùng, mỗi Chế độ xem giữ một tham chiếu đến Bitmap được hiển thị đại diện cho màn hình của nó.

Bất cứ khi nào bạn có một tham chiếu đến một Hoạt động (hoặc Bối cảnh hoạt động), điều này có nghĩa là bạn có thể theo chuỗi ENTIRE theo phân cấp bố cục. Đây là lý do tại sao rò rỉ bộ nhớ liên quan đến Hoạt động hoặc Lượt xem là một vấn đề rất lớn. Nó có thể là một tấn bộ nhớ bị rò rỉ cùng một lúc.

Hoạt động, quan điểm và lớp học bên trong

Với các thông tin ở trên về Lớp bên trong, đây là những rò rỉ bộ nhớ phổ biến nhất, nhưng cũng thường được tránh nhất. Mặc dù mong muốn có một lớp bên trong có quyền truy cập trực tiếp vào các thành viên của lớp Activity, nhiều người sẵn sàng chỉ làm cho họ tĩnh để tránh các vấn đề tiềm ẩn. Vấn đề với Hoạt động và Lượt xem đi sâu hơn nhiều.

Hoạt động bị rò rỉ, lượt xem và bối cảnh hoạt động

Tất cả đều thuộc về Bối cảnh và Vòng đời. Có một số sự kiện nhất định (như định hướng) sẽ giết chết Bối cảnh hoạt động. Vì rất nhiều lớp và phương thức yêu cầu Ngữ cảnh, đôi khi các nhà phát triển sẽ cố gắng lưu một số mã bằng cách lấy tham chiếu đến Ngữ cảnh và giữ nó. Thực tế là nhiều đối tượng chúng ta phải tạo để chạy Hoạt động của chúng ta phải tồn tại bên ngoài Vòng đời hoạt động để cho phép Hoạt động thực hiện những gì nó cần làm. Nếu bất kỳ đối tượng nào của bạn tình cờ có tham chiếu đến Hoạt động, Bối cảnh hoặc bất kỳ Chế độ xem nào của nó khi bị hủy, bạn vừa rò rỉ Hoạt động đó và toàn bộ cây Chế độ xem.

Giải pháp: Hoạt động và quan điểm

  • Tránh, bằng mọi giá, tạo tham chiếu tĩnh cho Chế độ xem hoặc Hoạt động.
  • Tất cả các tham chiếu đến Bối cảnh hoạt động nên tồn tại trong thời gian ngắn (thời lượng của chức năng)
  • Nếu bạn cần một bối cảnh tồn tại lâu dài, hãy sử dụng Bối cảnh ứng dụng ( getBaseContext()hoặc getApplicationContext()). Những điều này không giữ tài liệu tham khảo ngầm.
  • Ngoài ra, bạn có thể hạn chế việc hủy Hoạt động bằng cách ghi đè Thay đổi cấu hình. Tuy nhiên, điều này không ngăn các sự kiện tiềm năng khác phá hủy Hoạt động. Trong khi bạn có thể làm điều này, bạn vẫn có thể muốn tham khảo các thực tiễn trên.

Runnables: Giới thiệu

Runnables thực sự không phải là xấu. Ý tôi là, họ thể, nhưng thực sự chúng ta đã tấn công hầu hết các khu vực nguy hiểm. Runnable là một hoạt động không đồng bộ thực hiện một tác vụ độc lập từ luồng mà nó được tạo ra. Hầu hết các runnables được khởi tạo từ luồng UI. Về bản chất, sử dụng Runnable là tạo một luồng khác, chỉ cần quản lý nhiều hơn một chút. Nếu bạn lớp Runnable như một lớp tiêu chuẩn và làm theo các hướng dẫn ở trên, bạn sẽ gặp phải một số vấn đề. Thực tế là nhiều nhà phát triển không làm điều này.

Để dễ dàng, dễ đọc và dòng chương trình logic, nhiều nhà phát triển sử dụng Lớp bên trong ẩn danh để xác định Runnables của họ, chẳng hạn như ví dụ bạn tạo ở trên. Điều này dẫn đến một ví dụ giống như bạn đã gõ ở trên. Một lớp bên trong vô danh về cơ bản là một lớp bên trong riêng biệt. Bạn không cần phải tạo một định nghĩa hoàn toàn mới và chỉ cần ghi đè các phương thức thích hợp. Trong tất cả các khía cạnh khác, nó là Lớp bên trong, có nghĩa là nó giữ một tham chiếu ngầm đến vùng chứa của nó.

Runnables và Hoạt động / Lượt xem

Yay! Phần này có thể ngắn! Do thực tế là Runnables chạy bên ngoài luồng hiện tại, mối nguy hiểm với những điều này xảy ra đối với các hoạt động không đồng bộ chạy dài. Nếu runnable được định nghĩa trong Hoạt động hoặc Xem dưới dạng Lớp bên trong ẩn danh HOẶC Lớp bên trong lồng nhau, có một số nguy hiểm rất nghiêm trọng. Điều này là do, như đã nói ở trên, nó biết ai chứa của nó là. Nhập thay đổi hướng (hoặc giết hệ thống). Bây giờ chỉ cần tham khảo lại các phần trước để hiểu những gì vừa xảy ra. Vâng, ví dụ của bạn khá nguy hiểm.

Giải pháp: Runnables

  • Hãy thử và mở rộng Runnable, nếu nó không phá vỡ logic của mã của bạn.
  • Làm hết sức mình để làm cho Runnables mở rộng tĩnh, nếu chúng phải là các lớp lồng nhau.
  • Nếu bạn phải sử dụng Ẩn danh ẩn danh, hãy tránh tạo chúng trong bất kỳ đối tượng nào có tham chiếu lâu dài đến Hoạt động hoặc Chế độ xem đang sử dụng.
  • Nhiều Runnables có thể dễ dàng trở thành AsyncT task. Cân nhắc sử dụng AsyncTask vì đây là những VM được quản lý theo mặc định.

Trả lời câu hỏi cuối cùng ngay bây giờ để trả lời các câu hỏi không được giải quyết trực tiếp bởi các phần khác của bài đăng này. Bạn hỏi "Khi nào một đối tượng của một lớp bên trong có thể tồn tại lâu hơn lớp bên ngoài của nó?" Trước khi chúng ta hiểu điều này, hãy để tôi nhấn mạnh lại: mặc dù bạn có quyền lo lắng về điều này trong Hoạt động, nó có thể gây rò rỉ ở bất cứ đâu. Tôi sẽ cung cấp một ví dụ đơn giản (không sử dụng Hoạt động) chỉ để chứng minh.

Dưới đây là một ví dụ phổ biến của một nhà máy cơ bản (thiếu mã).

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

Đây không phải là một ví dụ phổ biến, nhưng đủ đơn giản để chứng minh. Chìa khóa ở đây là hàm tạo ...

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

Bây giờ, chúng tôi có Rò rỉ, nhưng không có Nhà máy. Mặc dù chúng tôi đã phát hành Factory, nó sẽ vẫn còn trong bộ nhớ vì mỗi Leak đều có một tham chiếu đến nó. Nó thậm chí không quan trọng rằng lớp bên ngoài không có dữ liệu. Điều này xảy ra thường xuyên hơn nhiều so với người ta có thể nghĩ. Chúng ta không cần người sáng tạo, chỉ cần sự sáng tạo của nó. Vì vậy, chúng tôi tạo một cái tạm thời, nhưng sử dụng sáng tạo vô thời hạn.

Hãy tưởng tượng những gì xảy ra khi chúng ta thay đổi hàm tạo chỉ một chút.

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

Bây giờ, mỗi một LeakFactories mới vừa bị rò rỉ. Bạn nghĩ gì về điều đó? Đó là hai ví dụ rất phổ biến về cách một lớp bên trong có thể tồn tại lâu hơn một lớp bên ngoài thuộc bất kỳ loại nào. Nếu lớp bên ngoài đó là một Hoạt động, hãy tưởng tượng nó sẽ tồi tệ đến mức nào.

Phần kết luận

Chúng liệt kê những nguy hiểm chủ yếu được biết đến của việc sử dụng các đối tượng này một cách không phù hợp. Nói chung, bài đăng này nên bao gồm hầu hết các câu hỏi của bạn, nhưng tôi hiểu đó là một bài đăng loooong, vì vậy nếu bạn cần làm rõ, chỉ cần cho tôi biết. Miễn là bạn làm theo các thực hành trên, bạn sẽ có rất ít lo lắng về rò rỉ.


3
Cảm ơn rất nhiều cho câu trả lời rõ ràng và chi tiết này. Tôi chỉ không hiểu ý của bạn là "nhiều nhà phát triển sử dụng các bao đóng để xác định Runnables của họ"
Sébastien

1
Các bao đóng trong Java là các Lớp bên trong vô danh, giống như Runnable mà bạn mô tả. Đó là một cách để sử dụng một lớp (gần như mở rộng nó) mà không cần viết một Class được xác định mở rộng Runnable. Nó được gọi là bao đóng bởi vì nó là "định nghĩa lớp đóng" ở chỗ nó có không gian bộ nhớ đóng riêng trong đối tượng chứa thực tế.
Logic mờ

26
Khai sáng viết lên! Một nhận xét liên quan đến thuật ngữ: Không có thứ gọi là lớp bên trong tĩnh trong Java. ( Tài liệu ). Một lớp lồng nhau là tĩnh hoặc bên trong , nhưng không thể cả hai cùng một lúc.
jenzz

2
Trong khi đó là chính xác về mặt kỹ thuật, Java cho phép bạn định nghĩa các lớp tĩnh bên trong các lớp tĩnh. Thuật ngữ này không phải vì lợi ích của tôi, mà vì lợi ích của những người khác không hiểu về ngữ nghĩa kỹ thuật. Đây là lý do tại sao lần đầu tiên được đề cập rằng họ là "cấp cao nhất". Các tài liệu dành cho nhà phát triển Android cũng sử dụng thuật ngữ này và đây là dành cho những người đang xem xét phát triển Android, vì vậy tôi nghĩ rằng tốt hơn là giữ sự nhất quán.
Logic Fuzzical

13
Bài đăng tuyệt vời, một trong những bài hay nhất tại StackOverflow, đặc biệt dành cho Android.
StackOverflow
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.