Tkinter+Matplotlib动画内存泄漏及启动异常技术求助
解决回流炉控制器GUI的内存泄漏与动画启动问题
一、内存泄漏的核心原因与修复方案
你的内存持续增长问题根源很明确:每次动画循环都重复创建FigureCanvasTkAgg画布对象。在animate函数里,你每一秒都执行:
canvas = FigureCanvasTkAgg(fig,plotFrame) canvas.draw() canvas.get_tk_widget().grid(row=3,column=0,columnspan = 4)
这会导致每一次循环都生成一个全新的画布实例,而旧的画布因为被tkinter的布局管理器持有引用,无法被Python垃圾回收机制清理,内存自然会像你说的那样每秒飙升3.5MB,最终触发MemoryError。
修复方法非常简单:只初始化一次画布,在动画循环中仅更新绘图内容
- 把画布的创建代码移到全局初始化区域(和
fig、ax一起),只做一次:
fig=plt.figure() ax = fig.add_subplot(1, 1, 1) # 初始化画布并完成布局,只执行一次 canvas = FigureCanvasTkAgg(fig, plotFrame) canvas.get_tk_widget().grid(row=3,column=0,columnspan = 4)
- 修改
animate函数,移除重复创建画布的代码,只保留绘图更新逻辑:
def animate(i): global timePoints,tempQuidance,numberOfPoints,measuredTemp, time time = time+1 measuredTemp.append(readTemperature()) numberOfPoints.append(len(measuredTemp)) ax.clear() ax.plot(timePoints,tempQuidance) ax.plot(numberOfPoints,measuredTemp) updateTimeLabel(time) updateTargetTempLabel(calculateTarget(time)) updateCurrentTempLabel(measuredTemp[-1]) updateHeaterStatus(measuredTemp[-1], 100) # 只更新已存在的画布,不再新建 canvas.draw()
这样修改后,整个程序生命周期内只会有一个画布实例被复用,内存就不会再持续增长了。
二、为什么必须执行animate(1)才能启动动画?
animation.FuncAnimation的默认逻辑是:启动后等待第一个interval时长(这里是1000ms),才会第一次调用animate函数。如果不提前调用animate(1),程序启动后你的图表区域会是空白的,要等整整1秒才会显示第一组数据。
提前调用animate(1)是为了让初始画面立刻渲染出来,给用户即时的视觉反馈,避免界面启动后的空白等待期。当然你也可以用FuncAnimation的init_func参数指定初始化函数,效果是完全一样的。
修改后的完整代码
import tkinter import matplotlib.pyplot as plt import random import matplotlib.animation as animation from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg) def exitHandler(): print("at exit handler") ani.event_source.stop() root.destroy() def startButtonClick(): print("start") def stopButtonClick(): print("stop") def readTemperature(): return random.randint(0,250) def updateTimeLabel(time = -99): timeLabel.config(text= "Time: {}".format (time)) def updateTargetTempLabel(temp = -99) : targetTempLabel.config(text="Target temp: \n {} ".format (temp)) def updateCurrentTempLabel(temp = -99) : currentTempLabel.config(text="Current temp: \n {} ".format (temp)) def updateHeaterStatus(temp,target): if (temp < target): heaterStatusLabel.config(text="Heater: \n On") else: heaterStatusLabel.config(text="Heater: \n Off") def calculateTarget(time): global timePoints, tempQuidance, targetTemp #find current slope and calculate the target temp for i in range (len(timePoints)): if (timePoints[i] < time < timePoints[i+1]): slope = (tempQuidance[i+1] - tempQuidance[i]) / (timePoints[i+1] -timePoints[i]) currentTarget = (time -timePoints[i]) * slope + tempQuidance[i] return (currentTarget) def animate(i): global timePoints,tempQuidance,numberOfPoints,measuredTemp, time time = time+1 measuredTemp.append(readTemperature()) numberOfPoints.append(len(measuredTemp)) ax.clear() ax.plot(timePoints,tempQuidance) ax.plot(numberOfPoints,measuredTemp) updateTimeLabel(time) updateTargetTempLabel(calculateTarget(time)) updateCurrentTempLabel(measuredTemp[-1]) updateHeaterStatus(measuredTemp[-1], 100) # 仅更新已有画布 canvas.draw() global measuredTemp, numberOfPoints, time, targetTemp time = 0 measuredTemp=[] numberOfPoints=[] targetTemp = 0 #temperature profile timePoints = [0,300,400,460,500] tempQuidance =[0,150,150,250,0] root=tkinter.Tk() root.title('Reflow oven controller') root.geometry("1600x800") controlFrame = tkinter.LabelFrame(root,width = 500, height = 800, borderwidth = 3, padx=5,pady=5) plotFrame = tkinter.LabelFrame(root,padx=5,pady=5) controlFrame.grid(row = 0, column = 0 ) controlFrame.grid_propagate(0) plotFrame.grid(row = 0, column= 1) timeLabel=tkinter.Label(plotFrame,text = "Time: ") targetTempLabel = tkinter.Label(plotFrame,text = "Target temp") currentTempLabel = tkinter.Label(plotFrame,text = "Current temp") heaterStatusLabel = tkinter.Label(plotFrame,text = "Heater status: \n unknown") timeLabel.grid(row= 0, column=0) targetTempLabel.grid(row=0, column= 1) currentTempLabel.grid(row=0, column= 2) heaterStatusLabel.grid(row = 0, column = 3) fig=plt.figure() ax = fig.add_subplot(1, 1, 1) # 初始化画布并布局 canvas = FigureCanvasTkAgg(fig, plotFrame) canvas.get_tk_widget().grid(row=3,column=0,columnspan = 4) startButton = tkinter.Button(controlFrame, text = "Start", command = startButtonClick) stopButton = tkinter.Button(controlFrame, text="Stop", command = stopButtonClick) startButton.grid(row = 0,column = 0) stopButton.grid(row = 0 ,column= 1 ) animate(1) ani = animation.FuncAnimation(fig, animate, interval=1000) #run animation every 1s, animate func takes care of getting new values and GUI update root.protocol("WM_DELETE_WINDOW", exitHandler) root.mainloop()
内容的提问来源于stack exchange,提问作者KaMa
相关产品推荐
相关产品推荐

