Build local push connectivity for restricted networks
System Frameworks 高级 22m

为受限网络构建本地推送连接

Build local push connectivity for restricted networks

2020年6月25日

在 Apple 官方观看视频

一句话判断

如果你的 App 需要在没有互联网的环境中接收实时推送(比如企业内网、智能家居局域网),Local Push Connectivity 是你的解决方案——它允许设备在后台维持与本地服务器的持久连接。

这场 Session 讲了什么

传统的推送通知依赖 APNs(Apple Push Notification service),这需要互联网连接。但在某些场景中——企业内网、工厂车间、智能家居系统——设备可能没有互联网访问,但需要接收来自本地服务器的推送。iOS 14 引入了 Local Push Connectivity 框架来解决这个问题。

Session 介绍了 NWPushManagerNWConnection 的配合使用。你的 App 可以声明一个 Network Extension,系统会在后台帮你维持与指定本地服务器的 TCP/TLS 连接。当服务器发送数据时,系统会唤醒你的 App 来处理。这个框架特别适合 MDM(移动设备管理)、IoT 设备控制、和工厂自动化等场景。

值得深挖的点

Network Extension 的后台连接

Local Push Connectivity 基于 Network Extension 框架。你需要在项目中添加一个 Network Extension target,声明 com.apple.developer.networking.networkextension entitlement。系统会在后台管理这个 Extension 的生命周期——即使你的 App 被系统挂起,Extension 仍然保持与本地服务器的连接。当有数据到达时,Extension 可以通过 contentProvider 唤起主 App。

与 APNs 的对比

Local Push 不是 APNs 的替代品,而是互补方案。APNs 适合互联网场景,Local Push 适合局域网场景。两者的 API 设计理念不同:APNs 是”推送后不管”,Local Push 是”保持长连接”。你的 App 可以同时使用两种推送方式,根据网络环境自动选择。

代码片段

创建 Network Extension

场景:配置 Local Push Connectivity 的 Network Extension。

// 在 Network Extension target 中创建 Push Provider
import NetworkExtension

class LocalPushProvider: NEAppPushProvider {
    
    override func start() {
        print("Local Push Provider 启动")
        
        // 建立与本地服务器的连接
        connectToLocalServer()
    }
    
    override func stop(with reason: NEProviderStopReason) {
        print("Local Push Provider 停止: \(reason)")
        // 清理连接
    }
    
    private func connectToLocalServer() {
        // 配置本地服务器地址
        let localHost = NWEndpoint.Host("192.168.1.100")
        guard let port = NWEndpoint.Port(rawValue: 8443) else { return }
        
        // 使用 TLS 加密连接
        let tlsOptions = NWProtocolTLS.Options()
        let tcpOptions = NWProtocolTCP.Options()
        tcpOptions.keepaliveCount = 3
        tcpOptions.keepaliveIdle = 60  // 60 秒无活动后发送心跳
        
        let parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions)
        
        let connection = NWConnection(host: localHost, port: port, using: parameters)
        
        connection.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                print("已连接到本地推送服务器")
                self?.startReceiving(connection: connection)
            case .failed(let error):
                print("连接失败: \(error)")
                // 自动重连
                self?.scheduleReconnect()
            default:
                break
            }
        }
        
        connection.start(queue: .main)
    }
    
    private func startReceiving(connection: NWConnection) {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) {
            [weak self] data, context, isComplete, error in
            
            if let data = data {
                self?.handleIncomingData(data)
            }
            
            if !isComplete {
                // 继续接收
                self?.startReceiving(connection: connection)
            }
        }
    }
    
    private func handleIncomingData(_ data: Data) {
        // 解析服务器推送的数据
        guard let message = String(data: data, encoding: .utf8) else { return }
        print("收到本地推送: \(message)")
        
        // 通知主 App
        let contentProvider = NEAppPushContent()
        contentProvider.title = "新消息"
        contentProvider.body = message
        contentProvider.sound = .default
        
        deliverPushContent(contentProvider) { error in
            if let error = error {
                print("推送投递失败: \(error)")
            }
        }
    }
    
    private func scheduleReconnect() {
        // 延迟 5 秒后重连
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.connectToLocalServer()
        }
    }
}

在主 App 中配置 Network Extension

场景:在 App 启动时激活 Local Push。

import NetworkExtension

class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        setupLocalPush()
        return true
    }
    
    private func setupLocalPush() {
        // 查找 Local Push Manager
        let manager = NEAppPushProviderManager.shared()
        
        manager.loadAllFromPreferences { providers, error in
            if let error = error {
                print("加载 Push Provider 失败: \(error)")
                return
            }
            
            if providers?.isEmpty ?? true {
                // 首次使用:创建新的 Provider 配置
                self.createPushProvider()
            } else {
                // 已有配置,检查状态
                print("Push Provider 已配置")
            }
        }
    }
    
    private func createPushProvider() {
        let provider = NEAppPushProvider()
        provider.providerBundleIdentifier = "com.yourapp.local-push-extension"
        provider.isEnabled = true
        
        // 配置本地服务器地址
        provider.providerConfiguration = [
            "serverHost": "192.168.1.100",
            "serverPort": 8443
        ]
    }
}

处理收到的本地推送

场景:在主 App 中响应 Local Push 唤起。

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    // 当 Network Extension 唤起主 App 时调用
    func scene(_ scene: UIScene,
               openURLContexts URLContexts: Set<UIOpenURLContext>) {
        // 处理本地推送带来的 URL
    }
    
    // 处理推送内容
    func handleLocalPushContent(_ content: NEAppPushContent) {
        switch content.userInfo?["type"] as? String {
        case "alert":
            showEmergencyAlert(content.body)
        case "update":
            refreshData()
        case "command":
            executeRemoteCommand(content.userInfo)
        default:
            break
        }
    }
    
    private func showEmergencyAlert(_ message: String) {
        let alert = UIAlertController(
            title: "紧急通知",
            message: message,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "确认", style: .default))
        window?.rootViewController?.present(alert, animated: true)
    }
}

最佳实践

已有项目:如果你的 App 需要在无互联网的环境中接收推送(比如企业内网的即时通讯),评估 Local Push Connectivity 是否适合。它需要用户在设置中授权 Network Extension,并且只支持 TLS 加密连接。

新项目:在设计局域网通信架构时就考虑 Local Push。传统方案是轮询(Polling),效率低下且耗电。Local Push 使用长连接,只在有数据时才唤醒 App。确保你的本地服务器支持 TLS——Local Push 不支持明文连接。

还有什么值得关注

  • Local Push Connectivity 需要特殊的 Entitlement,需要向 Apple 申请。
  • 连接的 Keepalive 间隔建议设置为 60 秒,太频繁会消耗电量。
  • 这个框架与企业环境的 MDM 推送场景高度相关。
WWDC 2020