为URLSession实现依赖注入时触发BAD ACCESS无限循环错误求助
Hey there! Let's tackle that frustrating BAD ACCESS crash you're hitting when using URLSession.shared while setting up dependency injection for mock-based unit testing.
First, Why Is This Happening?
That infinite repeat of URLSession.dataTask(with:completionHandler) until crash almost always boils down to one of two issues:
- Unlimited recursive requests: If your completion handler logic re-triggers the same network call (like an unguarded retry on failure) without a stop condition, you’ll spawn an endless chain of tasks that gobble up memory until the app crashes.
- Retain cycles: If you’re capturing
selfin your completion handler without usingweak/unowned, you might create a cycle that prevents memory from being released—combining this with recursive calls makes the crash happen even faster.
The Fix: Proper Dependency Injection + Clean Request Logic
Let’s walk through the right way to structure your code to avoid this crash and enable mock testing.
1. Abstract URLSession with a Protocol
First, create a protocol to decouple your network code from the concrete URLSession class. This makes mocking trivial:
protocol NetworkSession { func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask } // Make the real URLSession conform to our protocol extension URLSession: NetworkSession {}
2. Inject the Session into Your Network Service
Instead of hardcoding URLSession.shared inside your service, inject it via the initializer. This lets you use the real session in production and a mock in tests:
class NetworkService { private let session: NetworkSession // Default to URLSession.shared for real-world use init(session: NetworkSession = URLSession.shared) { self.session = session } func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) { let task = session.dataTask(with: url) { [weak self] data, response, error in // Guard against self being deallocated mid-execution guard let self = self else { return } // Handle errors without infinite retries! if let error = error { completion(.failure(error)) return // Critical: don't re-call fetchData here without a retry limit } guard let responseData = data else { completion(.failure(NetworkError.noData)) return } completion(.success(responseData)) } task.resume() } } // Helper error enum for clarity enum NetworkError: Error { case noData }
3. Fix the Root Crash Cause
- Add retry limits: If you do need retry logic, implement a counter that stops after a set number of attempts (e.g., 3 retries max) instead of looping infinitely.
- Use weak self: Always capture
selfweakly in completion handlers to avoid retain cycles—this ensures objects can be deallocated properly when they’re no longer needed.
Mocking for Unit Testing
With the protocol in place, creating a mock session is straightforward. For example:
class MockNetworkSession: NetworkSession { var mockData: Data? var mockError: Error? func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { // Immediately trigger the completion with our mock data/error completionHandler(mockData, nil, mockError) // Return a dummy task (we won't resume it in tests) return URLSessionDataTask() } }
You can then use this mock in your tests to simulate success/failure cases without hitting real network calls.
内容的提问来源于stack exchange,提问作者SwiftyJD

