Освобождение неиспользуемых ресурсов в приложении

dymitri

Помогите решить проблему!!! В создаваемом приложении появлялась ошибка переполнения памяти, после поиска ответа на форумах нашёл в настройках компиляции Visual Studio 2013 необходимые настройки (выбор Any CPU). Ошибка исчезла, но появилась другая проблема: по мере работы приложения оперативная память загружается до конца, немного выгружается, опять загружается и выгружается и так непрерывно, пока работает приложение. Файл подкачки увеличивается до максимально возможного предела так, что на системном диске не остаётся свободного места, система зависает и долгое время её даже невозможно перезапустить (Windows 8.1 x64). Поясню происходящее в приложении. Путём отключения процедур программы нашёл причинную - как и предполагал - это работа с PictureBox. В программу загружаются поочерёдно все jpg файлы из директории для поиска повреждённых. Казалось бы ничего страшного, в один и тот же PictureBox загружается новый файл, соответственно старый должен удаляться, но такого не происходит. Пробовал различные способы выгрузки (закомментировано), ничего не помогает. Подскажите знающие люди, как выгружать ресурсы PictureBox принудительно перед загрузкой нового изображения правильно? Привожу актуальный код подпрограммы поиска повреждённых файлов:
Private Sub СписокФайлов(ByVal folderPath As String)
        ListBox1.Items.Clear()
        Dim ИменаФайлов = My.Computer.FileSystem.GetFiles(folderPath, FileIO.SearchOption.SearchAllSubDirectories, "*.jpeg", "*.jpg")
        ProgressBar1.Minimum = 0
        ProgressBar1.Maximum = ИменаФайлов.Count
        ProgressBar1.Step = 1
        ProgressBar1.Value = 0
 
        For Each ИмяФайла As String In ИменаФайлов
            ProgressBar1.Increment(1)
            'PictureBox1.Dispose()
            'PictureBox1.Refresh()
            'PictureBox1.Update()
            Try
                PictureBox1.Image = System.Drawing.Image.FromFile(ИмяФайла)
            Catch ex As Exception
                ListBox1.Items.Add(ИмяФайла)
            End Try
            'Me.Finalize()
            'Me.Dispose()
            'PictureBox1.Dispose()
            Label1.Refresh()
            Label1.Text = ИмяФайла
        Next
 
        ProgressBar1.Visible = False
        Label1.Text = "Поиск в директории завершён!"
    End Sub
14 ответов

dymitri

dymitri, это известный прикол с методом Image.FromFile(). Он продолжает держать файл открытым. Обходится с помощью чтения файла в память.
For Each ИмяФайла As String In ИменаФайлов
    ProgressBar1.Increment(1)
    Try
        Using mstream As New System.IO.MemoryStream(File.ReadAllBytes(ИмяФайла))
            PictureBox1.Image = System.Drawing.Image.FromStream(mstream)
        End Using
    Catch ex As Exception
        ListBox1.Items.Add(ИмяФайла)
    End Try
    Label1.Refresh()
    Label1.Text = ИмяФайла
Next
Еще можно уменьшить потребление памяти переписав инициализацию переменной ИменаФайлов следующим образом (нужен Imports System.IO)
Dim ИменаФайлов = Directory.EnumerateFiles(folderPath, "*.jp*g", SearchOption.AllDirectories)
Метод EnumerateFiles возвращает файлы по одному вместо возврата коллекции целиком. Кроме того благодаря маске "*.jp*g" мы обходим каждый каталог только один раз. Для надежности можно добавить Where
Dim ИменаФайлов = Directory.EnumerateFiles(folderPath, "*.jp*g", SearchOption.AllDirectories). _
    Where(Function(n) ".jpg".Equals(Path.GetExtension(n), StringComparison.OrdinalIgnoreCase) OrElse ".jpeg".Equals(Path.GetExtension(n), StringComparison.OrdinalIgnoreCase))


dymitri

Спасибо огромное за дельные советы! Всё исправил по вашему примеру, но... Проблема остаётся практически та же. Приложение работает теперь чуть дольше, но переполнение памяти, увеличение файла подкачки до возможных пределов остаются. Система зависает, а в варианте на Windows 7 x64 после закрытия приложения вообще ушла в синий экран. Уже пытался компилировать в Visual Studio 2010 - безрезультатно. Если раньше программа зависала (вернее система) после 2000 проверенных изображений, то теперь её хватает аж до 3000, но этого мало, учитывая, что изображений десятки тысяч. В Семёрке хватило ещё дольше, но это лишь потому, что на системном диске больше свободного пространства было под файл подкачки. Понимаю, что возможно есть другие алгоритмы для проверки целостности изображений jpg, но просто не даёт покоя причина невозможности выгрузки неиспользующихся ресурсов. Что никак не умаляет ваших трудов, OwenGlendower, ещё раз спасибо за потраченное время. Но может есть дополнительные советы в данной ситуации?


dymitri

dymitri, я забыл про Dispose для картинки. Он здесь все равно нужен
For Each ИмяФайла As String In ИменаФайлов
    ProgressBar1.Increment(1)
    Try
        Using mstream As New System.IO.MemoryStream(File.ReadAllBytes(ИмяФайла))
            PictureBox1.Image = System.Drawing.Image.FromStream(mstream)
            PictureBox1.Image.Dispose()
            PictureBox1.Image = Nothing
        End Using
    Catch ex As Exception
        ListBox1.Items.Add(ИмяФайла)
    End Try
    Label1.Refresh()
    Label1.Text = ИмяФайла
Next


dymitri

OwenGlendower, всё, теперь работает, без каких-либо багов. Осталось только мелочь - настройка интерфейса. Ещё раз огромное человеческое спасибо , про Dispose помню, а про Using...End Using вылетело из головы (т.к. кроме теории, на практике не помню что бы приходилось использовать). Удачи вам и дай Бог здоровья!!!


dymitri

Обязательно ли грузить картинку в PictureBox? М.б. достаточно такого кода
    For Each fName As String In fNames
        findBad(fName)
    Next
    MsgBox("theEnd")
'…
Public Sub findBad(ByVal fNm As String)
    Try
        Dim bmp As Bitmap = New Bitmap(fNm)
        Using bmp
            If Not bmp.RawFormat.Equals(ImageFormat.Jpeg) Then
                ListBox1.Items.Add(fNm)
            End If
        End Using
    Catch ex As Exception
        ListBox1.Items.Add(fNm)
    End Try
End Sub


dymitri

ovva, спасибо за иной вариант проверки. Но этот вариант работает почему-то в несколько раз медленнее. Хотя и находит больше "ошибочных", которые визуально совсем не выглядят испорченными файлами. Думаю идеальный алгоритм - это использование обоих вариантов один после другого. Надо будет подумать и воплотить... Ещё раз большое спасибо всем небезразличным!


dymitri

Скорость обработки сравняется если заменить
Dim bmp As Bitmap = New Bitmap(fNm)
на
Dim bmp As Bitmap = System.Drawing.Image.FromStream(New System.IO.MemoryStream(File.ReadAllBytes(fNm)))


dymitri

ovva, протестировал сам, просидел с секундомером в двух вариантах кода в одной и той же директории засёк проверку 500 файлов, к сожалению везде выдало 1 мин. 31 сек. независимо от того используется "New Bitmap(fNm)" или "System.Drawing.Image.FromStream(New System.IO.MemoryStream(File.ReadAllBytes(fNm)))". Результат полностью идентичен даже секунду в секунду!Кстати... В варианте от OwenGlendower ушло ровно 24 сек. на те же 500 файлов. Но для объективности напомню, что при вашем, ovva, варианте находит больше ошибок. Этимологию этих ошибок файлов ещё буду изучать в различных редакторах, позже отпишусь, действительно ли эти файлы ошибочны (т.е. ваш вариант действительно прав), или же файлы целы, и возникли левые причины (вариант видит неактуальные причины, хотя в одних и тех же файлах, - несколько раз проверял). Ещё раз спасибо за советы!


dymitri

Заинтересовал вопрос, почему равнозначные в общем то процедуры имеют различие в скорости выполнения. Несколько подкорректировал процедуру и провел тест на сравнение скорости выполнения. Т.к. "плохих" файлов нет, то и запись в ListBox не влияет на результаты. Всего файлов 2621. Время в миллисекундах. V1 / V2 18078 / 15469 15511 / 15477 15515 / 15564 15478 / 15555
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Dim fNames = My.Computer.FileSystem.GetFiles("C:\Users\Pictures\ImagesPrj", FileIO.SearchOption.SearchAllSubDirectories, "*.jpeg", "*.jpg")
    Dim sW As New Stopwatch
    sW.Start()
    For Each fName As String In fNames
        findBad(fName)
    Next
    sW.Stop()
    Dim v1 As String = "v1=" & sW.ElapsedMilliseconds.ToString
    sW = New Stopwatch
    sW.Start()
    For Each fName As String In fNames
        findBad2(fName)
    Next
    sW.Stop()
    Dim v2 As String = "v2=" & sW.ElapsedMilliseconds.ToString
    MsgBox("Всего файлов " & fNames.Count & vbCrLf & v1 & vbCrLf & v2, , "test")
End Sub
Public Sub findBad(ByVal fNm As String)
    Try
        Using mstream As New System.IO.MemoryStream(File.ReadAllBytes(fNm))
            If System.Drawing.Image.FromStream(mstream).RawFormat.Equals(ImageFormat.Jpeg) Then Exit Sub
            ListBox1.Items.Add(fNm)
        End Using
    Catch ex As Exception
        ListBox1.Items.Add(fNm)
    End Try
End Sub
Public Sub findBad2(ByVal fNm As String)
    Try
        Using mstream As New System.IO.MemoryStream(File.ReadAllBytes(fNm))
            PictureBox1.Image = System.Drawing.Image.FromStream(mstream)
            PictureBox1.Image.Dispose()
            PictureBox1.Image = Nothing
        End Using
    Catch ex As Exception
        ListBox1.Items.Add(fNm)
    End Try
End Sub


dymitri

ovva, в приведённом вами примере сделал лишь изменение с расположением десятков тысяч фото в директории, и запустил. После некоторого времени выскочила ошибка: "Необработанное исключение типа "System.OutOfMemoryException" в System.Windows.Forms.dll" Когда же выбрал директорию всего с несколькими тысячами, то всё прошло удачно и результат действительно сильно не отличался (v1 немного больше чем v2). Иду сейчас в больницу (на больничном) после - изучу ещё раз отличия в моей программе и в вашем примере кода. В чём же может быть такая большая разница в продолжительности выполнения?... Спасибо за пример.


dymitri

Протестировал сейчас вчерашние варианты с одинаковыми настройками компиляции - время практически одинаковое. Так что извините что запутал со временем выполнения. Видимо просто разные настройки CPU были или ещё что... Но, к сожалению, ни один вариант не помогает найти повреждённые фото. Вернее находят или совсем испорченные, или полностью нормально отображаемые стандартным просмотрщиком, но с неправильными метаданными (Фотошоп ругается тоже). А как бы недогруженные или из-за сбоя хранилища получившие ошибки - пропускает любой из вариантов программы. Вот очень бы интересовал побитовый алгоритм построения JPG (что бы понять принцип сжатия и обратного процесса для программного кода проверки целостности). Ведь есть же сторонние программы, которые видят эти нарушения (как Фотошоп). Просто невозможно через Фотошоп пропустить тысячи фото, дабы выявить ошибки... Прикрепляю примеры файлов, о которых говорю:и так далее...


dymitri

Насколько понимаю, ваши файлы, с формальной точки зрения это вполне корректные jpeg файлы. И вас в них не устраивает их содержание, или вы можете указать конкретные признаки их некорректности. Неплохо бы посмотреть на пример подобного файла (не картинки).


dymitri

ovva, Если вы имеете ввиду оригиналы изображений "не картинки" , то могу в архиве: Плохие файлы.rar. Здесь в первой папке те файлы, которые находит приложение (путём ошибки открытия). Фотошоп тоже говорит что нельзя открыть ввиду неправильных маркеров. В просмотрщике Windows эти файлы никак не выглядят повреждёнными. Во второй папке представлены файлы урезанные, затёртые и т.д., которые приложение не распознаёт за "ошибочные", но визуально в просмотрищике видны явные повреждения, да и Фотошоп тоже говорит о неполном файле. Вот их-то и хотелось бы "узнавать программно", неким вычислением бит между маркерами, что ли, размером блоков в файле, дабы выявлять подобные ошибки в коде.


dymitri

Открыть архив не смог ("Архив поврежден…")