Открытие документа XPS в .Net вызывает утечку памяти - PullRequest
6 голосов
/ 20 октября 2008

Следующий фрагмент кода иллюстрирует утечку памяти при открытии файлов 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

'****** Консольное приложение ENDS.

Причина, по которой этот цикл повторяется тысячу раз, заключается в том, что мой код обрабатывает много файлов и быстро теряет память, вызывая исключение OutOfMemoryException. Принудительная сборка мусора не работает (я подозреваю, что это неуправляемый кусок памяти во внутренних структурах XPS).

Код изначально был в другом потоке и классе, но был упрощен до этого.

Любая помощь с благодарностью.

Ryan

Ответы [ 5 ]

7 голосов
/ 20 октября 2008

Ну, я нашел это. Это ошибка в структуре, и чтобы обойти ее, вы добавляете вызов UpdateLayout. Использование оператора может быть изменено на следующее, чтобы обеспечить исправление;

        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
5 голосов
/ 09 марта 2010

Наткнулся на это сегодня. Интересно, что когда я смотрел на вещи с помощью Reflector.NET, я обнаружил, что исправление включает вызов UpdateLayout () для ContextLayoutManager, связанного с текущим Dispatcher. (читай: не нужно перебирать страницы).

По сути, вызываемый код (используйте отражение здесь):

ContextLayoutManager.From(Dispatcher.CurrentDispatcher).UpdateLayout();

Определенно ощущается как маленький недосмотр MS.

Для ленивых или незнакомых, этот код работает:

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 будет жаловаться, но, возможно, это исправлено в следующей версии фреймворка. Код, размещенный автором, кажется «более безопасным», если вы предпочитаете не использовать рефлексию.

НТН!

0 голосов
/ 18 августа 2011

Интересно. Проблема по-прежнему присутствует в .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 (опущена для ясности), которая в значительной степени идентична ранее опубликованному коду.

Спасибо всем, кто внес свой вклад.

0 голосов
/ 08 декабря 2010

Добавить UpdateLayout не может решить проблему. Согласно http://support.microsoft.com/kb/942443, «требуется предварительная загрузка файла PresentationCore.dll или файла PresentationFramework.dll в основном домене приложения».

0 голосов
/ 20 октября 2008

Я не могу дать вам никакого авторитетного совета, но у меня было несколько мыслей:

  • Если вы хотите наблюдать за своей памятью внутри цикла, вам также необходимо собирать память внутри цикла. В противном случае будет казаться утечкой памяти, поскольку более эффективно собирать большие блоки реже (по мере необходимости), а не постоянно собирать небольшие количества. В этом случае блока области действия, создающего оператор использования , должно быть достаточно , но использование вами GC.Collect указывает на то, что, возможно, происходит что-то еще.
  • Даже GC.Collect - это только предложение (хорошо, очень сильное предложение, но все же предложение): оно не гарантирует, что вся выдающаяся память будет собрана.
  • Если внутренний код XPS действительно вызывает утечку памяти, единственный способ заставить ОС собирать его - заставить операционную систему думать, что приложение закончилось. Чтобы сделать это, вы, возможно, могли бы создать фиктивное приложение, которое обрабатывает ваш код xps и вызывается из основного приложения, или может быть также достаточно перемещения кода xps в собственный домен AppDomain внутри основного кода.
...