在异步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的捕获虽然是值类型复制,但如果其中嵌套的引用类型存在提前释放或循环引用,也可能引发内存访问错误。
解决方案
替换
unowned self为weak self
修改setter中的闭包捕获列表,避免野指针访问:set { queue.async { [weak self] in self?._last = newValue } }闭包执行时会先判断self是否存在,不存在则直接跳过赋值,避免访问已释放的实例。
确保单例的稳定性
检查Storage单例的实现,推荐使用标准的线程安全单例写法:static let shared = Storage() private init() {} // 禁止外部初始化避免单例被意外销毁或重复初始化。
检查引用类型的内存安全
排查MyRequest、MyInfo等引用类型的内存管理逻辑,确保没有循环引用,且所有释放操作都是线程安全的。可以尝试简化LastItems结构,移除不必要的引用类型,或对引用类型的访问添加线程保护。考虑同步赋值替代异步
若对主线程性能影响不大,将setter改为sync执行,消除异步带来的生命周期不确定性:set { queue.sync { [weak self] in self?._last = newValue } }
内容的提问来源于stack exchange,提问作者Tometoyou
相关产品推荐
相关产品推荐

