主线程中也不绝对安全的UI操作

从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行。这是因为 UIKit 的方法不是线程安全的,保证线程安全需要极大的开销。那么问题来了,在主线程中进行 UI 操作一定是安全的么?

显然,答案是否定的!

在苹果的 MapKit 框架中,有一个叫做 addOverlay 的方法,它在底层实现的时候,不仅仅要求代码执行在主线程上,还要求执行在 GCD 的主队列上。这是一个极罕见的问题,但已经有人在使用 ReactiveCocoa 时踩到了坑,并提交了 issue。

苹果的 Developer Technology Support 承认这是一个 bug。不管这是 bug 还是历史遗留设计,也不管是不是在钻牛角尖,为了避免再次掉进同样的坑,我认为都有必要分析一下问题发生的原因和解决方案。

GCD 知识复习

在 GCD 中,使用 dispatch_get_main_queue() 函数可以获取主队列。调用 dispatch_sync() 方法会把任务同步提交到指定的队列。

注意一下队列和线程的区别,他们之间并没有“拥有关系(ownership)”,当我们同步的提交一个任务时,首先会阻塞当前队列,然后等到下一次 runloop 时再在合适的线程中执行 block。

在执行 block 之前,首先会寻找合适的线程来执行block,然后阻塞这个线程,直到 block 执行完毕。寻找线程的规则是: 任何提交到主队列的 block 都会在主线程中执行,在不违背此规则的前提下,文档还告诉我们系统会自动进行优化,尽可能的在当前线程执行 block。

顺便补充一句,GCD 死锁的充分条件是:“向当前队列重复同步提交 block”。从原理来看,死锁的原因是提交的 block 阻塞了队列,而队列阻塞后永远无法执行完 dispatch_sync(),可见这里完全和代码所在的线程无关。

另一个例子也可以证明这一点,在主线程中向一个串行队列同步的派发 block,根据上文选择线程的原则,block 将在主线程中执行,但同样不会导致死锁:


  1. dispatch_queue_t queue = dispatch_queue_create("com.kt.deadlock", nil); 
  2. dispatch_sync(queue, ^{ 
  3.     NSLog(@"current thread = %@", [NSThread currentThread]); 
  4. }); 
  5. // 输出结果: 
  6. // current thread = {number = 1, name = main}  

原因分析

啰嗦了这么多,回到之前描述的 bug 中来。现在我们知道,即使是在主线程中执行的代码,也很可能不是运行在主队列中(反之则必然)。如果我们在子队列中调用 MapKit 的 addOverlay 方法,即使当前处于主线程,也会导致 bug 的产生,因为这个方法的底层实现判断的是主队列而非主线程。

更进一步的思考,有时候为了保证 UI 操作在主线程运行,如果有一个函数可以用来创建新的 UILabel,为了确保线程安全,代码可能是这样:


  1. - (UILabel *)labelWithText: (NSString *)text { 
  2.     __block UILabel *theLabel; 
  3.     if ([NSThread isMainThread]) { 
  4.         theLabel = [[UILabel alloc] init]; 
  5.         [theLabel setText:text]; 
  6.     } 
  7.     else { 
  8.         dispatch_sync(dispatch_get_main_queue(), ^{ 
  9.             theLabel = [[UILabel alloc] init]; 
  10.             [theLabel setText:text]; 
  11.         }); 
  12.     } 
  13.     return theLabel; 
  14. }  

从严格意义上来讲,这样的写法不是 100% 安全的,因为我们无法得知相关的系统方法是否存在上述 Bug。

解决方案

由于提交到主队列的 block 一定在主线程运行,并且在 GCD 中线程切换通常都是由指定某个队列引起的,我们可以做一个更加严格的判断,即用判断是否处于主队列来代替是否处于主线程。

GCD 没有提供 API 来进行相应的判断,但我们可以另辟蹊径,利用 dispatch_queue_set_specific 和 dispatch_get_specific 这一组方法为主队列打上标记:


  1. + (BOOL)isMainQueue { 
  2.     static const void* mainQueueKey = @"mainQueue"; 
  3.     static void* mainQueueContext = @"mainQueue"; 
  4.   
  5.     static dispatch_once_t onceToken; 
  6.     dispatch_once(&onceToken, ^{ 
  7.         dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil); 
  8.     }); 
  9.   
  10.     return dispatch_get_specific(mainQueueKey) == mainQueueContext; 
  11. }  

用 isMainQueue 方法代替 [NSThread isMainThread] 即可获得更好的安全性。

作者:伯乐专栏/张星宇

来源:51CTO

时间: 2024-08-30 12:15:45

主线程中也不绝对安全的UI操作的相关文章

主线程中的handle问题

昨天遇到的一个问题,就是我在主线程中生成了handler对象,但是在下边进行消息的发送的时候意外的报了异常,说是 E/AndroidRuntime( 1819): java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()   E/AndroidRuntime( 1819):        at android.os.Handler.(Handler.jav

java在一个主线程中开了很多小线程,当主程异常后,希望能关闭这些服务用的小线程,以免冲突。该怎么做

问题描述 java在一个主线程中开了很多小线程,当主程异常后,希望能关闭这些服务用的小线程,以免冲突.该怎么做 我是这样写的: thread mainThread(){ while(true){ init; try{ new serverThread1; ... new serverThread2; ... ... }catch(e){ ... if(flag_exit)break; ... } } } 通常,这个主线程会经常遇问题,所以会抛出异常,因为有while(true),它会从头开始运行

多线程-c#如何在子线程中获取form主线程中按钮点击事件

问题描述 c#如何在子线程中获取form主线程中按钮点击事件 如何在子线程中获取form主线程中按钮点击事件,子线程B中定义了一个新类classnew,获取点击事件也是在新类classnew中,并且获取完点击事件以后执行此类中下边的任务. 解决方案 你可以主线程中得到点击事件后,发送一个Event事件通知等告诉子线程,然后子线程就可以进行后面的任务处理 解决方案二: 不知道你说的获取事件是什么意思,是事件挂钩还是获得挂钩的事件处理函数的委托.请你说清楚. 解决方案三: 主线程中得到点击事件后,发

Android中主线程与子线程之间相互通信教程

有时候,我们也可能碰到这样子的一种需求:需要主线程来向子线程发送消息,希望子线程来完成什么任务.如果这样子应该怎么做呢?这就是这篇文章将要讨论的内容. 一.HandlerThread类 主线程发送消息给子线程,通常思维逻辑就是:其实很简单,在主线程中实例化一个Handler,然后让他与子线程相关联(只要它与子线程的Looper相关联即可),这样子它处理的消息就是该子线程中的消息队列,而处理的逻辑都是在该子线程中执行的,不会占用主线程的时间.那么我们就来实现一下,看看这样子到底行得通还是行不通.新

Android任意时刻从子线程切换到主线程的实现

引入 在Android开发中常常会遇到网络请求,数据库数据准备等一些耗时的操作:而这些操作是不允许在主线程中进行的.因为这样会堵塞主线程导致程序出现未响应情况. 所以只能另起一个子线程进行这些耗时的操作,完成后再显示到界面.众所周知,界面等控件操作只能在主线程中完成:所以不可避免的需要从子线程切换到主线程. 方法 对于这样的情况在Android 中比较常见的是使用AsynTask类或者 Handler来进行线程切换:而其中AsynTask是官方封装的类,较为简单,效率也比较可以,但是并不适合所有

windows主线程如何等待子线程结束

问题描述 windows主线程如何等待子线程结束 我在主线程中起了多个子线程,想等所有子线程结束主线程再继续做后面的事情. 但是我自起子线程的函数下面用WaitForMultipleObjects等待所有子线程结束,会阻塞主线程导致程序无反应死掉. 请各位大侠帮忙看下应该如何处理? 解决方案 WaitForMultipleObjects 确实会阻塞主线程的,不想阻塞主线程就只能自己实现,使用事件或者其它类似的机制. 系统没有现成的,只能自己使用线程间通讯的机制来实现. 解决方案二: MsgWai

Android Handler主线程和一般线程通信的应用分析_Android

Handler的定义:主要接受子线程发送的数据, 并用此数据配合主线程更新UI.解释: 当应用程序启动时,Android首先会开启一个主线程 (也就是UI线程) , 主线程为管理界面中的UI控件,进行事件分发, 比如说, 你要是点击一个 Button ,Android会分发事件到Button上,来响应你的操作.如果此时需要一个耗时的操作,例如: 联网读取数据,或者读取本地较大的一个文件的时候,你不能把这些操作放在主线程中,如果你放在主线程中的话,界面会出现假死现象, 如果5秒钟还没有完成的话,会

Handler详解系列(二)——主线程向自身消息队列发消息

MainActivity如下: package cc.c; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.w

Java多线程--让主线程等待所有子线程执行完毕在执行_java

朋友让我帮忙写个程序从文本文档中导入数据到oracle数据库中,技术上没有什么难度,文档的格式都是固定的只要对应数据库中的字段解析就行了,关键在于性能. 数据量很大百万条记录,因此考虑到要用多线程并发执行,在写的过程中又遇到问题,我想统计所有子进程执行完毕总共的耗时,在第一个子进程创建前记录当前时间用System.currentTimeMillis()在最后一个子进程结束后记录当前时间,两次一减得到的时间差即为总共的用时,代码如下  long tStart = System.currentTim