SwiftUI iOS中AppDelegate的applicationWillTerminate未触发,如何关闭WireGuard VPN?
开发了一款基于WireGuard框架的iOS VPN应用,采用SwiftUI并使用AppDelegate生命周期,真机测试。需求是:用户手动划掉应用(后台时需保持VPN连接)时,调用applicationWillTerminate并执行turnOffTunnel()关闭VPN,但目前应用关闭后VPN仍在iOS设置中运行。安卓端可实现该功能,询问iOS是否可行。
已尝试的方案:
- Notification Center监听终止事件
- 使用信号量(semaphores)
- 监听主视图ContentView的消失事件
- 将“用户关机或注销时可立即终止应用”设置为NO
设置最后一项后,applicationWillTerminate偶尔被调用,但turnOffTunnel()始终未执行。当前应用不支持后台活动,未来可能会支持,曾看到旧资料称这可能有影响。
- iOS应用终止的时间限制:用户手动划掉应用时,系统留给应用的执行窗口极短。你的
turnOffTunnel()依赖异步的NETunnelProviderManager.loadAllFromPreferences,异步操作还未完成,应用就已被系统杀死,导致stopTunnel()根本没机会执行。 applicationWillTerminate触发条件:无后台权限的应用被划掉时,系统可能直接终止进程而非调用该方法,只有应用处于后台未挂起状态时,才大概率触发。- VPN扩展的独立性:WireGuard的VPN扩展独立于容器App运行,容器App终止后,扩展仍可能维持连接,必须明确发送停止指令才能终止。
iOS完全可以实现该需求,核心是优化终止逻辑,确保VPN关闭操作在系统终止应用前完成,具体方案如下:
1. 提前持有tunnelManager引用
在MyManager中保存已加载的tunnelManager实例,避免在turnOffTunnel()时才异步读取,省去异步加载的时间损耗。
2. 用后台任务延长执行时间
在applicationWillTerminate中启动后台任务,让系统给应用额外时间完成VPN关闭操作(通常最多30秒,足够完成stopTunnel())。
3. 配置VPN后台模式
在Info.plist的UIBackgroundModes数组中加入network-authentication(VPN后台模式),让系统更合理地关联应用与VPN扩展的生命周期,提升applicationWillTerminate的触发概率。
4. 同步化关闭逻辑
将turnOffTunnel()中的异步逻辑改为同步(或用信号量等待异步操作完成),确保操作在应用终止前执行完毕。
AppDelegate修改
class AppDelegate: UIResponder, UIApplicationDelegate { let myManager = MyManager.shared var terminationBackgroundTask: UIBackgroundTaskIdentifier = .invalid func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillTerminate(_ application: UIApplication) { Logger.info("applicationWillTerminate") // 启动后台任务延长执行时间 terminationBackgroundTask = application.beginBackgroundTask(withName: "TerminateVPN", expirationHandler: { application.endBackgroundTask(self.terminationBackgroundTask) self.terminationBackgroundTask = .invalid }) // 同步执行VPN关闭操作 myManager.turnOffTunnelSync() // 结束后台任务 if terminationBackgroundTask != .invalid { application.endBackgroundTask(terminationBackgroundTask) terminationBackgroundTask = .invalid } } }
MyManager类修改
class MyManager: ObservableObject { static let shared = MyManager() var tunnelManager: NETunnelProviderManager? // 提前持有隧道管理器引用 var wgQuickConfig: String? @Published var isConnected: Bool = false private init() {} // ... 原有turnOnTunnel等方法保留 ... // 同步版本的VPN关闭方法 func turnOffTunnelSync() { // 优先使用已持有的tunnelManager if let tunnelManager = self.tunnelManager { guard let session = tunnelManager.connection as? NETunnelProviderSession else { NSLog("Invalid tunnel session") return } switch session.status { case .connected, .connecting, .reasserting: NSLog("Stopping the tunnel sync") session.stopTunnel() DispatchQueue.main.async { self.isConnected = false } self.logConnecitonStatusEX() default: break } return } // fallback:无引用时用信号量等待异步加载完成 let semaphore = DispatchSemaphore(value: 0) NETunnelProviderManager.loadAllFromPreferences { tunnelManagersInSettings, error in defer { semaphore.signal() } if let error = error { NSLog("Error (loadAllFromPreferences): \(error)") return } if let tunnelManager = tunnelManagersInSettings?.first { guard let session = tunnelManager.connection as? NETunnelProviderSession else { NSLog("Invalid tunnel session") return } switch session.status { case .connected, .connecting, .reasserting: NSLog("Stopping the tunnel fallback") session.stopTunnel() DispatchQueue.main.async { self.isConnected = false } self.logConnecitonStatusEX() default: break } } } // 最多等待5秒,避免无限阻塞 _ = semaphore.wait(timeout: .now() + 5) } // 重写turnOnTunnel,成功后保存tunnelManager引用 func turnOnTunnel(completionHandler: @escaping (Bool) -> Void) { // ... 原有代码保留 ... tunnelManager.loadFromPreferences { error in if let error = error { NSLog("Error (loadFromPreferences): \(error)") completionHandler(false) return } // 保存tunnelManager引用 self.tunnelManager = tunnelManager // ... 原有启动隧道的代码 ... } // ... 原有代码保留 ... } }
通过上述修改,iOS端可实现手动划掉应用时关闭VPN的需求:
- 提前持有
tunnelManager避免异步加载延迟 - 后台任务确保系统给足执行时间
- 同步逻辑保证操作在应用终止前完成
- VPN后台模式提升方法触发可靠性
内容的提问来源于stack exchange,提问作者biggreentree

