Открытие документа XPS в .Net приводит к утечке памяти
-
03-07-2019 - |
Вопрос
Следующий фрагмент кода иллюстрирует утечку памяти при открытии файлов XPS.Если вы запустите его и понаблюдаете за диспетчером задач, он будет расти и не освободит память до тех пор, пока приложение не завершит работу.
'****** Запускается консольное приложение.
Module Main
Const DefaultTestFilePath As String = "D:\Test.xps"
Const DefaultLoopRuns As Integer = 1000
Public Sub Main(ByVal Args As String())
Dim PathToTestXps As String = DefaultTestFilePath
Dim NumberOfLoops As Integer = DefaultLoopRuns
If (Args.Count >= 1) Then PathToTestXps = Args(0)
If (Args.Count >= 2) Then NumberOfLoops = CInt(Args(1))
Console.Clear()
Console.WriteLine("Start - {0}", GC.GetTotalMemory(True))
For LoopCount As Integer = 1 To NumberOfLoops
Console.CursorLeft = 0
Console.Write("Loop {0:d5}", LoopCount)
' The more complex the XPS document and the more loops, the more memory is lost.
Using XPSItem As New Windows.Xps.Packaging.XpsDocument(PathToTestXps, System.IO.FileAccess.Read)
Dim FixedDocSequence As Windows.Documents.FixedDocumentSequence
' This line leaks a chunk of memory each time, when commented out it does not.
FixedDocSequence = XPSItem.GetFixedDocumentSequence
End Using
Next
Console.WriteLine()
GC.Collect() ' This line has no effect, I think the memory that has leaked is unmanaged (C++ XPS internals).
Console.WriteLine("Complete - {0}", GC.GetTotalMemory(True))
Console.WriteLine("Loop complete but memory not released, will release when app exits (press a key to exit).")
Console.ReadKey()
End Sub
End Module
'****** Консольное приложение ЗАВЕРШАЕТСЯ.
Причина, по которой это повторяется тысячу раз, заключается в том, что мой код обрабатывает множество файлов и быстро приводит к утечке памяти, вызывая исключение OutOfMemoryException .Принудительная сборка мусора не работает (я подозреваю, что это неуправляемый фрагмент памяти во внутренних компонентах XPS).
Первоначально код был в другом потоке и классе, но был упрощен до этого.
Любая помощь очень ценится.
Райан
Решение
Что ж, я нашел это.Это ошибка в фреймворке, и чтобы обойти ее, вы добавляете вызов UpdateLayout.Оператор Using может быть изменен на следующий, чтобы обеспечить исправление;
Using XPSItem As New Windows.Xps.Packaging.XpsDocument(PathToTestXps, System.IO.FileAccess.Read)
Dim FixedDocSequence As Windows.Documents.FixedDocumentSequence
Dim DocPager As Windows.Documents.DocumentPaginator
FixedDocSequence = XPSItem.GetFixedDocumentSequence
DocPager = FixedDocSequence.DocumentPaginator
DocPager.ComputePageCount()
' This is the fix, each page must be laid out otherwise resources are never released.'
For PageIndex As Integer = 0 To DocPager.PageCount - 1
DirectCast(DocPager.GetPage(PageIndex).Visual, Windows.Documents.FixedPage).UpdateLayout()
Next
FixedDocSequence = Nothing
End Using
Другие советы
Столкнулся с этим сегодня.Интересно, что когда я изучил ситуацию с помощью Reflector.NET, я обнаружил, что исправление связано с вызовом UpdateLayout() в ContextLayoutManager, связанном с текущим диспетчером.(читать:нет необходимости перебирать страницы).
По сути, вызываемый код (используйте отражение здесь) является:
ContextLayoutManager.From(Dispatcher.CurrentDispatcher).UpdateLayout();
Определенно ощущается как небольшая оплошность со стороны мисс
Для ленивых или незнакомых этот код работает:
Assembly presentationCoreAssembly = Assembly.GetAssembly(typeof (System.Windows.UIElement));
Type contextLayoutManagerType = presentationCoreAssembly.GetType("System.Windows.ContextLayoutManager");
object contextLayoutManager = contextLayoutManagerType.InvokeMember("From",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, new[] {dispatcher});
contextLayoutManagerType.InvokeMember("UpdateLayout", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, contextLayoutManager, null);
FxCop будет жаловаться, но, возможно, это исправлено в следующей версии фреймворка.Код, опубликованный автором, кажется "более безопасным", если вы предпочли бы не использовать отражение.
ХТХ!
Я не могу дать вам никакого авторитетного совета, но у меня действительно есть несколько мыслей:
- Если вы хотите следить за своей памятью внутри цикла, вам также необходимо собирать память внутри цикла.В противном случае вы будете появиться для утечки памяти по замыслу, поскольку более эффективно собирать большие блоки реже (по мере необходимости), а не постоянно собирать небольшие объемы.В этом случае блок scope создает инструкцию using следует этого было бы достаточно, но вы используете GC.Сбор указывает на то, что, возможно, происходит что-то еще.
- Даже GC.Collect - это всего лишь предложение (хорошо, очень сильный предложение, но все же предложение):это не гарантирует, что будет собрана вся оставшаяся память.
- Если во внутреннем коде XPS действительно происходит утечка памяти, единственный способ заставить ОС собрать ее - это заставить ОС думать, что приложение завершило работу.Для этого вы, возможно, могли бы создать фиктивное приложение, которое обрабатывает ваш xps-код и вызывается из основного приложения, или, возможно, достаточно переместить xps-код в его собственный AppDomain внутри вашего основного кода.
Добавление описания обновления не может решить проблему.Согласно http://support.microsoft.com/kb/942443, "предварительно загрузите файл PresentationCore.dll или PresentationFramework.требуется файл dll в домене основного приложения".
Интересно.Проблема все еще присутствует в .net framework 4.0.Мой код сильно просачивался.
Предлагаемое исправление, при котором UpdateLayout вызывается в цикле сразу после создания последовательности FixedDocumentSequence, НЕ устранило проблему для меня в 400-страничном тестовом документе.
Однако следующее решение ДЕЙСТВИТЕЛЬНО устранило проблему для меня.Как и в предыдущих исправлениях, я переместил вызов GetFixedDocumentSequence() за пределы цикла для каждой страницы.Предложение "using"...честно предупреждаю, что я все еще не уверен, что это правильно.Но это не причиняет боли.Впоследствии документ повторно используется для создания предварительного просмотра страниц на экране.Так что, кажется, это не причиняет боли.
DocumentPaginator paginator
= document.GetFixedDocumentSequence().DocumentPaginator;
int numberOfPages = paginator.ComputePageCount();
for (int i = 0; i < NumberOfPages; ++i)
{
DocumentPage docPage = paginator.GetPage(nPage);
using (docPage) // using is *probably* correct.
{
// VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
((FixedPage)(docPage.Visual)).UpdateLayout();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Adding THAT line cured my leak.
RenderTargetBitmap bitmap = GetXpsPageAsBitmap(docPage, dpi);
.... etc...
}
}
На самом деле строка исправления находится внутри моей процедуры GetXpsPageAsBitmap (опущена для наглядности), которая в значительной степени идентична ранее опубликованному коду.
Спасибо всем, кто внес свой вклад.