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

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()始终未执行。当前应用不支持后台活动,未来可能会支持,曾看到旧资料称这可能有影响。

核心原因分析
  1. iOS应用终止的时间限制:用户手动划掉应用时,系统留给应用的执行窗口极短。你的turnOffTunnel()依赖异步的NETunnelProviderManager.loadAllFromPreferences,异步操作还未完成,应用就已被系统杀死,导致stopTunnel()根本没机会执行。
  2. applicationWillTerminate触发条件:无后台权限的应用被划掉时,系统可能直接终止进程而非调用该方法,只有应用处于后台未挂起状态时,才大概率触发。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 10:24:56