一、现象
在解决第一个 Swizzling 崩溃问题后,模拟器仍然卡在 Launching 界面无法进入。
LLDB thread list 显示:
thread #1: tid = 0xb3c4e9, libsystem_platform.dylib`_platform_memmove, queue = 'com.apple.main-thread'
thread #9: tid = 0xb3c8ad, libsystem_kernel.dylib`__ulock_wait + 10, queue = 'com.sdk.init.asyncqueue.serial'
主线程在做内存操作(正常),但 thread #9 持续卡在 __ulock_wait,队列名是 com.sdk.init.asyncqueue.serial——这是 SDK 初始化队列,不应该卡住。
二、定位过程
执行 thread select 9 + bt,得到调用栈:
frame #0: libsystem_kernel.dylib`__ulock_wait + 10
frame #1: libdispatch.dylib`_dispatch_thread_main_event_wait_slow + 48
frame #2: libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__
frame #3: libdispatch.dylib`_dispatch_sync_f_slow
frame #4: Platform.debug.dylib`-[WPKFileUploaderManager startUploader] + 263
frame #5: Platform.debug.dylib`-[WPKConfigManager startWithAsync] + 947
关键结论:WPKFileUploaderManager(友盟 UMAPM 内部的鹰眼 WPK 组件)在 com.sdk.init.asyncqueue.serial 这个子线程串行队列上执行,但 startUploader 内部调用了 dispatch_sync 回主线程。
三、根本原因
这是一个经典的 dispatch_sync 死锁:
子线程串行队列(com.sdk.init.asyncqueue.serial)
└── WPKConfigManager startWithAsync
└── WPKFileUploaderManager startUploader
└── dispatch_sync(main_queue, ^{ ... }) ← 等主线程空闲
↑
主线程(didFinishLaunching 流程中) |
└── 等待 SDK 初始化完成 ──────────────────────────────────┘
WPK 在子线程异步队列里调用 dispatch_sync(main_queue, ...),而主线程此时正忙于 didFinishLaunching 流程,双方互相等待,形成死锁。
为什么 iOS 26 之前没出现这个问题?iOS 26 改变了 didFinishLaunching 阶段的线程调度时序,使得主线程在 SDK 初始化期间保持更长时间的"忙碌"状态,触发了 WPK 内部这个长期存在的写法隐患。
这是 WPK SDK 本身的 bug:在异步子线程上 dispatch_sync 回主线程是危险写法,主线程任何一段忙碌窗口期都可能触发死锁。
四、踩过的坑
坑 1:用 dispatch_async 包裹初始化,以为能解决
第一个想法是把 configureUMessageWithLaunchOptions: 放进 dispatch_async(dispatch_get_main_queue(), ^{ ... }) 里,把 SDK 初始化推迟到下一个 RunLoop cycle。
但这样治标不治本——WPK 内部的 dispatch_sync 还在,推迟初始化只是改变了触发时机,主线程只要在处理任何任务时被 WPK 调用,死锁依然会发生。实际验证也证明这个方案无效,app 依然卡住。
坑 2:误判为 Swizzling 问题的延续
第一轮修复后 app 还是卡,一度以为是 Swizzling 没修干净,花时间反复排查。实际上第一个问题已经彻底解决,这是一个完全独立的新问题,要重新 thread select + bt 定位。
五、修复方案
思路
UMAPM / WPK 的性能监控和崩溃上报在模拟器上没有实际意义(崩溃上报面向真机生产环境),因此在模拟器上直接跳过初始化,彻底规避死锁。真机流程不受影响。
实现(AppDelegate.m)
- (void)configureUMessageWithLaunchOptions:(NSDictionary *)launchOptions {
// UMAPM: 模拟器上跳过,WPK 内部 dispatch_sync 回主线程会死锁(iOS 26 模拟器复现)
#if !TARGET_OS_SIMULATOR
UMAPMConfig *config = [UMAPMConfig defaultConfig];
config.networkEnable = YES;
config.javaScriptBridgeEnable = YES;
[UMCrashConfigure setAPMConfig:config];
#endif
// 其余友盟初始化(推送、统计等)正常执行...
[UMConfigure initWithAppkey:kUMAppKey channel:channel];
// ...
}
六、局限性与后续
当前方案的局限
在模拟器上禁用 UMAPM 意味着:
- 无性能监控数据:启动时间、页面加载时长、网络请求耗时等指标在模拟器上均无法收集
- 无崩溃上报:模拟器上发生的崩溃不会上报到友盟后台
但这对实际开发影响极小——UMAPM 的监控数据本就应该来自真机(模拟器的性能指标与真机差异极大,参考价值有限),崩溃排查在模拟器上有 LLDB 可以直接定位,不依赖上报数据。
推荐后续方案
等待友盟官方修复:这是 UMAPM/WPK SDK 本身的 bug,正确修法是 WPK 将内部的 dispatch_sync(main_queue, ...) 改为 dispatch_async,或在调用前检测是否在主线程。关注友盟 SDK 更新日志,一旦官方修复,更新 Pod 版本并移除 #if !TARGET_OS_SIMULATOR 保护即可。
七、总结
| 项目 | 说明 |
|---|---|
| 问题类型 | 第三方 SDK 内部死锁(dispatch_sync 回主线程) |
| 触发条件 | iOS 26 改变了 didFinishLaunching 线程调度时序,主线程忙碌窗口变长 |
| 卡死位置 | WPKFileUploaderManager startUploader → dispatch_sync(main_queue) |
| 临时修复 | #if !TARGET_OS_SIMULATOR 在模拟器上跳过 UMAPM 初始化 |
| 副作用 | 模拟器无性能监控和崩溃上报(真机不受影响) |
| 根本解法 | 等友盟官方修复 WPK SDK,更新版本后移除临时保护 |