一、簡介
在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 ... // 以下略
- 首先從第14行找到是
MKLocationRecorder
類的locationAuthorised
方法調用後,執行到了系統庫函數,最終導致了卡死、卡頓。 - 對堆棧中第0-13行中的方法做一番瞭解,初步發現
xpc_connection_send_message_with_reply_sync
函數涉及進程間同步通信,可能會阻塞當前線程點擊查看官方方法說明。
該函數說明:Sends a message over the connection and blocks the caller until it receives a reply.
- 接下來添加符號斷點
xpc_connection_send_message_with_reply_sync
, 注意如果是系統庫中的帶下劃線的函數,我們添加符號斷點
的時候一般需要少一個下劃線_.
執行後,從Xcode的方法調用棧視圖中查看,可以發現MKLocationRecorder
類的locationAuthorised
方法內部中調用CLLocationManager
類的locationServicesEnabled
和authorizationStatus
方法都會來到這個符號斷點
.所以確定了是這兩個方法導致的卡頓。(調試時並未發現卡頓,只是線上用戶的使用環境更加複雜,卡頓時間長一點就被監控到了,我們目前卡頓監控是3秒,卡死監控是10s+)。 - 然後通過
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用戶端卡頓治理實踐