You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kivy RecycleView操作后对象持续堆积,无法被垃圾回收,求排查方向

Kivy RecycleView 内存对象持续增长问题

我基于Kivy官方示例修改,仅添加垃圾回收(GC)统计功能后发现:每次执行RecycleView操作(填充、清空、滚动等)都会生成对象,且对象总数持续攀升,未被垃圾回收器及时回收。其中traceback、frame、function类对象的增长数量最多。

测试环境

  • Windows 10 Pro 20H2 64位,Python 3.12.4 32位(需调用32位遗留.NET DLL),Kivy v2.3.0
  • Windows 10 Pro 22H2,Python 3.8.10 64位,Kivy v2.3.1(仅上下滚动列表也会导致对象数量增加)

内存检测验证

我使用了Kivy官方issue中的内存检测函数,单独检测内存占用,排除了自定义统计方法的问题,结果显示内存占用随滚动操作持续上升:

def memcheck():
    # 返回内存占用(MB)
    process = psutil.Process()
    mem = process.memory_info().rss / float(2 ** 20)
    mem = str(mem) + "MB"
    return mem

请问这一现象的原因可能是什么?

修改后的完整代码

from random import sample, randint
from string import ascii_lowercase

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
import gc
import json
from colorama import init, deinit, Fore, Style

def cprint(*args, red=False, newline=True):
    init(autoreset=True) # colorama初始化
    text = '    '
    for arg in args:
        text+= (str(arg) + ' ')
    if newline:    
        print(Style.BRIGHT + (Fore.RED if red else Fore.YELLOW) + text + Fore.RESET)
    else:
        print(Style.BRIGHT + (Fore.RED if red else Fore.YELLOW) + text + Fore.RESET, end='')
    deinit() # colorama释放

kv = """
<Row@RecycleKVIDsDataViewBehavior+BoxLayout>:
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.5, 1
        Rectangle:
            size: self.size
            pos: self.pos
    value: ''
    Label:
        id: name
    Label:
        text: root.value

<Test>:
    canvas:
        Color:
            rgba: 0.3, 0.3, 0.3, 1
        Rectangle:
            size: self.size
            pos: self.pos
    rv: rv
    orientation: 'vertical'
    GridLayout:
        cols: 3
        rows: 2
        size_hint_y: None
        height: dp(108)
        padding: dp(8)
        spacing: dp(16)
        Button:
            text: '填充列表'
            on_press: root.populate()
        Button:
            text: '排序列表'
            on_press: root.sort()
        Button:
            text: '清空列表'
            on_press: root.clear()
        BoxLayout:
            spacing: dp(8)
            Button:
                text: '插入新项'
                on_press: root.insert(new_item_input.text)
            TextInput:
                id: new_item_input
                size_hint_x: 0.6
                hint_text: '值'
                padding: dp(10), dp(10), 0, 0
        BoxLayout:
            spacing: dp(8)
            Button:
                text: '更新第一项'
                on_press: root.update(update_item_input.text)
            TextInput:
                id: update_item_input
                size_hint_x: 0.6
                hint_text: '新值'
                padding: dp(10), dp(10), 0, 0
        BoxLayout:
            Button:
                text: '删除第一项'
                on_press: root.remove()
            Button:
                text: '生成报告'
                on_press: root.check_garbage()

    RecycleView:
        id: rv
        scroll_type: ['bars', 'content']
        scroll_wheel_distance: dp(114)
        bar_width: dp(10)
        viewclass: 'Row'
        RecycleBoxLayout:
            default_size: None, dp(56)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            spacing: dp(2)
"""

Builder.load_string(kv)


class Test(BoxLayout):

    def populate(self):
        self.rv.data = [
            {'name.text': ''.join(sample(ascii_lowercase, 6)),
             'value': str(randint(0, 2000))}
            for x in range(50)]

    def sort(self):
        self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])

    def clear(self):
        self.rv.data = []

    def insert(self, value):
        self.rv.data.insert(0, {
            'name.text': value or '默认值', 'value': '未知'})

    def update(self, value):
        if self.rv.data:
            self.rv.data[0]['name.text'] = value or '默认新值'
            self.rv.refresh_from_data()

    def remove(self):
        if self.rv.data:
            self.rv.data.pop(0)

    def check_garbage(self):
        '''
        将垃圾对象类型存储到json文件,避免每次运行生成新对象
        '''
        
        if not hasattr(self, 'softwarerestarted'):
            self.softwarerestarted = True
        
        ''' 记录所有对象统计信息 '''
        gc.collect()
        gc_objectsnow = gc.get_objects()
        uncollectable_garbage = len(gc.garbage)
        garbagetypes = {}
        for obj in gc_objectsnow:
            ''' 按类型记录数量 '''
            if not str(type(obj)) in garbagetypes:
                garbagetypes[str(type(obj))] = 1
            else:
                garbagetypes[str(type(obj))] +=1
                
        if self.softwarerestarted:
            garbagetypesrecord=[garbagetypes]
            self.softwarerestarted = False
        else:
            with open('C:/Temp/garbagerecord.json') as file:
                garbagetypesrecord = json.load(file)
            garbagetypesrecord.append(garbagetypes)
        with open('C:/Temp/garbagerecord.json', 'w') as file:
            json.dump(garbagetypesrecord, file)

        ''' 输出报告结果 '''
        for index, record in enumerate(garbagetypesrecord):
            printrows = []
            firstprintrow = []
            firstprintrow.append("--- 垃圾报告 " + str(index+1) + ":")
            printrows2 = []
            totalcount, totalprevcount = 0, 0
            for key, value in record.items():
                totalcount+=value
                if index > 0:
                    totalprevcount+=garbagetypesrecord[index-1].get(key,0)
                    diff = value - garbagetypesrecord[index-1].get(key,0)
                    if diff > 0:
                        printrow =[]
                        printrow.append('    ' + str(key))
                        printrow.append(str(value))
                        printrow.append(("+" + str(diff)) if diff >= 0 else str(diff))
                        printrows2.append(printrow)
            diff = totalcount - totalprevcount
            firstprintrow.append("总对象数 " + str(totalcount))
            if index > 0: firstprintrow.append(( " +" + str(diff)) if diff >= 0 else str(diff))
            firstprintrow.append(" 其中不可回收对象数 " + str(uncollectable_garbage))
            printrows.append(firstprintrow)
            if index == len(garbagetypesrecord)-1: printrows+=printrows2
            for row in printrows:
                for word in row:
                    cprint(word, red = True if ('+' in word and not '+0' in word) else False, newline=False)
                cprint('')
        print('')


class TestApp(App):
    def build(self):
        return Test()


if __name__ == '__main__':
    TestApp().run()

可能的原因分析

  1. RecycleView内部回调/闭包引用泄漏:RecycleView在滚动、视图复用过程中,会创建大量临时回调函数、帧对象(frame)。如果这些对象被内部长期存在的对象意外持有引用,就无法被GC及时回收,这和你观察到的function、frame类对象增长高度匹配。

  2. KV语言绑定的临时对象残留:当RecycleView的item与数据绑定更新时,KV语言内部可能会生成临时的traceback对象用于错误处理(即使未抛出错误),这些对象若未被正确清理,会持续累积。

  3. Kivy版本的已知缺陷:你使用的Kivy 2.3.x版本可能存在RecycleView的内存泄漏问题,视图复用逻辑中的部分对象引用未被正确释放,可尝试升级到最新稳定版排查。

  4. Python GC的回收机制限制:虽然你手动调用了gc.collect(),但部分循环引用或大对象可能需要多轮GC周期才会被处理,不过你的统计中不可回收对象数量未增长,这条可能性较低。


内容的提问来源于stack exchange,提问作者Eugene B

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 02:55:55