Tại sao và Làm thế nào để tránh rò rỉ bộ nhớ Event Handler?


154

Tôi mới nhận ra, bằng cách đọc một số câu hỏi và câu trả lời trên StackOverflow, rằng việc thêm các trình xử lý sự kiện bằng +=C # (hoặc tôi đoán, các ngôn ngữ .net khác) có thể gây rò rỉ bộ nhớ chung ...

Tôi đã sử dụng các trình xử lý sự kiện như thế này trong nhiều lần và không bao giờ nhận ra rằng chúng có thể gây ra hoặc gây ra rò rỉ bộ nhớ trong các ứng dụng của mình.

Làm thế nào để nó hoạt động (có nghĩa là, tại sao điều này thực sự gây rò rỉ bộ nhớ)?
Làm thế nào tôi có thể khắc phục vấn đề này? Là sử dụng -=để xử lý cùng một sự kiện đủ?
Có các mẫu thiết kế chung hoặc thực tiễn tốt nhất để xử lý các tình huống như thế này không?
Ví dụ: Làm thế nào tôi có thể xử lý một ứng dụng có nhiều luồng khác nhau, sử dụng nhiều trình xử lý sự kiện khác nhau để đưa ra một số sự kiện trên UI?

Có cách nào tốt và đơn giản để giám sát điều này một cách hiệu quả trong một ứng dụng lớn đã được xây dựng không?

Câu trả lời:


188

Nguyên nhân rất đơn giản để giải thích: trong khi một trình xử lý sự kiện được đăng ký, nhà xuất bản của sự kiện giữ một tham chiếu đến người đăng ký thông qua đại biểu xử lý sự kiện (giả sử đại biểu là một phương thức ví dụ).

Nếu nhà xuất bản sống lâu hơn thuê bao, thì nó sẽ giữ cho thuê bao tồn tại ngay cả khi không có tài liệu tham khảo nào khác cho thuê bao.

Nếu bạn hủy đăng ký sự kiện với một trình xử lý bằng nhau, thì có, điều đó sẽ loại bỏ trình xử lý và rò rỉ có thể. Tuy nhiên, theo kinh nghiệm của tôi, điều này hiếm khi thực sự là một vấn đề - bởi vì thông thường tôi thấy rằng nhà xuất bản và người đăng ký có tuổi thọ gần bằng nhau.

Đó một nguyên nhân có thể ... nhưng theo kinh nghiệm của tôi thì nó quá cường điệu. Số dặm của bạn có thể thay đổi, tất nhiên ... bạn chỉ cần cẩn thận.


... Tôi đã thấy một số người viết về điều này trên các câu trả lời cho các câu hỏi như "rò rỉ bộ nhớ phổ biến nhất trong .net".
gillyb

32
Một cách để khắc phục điều này từ phía nhà xuất bản là đặt sự kiện thành null một khi bạn chắc chắn rằng bạn sẽ không kích hoạt nó nữa. Điều này sẽ loại bỏ hoàn toàn tất cả các thuê bao và có thể hữu ích khi các sự kiện nhất định chỉ được kích hoạt trong các giai đoạn nhất định trong vòng đời của đối tượng.
JSB

2
Phương pháp nhúng sẽ là thời điểm tốt để đặt sự kiện thành null
Davi Fiamenghi

6
@DaviFiamenghi: Chà, nếu một cái gì đó đang được xử lý, đó ít nhất là một dấu hiệu có khả năng rằng nó sẽ đủ điều kiện để thu gom rác sớm, tại thời điểm đó không có vấn đề gì với những người đăng ký.
Jon Skeet

1
@ BrainSlugs83: "và mẫu sự kiện điển hình bao gồm người gửi dù sao" - vâng, nhưng đó là nhà sản xuất sự kiện . Thông thường, đối tượng người đăng ký sự kiện có liên quan và người gửi thì không. Vì vậy, có, nếu bạn có thể đăng ký bằng phương pháp tĩnh, đây không phải là vấn đề - nhưng đó hiếm khi là một lựa chọn theo kinh nghiệm của tôi.
Jon Skeet

13

Vâng, -=là đủ, Tuy nhiên, có thể khá khó để theo dõi mọi sự kiện được giao, bao giờ hết. (để biết chi tiết, xem bài viết của Jon). Liên quan đến mẫu thiết kế, có một cái nhìn vào mẫu sự kiện yếu .



Nếu tôi biết một nhà xuất bản sẽ sống lâu hơn người đăng ký, tôi sẽ đăng ký IDisposablevà hủy đăng ký khỏi sự kiện.
Shimmy Weitzhandler

9

Tôi đã giải thích sự nhầm lẫn này trong một blog tại https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Tôi sẽ cố gắng tóm tắt nó ở đây để bạn có thể có một ý tưởng rõ ràng.

Phương tiện tham khảo, "Cần":

Trước hết, bạn cần hiểu rằng, nếu đối tượng A giữ một tham chiếu đến đối tượng B, thì, nó sẽ có nghĩa là, đối tượng A cần đối tượng B để hoạt động, phải không? Vì vậy, người thu gom rác sẽ không thu thập đối tượng B miễn là đối tượng A còn sống trong bộ nhớ.

Tôi nghĩ phần này nên rõ ràng đối với một nhà phát triển.

+ = Có nghĩa là, tiêm tham chiếu của đối tượng bên phải vào đối tượng bên trái:

Nhưng, sự nhầm lẫn đến từ toán tử C # + =. Toán tử này không nói rõ cho nhà phát triển rằng, phía bên phải của toán tử này thực sự đang tiêm một tham chiếu đến đối tượng phía bên trái.

nhập mô tả hình ảnh ở đây

Và bằng cách đó, đối tượng A nghĩ rằng, nó cần đối tượng B, mặc dù, theo quan điểm của bạn, đối tượng A không nên quan tâm liệu đối tượng B có sống hay không. Vì đối tượng A nghĩ rằng đối tượng B là cần thiết, đối tượng A bảo vệ đối tượng B khỏi bộ thu gom rác miễn là đối tượng A còn sống. Nhưng, nếu bạn không muốn sự bảo vệ đó được trao cho đối tượng người đăng ký sự kiện, thì, bạn có thể nói, đã xảy ra rò rỉ bộ nhớ.

nhập mô tả hình ảnh ở đây

Bạn có thể tránh rò rỉ như vậy bằng cách tách bộ xử lý sự kiện.

Làm thế nào để đưa ra quyết định?

Nhưng, có rất nhiều sự kiện và trình xử lý sự kiện trong toàn bộ cơ sở mã của bạn. Có nghĩa là, bạn cần phải tiếp tục xử lý sự kiện ở khắp mọi nơi? Câu trả lời là Không. Nếu bạn phải làm như vậy, cơ sở mã của bạn sẽ thực sự xấu xí với verbose.

Bạn có thể theo một biểu đồ dòng đơn giản để xác định xem một trình xử lý sự kiện tách rời có cần thiết hay không.

nhập mô tả hình ảnh ở đây

Hầu hết thời gian, bạn có thể thấy đối tượng người đăng ký sự kiện cũng quan trọng như đối tượng nhà xuất bản sự kiện và cả hai được cho là đang sống cùng một lúc.

Ví dụ về một kịch bản mà bạn không cần phải lo lắng

Ví dụ, một sự kiện nhấn nút của một cửa sổ.

nhập mô tả hình ảnh ở đây

Ở đây, nhà xuất bản sự kiện là Nút và người đăng ký sự kiện là MainWindow. Áp dụng biểu đồ luồng đó, đặt câu hỏi, Cửa sổ chính (người đăng ký sự kiện) có bị chết trước Nút (nhà xuất bản sự kiện) không? Rõ ràng là không phải không? Điều đó thậm chí sẽ không có ý nghĩa. Sau đó, tại sao phải lo lắng về việc tách trình xử lý sự kiện nhấp?

Một ví dụ khi tách biệt xử lý sự kiện là PHẢI.

Tôi sẽ cung cấp một ví dụ trong đó đối tượng thuê bao được cho là đã chết trước đối tượng nhà xuất bản. Giả sử, MainWindow của bạn xuất bản một sự kiện có tên "SomethingHappened" và bạn hiển thị một cửa sổ con từ cửa sổ chính bằng một nút bấm. Cửa sổ con đăng ký vào sự kiện đó của cửa sổ chính.

nhập mô tả hình ảnh ở đây

Và, cửa sổ con đăng ký một sự kiện của Cửa sổ chính.

nhập mô tả hình ảnh ở đây

Từ mã này, chúng ta có thể hiểu rõ rằng có một nút trong Cửa sổ chính. Nhấp vào nút đó sẽ hiển thị Cửa sổ con. Cửa sổ con lắng nghe một sự kiện từ cửa sổ chính. Sau khi làm một cái gì đó, người dùng đóng cửa sổ con.

Bây giờ, theo biểu đồ luồng mà tôi đã cung cấp nếu bạn đặt câu hỏi "Cửa sổ con (người đăng ký sự kiện) có bị chết trước nhà xuất bản sự kiện (cửa sổ chính) không? Câu trả lời phải là CÓ. Tôi thường làm điều đó từ sự kiện Unloaded của Window.

Nguyên tắc chung: Nếu chế độ xem của bạn (ví dụ: WPF, WinForm, UWP, Xamarin Form, v.v.) đăng ký vào một sự kiện của ViewModel, hãy luôn nhớ tách trình xử lý sự kiện. Bởi vì ViewModel thường sống lâu hơn một khung nhìn. Vì vậy, nếu ViewModel không bị hủy, mọi chế độ xem đã đăng ký của ViewModel đó sẽ nằm trong bộ nhớ, điều này không tốt.

Bằng chứng về khái niệm bằng cách sử dụng một hồ sơ bộ nhớ.

Sẽ không có gì thú vị nếu chúng ta không thể xác nhận khái niệm bằng trình lược tả bộ nhớ. Tôi đã sử dụng trình tạo hồ sơ dotMemory JetBrain trong thử nghiệm này.

Đầu tiên, tôi đã chạy MainWindow, hiển thị như thế này:

nhập mô tả hình ảnh ở đây

Sau đó, tôi chụp ảnh kỷ niệm. Sau đó tôi bấm nút 3 lần . Ba cửa sổ con hiện lên. Tôi đã đóng tất cả các cửa sổ con đó và nhấp vào nút Force GC trong trình lược tả dotMemory để đảm bảo rằng Trình thu gom rác được gọi. Sau đó, tôi chụp một bức ảnh chụp bộ nhớ khác và so sánh nó. Hãy chứng kiến! nỗi sợ của chúng tôi là sự thật. Cửa sổ trẻ em không được người thu gom rác thu thập ngay cả sau khi chúng bị đóng. Không chỉ vậy, số lượng đối tượng bị rò rỉ cho đối tượng ChildWindow cũng được hiển thị " 3 " (Tôi đã nhấp vào nút 3 lần để hiển thị 3 cửa sổ con).

nhập mô tả hình ảnh ở đây

Ok, sau đó, tôi tách trình xử lý sự kiện như dưới đây.

nhập mô tả hình ảnh ở đây

Sau đó, tôi đã thực hiện các bước tương tự và kiểm tra trình lược tả bộ nhớ. Lần này, wow! không bị rò rỉ bộ nhớ.

nhập mô tả hình ảnh ở đây


3

Một sự kiện thực sự là một danh sách liên kết của các trình xử lý sự kiện

Khi bạn thực hiện + = new EventHandler về sự kiện, điều đó không thực sự quan trọng nếu chức năng cụ thể này đã được thêm làm người nghe trước đó, nó sẽ được thêm một lần mỗi lần + +.

Khi sự kiện được đưa ra, nó sẽ đi qua danh sách được liên kết, theo từng mục và gọi tất cả các phương thức (trình xử lý sự kiện) được thêm vào danh sách này, đây là lý do tại sao các trình xử lý sự kiện vẫn được gọi ngay cả khi các trang không còn chạy miễn là chúng không còn chạy nữa. còn sống (đã bắt nguồn) và chúng sẽ sống miễn là chúng được nối lên. Vì vậy, họ sẽ được gọi cho đến khi sự kiện không được xử lý với một - = EventHandler mới.

Xem tại đây

MSDN TẠI ĐÂY


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.