Domanda

Il seguente frammento di codice illustra una perdita di memoria durante l'apertura di file XPS. Se lo esegui e guardi il task manager, crescerà e non rilascerà memoria fino alla chiusura dell'app.

'****** INIZIA l'applicazione console.

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

'****** CHIUDE l'applicazione console.

Il motivo per cui viene ripetuto mille volte è perché il mio codice elabora molti file e perde rapidamente memoria forzando una OutOfMemoryException. Forcing Garbage Collection non funziona (sospetto che sia una porzione di memoria non gestita negli interni XPS).

Il codice era originariamente in un altro thread e classe ma è stato semplificato per questo.

Qualsiasi aiuto è stato molto apprezzato.

Ryan

È stato utile?

Soluzione

Beh, l'ho trovato. È un bug nel framework e per aggirare il problema si aggiunge una chiamata a UpdateLayout. L'istruzione using può essere modificata come segue per fornire una correzione;

        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

Altri suggerimenti

Ho affrontato questo argomento oggi. È interessante notare che quando ho osservato le cose usando Reflector.NET, ho trovato la correzione che comportava la chiamata di UpdateLayout () sul ContextLayoutManager associato al Dispatcher corrente. (leggi: non è necessario scorrere le pagine).

Fondamentalmente, il codice da chiamare (usa la riflessione qui) è:

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

Sicuramente sembra una piccola svista da parte della SM.

Per i più pigri o non familiari, questo codice funziona:

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 si lamenterà, ma forse è stato risolto nella prossima versione del framework. Il codice pubblicato dall'autore sembra essere "più sicuro" se preferisci non usare la riflessione.

HTH!

Non posso darti alcun consiglio autorevole, ma ho avuto alcuni pensieri:

  • Se vuoi guardare la tua memoria all'interno del loop, devi anche raccogliere la memoria all'interno del loop. Altrimenti apparirà per perdere memoria in base alla progettazione, poiché è più efficiente raccogliere blocchi più grandi meno frequentemente (se necessario) anziché raccogliere costantemente piccole quantità. In questo caso il blocco dell'ambito che crea l'istruzione using dovrebbe dovrebbe essere sufficiente, ma l'uso di GC.Collect indica che forse sta succedendo qualcos'altro.
  • Anche GC.Collect è solo un suggerimento (ok, molto forte , ma comunque un suggerimento): non garantisce che tutta la memoria eccezionale sia raccolta.
  • Se il codice XPS interno perde davvero memoria, l'unico modo per forzare il sistema operativo a raccoglierlo è indurre il sistema operativo a pensare che l'applicazione sia terminata. Per fare ciò potresti forse creare un'applicazione fittizia che gestisce il tuo codice xps e viene chiamata dall'app principale, oppure spostare il codice xps nel suo AppDomain all'interno del tuo codice principale potrebbe essere sufficiente.

Aggiungi UpdateLayout non può risolvere il problema. Secondo http://support.microsoft.com/kb/942443 , "precaricare il PresentationCore .dll o il file PresentationFramework.dll nel dominio dell'applicazione principale " è necessario.

Interessante. Il problema è ancora presente in .net framework 4.0. Il mio codice perdeva ferocemente.

La correzione proposta - in cui UpdateLayout viene chiamato in un ciclo immediatamente dopo la creazione di FixedDocumentSequence NON ha risolto il problema per me su un documento di prova di 400 pagine.

Tuttavia, la seguente soluzione ha risolto il problema per me. Come nelle correzioni precedenti, ho spostato la chiamata a GetFixedDocumentSequence () all'esterno del ciclo per ogni pagina. Il "usando" clausola ... giusto avvertimento che non sono ancora sicuro che sia corretto. Ma non fa male. Il documento viene successivamente riutilizzato per generare sullo schermo anteprime di pagine. Quindi non sembra ferire.

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

}

In realtà, la linea di correzione rientra nella mia routine GetXpsPageAsBitmap (ommited per chiarezza), che è praticamente identica al codice precedentemente pubblicato.

Grazie a tutti coloro che hanno contribuito.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top