
kivy 的 `urlrequest` 回调(如 `on_success`)默认在主线程中执行,即使请求本身异步,回调中的耗时操作仍会阻塞 ui;解决方法是将回调逻辑移出主线程,或确保其轻量、非阻塞,并通过 `@mainThread` 显式控制 ui 更新时机。
在 Kivy 应用中,UrlRequest 常被误认为“完全异步”——实际上,它底层基于 urllib 或 requests(取决于配置),网络 I/O 是异步的,但所有回调函数(on_success、on_error、on_failure)默认在主线程(即 GUI 线程)中同步执行。这意味着:哪怕你在后台线程中调用了 UrlRequest(…),只要回调里包含 time.sleep(5)、jsON 解析、数据库写入或复杂计算等耗时操作,GUI 就会卡死,MDSpinner 停止旋转、界面无响应。
你原代码中的关键问题在于:
- mycallback 被直接用作 on_success,且内部含 time.sleep(5);
- 尽管 sync_thread 在新线程运行,但 UrlRequest 的回调不继承该线程上下文,始终回归主线程;
- @mainthread 装饰器只对 主动从子线程调用 UI 方法 有效(如 kill_spinner),无法“拯救”已在主线程中执行的阻塞回调。
✅ 正确做法是:将耗时处理逻辑移出回调,在子线程中完成,仅用回调触发轻量任务(如调度、标记状态),再通过 Clock.schedule_once 或 @mainthread 安全更新 UI。
以下是推荐重构方案:
from kivy.network.urlrequest import UrlRequest from functools import partial from kivy.clock import Clock, mainthread from threading import Thread from kivymd.uix.screen import MDScreen import json import time class MyScreen(MDScreen): def on_button(self): self.ids.spinner.active = True # 启动后台下载流程(不阻塞) t = Thread(target=self.sync_thread, daemon=True) t.start() def sync_thread(self): # ✅ 每个 download 应独立处理,避免串行阻塞 urls = ["https://httpbin.org/json", "https://httpbin.org/delay/1"] for url in urls: # 注意:此处 download 是普通函数,非绑定方法(需传 self) self.download(url) def download(self, url): # 回调只做最小调度:把数据和上下文传给后台处理 def on_success(req, result): # ? 错误:在此处解析/处理会阻塞主线程 # data = json.loads(result.decode()) # ❌ 危险! # ✅ 正确:启动子线程处理,回调仅触发调度 worker = Thread( target=self.process_response, args=(url, result), daemon=True ) worker.start() def on_failure(req, result): print(f"Download failed for {url}") self._finish_download_if_all_done() def on_error(req, error): print(f"Network error for {url}: {error}") self._finish_download_if_all_done() UrlRequest( url=url, on_success=on_success, on_failure=on_failure, on_error=on_error, timeout=10, req_headers={"User-Agent": "Kivyapp/1.0"} ) def process_response(self, url, raw_data): """在子线程中执行所有耗时操作""" try: # 模拟耗时解析(可替换为 pandas 处理、SQL 写入等) time.sleep(3) # ⚠️ 此 sleep 不影响 GUI data = json.loads(raw_data.decode()) # ✅ 处理完成后,安全更新 UI(必须用 @mainthread) Clock.schedule_once( lambda dt: self.on_response_processed(url, data), 0 ) except Exception as e: print(f"Processing error for {url}: {e}") @mainthread def on_response_processed(self, url, data): """仅在此处更新 UI 元素(安全)""" print(f"✅ Processed {url}, got {len(data)} keys") # 例如:更新列表、保存到本地、刷新表格... def _finish_download_if_all_done(self): # 可配合计数器/事件标志判断是否全部完成 pass @mainthread def kill_spinner(self): self.ids.spinner.active = False
? 关键要点总结:
- UrlRequest 回调 ≠ 子线程执行,它是主线程回调,务必保持轻量;
- 所有 CPU 密集型、I/O 密集型或 time.sleep() 操作,必须放入 Thread 或 asyncio.to_thread()(Kivy 2.3+)中;
- UI 更新(如 self.ids.spinner.active = False)只能在主线程进行,使用 @mainthread 或 Clock.schedule_once 是唯一安全方式;
- 避免在回调中直接调用 self.kill_spinner() —— 它应在所有下载逻辑真正收尾后触发(例如用 threading.Event 或 concurrent.futures.as_completed 统一管理完成信号);
- 若需链式处理(下载 → 解析 → 存储 → 刷新),建议改用 aiohttp + asyncio(需 Kivy 异步支持补丁)或 kivy.clock.CyClockBase 配合 ThreadPoolExecutor 提升可维护性。
遵循以上模式,你的 Spinner 将全程流畅旋转,用户交互零卡顿。