iOS開發解決定位權限卡頓

一、簡介

在iOS系統中,定位權限獲取是一個涉及進程間同步通信的方法,如果頻繁訪問可能會導致卡頓或者卡死。在一些打車或者地圖類的APP中,定位權限的卡頓報錯可能是大頭,亟需解決!
下面是系統類提供的訪問定位權限的方法:

// CLLocationManager是系統的定位服務管理類
open class CLLocationManager : NSObject {
    // 1.下面方法是訪問系統設置中定位是否打開
    @available(iOS 4.0, *)
    open class func locationServicesEnabled() -> Bool

    // 2.1 iOS 14.0之後,訪問定位的授權狀態
    @available(iOS 14.0, *)
    open var authorizationStatus: CLAuthorizationStatus { get }

    // 2.2 iOS 14.0之後,訪問定位的授權狀態
    @available(iOS, introduced: 4.2, deprecated: 14.0)
    open class func authorizationStatus() -> CLAuthorizationStatus
}
二、從卡頓堆棧例子中分析問題

爲了解決這個卡頓,首先要分析卡頓報錯堆棧。接下來舉一個定位權限頻繁獲取導致的卡頓的堆棧:

0 libsystem_kernel.dylib _mach_msg2_trap + 8
1 libsystem_kernel.dylib _mach_msg2_internal + 80
2 libsystem_kernel.dylib _mach_msg_overwrite + 388
3 libsystem_kernel.dylib _mach_msg + 24
4 libdispatch.dylib __dispatch_mach_send_and_wait_for_reply + 540
5 libdispatch.dylib _dispatch_mach_send_with_result_and_wait_for_reply + 60
6 libxpc.dylib _xpc_connection_send_message_with_reply_sync + 240
7 Foundation ___NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__ + 16
8 Foundation -[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:] + 2236
9 Foundation -[NSXPCConnection _sendSelector:withProxy:arg1:arg2:arg3:] + 136
10 Foundation __NSXPCDistantObjectSimpleMessageSend3 + 76
11 CoreLocation _CLCopyTechnologiesInUse + 30852
12 CoreLocation _CLCopyTechnologiesInUse + 25724
13 CoreLocation _CLClientStopVehicleHeadingUpdates + 104440
14 MyAPPName +[MKLocationRecorder locationAuthorised] + 40
15 ... // 以下略
  1. 首先從第14行找到是MKLocationRecorder類的locationAuthorised方法調用後,執行到了系統庫函數,最終導致了卡死、卡頓。
  2. 對堆棧中第0-13行中的方法做一番瞭解,初步發現xpc_connection_send_message_with_reply_sync函數涉及進程間同步通信,可能會阻塞當前線程點擊查看官方方法說明

該函數說明:Sends a message over the connection and blocks the caller until it receives a reply.

  1. 接下來添加符號斷點xpc_connection_send_message_with_reply_sync, 注意如果是系統庫中的帶下劃線的函數,我們添加符號斷點的時候一般需要少一個下劃線_.
    執行後,從Xcode的方法調用棧視圖中查看,可以發現MKLocationRecorder類的locationAuthorised方法內部中調用CLLocationManager類的locationServicesEnabledauthorizationStatus方法都會來到這個符號斷點.所以確定了是這兩個方法導致的卡頓。(調試時並未發現卡頓,只是線上用戶的使用環境更加複雜,卡頓時間長一點就被監控到了,我們目前卡頓監控是3秒,卡死監控是10s+)。
  2. 然後通過CLLocationManager類的authorizationStatus方法說明,發現也是說在權限發生改變後,系統會保證調用代理方法locationManagerDidChangeAuthorization(_:),所以就產生了我們的解決方案,最終上線後也是直接解決了這個卡頓,並且APP啓動耗時監控數據也因此上升了一些。
三、具體的解決方案

注意點:設置代理必須在有runloop的線程,如果業務量不多的話,就在主線程設置就可以。

四、Demo類,可以直接用
import CoreLocation

public class XLLocationAuthMonitor: NSObject, CLLocationManagerDelegate {
    // 單例類
    @objc public static let shared = XLLocationAuthMonitor()
    
    /// 定位服務是否可用, 這裏設置成變量避免過於頻繁調用系統方法時產生卡頓,系統方法涉及進程間通信
    @objc public private(set) var serviceEnabled: Bool {
        set {
            threadSafe { _serviceEnabled = newValue }
        }
        
        get {
            threadSafe { _serviceEnabled ?? CLLocationManager.locationServicesEnabled() }
        }
    }
    
    /// 定位服務授權狀態
    @objc public private(set) var authStatus: CLAuthorizationStatus {
        set {
            threadSafe { _authStatus = newValue }
        }
        
        get {
            threadSafe {
                if let auth = _authStatus {
                    return auth
                }
                if #available(iOS 14.0, *) {
                    return locationManager.authorizationStatus
                } else {
                    return CLLocationManager.authorizationStatus()
                }
            }
        }
    }
    
    /// 計算屬性,這裏返回當前定位是否可用
    @objc public var isLocationEnable: Bool {
        guard serviceEnabled else {
            return false
        }
        
        switch authStatus {
        case .authorizedAlways, .authorizedWhenInUse:
            return true
        case .denied, .notDetermined, .restricted:
            return false
        default: return false
        }
    }
    
    // MARK: - 內部使用的私有屬性
    private lazy var locationManager: CLLocationManager = CLLocationManager()
    private let _lock = NSLock()
    private var _serviceEnabled: Bool?
    private var _authStatus: CLAuthorizationStatus?
    
    private override init() {
        super.init()
        // 如果是主線程則直接設置,不是則在mainQueue中設置
        DispatchQueue.main.safeAsync {
            self.locationManager.delegate = self
        }
    }
    
    private func threadSafe<T>(task: () -> T) -> T {
        _lock.lock()
        defer { _lock.unlock() }
        return task()
    }
    
    // MARK: - CLLocationManagerDelegate
    /// iOS 14以上調用
    public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if #available(iOS 14.0, *) {
            authStatus = locationManager.authorizationStatus
            serviceEnabled = CLLocationManager.locationServicesEnabled()
        }
    }
    
    /// iOS 14以下調用
    public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        authStatus = status
        serviceEnabled = CLLocationManager.locationServicesEnabled()
    }
}

參考文章:
出行iOS用戶端卡頓治理實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章