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

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控制逻辑直接写入业务独立文件
问题根因
  1. 跨线程直接操作UI控件违反Kivy线程规则:Kivy/KivyMD的所有UI操作必须在主线程执行,子线程直接修改MDSpinner.active属性、修改对勾图标属性,会触发大量线程安全锁竞争,导致UI帧调度和业务线程互相阻塞,这是速度暴跌的核心原因。
  2. 使用exec动态拼接执行UI操作代码存在额外性能开销,同时进一步放大了跨线程操作的异常概率。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 05:24:52