如何解决 tkinter 的内存泄漏问题?

3
我有一个动态表格,行数固定(类似FIFO队列),通过tkinter的after()函数不断更新。表格内有一个可编辑文本的按钮。
为了使按钮的文本可编辑,我使用了BrenBarn的solution,并将循环变量引用到command属性的函数调用中。
当循环函数update_content_items()运行时,我发现内存使用量每秒都在增加MB。我可以确认,注释掉lambda表达式后,内存泄漏问题消失了。(在终端上实时运行“top”命令可见)
看来我必须使用lambda表达式,否则按钮会有错误的索引,用户就会在点击正确的按钮时编辑错误的行,尽管我只使用了self.list_items[i]
有没有办法解决这个问题?用户如何单击正确的按钮并进行编辑,同时具有正确的索引并且摆脱泄漏问题?
相应的代码:
    def update_content_items(self):
    """
    Continuously fills and updates the Table with rows and content.
    The size of the table rows is initially fixed by an external value at config.ini
    :return: nothing
    """
    if len(self.list_items) > self.queueMaxlen:
        self.queueMaxlen = len(self.list_items)
        self.build_table()

    try:
        for i in range(len(self.list_items)):
            item = self.list_items[i]
            self.barcodeImgList[i].image = item.plateimage
            orig_image = Image.open(io.BytesIO(item.plateimage))
            ein_image = ImageTk.PhotoImage(orig_image)
            self.barcodeImgList[i].configure(image=ein_image)
            # keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
            self.barcodeImgList[i].image = ein_image
            orig_image = None
            ein_image = None
            #FIXME Memory LEAK?
            self.numberList[i].configure(text=item.number,
                                         command=lambda K=i: self.edit_barcode(self.list_items[K]))
            self.timestampList[i].configure(text=item.timestamp)
            self.search_hitlist[i].config(bg='white', cursor="xterm")
            self.search_hitlist[i].unbind("<Button-1>")

            if item.queryresult is not None:
                if item.queryresult.gesamtstatus != 'Gruen':
                    self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
                                                  '\n' + item.queryresult.permitlevel)
                    self.search_hitlist[i].configure(bg='red', cursor="hand2")
                    self.search_hitlist[i].bind("<Button-1>", item.url_callback)
                else:
                    self.search_hitlist[i].configure(bg='green', cursor="xterm")
            self.search_hitlist[i].configure(state=tk.DISABLED)
        self.on_frame_configure(None)
        self.canvas.after(10, self.update_content_items)
    except IndexError as ie:
        for number, thing in enumerate(self.list_items):
            print(number, thing)
        raise ie

def edit_barcode(self, item=None):
    """
    Opens the number plate edit dialogue and updates the corresponding list item.
    :param item: as Hit DAO
    :return: nothing
    """
    if item is not None:
        new_item_number = EditBarcodeEntry(self.master.master, item)
        if new_item_number.mynumber != 0:
            item.number = new_item_number.mynumber
            self.list_items.request_work(item, 'update')
            self.list_items.edit_hititem_by_id(item)
            self.parent.master.queryQueue.put(item)
    else:
        print("You shouldn't get here at all. Please see edit_barcode function.")

编辑:似乎确实存在更深层次的内存泄漏问题(Python本身)。这些图像不会被垃圾回收。 Python 3.x中内存缓慢泄漏,我确实使用了PIL。此外,在这里:文件名加载图像的内存泄漏没有得到适当修复 我该怎么办?因为我必须遍历一个记录列表,并使用图像更新标签。有什么解决方法吗?PhotoImage没有显式的close()函数,如果我调用del,引用将被gc'ed,无法配置标签。

1
你正在不断创建匿名函数(这就是lambda的作用),这些函数必须保留在内存中,因此你使用的内存越来越多。唯一的解决方法是使用绑定而不是命令回调,在edit_barcode中找出小部件在列表中的索引(使用event.widget),然后使用该索引获取该项。 - James Kent
2
关于tkinter“忘记”您的图像的评论,那不是Python的一个错误,而是一项功能。垃圾收集器将从内存中删除不再被引用的项目,这就是为什么您必须将其分配给小部件的属性以保留对它的引用。 - James Kent
1个回答

1

以下是我提出的更改示例,已修正缩进:

def update_content_items(self):
    """
    Continuously fills and updates the Table with rows and content.
    The size of the table rows is initially fixed by an external value at config.ini
    :return: nothing
    """
    if len(self.list_items) > self.queueMaxlen:
        self.queueMaxlen = len(self.list_items)
        self.build_table()

    try:
        for i in range(len(self.list_items)):
            item = self.list_items[i]
            self.barcodeImgList[i].image = item.plateimage
            orig_image = Image.open(io.BytesIO(item.plateimage))
            ein_image = ImageTk.PhotoImage(orig_image)
            self.barcodeImgList[i].configure(image=ein_image)
            # keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
            self.barcodeImgList[i].image = ein_image
            orig_image = None
            ein_image = None

            self.numberList[i].configure(text=item.number) # removed lambda
            self.numberList[i].bind("<Button-1>", self.edit_barcode_binding) # added binding
            self.timestampList[i].configure(text=item.timestamp)
            self.search_hitlist[i].config(bg='white', cursor="xterm")
            self.search_hitlist[i].unbind("<Button-1>")

            if item.queryresult is not None:
                if item.queryresult.gesamtstatus != 'Gruen':
                    self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
                                                  '\n' + item.queryresult.permitlevel)
                    self.search_hitlist[i].configure(bg='red', cursor="hand2")
                    self.search_hitlist[i].bind("<Button-1>", item.url_callback)
                else:
                    self.search_hitlist[i].configure(bg='green', cursor="xterm")
            self.search_hitlist[i].configure(state=tk.DISABLED)
        self.on_frame_configure(None)
        self.canvas.after(10, self.update_content_items)
    except IndexError as ie:
        for number, thing in enumerate(self.list_items):
            print(number, thing)
        raise ie

def edit_barcode_binding(self, event): # new wrapper for binding
    K = self.numberList.index(event.widget) # get index from list
    self.edit_barcode(self.list_items[K]) # call the original function

def edit_barcode(self, item=None):
    """
    Opens the number plate edit dialogue and updates the corresponding list item.
    :param item: as Hit DAO
    :return: nothing
    """
    if item is not None:
        new_item_number = EditBarcodeEntry(self.master.master, item)
        if new_item_number.mynumber != 0:
            item.number = new_item_number.mynumber
            self.list_items.request_work(item, 'update')
            self.list_items.edit_hititem_by_id(item)
            self.parent.master.queryQueue.put(item)
    else:
        print("You shouldn't get here at all. Please see edit_barcode function.")

1
谢谢詹姆斯。你的改动是有效的,但内存泄漏仍然存在。我在调试模式下启动了应用程序,并使用了“import objgraph”并调用了:“objgraph.show_most_common_types(limit = 25)”。在这里,我看到方法类型快速增长,尽管应用程序正在空转。 - Semo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接