Rò rỉ GDI (hay đơn giản là việc sử dụng quá nhiều đối tượng GDI) là một trong những vấn đề phổ biến nhất. Cuối cùng, nó gây ra các vấn đề về kết xuất, lỗi và / hoặc các vấn đề về hiệu suất. Bài viết mô tả cách chúng tôi gỡ lỗi sự cố này.
Vào năm 2016, khi hầu hết các chương trình được thực thi trong hộp cát mà ngay cả nhà phát triển kém năng lực nhất cũng không thể gây hại cho hệ thống, tôi rất ngạc nhiên khi phải đối mặt với vấn đề mà tôi sẽ nói trong bài viết này. Thành thật mà nói, tôi hy vọng rằng vấn đề này đã biến mất vĩnh viễn cùng với Win32Api. Tuy nhiên, tôi đã phải đối mặt với nó. Trước đó, tôi vừa nghe những câu chuyện kinh dị về nó từ các nhà phát triển cũ nhiều kinh nghiệm hơn.
Vấn đề
Rò rỉ hoặc sử dụng số lượng lớn các đối tượng GDI.
Triệu chứng
- Cột đối tượng GDI trên tab Chi tiết của Trình quản lý Tác vụ hiển thị 10000 quan trọng (nếu cột này không có, bạn có thể thêm nó bằng cách nhấp chuột phải vào tiêu đề bảng và chọn Chọn Cột).
- Khi phát triển bằng C # hoặc bằng các ngôn ngữ khác được thực thi bởi CLR, lỗi kém thông tin sau sẽ xảy ra:
Thông báo:Đã xảy ra lỗi chung trong GDI +.
Nguồn:System.Drawing
TargetSite:IntPtr GetHbitmap (System.Drawing.Color)
Loại:System.Runtime.InteropServices.ExternalException
Lỗi có thể không xảy ra với một số cài đặt nhất định hoặc trong một số phiên bản hệ thống nhất định, nhưng ứng dụng của bạn sẽ không thể hiển thị một đối tượng: - Trong quá trình phát triển trong С / С ++, tất cả các phương thức GDI, như Create% SOME_GDI_OBJECT%, bắt đầu trả về NULL.
Tại sao?
Hệ thống Windows không cho phép tạo nhiều hơn 65535 Đối tượng GDI. Con số này, trên thực tế, rất ấn tượng và tôi khó có thể tưởng tượng được một kịch bản bình thường lại đòi hỏi một lượng đồ vật khổng lồ như vậy. Có một giới hạn đối với các quy trình - 10000 mỗi quy trình có thể được sửa đổi (bằng cách thay đổi HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ Windows \ GDIProcessHandleQuota trong phạm vi từ 256 đến 65535), nhưng Microsoft không khuyên bạn nên tăng giới hạn này. Nếu bạn vẫn làm điều đó, một quá trình sẽ có thể đóng băng hệ thống để nó không thể hiển thị ngay cả thông báo lỗi. Trong trường hợp này, hệ thống chỉ có thể được phục hồi sau khi khởi động lại.
Cách khắc phục?
Nếu bạn đang sống trong một thế giới CLR thoải mái và được quản lý, thì khả năng cao là bạn bị rò rỉ bộ nhớ thông thường trong ứng dụng của mình. Vấn đề là khó chịu, nhưng nó là một trường hợp khá bình thường. Có ít nhất một tá công cụ tuyệt vời để phát hiện điều này. Bạn sẽ cần sử dụng bất kỳ bộ hồ sơ nào để xem liệu số lượng đối tượng bao bọc tài nguyên GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) có tăng lên hay không. Nếu đúng như vậy, bạn có thể ngừng đọc bài viết này. Nếu không phát hiện thấy sự rò rỉ của các đối tượng trình bao bọc, mã của bạn sử dụng trực tiếp API GDI và có một trường hợp xảy ra khi chúng không bị xóa
Những người khác khuyên điều gì?
Hướng dẫn chính thức của Microsoft hoặc các bài viết khác về chủ đề này sẽ giới thiệu cho bạn những điều như sau:
Tìm tất cả Tạo % SOME_GDI_OBJECT% và phát hiện xem DeleteObject tương ứng (hoặc ReleaseDC cho các đối tượng HDC) tồn tại. Nếu DeleteObject như vậy tồn tại, có thể có một kịch bản không gọi nó.
Có một phiên bản được cải tiến một chút của phương pháp này có một bước bổ sung:
Tải xuống tiện ích GDIView. Nó có thể hiển thị số lượng chính xác các đối tượng GDI theo loại. Lưu ý rằng tổng số đối tượng không tương ứng với giá trị trong cột cuối cùng. Nhưng chúng ta có thể nhắm vào điều này nếu nó giúp thu hẹp phạm vi tìm kiếm.
Dự án mà tôi đang thực hiện có cơ sở mã gồm 9 triệu bản ghi, lượng bản ghi xấp xỉ bằng nhau nằm trong thư viện của bên thứ ba, hàng trăm lệnh gọi hàm GDI được trải dài trên hàng chục tệp. Tôi đã lãng phí rất nhiều thời gian và năng lượng trước khi hiểu rằng phân tích thủ công mà không có lỗi là không thể.
Tôi có thể cung cấp những gì?
Nếu phương pháp này có vẻ quá dài và gây mệt mỏi cho bạn, nghĩa là bạn vẫn chưa vượt qua tất cả các giai đoạn tuyệt vọng với phương pháp trước đó. Bạn có thể thử làm theo các bước trước đó, nhưng nếu nó không hiệu quả, đừng quên giải pháp này.
Để theo đuổi sự cố rò rỉ, tôi đã tự đặt câu hỏi: Các vật thể rò rỉ được tạo ra ở đâu? Không thể thiết lập các điểm ngắt ở tất cả các nơi mà hàm API được gọi. Ngoài ra, tôi không chắc rằng điều đó không xảy ra trong .NET Framework hoặc trong một trong các thư viện của bên thứ ba mà chúng tôi sử dụng. Vài phút tìm kiếm trên Google đã đưa tôi đến tiện ích API Monitor cho phép ghi lại và theo dõi các cuộc gọi đến tất cả các chức năng của hệ thống. Tôi đã dễ dàng tìm thấy danh sách tất cả các hàm tạo đối tượng GDI, định vị và chọn chúng trong API Monitor. Sau đó, tôi đặt các điểm ngắt.
Sau đó, tôi chạy quy trình gỡ lỗi trong Visual Studio và chọn nó trong cây Processes. Điểm dừng thứ năm đã hoạt động ngay lập tức:
Tôi nhận ra rằng tôi sẽ chết chìm trong torrent này và tôi cần một thứ khác. Tôi đã xóa các điểm ngắt khỏi các chức năng và quyết định xem nhật ký. Nó cho thấy hàng ngàn cuộc gọi. Rõ ràng là tôi sẽ không thể phân tích chúng theo cách thủ công.
Nhiệm vụ là Tìm lệnh gọi của các hàm GDI không gây ra việc xóa . Nhật ký nêu rõ mọi thứ tôi cần:danh sách các lệnh gọi hàm theo thứ tự thời gian, các giá trị trả về của chúng và các tham số. Do đó, tôi cần lấy giá trị trả về của hàm Create% SOME_GDI_OBJECT% và tìm lệnh gọi DeleteObject với giá trị này làm đối số. Tôi đã chọn tất cả các bản ghi trong API Monitor, chèn chúng vào một tệp văn bản và nhận được một cái gì đó giống như CSV với dấu phân cách TAB. Tôi chạy VS, nơi tôi định viết một chương trình nhỏ để phân tích cú pháp, nhưng trước khi nó có thể tải, tôi nảy ra ý tưởng hay hơn:xuất dữ liệu vào cơ sở dữ liệu và viết một truy vấn để tìm thứ tôi cần. Đó là sự lựa chọn đúng đắn vì nó cho phép tôi nhanh chóng đặt câu hỏi và nhận được câu trả lời.
Có nhiều công cụ để nhập dữ liệu từ CSV vào cơ sở dữ liệu, vì vậy tôi sẽ không chú trọng đến chủ đề này (mysql, mssql, sqlite).
Tôi có bảng sau:
CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
Tôi đã viết hàm MySQL sau để lấy bộ mô tả của đối tượng đã xóa khỏi lệnh gọi API:
CREATE FUNCTION getHandle(api varchar(1000)) RETURNS varchar(100) CHARSET utf8 BEGIN DECLARE start int(11); DECLARE result varchar(100); SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )' IF start = 0 THEN SET start := INSTR(api, '('); END IF; SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1); RETURN TRIM(result); END
Và cuối cùng, tôi đã viết một truy vấn để định vị tất cả các đối tượng hiện tại:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates LEFT JOIN (SELECT d.id, d.API, getHandle(d.API) handle FROM apicalls d WHERE API LIKE 'DeleteObject%' OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels ON dels.handle = creates.handle WHERE creates.API LIKE 'Create%';
(Về cơ bản, nó sẽ đơn giản tìm thấy tất cả các cuộc gọi Xóa cho tất cả các cuộc gọi Tạo).
Như bạn thấy từ hình trên, tất cả các lệnh gọi không có một lệnh Xóa nào đều được tìm thấy cùng một lúc.
Vì vậy, câu hỏi cuối cùng đã được để lại:Làm thế nào để xác định, những phương thức này được gọi từ đâu trong ngữ cảnh mã của tôi? Và đây một thủ thuật thú vị đã giúp tôi:
- Chạy ứng dụng trong VS để gỡ lỗi
- Tìm nó trong Api Monitor và chọn nó.
- Chọn một chức năng bắt buộc trong API và đặt một điểm ngắt.
- Tiếp tục nhấp vào 'Tiếp theo' cho đến khi nó được gọi với các thông số được đề cập (tôi thực sự đã bỏ lỡ các điểm ngắt có điều kiện từ VS)
- Khi bạn thực hiện cuộc gọi được yêu cầu, hãy chuyển sang CS và nhấp vào Break All .
- VS Debugger sẽ bị dừng ngay tại nơi tạo đối tượng rò rỉ và tất cả những gì bạn cần làm là tìm hiểu lý do tại sao nó không bị xóa.
Lưu ý:Mã được viết cho mục đích minh họa.
Tóm tắt:
Thuật toán được mô tả phức tạp và yêu cầu nhiều công cụ, nhưng nó cho kết quả nhanh hơn nhiều so với tìm kiếm ngu ngốc thông qua cơ sở mã khổng lồ.
Đây là bản tóm tắt của tất cả các bước:
- Tìm kiếm rò rỉ bộ nhớ của các đối tượng trình bao bọc GDI.
- Nếu chúng tồn tại, hãy loại bỏ chúng và lặp lại bước 1.
- Nếu không có rò rỉ, hãy tìm kiếm các lệnh gọi đến các hàm API một cách rõ ràng.
- Nếu số lượng của chúng không lớn, hãy tìm kiếm một tập lệnh mà một đối tượng không bị xóa.
- Nếu số lượng của chúng lớn hoặc chúng khó có thể được theo dõi, hãy tải xuống API Monitor và thiết lập nó để ghi nhật ký các lệnh gọi của các chức năng GDI.
- Chạy ứng dụng gỡ lỗi trong VS.
- Sao chép rò rỉ (nó sẽ khởi chạy chương trình để ẩn các đối tượng được rút tiền).
- Kết nối với Trình theo dõi API.
- Tái tạo rò rỉ.
- Sao chép nhật ký vào một tệp văn bản, nhập nó vào bất kỳ cơ sở dữ liệu nào trong tay (các tập lệnh có trong bài viết này là dành cho MySQL, nhưng chúng có thể dễ dàng được sử dụng cho bất kỳ hệ thống quản lý cơ sở dữ liệu quan hệ nào).
- So sánh các phương thức Tạo và Xóa (bạn có thể tìm thấy tập lệnh SQL trong bài viết này ở trên) và tìm các phương pháp không có lệnh gọi Xóa.
- Đặt một điểm ngắt trong Trình theo dõi API khi gọi phương thức bắt buộc.
- Tiếp tục nhấp vào Tiếp tục cho đến khi phương thức được gọi với các tham số được yêu cầu lại.
- Khi phương thức được gọi với các tham số bắt buộc, hãy nhấp vào Break All trong VS.
- Tìm hiểu lý do tại sao đối tượng này không bị xóa.
Tôi hy vọng rằng bài viết này sẽ hữu ích và giúp bạn tiết kiệm thời gian của bạn.