diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift index 07088a5cf68..3e428f8a88e 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift @@ -59,27 +59,23 @@ final class HeartbeatStorage: Sendable, HeartbeatStorageProtocol { // `nonisolated(unsafe)` to disable concurrency-safety checks. The // property's access is protected by an external synchronization mechanism // (see `instancesLock` property). - private nonisolated(unsafe) static var cachedInstances: [ - String: WeakContainer - ] = [:] + private nonisolated(unsafe) static var cachedInstances: AtomicBox< + [String: WeakContainer] + > = AtomicBox([:]) #else // TODO(Xcode 16): Delete this block when minimum supported Xcode is // Xcode 16. - private static var cachedInstances: [ - String: WeakContainer - ] = [:] + static var cachedInstances: AtomicBox<[String: WeakContainer]> = + AtomicBox([:]) #endif // compiler(>=6) - /// Used to synchronize concurrent access to the `cachedInstances` property. - private static let instancesLock = NSLock() - /// Gets an existing `HeartbeatStorage` instance with the given `id` if one exists. Otherwise, /// makes a new instance with the given `id`. /// /// - Parameter id: A string identifier. /// - Returns: A `HeartbeatStorage` instance. static func getInstance(id: String) -> HeartbeatStorage { - instancesLock.withLock { + cachedInstances.withLock { cachedInstances in if let cachedInstance = cachedInstances[id]?.object { return cachedInstance } else { @@ -110,8 +106,8 @@ final class HeartbeatStorage: Sendable, HeartbeatStorageProtocol { deinit { // Removes the instance if it was cached. - _ = Self.instancesLock.withLock { - Self.cachedInstances.removeValue(forKey: id) + Self.cachedInstances.withLock { value in + value.removeValue(forKey: id) } } diff --git a/FirebaseCore/Internal/Sources/Utilities/AtomicBox.swift b/FirebaseCore/Internal/Sources/Utilities/AtomicBox.swift new file mode 100644 index 00000000000..6346d05701c --- /dev/null +++ b/FirebaseCore/Internal/Sources/Utilities/AtomicBox.swift @@ -0,0 +1,45 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +final class AtomicBox { + private var _value: T + private let lock = NSLock() + + public init(_ value: T) { + _value = value + } + + public func value() -> T { + lock.withLock { + _value + } + } + + @discardableResult + public func withLock(_ mutatingBody: (_ value: inout T) -> Void) -> T { + lock.withLock { + mutatingBody(&_value) + return _value + } + } + + @discardableResult + public func withLock(_ mutatingBody: (_ value: inout T) throws -> R) rethrows -> R { + try lock.withLock { + try mutatingBody(&_value) + } + } +}