iOS Viber、Telegram、WhatsApp 等消息传递应用程序如何快速高效地获取联系人
How does iOS messaging apps like Viber, Telegram, WhatsApp fetch contacts so fast and efficiently
我不知道这个问题是否有资格出现在这里,但即使经过这么多研究,我也找不到合适的指南来解决这个问题。我希望我能在这里得到答案。
我看到所有的消息传递应用程序,如 Viber、WhatsApp、Telegram,都非常快速有效地获取用户联系人并解析它们,几乎没有延迟。我试图复制它,但从未成功。 运行 后台线程上的整个操作解析 3000 个联系人总是需要 40-60 秒的时间。即使这样也会导致 UI 在较慢的设备(如 5 和 5S)上冻结。获取联系人后,我必须将它们发送到后端以识别哪个用户在平台上注册,这也加起来就是总时间。上面提到的应用程序可以立即做到这一点!
如果有人可以建议一种在不阻塞主线程的情况下以最有效和更快的方式解析联系人的方法,我将很高兴。
这是我目前使用的代码。
final class CNContactsService: ContactsService {
private let phoneNumberKit = PhoneNumberKit()
private var allContacts:[Contact] = []
private let contactsStore: CNContactStore
init(network:Network) {
contactsStore = CNContactStore()
self.network = network
}
func fetchContacts() {
fetchLocalContacts { (error) in
if let uError = error {
} else {
let contactsArray = self.allContacts
self.checkContacts(contacts: contactsArray, checkCompletion: { (Users) in
let nonUsers = contactsArray.filter { contact in
return !Users.contains(contact)
}
self.Users.value = Users
self.nonUsers.value = nonUsers
})
}
}
}
func fetchLocalContacts(_ completion: @escaping (NSError?) -> Void) {
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
//User has denied the current app to access the contacts.
self.displayNoAccessMsg()
case CNAuthorizationStatus.notDetermined:
//This case means the user is prompted for the first time for allowing contacts
contactsStore.requestAccess(for: CNEntityType.contacts, completionHandler: { (granted, error) -> Void in
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
if (!granted ){
DispatchQueue.main.async(execute: { () -> Void in
completion(error as! NSError)
})
} else{
self.fetchLocalContacts(completion)
}
})
case CNAuthorizationStatus.authorized:
//Authorization granted by user for this app.
var contactsArray = [EPContact]()
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
do {
// let phoneNumberKit = PhoneNumberKit()
try self.contactsStore.enumerateContacts(with: contactFetchRequest, usingBlock: { (contact, stop) -> Void in
//Ordering contacts based on alphabets in firstname
if let contactItem = self.contactFrom(contact: contact) {
contactsArray.append(contactItem)
}
})
self.allContacts = contactsArray
completion(nil)
} catch let error as NSError {
print(error.localizedDescription)
completion(error)
}
}
}
private var allowedContactKeys: [CNKeyDescriptor]{
//We have to provide only the keys which we have to access. We should avoid unnecessary keys when fetching the contact. Reducing the keys means faster the access.
return [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactThumbnailImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
]
}
private func checkUsers(contacts:[Contact],checkCompletion:@escaping ([Contact])->Void) {
let phoneNumbers = contacts.flatMap{[=11=].phoneNumbers}
if phoneNumbers.isEmpty {
checkCompletion([])
return
}
network.request(.registeredContacts(numbers: phoneNumbersList), completion: { (result) in
switch result {
case .success(let response):
do {
let profiles = try response.map([Profile].self)
let contacts = profiles.map{ CNContactsService.contactFrom(profile: [=11=]) }
checkCompletion(contacts)
} catch {
checkCompletion([])
}
case .failure:
checkCompletion([])
}
})
}
static func contactFrom(profile:Profile) -> Contact {
let firstName = ""
let lastName = ""
let company = ""
var displayName = ""
if let fullName = profile.fullName {
displayName = fullName
} else {
displayName = profile.nickName ?? ""
}
let numbers = [profile.phone!]
if displayName.isEmpty {
displayName = profile.phone!
}
let contactId = String(profile.id)
return Contact(firstName: firstName,
lastName: lastName,
company: company,
displayName: displayName,
thumbnailProfileImage: nil,
contactId: contactId,
phoneNumbers: numbers,
profile: profile)
}
private func parsePhoneNumber(_ number: String) -> String? {
do {
let phoneNumber = try phoneNumberKit.parse(number)
return phoneNumberKit.format(phoneNumber, toType: .e164)
} catch {
return nil
}
}
}`
应用程序启动时会在此处获取联系人
private func ApplicationLaunched() {
DispatchQueue.global(qos: .background).async {
let contactsService:ContactsService = self.serviceHolder.get()
contactsService.fetchContacts()
}
我的猜测是您发送到后端的联系人数量很大。 3000 个联系人太多了,我认为正在发生以下情况之一:
- 要么是请求太大,后台投递需要时间
- 它对后端来说太重了,它需要时间来处理和 return 给客户端,这就是导致你延迟的原因。
最不可能的问题是:
- 你的解析方法在 CPU 上很重。但这不太可能。
您是否测量了解析开始和结束之间的持续时间?
我认为您应该测量您正在执行的所有操作之间的持续时间,例如:
- 测量从设备中获取联系人需要多长时间。
- 测量解析联系人需要多长时间。
- 测量从后端获得响应需要多长时间。
这将帮助您准确查明什么地方花费的时间过长。
希望对解决您的问题有所帮助。
另一个解决方案是在 PhoneNumberKit 中实际使用正确的方法 :-)
我遇到了和你一样的问题,然后意识到 PhoneNumberKit 有两种方法,而我使用了错误的方法:
- 第一个,用于解析单个 phone 号码(这是您在上面的代码中使用的号码)。它以单个对象作为输入。
- 另一个允许一次解析 phone 个数字的数组。它需要一个 phone 数字数组作为输入。
这两种方法的命名令人困惑,因为它们除了输入外完全相同,但性能上的差异是惊人的:
- 使用个phone个数解析方法(带for循环
像你一样)花了 ~60 秒
- 使用 array 解析方法在 < 2 秒内解析了 ~500 phone 个数字
因此,如果有人想使用 Swift 本机库,我会鼓励您使用 Phone Number Kit,因为它非常好用并且有很多方便的方法(例如自动格式化文本字段).
我不知道这个问题是否有资格出现在这里,但即使经过这么多研究,我也找不到合适的指南来解决这个问题。我希望我能在这里得到答案。
我看到所有的消息传递应用程序,如 Viber、WhatsApp、Telegram,都非常快速有效地获取用户联系人并解析它们,几乎没有延迟。我试图复制它,但从未成功。 运行 后台线程上的整个操作解析 3000 个联系人总是需要 40-60 秒的时间。即使这样也会导致 UI 在较慢的设备(如 5 和 5S)上冻结。获取联系人后,我必须将它们发送到后端以识别哪个用户在平台上注册,这也加起来就是总时间。上面提到的应用程序可以立即做到这一点!
如果有人可以建议一种在不阻塞主线程的情况下以最有效和更快的方式解析联系人的方法,我将很高兴。
这是我目前使用的代码。
final class CNContactsService: ContactsService {
private let phoneNumberKit = PhoneNumberKit()
private var allContacts:[Contact] = []
private let contactsStore: CNContactStore
init(network:Network) {
contactsStore = CNContactStore()
self.network = network
}
func fetchContacts() {
fetchLocalContacts { (error) in
if let uError = error {
} else {
let contactsArray = self.allContacts
self.checkContacts(contacts: contactsArray, checkCompletion: { (Users) in
let nonUsers = contactsArray.filter { contact in
return !Users.contains(contact)
}
self.Users.value = Users
self.nonUsers.value = nonUsers
})
}
}
}
func fetchLocalContacts(_ completion: @escaping (NSError?) -> Void) {
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
//User has denied the current app to access the contacts.
self.displayNoAccessMsg()
case CNAuthorizationStatus.notDetermined:
//This case means the user is prompted for the first time for allowing contacts
contactsStore.requestAccess(for: CNEntityType.contacts, completionHandler: { (granted, error) -> Void in
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
if (!granted ){
DispatchQueue.main.async(execute: { () -> Void in
completion(error as! NSError)
})
} else{
self.fetchLocalContacts(completion)
}
})
case CNAuthorizationStatus.authorized:
//Authorization granted by user for this app.
var contactsArray = [EPContact]()
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
do {
// let phoneNumberKit = PhoneNumberKit()
try self.contactsStore.enumerateContacts(with: contactFetchRequest, usingBlock: { (contact, stop) -> Void in
//Ordering contacts based on alphabets in firstname
if let contactItem = self.contactFrom(contact: contact) {
contactsArray.append(contactItem)
}
})
self.allContacts = contactsArray
completion(nil)
} catch let error as NSError {
print(error.localizedDescription)
completion(error)
}
}
}
private var allowedContactKeys: [CNKeyDescriptor]{
//We have to provide only the keys which we have to access. We should avoid unnecessary keys when fetching the contact. Reducing the keys means faster the access.
return [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactThumbnailImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
]
}
private func checkUsers(contacts:[Contact],checkCompletion:@escaping ([Contact])->Void) {
let phoneNumbers = contacts.flatMap{[=11=].phoneNumbers}
if phoneNumbers.isEmpty {
checkCompletion([])
return
}
network.request(.registeredContacts(numbers: phoneNumbersList), completion: { (result) in
switch result {
case .success(let response):
do {
let profiles = try response.map([Profile].self)
let contacts = profiles.map{ CNContactsService.contactFrom(profile: [=11=]) }
checkCompletion(contacts)
} catch {
checkCompletion([])
}
case .failure:
checkCompletion([])
}
})
}
static func contactFrom(profile:Profile) -> Contact {
let firstName = ""
let lastName = ""
let company = ""
var displayName = ""
if let fullName = profile.fullName {
displayName = fullName
} else {
displayName = profile.nickName ?? ""
}
let numbers = [profile.phone!]
if displayName.isEmpty {
displayName = profile.phone!
}
let contactId = String(profile.id)
return Contact(firstName: firstName,
lastName: lastName,
company: company,
displayName: displayName,
thumbnailProfileImage: nil,
contactId: contactId,
phoneNumbers: numbers,
profile: profile)
}
private func parsePhoneNumber(_ number: String) -> String? {
do {
let phoneNumber = try phoneNumberKit.parse(number)
return phoneNumberKit.format(phoneNumber, toType: .e164)
} catch {
return nil
}
}
}`
应用程序启动时会在此处获取联系人
private func ApplicationLaunched() {
DispatchQueue.global(qos: .background).async {
let contactsService:ContactsService = self.serviceHolder.get()
contactsService.fetchContacts()
}
我的猜测是您发送到后端的联系人数量很大。 3000 个联系人太多了,我认为正在发生以下情况之一:
- 要么是请求太大,后台投递需要时间
- 它对后端来说太重了,它需要时间来处理和 return 给客户端,这就是导致你延迟的原因。
最不可能的问题是:
- 你的解析方法在 CPU 上很重。但这不太可能。
您是否测量了解析开始和结束之间的持续时间?
我认为您应该测量您正在执行的所有操作之间的持续时间,例如:
- 测量从设备中获取联系人需要多长时间。
- 测量解析联系人需要多长时间。
- 测量从后端获得响应需要多长时间。
这将帮助您准确查明什么地方花费的时间过长。
希望对解决您的问题有所帮助。
另一个解决方案是在 PhoneNumberKit 中实际使用正确的方法 :-)
我遇到了和你一样的问题,然后意识到 PhoneNumberKit 有两种方法,而我使用了错误的方法:
- 第一个,用于解析单个 phone 号码(这是您在上面的代码中使用的号码)。它以单个对象作为输入。
- 另一个允许一次解析 phone 个数字的数组。它需要一个 phone 数字数组作为输入。
这两种方法的命名令人困惑,因为它们除了输入外完全相同,但性能上的差异是惊人的:
- 使用个phone个数解析方法(带for循环 像你一样)花了 ~60 秒
- 使用 array 解析方法在 < 2 秒内解析了 ~500 phone 个数字
因此,如果有人想使用 Swift 本机库,我会鼓励您使用 Phone Number Kit,因为它非常好用并且有很多方便的方法(例如自动格式化文本字段).