You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

主线程调用CLLocationManager.locationServicesEnabled()致UI卡顿求助

解决CLLocationManager主线程阻塞警告及天气查询失败问题

问题原因

你遇到的警告是因为CLLocationManager.locationServicesEnabled()是同步阻塞方法,在主线程调用可能导致UI卡顿。更关键的是,当前代码的授权和位置获取逻辑存在流程错误:

  • 调用requestWhenInUseAuthorization()后立刻检查服务状态、获取位置,但授权是异步操作,用户尚未完成授权选择时,状态判断和位置获取都是无效的,这也是页面无法加载的核心原因。
  • 错误类型映射错误:locationServicesEnabled()返回false是系统位置服务整体禁用,而非用户拒绝授权,两者需要区分。

解决方案

核心思路是依赖CLLocationManager的回调处理授权和位置更新,而非同步阻塞检查,具体步骤:

  • 让WeatherService遵守CLLocationManagerDelegate,通过回调处理授权状态变化和位置更新
  • 用Combine的Subject管理位置状态,实现异步的位置获取与API请求联动
  • 移除主线程的同步检查逻辑,在授权回调中处理服务启用状态和位置更新触发
  • 获取到有效位置后立即停止更新,减少不必要的耗电

修改后的完整代码

import Foundation
import CoreLocation
import Combine

typealias LocationNameResultType = Result<String, Error>

// 修正错误类型,区分不同场景
enum WeatherServiceErrors: Error {
    case locationNil
    case placeMarkNil
    case locationServicesDisabled
    case userDeniedAuthorization
    case authorizationNotDetermined
}

class WeatherService: WeatherServiceProtocol, CLLocationManagerDelegate {
    
    private let apiProvider = APIProvider<WeatherEndpoint>()
    private let locationManager = CLLocationManager()
    private var locationSubject = PassthroughSubject<CLLocation, Error>()
    private var cancellables = Set<AnyCancellable>()
    private var currentLocation: CLLocation?

    init() {
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
        // 初始化时请求授权,后续逻辑交给回调处理
        locationManager.requestWhenInUseAuthorization()
    }
    
    func getCityName(completion: @escaping (LocationNameResultType) -> Void) {
        guard let location = currentLocation else {
            completion(.failure(WeatherServiceErrors.locationNil))
            return
        }
        
        let geocoder = CLGeocoder()
        // 反向地理编码建议放在后台线程执行,避免阻塞主线程
        DispatchQueue.global().async {
            geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
                DispatchQueue.main.async {
                    if let error = error {
                        completion(.failure(error))
                        return
                    }
                    
                    guard let placemark = placemarks?.first,
                          let cityName = placemark.locality else {
                        completion(.failure(WeatherServiceErrors.placeMarkNil))
                        return
                    }
                    
                    completion(.success(cityName))
                }
            }
        }
    }
    
    func requestCurrentWeather() -> AnyPublisher<Data, Error> {
        // 如果已有位置,直接发起请求;否则等待位置Subject的回调
        if let location = currentLocation {
            return fetchWeather(for: location)
        } else {
            return locationSubject
                .first()
                .flatMap { [weak self] location in
                    guard let self = self else {
                        return Fail(error: WeatherServiceErrors.locationNil).eraseToAnyPublisher()
                    }
                    return self.fetchWeather(for: location)
                }
                .eraseToAnyPublisher()
        }
    }
    
    deinit {
        locationManager.stopUpdatingLocation()
        cancellables.forEach { $0.cancel() }
    }
    
    // MARK: - Private Methods
    private func fetchWeather(for location: CLLocation) -> AnyPublisher<Data, Error> {
        apiProvider.getData(
            from: .getCurrentWeather(latitude: location.coordinate.latitude,
                                     longitude: location.coordinate.longitude)
        )
        .eraseToAnyPublisher()
    }
    
    // MARK: - CLLocationManagerDelegate
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse, .authorizedAlways:
            // 授权通过,检查系统位置服务是否启用
            guard CLLocationManager.locationServicesEnabled() else {
                locationSubject.send(completion: .failure(WeatherServiceErrors.locationServicesDisabled))
                return
            }
            manager.startUpdatingLocation()
        case .denied, .restricted:
            locationSubject.send(completion: .failure(WeatherServiceErrors.userDeniedAuthorization))
        case .notDetermined:
            // 初始状态,等待用户选择
            break
        @unknown default:
            locationSubject.send(completion: .failure(WeatherServiceErrors.authorizationNotDetermined))
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let latestLocation = locations.last else {
            locationSubject.send(completion: .failure(WeatherServiceErrors.locationNil))
            return
        }
        // 保存位置并停止更新
        currentLocation = latestLocation
        locationSubject.send(latestLocation)
        manager.stopUpdatingLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        locationSubject.send(completion: .failure(error))
    }
}

关键修改点说明

  • 授权与位置回调:通过locationManagerDidChangeAuthorization处理授权状态,只有授权通过且服务启用时才开始更新位置
  • 异步位置传递:用PassthroughSubject实现位置的异步传递,requestCurrentWeather可以等待位置就绪后再发起API请求
  • 主线程优化:反向地理编码放在后台线程执行,避免阻塞UI;移除了主线程的同步locationServicesEnabled()检查,改在授权回调中处理(此时用户已完成授权选择,操作更安全)
  • 错误类型修正:区分了系统位置服务禁用、用户拒绝授权等不同错误场景,便于后续处理
  • 资源优化:获取到位置后立即停止更新位置,减少耗电

内容的提问来源于stack exchange,提问作者skyline1801

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 14:13:27