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()
可能的原因分析
RecycleView内部回调/闭包引用泄漏:RecycleView在滚动、视图复用过程中,会创建大量临时回调函数、帧对象(frame)。如果这些对象被内部长期存在的对象意外持有引用,就无法被GC及时回收,这和你观察到的
function、frame类对象增长高度匹配。KV语言绑定的临时对象残留:当RecycleView的item与数据绑定更新时,KV语言内部可能会生成临时的traceback对象用于错误处理(即使未抛出错误),这些对象若未被正确清理,会持续累积。
Kivy版本的已知缺陷:你使用的Kivy 2.3.x版本可能存在RecycleView的内存泄漏问题,视图复用逻辑中的部分对象引用未被正确释放,可尝试升级到最新稳定版排查。
Python GC的回收机制限制:虽然你手动调用了
gc.collect(),但部分循环引用或大对象可能需要多轮GC周期才会被处理,不过你的统计中不可回收对象数量未增长,这条可能性较低。
内容的提问来源于stack exchange,提问作者Eugene B
相关产品推荐
相关产品推荐

