Search code examples
iosswiftcrashios16

Crash on iOS 16+ _nw_path_evaluator_call_update_handler_block_invoke NWPathMonitor.pathUpdateHandler callback in Network framework of apple


Trying to monitor network changes in iOS app with Network framework from apple.

import Foundation
import Network

protocol NWPathMonitorInterface {
    var pathUpdateHandler: ((_ newPath: NWPath) -> Void)? {get set}
    func start(queue: DispatchQueue)
    func cancel()
}

extension NWPathMonitor: NWPathMonitorInterface {}

final class ReachabilityManager {
   
   private(set) var isNetworkAvailable: Bool = false
   private(set) var connectionType: NWInterface.InterfaceType?
   
   private let queue: DispatchQueue
   private var monitor: NWPathMonitorInterface?
   
   public var onUpdateNetworkStatus: ((Bool) -> Void)?
   static let shared = ReachabilityManager(monitor: NWPathMonitor(),
                                           queue: DispatchQueue(label: "com.rapido.networkMonitoring"))

   deinit {
       stopMonitoring()
   }
   
   init(monitor: NWPathMonitorInterface,
        queue: DispatchQueue) {
       self.monitor = monitor
       self.queue = queue
   }
   
   enum ConnectionType {
       case wifi
       case cellular
       case ethernet
   }
   
   public func startMonitoring() {
       if monitor == nil {
           monitor = NWPathMonitor()
       }
       self.monitor?.start(queue: queue)
       self.monitor?
           .pathUpdateHandler = { [weak self] path in
               guard let self = self else { return }
               self.updateStatus(status: path.status)
               self.updateConnectionType(interface: path
                   .availableInterfaces
                   .map(\.type))
       }
   }
   
   
   func updateStatus(status: NWPath.Status) {
       isNetworkAvailable = status == .satisfied ? true : false
       onUpdateNetworkStatus?(isNetworkAvailable)
   }
   
   func updateConnectionType(interface: [NWInterface.InterfaceType]) {
       connectionType = interface.first
   }
   
   func stopMonitoring() {
       monitor?.cancel()
       monitor = nil
   }
}

Start Monitoring when application state change to foreground and stop monitoring when application enter background

 func applicationDidBecomeActive(_ application: UIApplication) {
          ReachabilityManager.shared.startMonitoring()
 }

 func applicationDidEnterBackground(_ application: UIApplication) {
        ReachabilityManager.shared.stopMonitoring()
 }

crash

Adding crash log

Crashed: com.apple.root.default-qos
0  libswiftCore.dylib             0x3da1bc _swift_release_dealloc + 32
1  libswiftNetwork.dylib          0x372a4 closure #1 in NWPathMonitor.init(requiredInterfaceType:) + 296
2  libswiftNetwork.dylib          0x2470 thunk for @escaping @callee_guaranteed (@guaranteed OS_nw_path) -> () + 52
3  Network                        0x91dad8 __nw_path_evaluator_call_update_handler_block_invoke + 336
4  libdispatch.dylib              0x24b4 _dispatch_call_block_and_release + 32
5  libdispatch.dylib              0x3fdc _dispatch_client_callout + 20
6  libdispatch.dylib              0x70c8 _dispatch_queue_override_invoke + 788
7  libdispatch.dylib              0x15a6c _dispatch_root_queue_drain + 396
8  libdispatch.dylib              0x16284 _dispatch_worker_thread2 + 164
9  libsystem_pthread.dylib        0xdbc _pthread_wqthread + 228
10 libsystem_pthread.dylib        0xb98 start_wqthread + 8

One strange thing about crash happen only in iOS 16+ devices and 23% background state. I am not able to reproduce this still locally as frequency is very low. Any help is appreciated.


Solution

  • It seems somehow instance of NWMonitor get dealloc in some edge although not replicated to me. Adding more sanity with isMonitoring flag. Now it is 100% crash free.

    Updated code

    import Network
    
    protocol NWPathMonitorInterface {
         var pathUpdateHandler: ((_ newPath: NWPath) -> Void)? {get set}
         func start(queue: DispatchQueue)
         func cancel()
         var currentPath: NWPath { get }
    }
    
    extension NWPathMonitor: NWPathMonitorInterface {}
    
    final class ReachabilityManager {
        
        private(set) var isNetworkAvailable: Bool = false
        private(set) var connectionType: NWInterface.InterfaceType?
        private(set) var isMonitoring: Bool = false
        
        private let queue: DispatchQueue
        private var monitor: NWPathMonitorInterface?
        
        public var onUpdateNetworkStatus: ((Bool) -> Void)?
        static let shared = ReachabilityManager(monitor: NWPathMonitor(),
                                                queue: DispatchQueue(label: "com.rapido.networkMonitoring"))
    
        deinit {
            stopMonitoring()
        }
        
        init(monitor: NWPathMonitorInterface,
             queue: DispatchQueue) {
            self.monitor = monitor
            self.queue = queue
        }
        
        enum ConnectionType {
            case wifi
            case cellular
            case ethernet
        }
        
        public func startMonitoring() {
            guard !isMonitoring else {
                return
            }
            if monitor == nil {
                monitor = NWPathMonitor()
            }
            self.monitor?.start(queue: queue)
            self.monitor?
                .pathUpdateHandler = { [weak self] status in
                    guard let self = self,
                          let monitor = self.monitor else { return }
                
                    self.updateStatus(status: monitor.currentPath.status)
                    self.updateConnectionType(interface: monitor.currentPath
                        .availableInterfaces
                        .map(\.type))
            }
            isMonitoring = true
        }
        
        
        func updateStatus(status: NWPath.Status) {
            isNetworkAvailable = status == .satisfied ? true : false
            onUpdateNetworkStatus?(isNetworkAvailable)
        }
        
        func updateConnectionType(interface: [NWInterface.InterfaceType]) {
            connectionType = interface.first
        }
        
        func stopMonitoring() {
            monitor?.cancel()
            monitor = nil
            isMonitoring = false 
        }
    }