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

在异步DispatchQueue中设置值时出现罕见崩溃,求排查方案

iOS Swift 异步设置属性时EXC_BAD_ACCESS崩溃排查与解决

问题场景

实现了一个Storage类,通过DispatchQueue异步设置属性last,在ViewController展示完成后执行赋值逻辑,极少数情况下会在self._last = newValue处触发EXC_BAD_ACCESS崩溃。

代码片段

final class Storage {
  ...

  var last: LastItems? {
    get {
      queue.sync { [unowned self] in
        return self._last
      }
    }
    set {
      queue.async { [unowned self] in
        self._last = newValue // 崩溃位置
      }
    }
  }
  private var _last: LastItems?
}

struct LastItems {
  let request: MyRequest
  let myPublisher: PassthroughSubject<MyState, Never>
}

// 视图控制器展示完成后执行以下代码:
let last = LastItems(
  request: request,
  myPublisher: myPublisher
)
Storage.shared.items.last = last

崩溃调用栈

#0  0x000000018bf86e04 in bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) ()
#1  0x0000000104b32884 in outlined destroy of MyInfo ()
#2  0x0000000104deb7d8 in outlined assign with take of LastItems? ()
#3  0x0000000104de7a28 in closure #1 in Items.last.setter at /Items.swift:21 // Crash here
#4  0x0000000104bb4044 in thunk for @escaping @callee_guaranteed () -> () ()
#5  0x0000000106444528 in _dispatch_call_block_and_release ()
#6  0x0000000106445d50 in _dispatch_client_callout ()
#7  0x000000010644e014 in _dispatch_lane_serial_drain ()
#8  0x000000010644ed6c in _dispatch_lane_invoke ()
#9  0x000000010645cb74 in _dispatch_workloop_worker_thread ()
#10 0x00000001b18348fc in _pthread_wqthread ()
Enqueued from com.apple.main-thread (Thread 1) Queue : com.apple.main-thread (serial)
#0  0x000000010644b0ac in dispatch_async ()
#1  0x00000001a2af80dc in _swift_dispatch_async ()
#2  0x00000001a2af6ed0 in OS_dispatch_queue.async(group:qos:flags:execute:) ()
#3  0x0000000104de7880 in Items.last.setter at MyApp/Items.swift:20
#4  0x0000000104dd8428 in closure #1 in AnyPublisher<>.storeObjects(_:_:) at MyApp/StoreObjectsOperator.swift:28
#5  0x0000000104dd84bc in partial apply for closure #1 in AnyPublisher<>.storeObjects(_:_:) ()
#6  0x000000019c9d8ce4 in Publishers.Map.Inner.receive(_:) ()
#7  0x000000019c9f5570 in Publishers.FlatMap.Outer.receiveInner(_:_:) ()
#8  0x000000019c9f5470 in Publishers.FlatMap.Outer.Side.receive(_:) ()
#9  0x000000019c96a9bc in Future.Conduit.fulfill(_:) ()
#10 0x000000019c96ab34 in Future.Conduit.offer(_:) ()
#11 0x000000019c96ce48 in partial apply for closure #1 in Future.promise(_:) ()
#12 0x000000019c98b068 in ConduitList.forEach(_:) ()
#13 0x000000019c969088 in Future.promise(_:) ()
#14 0x000000019c96cdf8 in partial apply for closure #1 in Future.init(_:) ()
#15 0x0000000104dd6064 in closure #1 in closure #1 in closure #1 in closure #1 in closure #1 in AnyPublisher<>.showView(_:) at MyApp/ShowView.swift:35
#16 0x0000000104e2b2bc in closure #1 in MyViewController.show() at MyApp/MyViewController.swift:485
#17 0x0000000104bb4044 in thunk for @escaping @callee_guaranteed () -> () ()
#18 0x0000000115f9fc84 in -[UIPresentationController transitionDidFinish:] ()
#19 0x0000000115fa7d8c in -[_UICurrentContextPresentationController transitionDidFinish:] ()
#20 0x0000000115fa3438 in __56-[UIPresentationController runTransitionForCurrentState]_block_invoke.88 ()
#21 0x000000011609ce18 in -[_UIViewControllerTransitionContext completeTransition:] ()
#22 0x0000000116b810dc in -[UITransitionView notifyDidCompleteTransition:] ()
#23 0x0000000116b80e78 in -[UITransitionView _didCompleteTransition:] ()
#24 0x0000000116bacd8c in __UIVIEW_IS_EXECUTING_ANIMATION_COMPLETION_BLOCK__ ()
#25 0x0000000116bacff0 in -[UIViewAnimationBlockDelegate _didEndBlockAnimation:finished:context:] ()
#26 0x0000000116b88a08 in -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] ()
#27 0x0000000116b88e44 in -[UIViewAnimationState animationDidStop:finished:] ()
#28 0x0000000116b88f44 in -[UIViewAnimationState animationDidStop:finished:] ()
#29 0x0000000187e09320 in CA::Layer::run_animation_callbacks(void*) ()
#30 0x0000000106445d50 in _dispatch_client_callout ()
#31 0x0000000106456808 in _dispatch_main_queue_drain ()
#32 0x00000001064562d4 in _dispatch_main_queue_callback_4CF ()
#33 0x000000018039a784 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
#34 0x0000000180394de4 in __CFRunLoopRun ()
#35 0x0000000180394254 in CFRunLoopRunSpecific ()
#36 0x0000000188eb7c9c in GSEventRunModal ()
#37 0x0000000116712ff0 in -[UIApplication _run] ()
#38 0x0000000116716f3c in UIApplicationMain ()
#39 0x000000010897c454 in UIApplicationMain(_:_:_:_:) ()
#40 0x0000000104af7d30 in static UIApplicationDelegate.main() ()
#41 0x0000000104af7cac in static AppDelegate.$main() at MyApp/AppDelegate.swift:11
#42 0x0000000104af7dac in main ()
#43 0x0000000105945514 in start_sim ()
#44 0x00000001056a1e50 in start ()
#45 0xaf21800000000000 in 0xaf21800000000000 ()

排查思路

  • unowned self 野指针风险:异步闭包中使用unowned self,若Storage实例在闭包执行前被意外释放,会直接访问已销毁的内存地址,触发EXC_BAD_ACCESS。即使是单例,也可能存在特殊场景下的内存释放问题。
  • 引用类型的释放竞态:LastItems中的myPublisher是引用类型,赋值_last = newValue时会释放旧值。若旧值中的引用类型正在其他线程被访问,或其释放逻辑存在线程不安全问题,可能导致崩溃。
  • 异步赋值的生命周期问题:setter用async延迟执行赋值,newValue的捕获虽然是值类型复制,但如果其中嵌套的引用类型存在提前释放或循环引用,也可能引发内存访问错误。

解决方案

  1. 替换unowned self为weak self
    修改setter中的闭包捕获列表,避免野指针访问:

    set {
      queue.async { [weak self] in
        self?._last = newValue
      }
    }
    

    闭包执行时会先判断self是否存在,不存在则直接跳过赋值,避免访问已释放的实例。

  2. 确保单例的稳定性
    检查Storage单例的实现,推荐使用标准的线程安全单例写法:

    static let shared = Storage()
    private init() {} // 禁止外部初始化
    

    避免单例被意外销毁或重复初始化。

  3. 检查引用类型的内存安全
    排查MyRequest、MyInfo等引用类型的内存管理逻辑,确保没有循环引用,且所有释放操作都是线程安全的。可以尝试简化LastItems结构,移除不必要的引用类型,或对引用类型的访问添加线程保护。

  4. 考虑同步赋值替代异步
    若对主线程性能影响不大,将setter改为sync执行,消除异步带来的生命周期不确定性:

    set {
      queue.sync { [weak self] in
        self?._last = newValue
      }
    }
    

内容的提问来源于stack exchange,提问作者Tometoyou

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 13:09:55