Updating the UI from another thread in iOS

本文探讨了在iOS应用开发中如何从非主线程安全地更新用户界面元素的问题。使用Grand Central Dispatch (GCD) API可以在后台线程完成任务后确保UI组件在主线程上进行更新,避免了潜在的崩溃和不稳定行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://chritto.wordpress.com/2012/12/20/updating-the-ui-from-another-thread/

Apple’s UIKit is generally designed to be dealt with from within the main thread – operating with it from another thread can result in unpredictable results, including:

  • Everything may be fine
  • The user interface element may not be redrawn after your property changes
  • UIKit will give you a warning in the console, and throw an exception
  • The application will crash
  • The application won’t crash, and the UI will be left in an undefined state

The question then becomes: how can we update our user interface from a background thread? Utilising Apple’s Grand Central Dispatch APIs is one such way.

Suppose one of your UIViewControllers receives a message to update the background color of its main view. This method is called from a background thread (say, when a particular network packet arrives). We can attach ourselves to the main thread, make the call, and then return to our background thread so we can continue to process other network packets.

 
- ( void )updateBackgroundColor:(UIColor *)color
{
     dispatch_sync(dispatch_get_main_queue(), ^{
         self .view.backgroundColor = color;
     });
}

Taking a closer look at this, we have:

 
dispatch_sync(dispatch_queue_t queue, ^( void )block)

The “sync” part of the function name (as differentiated from “async”) means that the code that comes after the dispatch_sync call (and in this case, the code which sent the -updateBackgroundColor: message) will not continue to run until the work inside the block is done. This can be useful where the code that follows makes the assumption that the work inside the block has been done (e.g. if it were then to query the backgroundColor property of the view later on). Making the “async” version of the call throws the work to be done on a queue, and processes it later on, when the specified queue gets its chance to run again. It looks similar to the synchronous version:

1
dispatch_async(dispatch_queue_t queue, ^( void )block)

Many of the dispatch_* functions operate on dispatch queues. Here, we are operating on the main queue, which is connected with the main run loop (our UI thread):

 
dispatch_get_main_queue()

What about if we want to be able to call the same method, but from the UI thread? This creates a problem! The dispatch_sync function places its block of work to be done at the end of the main dispatch queue. It will then block until that work has been done. However, we’re already in the main thread, and until we return to the run loop, the block won’t be scheduled to run. This creates a deadlock – something we want to avoid. We can fix this with the following modification:

 
- ( void )updateBackgroundColor:(UIColor *)color
{
     if ([ NSThread isMainThread])
     {
         self .view.backgroundColor = color;
     }
     else
     {
         dispatch_sync(dispatch_get_main_queue(), ^{
             self .view.backgroundColor = color;
         });
     }
}

This is a little ugly, and duplicates the code – we can change it like this:

 
- ( void )updateBackgroundColor:(UIColor *)color
{
     dispatch_block_t work_to_do = ^{
         self .view.backgroundColor = color;
     }
     if ([ NSThread isMainThread])
     {
         work_to_do();
     }
     else
     {
         dispatch_sync(dispatch_get_main_queue(), work_to_do);
     }
}

Even better, we can throw this into a function which can be used elsewhere:

 
void RUN_ON_UI_THREAD(dispatch_block_t block)
{
     if ([ NSThread isMainThread])
         block();
     else
         dispatch_sync(dispatch_get_main_queue(), block);
}

so that our code is simplified to:

 
- ( void )updateBackgroundColor:(UIColor *)color
{
     RUN_ON_UI_THREAD(^{
         self .view.backgroundColor = color;
     });
}
### 关于 Matplotlib 主线程未处于主循环的问题 当使用 `Matplotlib` 更新图表时,如果遇到错误提示 **Error updating plot: main thread is not in main loop**,这通常是因为 `Matplotlib` 的图形界面依赖于 GUI 后端运行在一个事件循环中。然而,在某些情况下(例如在 Jupyter Notebook 或 Streamlit 中),主线程可能并未启动该事件循环。 以下是解决方案: #### 使用非交互模式绘图 可以通过关闭 `Matplotlib` 的交互模式来避免此问题。默认情况下,`plt.ion()` 开启了交互模式,而将其禁用可以防止事件循环冲突[^1]。 ```python import matplotlib.pyplot as plt # 禁用交互模式 plt.ioff() fig, ax = plt.subplots() ax.plot([1, 2, 3], [4, 5, 6]) plt.show() ``` #### 利用后台线程处理绘图 另一种方法是在单独的线程中执行绘图操作,从而避免阻塞主线程。通过 Python 的 `threading` 模块实现这一点。 ```python import threading import time import matplotlib.pyplot as plt def update_plot(): fig, ax = plt.subplots() while True: ax.clear() ax.plot([1, 2, 3], [4, 5, 6]) # 动态更新数据 plt.pause(0.1) # 延迟刷新时间 time.sleep(1) # 创建并启动新线程 plot_thread = threading.Thread(target=update_plot, daemon=True) plot_thread.start() # 主程序继续运行其他逻辑 while True: time.sleep(1) ``` #### 在 Streamlit 中集成 Matplotlib 图表 由于 Streamlit 提供了专门的方法用于渲染静态图像 (`st.pyplot`),因此可以直接调用它而不需担心多线程问题。每次重新绘制图表前只需清空之前的画布即可。 ```python import streamlit as st import numpy as np import matplotlib.pyplot as plt chart_placeholder = st.empty() # 占位符容器 for i in range(10): fig, ax = plt.subplots() x = np.linspace(0, 10, 100) y = np.sin(x + i * 0.5) # 数据动态变化 ax.plot(x, y) chart_placeholder.pyplot(fig) # 将当前 figure 渲染到页面上 time.sleep(0.5) ``` 以上三种方式均能够有效规避因缺少主循环而导致的异常行为。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值