WPF使用BitmapImage内存释放问题

在WPF中进行图片的相关操作是一件比较麻烦的事,并不是说它复杂,而是不注意的话很容易引起内存暴涨甚至溢出。关于BitmapImage使用的相关说明如下:

一、 创建方式

使用Uri设置BitmapImage会自动形成缓存,不关闭整个模块的话GC不会回收。 故如果在单个模块多次显示图片,不要使用这种方式:

var bitmap = new BitmapImage(new Uri(@"c:\test.bmp"));

建议通过流的方式加载图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
byte[] imageData;

using (var fileStream = new FileStream(@"c:\test.bmp", FileMode.Open, FileAccess.Read))
using (var binaryReader = new BinaryReader(fileStream))
{
imageData = binaryReader.ReadBytes((int)fileStream.Length);
}

var bitmap = new BitmapImage();

bitmap.BeginInit();
bitmap.StreamSource = new MemoryStream(imageData);
bitmap.EndInit();

二、 释放方式

创建完BitmapImage对象后必须消除所有对该对象的引用后GC才会回收。

这里举个例子:

XAML:

1
2
3
4
5
6
7
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="411" Margin="45,24,0,0" VerticalAlignment="Top" Width="336">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding ImgSource}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

后台代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class test
{
public ImageSource ImgSource { get; set; }
}

List<test> lstImage = new List<test>();
for (int i = 0; i < 100; i++)
{
test t = new test();

byte[] imageData;

using (var fileStream = new FileStream(lstTest[i], FileMode.Open, FileAccess.Read))
using (var binaryReader = new BinaryReader(fileStream))
{
imageData = binaryReader.ReadBytes((int)fileStream.Length);
}

var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = new MemoryStream(imageData);
bitmap.EndInit();

t.ImgSource = bitmap;

lstImage.Add(t);
}
listBox.ItemsSource = lstImage;

这里创建了100个BitmapImage对象,存在List中,当需要释放BitmapImage内存的时候切记不可以
lstImage = null 或者 lstImage = new List<test>(),如果这样操作,BitmapImage对象还存在引用,GC不会回收这部分内存。一定要使用lstImage.Clear()或者使用lstImage.Remove()移除需要删除的BitmapImage后,GC才会回收。

如上图所示:

上为第一次加载列表后内存使用情况

中为使用lstImage.Clear()方法后内存使用情况

下为使用 lstImage = nulllstImage = new List<test>() 后再一次加载列表内存使用情况

三、 图片列表与分页显示

如果在一个模块或一个页面显示一个图片列表,包含大量的图片信息,就需要加入虚拟化与分页,在上面的例子中,使用了ListBox控件,在默认情况下ListBox自动开启虚拟化,如果要关闭虚拟化,则加入下面这段代码VirtualizingPanel.IsVirtualizing="False"

如上图所示:

上为不适用虚拟化,一次性加载列表中所有图片,内存使用情况

下为使用虚拟化后,内存使用情况,当列表滚动,图片开始逐一显示,内存也随之增长

使用虚拟化后可以解决一次性加载过多图片的问题,但已经加载到的图片不能及时自动释放,所以需要加入分页,下面是一个简单的例子:

XAML:

1
2
3
4
5
6
7
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="411" Margin="45,24,0,0" VerticalAlignment="Top" Width="336">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding ImgSource}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

后台代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class test
{
public ImageSource ImgSource { get; set; }
}

List<string> lstTest = new List<string>();
List<test> lstShow = new List<test>();

private void btnLoad_Click(object sender, RoutedEventArgs e)
{
lstTest.Clear();
for (int i = 0; i < 100; i++)
{
lstTest.Add(@"d:\1.bmp");
}
}

private void btnPage1_Click(object sender, RoutedEventArgs e)
{
lstShow.Clear();
for (int i = 0; i < 80; i++)
{
test t = new test();

byte[] imageData;

using (var fileStream = new FileStream(lstTest[i], FileMode.Open, FileAccess.Read))
using (var binaryReader = new BinaryReader(fileStream))
{
imageData = binaryReader.ReadBytes((int)fileStream.Length);
}

var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = new MemoryStream(imageData);
bitmap.EndInit();

t.ImgSource = bitmap;

lstShow.Add(t);
}

listBox.ItemsSource = lstShow;
}

private void btnPage2_Click(object sender, RoutedEventArgs e)
{
lstShow.Clear();
for (int i = 80; i < 100; i++)
{
test t = new test();

byte[] imageData;

using (var fileStream = new FileStream(lstTest[i], FileMode.Open, FileAccess.Read))
using (var binaryReader = new BinaryReader(fileStream))
{
imageData = binaryReader.ReadBytes((int)fileStream.Length);
}

var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = new MemoryStream(imageData);
bitmap.EndInit();

t.ImgSource = bitmap;

lstShow.Add(t);
}

listBox.ItemsSource = lstShow;
}

private void btnClear_Click(object sender, RoutedEventArgs e)
{
lstShow.Clear();
listBox.ItemsSource = lstShow;
}

在这个例子中,用一个列表lstTest存所有图片的路径,但不创建BitmapImage对象,只在打开分页的时候用另一个列表lstShow存储新的BitmapImage对象,同时清除旧的BitmapImage对象。这样即满足了BitmapImage内存管理原则。

最后值得一提的是在上面的例子中,如果只需要在列表中使用缩略图,则尽量用BitmapImage读缩略图的文件流,这样能大大减少内存消耗。

WPF使用BitmapImage内存释放问题

https://wurang.net/wpf_image_source/

作者

Wu Rang

发布于

2015-08-26

更新于

2021-12-06

许可协议

评论