
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 将全程流畅旋转,用户交互零卡顿。









