Matpotlib是在Python中使用的非常广泛的数据可视化库,使用也比较简单。在最近的项目中需要做一个简单的GUI并动态显示从串口读取的数据,想到用Python就可以实现,并且用Matplotlib库动态刷新。然而,这个功能却发现了Matplotlib中内存泄漏的问题,每次刷新图片GUI占用内存就快速增大,网络上搜索了一通好像也没有好的解决办法。本文实现了一种避免matplotlib动态刷新数据内存泄漏的方法,实测在GUI应用中不停打开、关闭Matplotlib图像不会造成内存泄漏。
1. GUI框架
首先介绍一下本文使用的GUI框架,在Python中常用的GUI框架有Tkinter、PyQT5、wxPython,其中PyQT5是强大完善的库,但是由于商业使用存在许可证问题,并且本应用比较简单暂时还不需要这么强大的库。在wxPython和Tkinter中选择了Tkinter,因为这个是Python自带的标准GUI库,随着Python升级同步在维护,而且各方面兼容性比较好。
本应用并没有直接使用Tkinter,而是使用PySimpleGUI来开发,PySimpleGUI不是一个新的GUI框架,其本身基于tkinter, Qt, Remi, WxPython等开发,其中Tkinter支持的比较完善。PySimpleGUI的特点就像它的名字,使用非常简单,仅使用List来实现界面布局,自带几十种主题风格,而且将事件回调机制转换为Event查询机制,仅需要在主循环中根据不同Event做不同处理就可以了。并且,PySimpleGUI自带了200多个demo,各种功能都可以方便的查看效果。
Demo仓库:https://github.com/PySimpleGUI/PySimpleGUI/tree/master/DemoPrograms
本应用就是点击按钮弹出Matplotlib图像,实现在GUI中显示图形并刷新,而正是因为这样,才暴露出内存泄漏的问题。
2.内存泄漏现象
演示代码比较简单,运行后是一个窗口包含三个按钮,按Plot按钮会弹出Matplotlib窗口,如下图所示。
import PySimpleGUI as sg
import matplotlib.pyplot as plt
import numpy as np
import random
def draw_plot():
x = np.linspace(0, random.randint(10, 100) * np.pi, num=10000)
y = np.sin(x)
plt.plot(x, y)
plt.show(block=False)
def mem_leak_demo():
layout = [[sg.Button('Plot'), sg.Cancel(), sg.Button('Popup')]]
window = sg.Window('Memleak demo....', layout)
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Cancel'):
break
elif event == 'Plot':
draw_plot()
elif event == 'Popup':
sg.popup('Yes, your application is still running')
window.close()
if __name__ == '__main__':
mem_leak_demo()
![](https://i-blog.csdnimg.cn/blog_migrate/a5feabcbbb0490e43dea22ae160e9bcb.png)
这个简单的应用却存在内存泄漏问题,具体现象是按Plot按钮->关闭Matplotlib图像窗口,再重复这个过程,每次操作内存占用都在增加,并且等待一段时间后并不会减少,可以看到存在内存泄漏问题。只有整个应用都关掉后内存才会被释放。
![](https://i-blog.csdnimg.cn/blog_migrate/810db073b17072514b46cdacf5208ee9.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9fa0db488278a90f3f8160aab9b2ca1f.png)
尝试过使用cla/clf/close等方法都无法解决内存泄漏的问题,猜测可能是Matplotlib每次画图都新建了一个窗口,并放入画图的数据,而关闭Matplotlib窗口后这个数据无法再被引用到,从而导致内存泄漏。
3.内存泄漏解决办法
猜测内存泄漏是由于每次都新建图像导致的,尝试只新建一次图像,然后刷新图像数据的办法解决内存泄漏。这时候就要利用到PySimpleGUI新建一个窗口,并把Matplotlib放到Canvas空间中,每次画图都用cla先清除数据然后再画图,这样就能反复利用同一个Matplotlib图像。
这里有个小技巧,PySimpleGUI的窗口同样会被关闭导致无法画图,所以在新建窗口时禁止这个窗口被关闭,并且默认窗口隐藏,按Plot按钮只是将这个窗口显示出来,而在窗口上提供一个自定义的关闭按钮,按这个按钮重新隐藏窗口。具体代码如下:
import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import numpy as np
import random
import matplotlib
matplotlib.use('TkAgg')
def draw_plot(axis, canvas, toolbar):
axis.cla()
x = np.linspace(0, random.randint(10, 100) * np.pi, num=10000)
y = np.sin(x)
axis.plot(x, y)
toolbar.update()
canvas.draw()
def draw_figure_w_toolbar(canvas, fig, canvas_toolbar):
figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
figure_canvas_agg.draw()
toolbar = Toolbar(figure_canvas_agg, canvas_toolbar)
toolbar.update()
figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)
return figure_canvas_agg, toolbar
class Toolbar(NavigationToolbar2Tk):
def __init__(self, *args, **kwargs):
super(Toolbar, self).__init__(*args, **kwargs)
def main():
layout = [[sg.Button('Plot'), sg.Cancel(), sg.Button('Popup')]]
window1 = sg.Window('Memleak demo....', layout, finalize=True)
fig = plt.figure() #Matplotlib图像
axis = fig.add_subplot(1,1,1)
layout2 = [
[sg.B('Exit')],
[sg.Canvas(size=(640, 480), key='fig_cv', expand_x=True, expand_y=True, pad=(0, 0))],
[sg.Canvas(key='controls_cv')]
]
window2 = sg.Window('Figure', layout2, alpha_channel=0,
disable_close=True, #禁止关闭窗口
resizable=True, finalize=True, background_color='white')
fig_canvas, toolbar = draw_figure_w_toolbar(window2['fig_cv'].TKCanvas, fig, window2['controls_cv'].TKCanvas)
window2.disable()
window2.disappear()
window2.hide() #隐藏窗口
while True:
window, event, values = sg.read_all_windows()
if window == sg.WIN_CLOSED: # if all windows were closed
break
if window == window1 and event in (sg.WIN_CLOSED, 'Cancel'):
break
elif event == 'Plot':
draw_plot(axis, fig_canvas, toolbar) #刷新Matplotlib图像
window2.enable()
window2.reappear()
window2.un_hide() #显示窗口
elif event == 'Popup':
sg.popup('Yes, your application is still running')
elif window == window2 and event == 'Exit':
window2.disable()
window2.disappear()
window2.hide() #重新隐藏窗口
window2.close()
window1.close()
if __name__ == '__main__':
main()
4.总结
严格说来本文所述方法并没有根本上解决Matplotlib内存泄漏问题,而是通过反复利用同一个图像从而避免反复新建图像而导致内存泄漏。经测试,多次按Plot按钮->关闭Matplotlib图像窗口,内存占用比较稳定。