Tkinter - 内存泄漏 canvas

Tkinter - memory leak with canvas

我有一个处理 Modbus 通信的 Python 脚本。我添加的一项功能是 "graph",它显示响应时间以及指示响应是成功、有异常还是错误的颜色编码线。该图只是来自 Tkinter 的可滚动 canvas 小部件。

绘制一定数量的线后,旧线将被删除,然后新线将添加到末尾。对于这个例子,我将它设置为 10,这意味着 canvas 上一次不会超过 10 行。

代码工作正常,但此函数某处存在内存泄漏。我让它 运行 大约 24 小时,24 小时后它占用了大约 6 倍的内存。该函数是更大 class 的一部分。

我目前的猜测是我的代码导致 canvas 大小不断 "expand,",这会慢慢耗尽内存。

self.lineList = []
self.xPos = 0

def UpdateResponseTimeGraph(self):
    if not self.graphQueue.empty():
        temp = self.graphQueue.get() #pull from queue. A separate thread handles calculating the length and color of the line. 
        self.graphQueue.task_done()

        lineName     = temp[0] #assign queue values to variables
        lineLength   = temp[1]
        lineColor    = temp[2]

        if len(self.lineList) >= 10: #if more than 10 lines are on the graph, delete the first one.
            self.responseTimeCanvas.delete(self.lineList[0])
            del self.lineList[0]

        #Add line to canvas and a list so it can be referenced.
        self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-lineLength, 
                                                fill=lineColor, outline=''))

        self.xPos += 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?

        self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))

        self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.



    self.graphFrame.after(10, self.UpdateResponseTimeGraph)

一个解决方案可以是在达到限制后循环回到图表的开头,但我宁愿不这样做,因为它可能会混淆图表的开始位置。通常我的回复比 10 多得多。

编辑:

我仍在做跟踪和错误的事情,但看起来内存泄漏可以通过 Bryan 的建议消除,只要行属性不通过 itemconfig 更改。如果您使用 python 2.7,则下面的代码应该能够 运行 将导入语句从 tkinter 更改为 Tkinter(小写 t 与大写 t)。此代码将有内存泄漏。注释掉 itemconfig 行,它将被删除。

import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random

def RGB(r, g, b):
    return '#{:02x}{:02x}{:02x}'.format(r, g, b)

class MainUI:
    def __init__(self, master):
        self.master = master
        self.lineList = []
        self.xPos = 0

        self.maxLine = 122

        self.responseIndex = 0 


        self.responseWidth = 100
        self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
        self.responseTimeCanvas.pack()

        self.UpdateResponseTimeGraph()

    def UpdateResponseTimeGraph(self):
        self.lineLength   = random.randint(10,99)

        if len(self.lineList) >= self.maxLine:
            self.lineLength = random.randint(5,95)
            self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength)

            #if i comment out the line below the memory leak goes away.
            self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
        else:
            self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength, 
                                                fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)), outline=''))


        self.xPos += 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?
        self.responseIndex += 1

        self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))

        self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.



        self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)


mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()

底层 tk canvas 不重用或回收对象标识符。每当您创建一个新对象时,都会生成一个新的标识符。这些对象的内存永远不会被回收。

注意:这是嵌入式tcl解释器内部的内存,而不是由python管理的内存。

解决方案是重新配置不再使用的旧元素,而不是删除它们并创建新元素。

这是没有内存泄漏的代码。泄漏的原始来源是我删除旧行然后创建新行。该解决方案首先将行移动到末尾,然后根据需要更改其属性。我的示例代码中有第二个 'leak',我每次都选择一种随机颜色,这导致使用的颜色数量占用了大量内存。此代码仅打印绿线,但长度是随机的。

import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random

def RGB(r, g, b):
    return '#{:02x}{:02x}{:02x}'.format(r, g, b)

class MainUI:
    def __init__(self, master):
        self.master = master
        self.lineList = []
        self.xPos = 0

        self.maxLine = 122

        self.responseIndex = 0 


        self.responseWidth = 100
        self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
        self.responseTimeCanvas.pack()

        self.UpdateResponseTimeGraph()

    def UpdateResponseTimeGraph(self):
        self.lineLength   = random.randint(10,99)

        if len(self.lineList) >= self.maxLine:
            self.lineLength = random.randint(5,95)
            self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength)

            self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(100, 255, 100))
        else:
            self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength, 
                                                fill=RGB(100, 255, 100), outline=''))


        self.xPos += 5 #will cause the next line to start 5 pixels later. 
        self.responseIndex += 1

        self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))

        self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.



        self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)


mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()