返回

iOS网络崩溃(EXC_BAD_ACCESS)排查与解决

IOS

iOS 网络连接崩溃 (EXC_BAD_ACCESS KERN_INVALID_ADDRESS) 问题排查与解决

直接点说,就是从 Firebase Crashlytics 看到一个崩溃报告:Crashed: com.apple.network.connections EXC_BAD_ACCESS KERN_INVALID_ADDRESS,但自己又没法重现,完全没头绪。给的堆栈信息也没具体指出哪里出问题,日志和面包屑也没啥规律。

这种崩溃通常和网络连接相关,出现 EXC_BAD_ACCESSKERN_INVALID_ADDRESS 意味着程序试图访问无效的内存地址。这多半是因为对象已经被释放,或者在多线程环境下访问共享资源时出了问题。

一、 问题原因分析

网络连接相关的崩溃通常原因比较隐蔽, 下面几种情况都可能中招:

  1. 过早释放对象: 网络请求通常是异步的,如果请求还没完成,就把相关的对象(比如 delegate、completion handler 使用的对象等)给释放了,回调的时候就会出事。
  2. 多线程问题: 如果多个线程同时访问和修改网络相关的资源(例如,连接状态、数据缓冲区),没有做好同步,就可能导致数据错乱或访问到无效内存。
  3. 底层网络库问题: 虽然不太常见,但 iOS 系统底层网络框架 (com.apple.network.connections) 本身也可能存在 bug,导致崩溃。这通常需要系统更新才能解决。
  4. 内存损坏: 比较极端的情况下,设备的内存出现问题也可能导致这类崩溃。 但这种情况通常不只是网络连接会出问题, 别的地方也会蹦。
  5. 第三方库的问题 :如果使用了第三方网络库, 则这个库本身的bug也会导致此类问题。
  6. 野指针或悬垂指针 :访问已释放的对象或未正确初始化的指针。

二、解决方案

遇到这种问题, 先别慌, 逐步排查:

1. 仔细审查网络请求相关的代码

重点关注网络请求的发起、回调处理、以及相关对象的生命周期。

  • 检查 Delegate: 确保网络请求的 delegate 没有被提前释放。例如,如果 delegate 是 ViewController,确保在 ViewController 销毁前取消网络请求,或者将 delegate 设置为 nil。

    class MyViewController: UIViewController, URLSessionDataDelegate {
        var dataTask: URLSessionDataTask?
    
        func startRequest() {
            let url = URL(string: "https://example.com")!
            dataTask = URLSession.shared.dataTask(with: url) { data, response, error in
                // ... 处理回调 ...
            }
            dataTask?.delegate = self //如果用了自定义的delegate
            dataTask?.resume()
        }
    
        func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
             //处理错误, 注意检查是否为nil
        }
    
        // 确保在 ViewController 销毁前取消请求或设置delegate 为 nil
        deinit {
            dataTask?.cancel()
            dataTask?.delegate = nil
        }
    }
    
  • 检查 Completion Handler: 如果使用了 completion handler,确保 handler 中访问的对象在回调时仍然有效。 避免在block 里直接访问 self, 很容易出现循环引用, 建议使用 [weak self]

    func fetchData(completion: @escaping (Data?, Error?) -> Void) {
        let url = URL(string: "https://example.com")!
        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self = self else { return } //避免循环引用
            // ... 处理回调 ...
            completion(data, error)
        }
        task.resume()
    }
    
  • 生命周期管理 : 使用 URLSessionTask 时, 仔细跟踪task的生命周期。 必要时可以打印 task 的 state (.running, .suspended, .canceling, .completed) 来帮助判断。

2. 使用 Instruments 进行内存分析

Instruments 是 Xcode 提供的强大的性能分析工具,可以帮助你找到内存泄漏、僵尸对象等问题。

  • Leaks 工具: 检测内存泄漏。如果发现有网络相关的对象泄漏,说明它们可能在不应该被释放的时候被持有了,这可能导致后续访问时崩溃。

  • Zombies 工具: 检测僵尸对象。开启 Zombies 后,如果访问了已经释放的对象,程序会立即停止,并告诉你具体是哪个对象出了问题。这个非常适合用来排查 EXC_BAD_ACCESS

    使用方法:

    1. 在 Xcode 中,选择 Product > Profile。
    2. 选择 Leaks 或 Zombies 模板。
    3. 运行你的应用,复现崩溃(或者尽量模拟崩溃的场景)。
    4. 观察 Instruments 的报告,查找可疑的内存泄漏或僵尸对象。

3. 多线程环境下加锁

如果在多个线程中操作网络相关的资源,一定要确保线程安全。

  • 使用 GCD 队列: 将对共享资源的操作放在同一个串行队列中,确保同一时间只有一个线程可以访问这些资源。

    let networkQueue = DispatchQueue(label: "com.example.networkQueue")
    
    func updateNetworkStatus(status: String) {
        networkQueue.async {
            // ... 更新网络状态 ...
        }
    }
    
    func getNetworkStatus() -> String {
        var status = ""
        networkQueue.sync {
            // ... 获取网络状态 ...
            status = //获取到的值
        }
        return status
    }
    
  • 使用锁: 可以使用 NSLock@synchronized 来保护临界区代码。

    let lock = NSLock()
    
    func updateSomeSharedData() {
        lock.lock()
        // ... 更新共享数据 ...
        lock.unlock()
    }
    

    但是尽量别用 @synchronized, 性能比较差.

4. 检查第三方网络库

如果使用了第三方网络库(如 Alamofire、AFNetworking),检查一下:

  • 更新到最新版本: 确保使用的第三方库是最新版本,因为新版本可能已经修复了已知的 bug。
  • 查看 issue 和文档: 看看第三方库的 issue 列表或文档,有没有其他用户遇到类似的问题。

5. 符号化崩溃日志

如果 Crashlytics 报告中的堆栈信息没有完全符号化(也就是没有显示出具体的类名和方法名),尝试手动符号化。

  1. 找到对应的 dSYM 文件: 每次构建应用时,都会生成一个 dSYM 文件,其中包含了符号信息。确保你找到了崩溃版本对应的 dSYM 文件。

  2. 使用 atos 命令: 可以使用 atos 命令手动符号化堆栈信息。

    atos -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp -arch arm64 -l 0x100000000 0x100004000
    
    • -o:指定 dSYM 文件路径。
    • -arch:指定架构(例如 arm64)。
    • -l:指定加载地址(可以在 Crashlytics 报告中找到)。
    • 0x100004000:需要符号化的地址。

6. 高级调试技巧

  • 添加自定义日志: 在网络请求的关键位置(发起请求、收到响应、处理回调等)添加自定义日志,记录关键信息(请求 URL、参数、响应数据、错误信息等),有助于分析问题。可以考虑把关键的日志信息上传到crashlytics, 帮助在后台进行分析。

  • 查看 NWConnection 的状态信息 (进阶) :

    如果使用了 Network.framework (iOS 12+), 可以利用 NWConnectionstateUpdateHandler 获得更详细的状态信息.

```swift
    let connection = NWConnection(host: "example.com", port: .http, using: .tcp)
       connection.stateUpdateHandler = { (newState) in
            switch newState {
            case .ready:
                print("Connection ready")
            case .waiting(let error):
                print("Connection waiting: \(error)")
            case .failed(let error):
                print("Connection failed: \(error)") //详细的连接错误
            case .cancelled:
                print("Connection cancelled")
            default:
                break
            }
        }
    connection.start(queue: .main)
```

这些错误信息也许可以帮你确定具体哪里出了问题。

7. 安全建议

  • 不要在 dealloc/deinit 中进行耗时的网络操作 : 这可能阻塞主线程,甚至可能导致上述问题. dealloc/deinit 中应该尽快清理资源。
  • 避免过多的网络连接 : 控制同时进行中的网络连接数量. 过多的并发可能增加系统负担.

如果上述方法都不能解决问题,可以考虑向 Apple 提交 bug report,或者寻求社区的帮助。 但这种崩溃大部分时候还是代码的问题。仔细排查, 多半能搞定!