Skip to content

01 - iOS 地图与位置服务全栈

目录

  1. Core Location 架构
  2. CLLocationManager 核心 API
  3. 地理编码与反地理编码
  4. MapKit 地图框架
  5. 地图标记与覆盖物
  6. 地理围栏与区域监测
  7. 导航与路线
  8. 地图性能优化
  9. 位置与地图 Android 对比
  10. 面试考点汇总

1. Core Location 架构

1.1 Core Location 框架结构

Core Location 框架:

┌──────────────────────────────────────────┐
│              Core Location               │
├────────────────┬────────────────────────┤
│  CLLocationMan  │  CLLocation            │
│  (定位管理器)   │  (位置数据)             │
├────────────────┼────────────────────────┤
│  CLBeaconRegion │  CLGeocoder            │
│  (信标区域)    │  (地理编码)             │
├────────────────┼────────────────────────┤
│  CLRegion      │  CLVisit               │
│  (区域类)      │  (访问历史)             │
└────────────────┴────────────────────────┘

1.2 位置精度

位置精度层级:

┌──────────────────────┬──────────────────────┐
│ 精度级别              │  误差范围           │
├──────────────────────┼──────────────────────┤
│ kCLLocationAccuracyBest │  ~3m (GPS)        │
│ kCLLocationAccuracyNear │  ~10m (Wi-Fi)     │
│ kCLLocationAccuracyHundredMeters │ ~100m    │
│ kCLLocationAccuracyKilometer │ ~1000m      │
│ kCLLocationAccuracyThreeKilometers │ ~3km   │
│ kCLLocationAccuracyBestForNavigation │ GPS + 惯性  │
└──────────────────────┴──────────────────────┘

2. CLLocationManager 核心 API

2.1 基本定位

swift
class LocationManager: NSObject, CLLocationManagerDelegate {
    
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = 10  // 每移动 10 米更新
    }
    
    func requestAlwaysAuthorization() {
        locationManager.requestAlwaysAuthorization()
    }
    
    func requestWhenInUseAuthorization() {
        locationManager.requestWhenInUseAuthorization()
    }
    
    func startUpdatingLocation() {
        locationManager.startUpdatingLocation()
    }
    
    // MARK: - CLLocationManagerDelegate
    
    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        print("位置: \(location.coordinate.latitude), \(location.coordinate.longitude)")
        print("海拔: \(location.altitude) m")
        print("精度: \(location.horizontalAccuracy) m")
        print("速度: \(location.speed) m/s")
        print("航向: \(location.course}°")
    }
    
    func locationManager(_ manager: CLLocationManager,
                         didFailWithError error: Error) {
        print("定位失败: \(error.localizedDescription)")
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedAlways, .authorizedWhenInUse:
            manager.startUpdatingLocation()
        case .denied, .restricted:
            print("用户拒绝定位权限")
        case .notDetermined:
            requestWhenInUseAuthorization()
        @unknown default:
            break
        }
    }
}

2.2 权限类型

权限类型Info.plist 键行为
AlwaysNSLocationAlwaysAndWhenInUseUsageDescription前台+后台
When In UseNSLocationWhenInUseUsageDescription仅前台
Always (旧)NSLocationAlwaysUsageDescription已弃用

2.3 停止定位

swift
// 停止更新以节省电量
locationManager.stopUpdatingLocation()

// 按距离触发更新
locationManager.startMonitoringSignificantLocationChanges()
// 显著位置变化(基站切换),后台也能触发

// 按时间触发更新
locationManager.startUpdatingLocation()
locationManager.pausesLocationUpdatesAutomatically = true

3. 地理编码与反地理编码

3.1 坐标 → 地址(反地理编码)

swift
let geocoder = CLGeocoder()

geocoder.reverseGeocodeLocation(clLocation) { placemarks, error in
    guard let placemark = placemarks?.first else { return }
    
    // 地址组成部分
    print("街道: \(placemark.thoroughfare)")
    print("城市: \(placemark.locality)")
    print("省: \(placemark.administrativeArea)")
    print("国家: \(placemark.country)")
    print("邮编: \(placemark.postalCode)")
    print("时区: \(placemark.timeZone?.identifier)")
}

// CLPlacemark 属性:
// thoroughfare - 街道
// subThoroughfare - 门牌号
// locality - 城市
// administrativeArea - 省/州
// country - 国家
// postalCode - 邮编
// timezone - 时区
// timeZone - 时区对象

3.2 地址 → 坐标(地理编码)

swift
geocoder.geocodeAddressString("北京市海淀区中关村大街1号") { placemarks, error in
    guard let placemark = placemarks?.first else { return }
    print("坐标: \(placemark.location?.coordinate)")
}

4. MapKit 地图框架

4.1 MKMapView 基础

swift
import MapKit

class MapViewController: UIViewController {
    
    @IBOutlet weak var mapView: MKMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        mapView.delegate = self
        mapView.mapType = .standard  // .standard/.satellite/.hybrid/.mutedStandard
        mapView.isZoomEnabled = true
        mapView.isScrollEnabled = true
        mapView.showsUserLocation = true
    }
}

// MARK: - MKMapViewDelegate

extension MapViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView,
                 regionDidChangeAnimated animated: Bool) {
        // 地图区域变化
        let center = mapView.centerCoordinate
        print("中心: \(center.latitude), \(center.longitude)")
    }
    
    func mapView(_ mapView: MKMapView,
                 didUpdate userLocation: MKUserLocation) {
        // 用户位置更新
        let loc = userLocation.location?.coordinate
    }
}

4.2 地图类型

类型MKMapType说明
标准.standard道路地图
卫星.satellite卫星图像
混合.hybrid卫星+道路叠加
暗色标准.mutedStandard低饱和度暗色地图
地形.terrain地形图
卫星地形.satelliteFlyover3D 卫星
混合地形.hybridFlyover3D 混合

4.3 地图区域控制

swift
// 设置显示区域
let region = MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074),
    span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
mapView.setRegion(region, animated: true)

// 显示特定区域(带边距)
let visibleRect = mapView.visibleMapRect
let insets = UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50)
mapView.setVisibleMapRect(visibleRect, edgePadding: insets, animated: true)

// 从坐标到屏幕位置
let point = mapView.convert(coordinate, toPointTo: mapView)

// 从屏幕位置到坐标
let coordinate = mapView.convert(point, toCoordinateFrom: mapView)

5. 地图标记与覆盖物

5.1 MKAnnotation

swift
// 自定义标注
struct PlaceAnnotation: MKAnnotation {
    let coordinate: CLLocationCoordinate2D
    let title: String?
    let subtitle: String?
    let identifier: String
}

// 添加标注
let annotation = PlaceAnnotation(
    coordinate: CLLocationCoordinate2D(latitude: 39.9, longitude: 116.4),
    title: "北京",
    subtitle: "首都",
    identifier: "beijing"
)
mapView.addAnnotation(annotation)

// 批量添加
mapView.addAnnotations(annotations)

// 自定义标注视图
func mapView(_ mapView: MKMapView,
             viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    guard !(annotation is MKUserLocation) else { return nil }
    
    let identifier = "placePin"
    var annotationView = mapView.dequeueReusableAnnotationView(
        withIdentifier: identifier
    )
    
    if annotationView == nil {
        annotationView = MKPinAnnotationView(annotation: annotation,
                                             reuseIdentifier: identifier)
        (annotationView as? MKPinAnnotationView)?.pinTintColor = .red
        (annotationView as? MKPinAnnotationView)?.animatesDrop = true
        annotationView?.isDraggable = true
    } else {
        annotationView?.annotation = annotation
    }
    
    // 添加右按钮
    let detailButton = UIButton(type: .detailDisclosure)
    annotationView?.rightCalloutAccessoryView = detailButton
    annotationView?.canShowCallout = true
    
    return annotationView
}

5.2 MKOverlay(覆盖物)

swift
// 绘制圆形覆盖物
let circle = MKCircle(center: coordinate, radius: 1000)  // 1km 半径
mapView.addOverlay(circle)

func mapView(_ mapView: MKMapView,
             rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if let circle = overlay as? MKCircle {
        let renderer = MKCircleRenderer(circle: circle)
        renderer.fillColor = UIColor.red.withAlphaComponent(0.3)
        renderer.strokeColor = .red
        renderer.lineWidth = 2
        return renderer
    }
    return MKOverlayRenderer()
}

// 多边形覆盖物
let path = CGMutablePath()
path.addLines(between: [p1, p2, p3, p4])
let polygon = MKPolygon(path: path, coordinateSystem: .linear)
mapView.addOverlay(polygon)

// 自定义折线
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)

6. 地理围栏与区域监测

6.1 CLCircularRegion

swift
// 创建地理围栏
let region = CLCircularRegion(
    center: CLLocationCoordinate2D(latitude: 39.9, longitude: 116.4),
    radius: 500,  // 500 米半径
    identifier: "home"
)
region.notifyEntryStateOnDisplay = true

locationManager.startMonitoring(for: region)

// MARK: - CLLocationManagerDelegate

func locationManager(_ manager: CLLocationManager,
                     didEnterRegion region: CLRegion) {
    print("进入围栏: \(region.identifier)")
}

func locationManager(_ manager: CLLocationManager,
                     didExitRegion region: CLRegion) {
    print("离开围栏: \(region.identifier)")
}

func locationManager(_ manager: CLLocationManager,
                     monitoringDidFailFor region: CLRegion?,
                     withError error: Error) {
    print("围栏监测失败: \(error.localizedDescription)")
}

7. 导航与路线

7.1 MKMapItem & MKDirections

swift
let source = MKMapItem(forCurrentLocation())!
let destination = MKMapItem(placemark: destinationPlacemark)

let request = MKDirectionsRequest()
request.source = source
request.destination = destination
request.transportType = .automobile  // .automobile/.transit/.walking
request.requestsAlternateRoutes = true

let directions = MKDirections(request: request)
directions.calculate { response, error in
    guard let route = response?.routes.first else { return }
    
    print("距离: \(route.distance / 1000) km")
    print("预计时间: \(route.expectedTravelTime / 60) 分钟")
    
    // 在地图上显示路线
    mapView.addOverlay(route.polyline)
    mapView.setVisibleMapRect(route.polyline.boundingMapRect, animated: true)
}

7.2 MKMapUserTrackingMode

swift
mapView.userTrackingMode = .follow          // 跟随用户
mapView.userTrackingMode = .followWithHeading // 跟随+航向
mapView.userTrackingMode = .none             // 不跟随

8. 地图性能优化

8.1 渲染优化

优化手段说明效果
按需加载标注regionWillAnimate 时加载⭐⭐⭐
复用 annotationViewdequeueReusableAnnotationView⭐⭐⭐
减少 overlay 数量聚合标注⭐⭐⭐
自定义 rendererMKOverlayRenderer⭐⭐
缓存 placemark避免重复地理编码⭐⭐
异步地理编码dispatch queue⭐⭐

9. 位置与地图 Android 对比

概念iOSAndroid/Kotlin
定位管理器CLLocationManagerFusedLocationProviderClient
地图MapKit (Apple 原生)Google Maps SDK / 高德/百度
地理编码CLGeocoderGeocoder / Maps SDK
地理围栏CLCircularRegion.startMonitoringGeofencingApi
导航MKDirections + MapsDirections API
用户位置mapView.showsUserLocationGoogleMap.myLocationEnabled
权限NSLocationWhenInUseUsageDescriptionACCESS_FINE_LOCATION
后台定位significantLocationChangesforegroundService

10. 面试考点汇总

高频面试题

  1. iOS 定位权限有哪几种?区别?

    • When In Use:仅前台
    • Always:前台+后台
    • 用户拒绝后无法自动获取,需在设置中手动开启
  2. CLLocationManager 的主要职责?

    • 管理定位权限请求
    • 控制定位精度和更新频率
    • 处理位置更新回调和错误
  3. 地理围栏的工作原理?

    • 基于基站/Wi-Fi/GPS 监测用户进入/离开区域
    • 系统级别监测(后台/锁屏也能触发)
    • 最多监控 25 个区域
  4. MapKit 和 MKMapView 的核心 API?

    • MKMapView 显示地图
    • MKAnnotation 添加标注
    • MKOverlay 添加覆盖物
    • MKCircleRenderer/MKPolylineRenderer 渲染
  5. 如何优化地图性能?

    • 按需加载标注
    • 复用 annotationView
    • 减少 overlay 数量(聚合)
    • 异步地理编码

面试回答模板

"iOS 位置服务用 Core Location 框架,MKMapView 展示地图。定位权限分前台和后台两种。地理围栏用 CLCircularRegion 监测进入/离开事件。地图标注用 MKAnnotation + MKAnnotationView,覆盖物用 MKOverlay。性能优化关键是按需加载和复用视图。"


本文档对标 Android 28_Location_Map/ 的深度