From d8a47d01824585e4a0dcf6c21e4c8a034428daab Mon Sep 17 00:00:00 2001 From: Marcin Rufer Date: Thu, 19 Feb 2026 13:49:28 +0100 Subject: [PATCH 1/3] Fixed race condition in usage of URLCache. # Conflicts: # CHANGELOG.md --- CHANGELOG.md | 2 ++ Sources/MapboxNavigation/Cache.swift | 17 +++++++++++++++++ .../NavigationViewController.swift | 4 ---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3ae56e256..4813be844fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Added `RouteControllerNotificationUserInfoKey.shouldPlayRerouteSoundKey` to the user info dictionary of `Notification.Name.routeControllerDidReroute` notification. ([#4822](https://github.com/mapbox/mapbox-navigation-ios/pull/4822)) * Fixed a bug with excessive `VisualInstructionDelegate.label(_:willPresent:as:)` delegate method call during initialization. +* Fixed a randomly occuring race condition related to the usage of `URLCache` that could cause a crash. ## v2.20.4 @@ -17,6 +18,7 @@ * Reroute notification sound now plays only after a new route is successfully retrieved. * Fixed a bug with excessive `VisualInstructionDelegate.label(_:willPresent:as:)` delegate method call during initialization. +* Fixed a randomly occuring race condition related to the usage of `URLCache` that could cause a crash. ## v2.20.3 diff --git a/Sources/MapboxNavigation/Cache.swift b/Sources/MapboxNavigation/Cache.swift index e2ec8883b27..d3ccfcf0ca6 100644 --- a/Sources/MapboxNavigation/Cache.swift +++ b/Sources/MapboxNavigation/Cache.swift @@ -47,6 +47,7 @@ internal class URLDataCache: URLCaching { let urlCache: URLCache let defaultCapacity = 5 * 1024 * 1024 + let cacheLock = NSLock() init(memoryCapacity: Int? = nil, diskCapacity: Int? = nil, diskCacheURL: URL? = nil) { let memoryCapacity = memoryCapacity ?? defaultCapacity @@ -60,18 +61,34 @@ internal class URLDataCache: URLCaching { } func store(_ cachedResponse: CachedURLResponse, for url: URL) { + cacheLock.lock() + defer { + cacheLock.unlock() + } urlCache.storeCachedResponse(cachedResponse, for: URLRequest(url)) } func response(for url: URL) -> CachedURLResponse? { + cacheLock.lock() + defer { + cacheLock.unlock() + } return urlCache.cachedResponse(for: URLRequest(url)) } func clearCache() { + cacheLock.lock() + defer { + cacheLock.unlock() + } urlCache.removeAllCachedResponses() } func removeCache(for url: URL) { + cacheLock.lock() + defer { + cacheLock.unlock() + } urlCache.removeCachedResponse(for: URLRequest(url)) } } diff --git a/Sources/MapboxNavigation/NavigationViewController.swift b/Sources/MapboxNavigation/NavigationViewController.swift index 01ee34115d0..197ac967dac 100644 --- a/Sources/MapboxNavigation/NavigationViewController.swift +++ b/Sources/MapboxNavigation/NavigationViewController.swift @@ -867,10 +867,6 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter styleManager = StyleManager() styleManager.delegate = self styleManager.styles = navigationOptions?.styles ?? [DayStyle(), NightStyle()] - - if let currentStyle = styleManager.currentStyle { - updateMapStyle(currentStyle) - } } var currentStatusBarStyle: UIStatusBarStyle = .default From 0cc6e834046eed6771c1f33c33c7f709f597d5a2 Mon Sep 17 00:00:00 2001 From: Marcin Rufer Date: Mon, 23 Feb 2026 18:57:42 +0100 Subject: [PATCH 2/3] Corrections. --- Sources/MapboxNavigation/Cache.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/MapboxNavigation/Cache.swift b/Sources/MapboxNavigation/Cache.swift index d3ccfcf0ca6..0756314fc8b 100644 --- a/Sources/MapboxNavigation/Cache.swift +++ b/Sources/MapboxNavigation/Cache.swift @@ -38,21 +38,21 @@ protocol URLCaching { A general purpose URLCache used by `SpriteRepository` implementations. */ internal class URLDataCache: URLCaching { - let defaultDiskCacheURL: URL = { + private static var defaultDiskCacheURL: URL { let fileManager = FileManager.default let basePath = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! let identifier = Bundle.mapboxNavigation.bundleIdentifier! return basePath.appendingPathComponent(identifier).appendingPathComponent("URLDataCache") - }() + } - let urlCache: URLCache - let defaultCapacity = 5 * 1024 * 1024 - let cacheLock = NSLock() + private let urlCache: URLCache + private static let defaultCapacity = 5 * 1024 * 1024 + private let cacheLock = NSLock() init(memoryCapacity: Int? = nil, diskCapacity: Int? = nil, diskCacheURL: URL? = nil) { - let memoryCapacity = memoryCapacity ?? defaultCapacity - let diskCapacity = diskCapacity ?? defaultCapacity - let diskCacheURL = diskCacheURL ?? defaultDiskCacheURL + let memoryCapacity = memoryCapacity ?? Self.defaultCapacity + let diskCapacity = diskCapacity ?? Self.defaultCapacity + let diskCacheURL = diskCacheURL ?? Self.defaultDiskCacheURL if #available(iOS 13.0, *) { urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, directory: diskCacheURL) } else { From 787cf4bbfefa29bff18c2765ec39ebddfbb4a70b Mon Sep 17 00:00:00 2001 From: Marcin Rufer Date: Tue, 24 Feb 2026 13:09:49 +0100 Subject: [PATCH 3/3] Updated tests. --- Sources/MapboxNavigation/Cache.swift | 8 +++++ .../URLDataCacheTests.swift | 34 ++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/Sources/MapboxNavigation/Cache.swift b/Sources/MapboxNavigation/Cache.swift index 0756314fc8b..d4c1745e67e 100644 --- a/Sources/MapboxNavigation/Cache.swift +++ b/Sources/MapboxNavigation/Cache.swift @@ -49,6 +49,14 @@ internal class URLDataCache: URLCaching { private static let defaultCapacity = 5 * 1024 * 1024 private let cacheLock = NSLock() + var currentMemoryUsage: Int { + urlCache.currentMemoryUsage + } + + var currentDiskUsage: Int { + urlCache.currentDiskUsage + } + init(memoryCapacity: Int? = nil, diskCapacity: Int? = nil, diskCacheURL: URL? = nil) { let memoryCapacity = memoryCapacity ?? Self.defaultCapacity let diskCapacity = diskCapacity ?? Self.defaultCapacity diff --git a/Tests/MapboxNavigationTests/URLDataCacheTests.swift b/Tests/MapboxNavigationTests/URLDataCacheTests.swift index e728d774a15..809764e4795 100644 --- a/Tests/MapboxNavigationTests/URLDataCacheTests.swift +++ b/Tests/MapboxNavigationTests/URLDataCacheTests.swift @@ -5,13 +5,25 @@ import TestHelper class URLDataCacheTest: TestCase { let url = ShieldImage.i280.baseURL var cache: URLDataCache! + + private static var cacheURL: URL { + let fileManager = FileManager.default + let basePath = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! + let identifier = Bundle.main.bundleIdentifier! + return basePath.appendingPathComponent(identifier).appendingPathComponent("TestURLDataCache") + } override func setUp() { super.setUp() self.continueAfterFailure = false - cache = URLDataCache() - cache.urlCache.diskCapacity = 0 + cache = URLDataCache(diskCapacity: 0, diskCacheURL: Self.cacheURL) + cache.clearCache() + } + + override func tearDown() { + cache.clearCache() + super.tearDown() } private func exampleResponse(with storagePolicy: URLCache.StoragePolicy) -> CachedURLResponse { @@ -39,7 +51,7 @@ class URLDataCacheTest: TestCase { cache.clearCache() XCTAssertNil(cache.response(for: url)?.data) - XCTAssertEqual(cache.urlCache.currentMemoryUsage, 0) + XCTAssertEqual(cache.currentMemoryUsage, 0) } func testRemoveRequestCache() { @@ -59,7 +71,7 @@ class URLDataCacheTest: TestCase { let cachedResponse = cache.response(for: url) XCTAssertEqual(cachedResponse, response) - XCTAssertEqual(cache.urlCache.currentMemoryUsage, response.data.count) + XCTAssertEqual(cache.currentMemoryUsage, response.data.count) } func testStoreCacheWithMemoryWarning() { @@ -76,15 +88,19 @@ class URLDataCacheTest: TestCase { let response = exampleResponse(with: .allowedInMemoryOnly) let limitCapacity = 1 - let limitCache = URLDataCache(memoryCapacity: limitCapacity, diskCapacity: limitCapacity) XCTAssertTrue(response.data.count > limitCapacity) + let limitCache = URLDataCache( + memoryCapacity: limitCapacity, + diskCapacity: limitCapacity, + diskCacheURL: Self.cacheURL + ) + limitCache.clearCache() + limitCache.store(response, for: url) XCTAssertNil(cache.response(for: url)) - XCTAssertEqual(limitCache.urlCache.currentMemoryUsage, 0) - - limitCache.urlCache.diskCapacity = 0 - limitCache.clearCache() + XCTAssertEqual(limitCache.currentMemoryUsage, 0) + // not checking disk usage as it is always non-zero } }