KivyMD中MDSpinner激活导致后台任务执行速度大幅下降
问题现象
- 开发KivyMD桌面应用时,使用
MDSpinner作为Excel写入任务的后台运行状态提示:任务执行中显示Spinner,完成后隐藏Spinner、显示对勾标识写入成功 - 启用Spinner后任务执行速度大幅下降:不开启Spinner时写入Excel耗时不足10秒,开启后耗时超过1分钟;即使把业务逻辑替换为打印1-1000的简单循环,速度下降问题依然存在,注释Spinner启停代码后循环可瞬间执行完成
现有实现代码
初始版本核心逻辑片段
Python侧调用代码:
def SingleSave(self): threading.Thread(target=writecutsheet.main.SingleSave, args=(self)).start() self.spinner_start() def spinner_start(self): self.root.ids.spinid.active = True
对应的KV布局片段:
FloatLayout: TickIcon: id: complete LoadingIcon: id: loading MDSpinner: size_hint: None, None id: prog1 size: dp(46), dp(46) pos_hint: {"center_y": 0.9, "center_x": 0.94} active: False FloatLayout: MDNavigationRail: NavHomeButton: on_release: screen_manager.current = "Landing" screen_manager.transition.direction = 'left' NavSaveButton: on_press: app.SingleSave() complete.icon: "" NavGenerateButton: on_release: screen_manager.current = "Generate" screen_manager.transition.direction = 'left' NavSettingsButton: on_release: screen_manager.current = "Settings" screen_manager.transition.direction = 'left'
最小可复现代码
main.py
from kivymd.app import MDApp from kivymd.uix.label import MDLabel from kivymd.uix.boxlayout import MDBoxLayout from kivy.lang import Builder from kivy.uix.screenmanager import ScreenManager, Screen from kivymd.uix.navigationrail import MDNavigationRail from kivy.core.window import Window from kivy.factory import Factory # NOQA: F401 from kivy.loader import Loader from kivymd import images_path from kivymd.uix.navigationdrawer import MDNavigationDrawer from kivy.properties import ObjectProperty from kivy.clock import Clock, mainthread from kivy.core.window import Window from kivy.metrics import dp from kivy.uix.scrollview import ScrollView from kivymd.uix.behaviors import HoverBehavior from kivymd.theming import ThemableBehavior from kivy.uix.floatlayout import FloatLayout from kivy.uix.image import Image from kivy.uix.gridlayout import GridLayout from kivymd.icon_definitions import md_icons from kivy.properties import StringProperty from kivy.base import ExceptionHandler, ExceptionManager from kivy.uix.textinput import TextInput from kivy.animation import Animation import writecutsheets import threading Window.maximize() class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "DeepPurple" self.theme_cls.theme_style = "Dark" return Builder.load_file('myapp.kv') def SingleSave(self, screenid, spinid, tickid): reset = "self.root.ids." + tickid + ".icon = \"\"" exec(reset) threading.Thread(target=writecutsheets.main.SingleSave, args=(self, screenid, spinid, tickid, )).start() spinner_string = "self.root.ids." + spinid + ".active = True" exec(spinner_string) if __name__ == '__main__': MainApp().run()
writecutsheets.py
class main: def SingleSave(self, screenid, spinid, tickid): for i in range(1, 1000): print(i) spinner_string = "self.root.ids." + spinid + ".active = False" exec(spinner_string) complete_string = "self.root.ids." + tickid + ".icon = \"check\"" exec(complete_string)
myapp.kv
#:import get_color_from_hex kivy.utils.get_color_from_hex #:import FadeTransition kivy.uix.screenmanager.FadeTransition #:import toast kivymd.toast.toast #:import StiffScrollEffect kivymd.effects.stiffscroll.StiffScrollEffect #:import Snackbar kivymd.uix.snackbar.Snackbar #:import ScrollEffect kivy.effects.scroll.ScrollEffect #:import Clock kivy.clock.Clock #:import threading threading <NavSaveButton@MDNavigationRailItem> icon: "content-save-outline" <NavGenerateButton@MDNavigationRailItem> icon: "check-circle-outline" <NavSettingsButton@MDNavigationRailItem> icon: "cog-outline" <NavHomeButton@MDNavigationRailItem> icon: "home" ##### 导航抽屉按钮 ##### <DrawerClickableItem@MDNavigationDrawerItem> text_color: text_color icon_color: text_color ##### 下拉菜单按钮 ##### <DropDownButton@MDFillRoundFlatIconButton> size_hint_x: .2 icon: "menu-down" text: "Select an option" ##### 各页面标题文本 ##### <TitleLabel@MDLabel> halign: "center" font_style: "H3" pos_hint: {"center_y": 0.85} ##### 文本输入框 ##### <Textbox@MDTextField> size_hint_x: .2 write_tab: False <TickIcon@MDIconButton> icon: "" icon_size: "40sp" theme_icon_color: "Custom" icon_color: "#00ff00" pos_hint: {"center_y": 0.9, "center_x": 0.94} ##### 帮助文本图标 ##### <HelpIcon@MDIconButton+MDTooltip> icon: "help-circle-outline" icon_size: "16sp" ##### 页面值写入图标 ##### <SaveButton@MDIconButton> icon: "content-save-outline" pos_hint: {"center_y": 0.98, "center_x": 0.94} ##### 跳转生成CLI页面图标 ##### <CompleteButton@MDIconButton> icon: "check-circle-outline" pos_hint: {"center_y": 0.98, "center_x": 0.96} theme_icon_color: "Custom" icon_color: "#00ff00" ##### 复选框 ##### <Check@MDCheckbox> size_hint: None, None size: "38dp", "38dp" ##### 网格布局 ##### <Grid@MDGridLayout> col: 1 rows: 1 orientation: "lr-tb" pos_hint: {"center_y": 0.85} ##### 返回按钮 ##### <BackButton@MDIconButton+MDTooltip> icon: "arrow-left" tooltip_text: "Back" pos_hint: {"center_y": 0.08, "center_x": 0.1} ##### 前进按钮 ##### <ForwardButton@MDIconButton+MDTooltip> tooltip_text: "Next" icon: "arrow-right" pos_hint: {"center_y": 0.08, "center_x": 0.94} ##### 滚动视图模板 ###### <Scroll@ScrollView> scroll_y: 1 pos_hint: {"center_y": 0.3} do_scroll_x: False ##### 工具栏模板 ##### <Toolbar@MDToolbar> title: "Perimeta Config Generator" elevation: 10 pos_hint: {"top": 1} MDNavigationLayout: id: nav_layout Toolbar: left_action_items: [['menu', lambda x: nav_drawer.set_state("toggle")]] ScreenManager: id: screen_manager Screen: name: "screen1" Grid: TitleLabel: text: "My screen" Scroll: FloatLayout: MDLabel: text: "Network Name" pos_hint: {"center_y": 0.95, "center_x": 0.8} HelpIcon: tooltip_text: "Enter the name of yournetwork" pos_hint: {"center_y": 0.95, "center_x": 0.29} DropDownButton: pos_hint: {"center_y": 0.95, "center_x": 0.6} id: servnetname on_release: app.servdropdown(servnetname) MDLabel: text: "Source Description" pos_hint: {"center_y": 0.9, "center_x": 0.8} HelpIcon: tooltip_text: "Enter the description of thesource" pos_hint: {"center_y": 0.9, "center_x": 0.29} Textbox: pos_hint: {"center_y": 0.91, "center_x": 0.6} id: tsdesc MDLabel: text: "IP Version" pos_hint: {"center_y": 0.85, "center_x": 0.8} HelpIcon: tooltip_text: "Enter the IP Version" pos_hint: {"center_y": 0.85, "center_x": 0.29} DropDownButton: pos_hint: {"center_y": 0.85, "center_x": 0.6} id: ipversts on_release: app.ipdropdown(ipversts) MDLabel: text: "Start IP" pos_hint: {"center_y": 0.8, "center_x": 0.8} HelpIcon: tooltip_text: "" pos_hint: {"center_y": 0.8, "center_x": 0.29} Textbox: pos_hint: {"center_y": 0.81, "center_x": 0.6} id: trustip MDLabel: text: "End IP" pos_hint: {"center_y": 0.75, "center_x": 0.8} HelpIcon: tooltip_text: "" pos_hint: {"center_y": 0.75, "center_x": 0.29} Textbox: pos_hint: {"center_y": 0.76, "center_x": 0.6} id: rangeendts FloatLayout: TickIcon: id: complete3 MDSpinner: size_hint: None, None id: prog3 size: dp(46), dp(46) pos_hint: {"center_y": 0.9, "center_x": 0.94} active: False FloatLayout: size_hint_y: 0.936 MDNavigationRail: NavSaveButton: on_press: app.SingleSave(3, "prog3", "complete3") NavGenerateButton: on_release: screen_manager.current = "Generate" screen_manager.transition.direction = 'left' NavSettingsButton: on_release: screen_manager.current = "Settings" screen_manager.transition.direction = 'left' MDNavigationDrawer: id: nav_drawer MDNavigationDrawerMenu: MDNavigationDrawerLabel: text: "Basic Configuration" title_color: text_color title_color: text_color DrawerClickableItem: icon: "" text: "Start" on_press: nav_drawer.set_state("close")
已尝试的无效方案
- 为Spinner启动逻辑单独创建线程
- 在KV中通过保存按钮触发直接设置
prog1.active = True - 将Spinner控制逻辑直接写入业务独立文件
问题根因
- 跨线程直接操作UI控件违反Kivy线程规则:Kivy/KivyMD的所有UI操作必须在主线程执行,子线程直接修改
MDSpinner.active属性、修改对勾图标属性,会触发大量线程安全锁竞争,导致UI帧调度和业务线程互相阻塞,这是速度暴跌的核心原因。 - 使用
exec动态拼接执行UI操作代码存在额外性能开销,同时进一步放大了跨线程操作的异常概率。 MDSpinner默认每帧都会重绘动画,如果业务逻辑里存在大量和主线程抢占GIL的操作(比如高频print、逐单元格Excel写入),Spinner的动画重绘会和业务逻辑频繁争抢CPU时间片,进一步拖慢速度。
修复方案
核心规则
- 所有UI操作全部通过
@mainthread装饰器或Clock.schedule_once切回主线程执行,绝对不能在子线程里直接修改任何Kivy控件属性 - 移除所有不必要的
exec动态代码,直接通过ID引用控件 - 业务逻辑文件不要持有任何UI引用,只做纯业务计算/IO操作
优化后代码示例
修改main.py
from kivymd.app import MDApp from kivy.lang import Builder from kivy.core.window import Window from kivy.clock import mainthread import writecutsheets import threading Window.maximize() class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "DeepPurple" self.theme_cls.theme_style = "Dark" return Builder.load_file('myapp.kv') @mainthread def set_spinner_state(self, spinid, active): # 主线程修改Spinner状态 self.root.ids[spinid].active = active @mainthread def set_tick_state(self, tickid, icon_name): # 主线程修改对勾图标 self.root.ids[tickid].icon = icon_name def SingleSave(self, screenid, spinid, tickid): # 先在主线程重置状态、启动Spinner self.set_tick_state(tickid, "") self.set_spinner_state(spinid, True) # 启动子线程执行业务逻辑,任务完成后通过主线程方法更新UI def task(): writecutsheets.main.SingleSave() self.set_spinner_state(spinid, False) self.set_tick_state(tickid, "check") threading.Thread(target=task, daemon=True).start() if __name__ == '__main__': MainApp().run()
修改writecutsheets.py
class main: def SingleSave(): # 纯业务逻辑,不要在这里操作任何UI for i in range(1, 1000): print(i) # 实际Excel写入逻辑放在这里
额外性能优化
- 逐单元格写入Excel的逻辑可以做批量聚合,减少和主线程的GIL争抢次数
- 如果Spinner动画还是影响速度,可以在build方法开头加
Config.set('graphics', 'maxfps', 30),把最高帧率从默认60降到30,减少Spinner的重绘开销 - 给子线程设置
daemon=True,避免窗口关闭后子线程还在后台运行导致程序退出异常
内容的提问来源于stack exchange,提问作者Mlupe
相关产品推荐
相关产品推荐

