How does iOS messaging apps like Viber, Telegram, WhatsApp fetch contacts so fast and efficiently
Asked Answered
C

2

7

I don't know if this question qualifies to be here or not, but even after so much of research, I could not find a suitable guide for this question. I hope I get an answer here.

I see that all the messaging apps like Viber, WhatsApp, Telegram fetch the user contacts and parse them so fast and efficiently that there is almost zero delay. I was trying to replicate that but was never successful. It always takes good 40-60 seconds time for parsing 3000 contacts by running the whole operation on the background thread. Even that is causing the UI freezing on slower devices like 5 and 5S. After fetching the contacts I have to send them to the backend to identify which user are registered on the platform which also adds up to the total time. The above mentioned apps does this in no time!

I would be glad if someone can suggest a way to parse the contacts in the most efficient and faster way without blocking the main thread.

Here is the code, I use at the moment.

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{$0.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: $0) }
                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
    }
}


}`

And the contacts are fetched here when the app is launched

private func ApplicationLaunched() {
    DispatchQueue.global(qos: .background).async {
        let contactsService:ContactsService = self.serviceHolder.get()
        contactsService.fetchContacts()
    }
Chondro answered 25/10, 2017 at 9:11 Comment(7)
Just a question, have you tried to play around with the allowedContactKeys? Maybe the CNContactThumbnailImageDataKey is too heavy for 3000 contacts? I never tried it for that many contacts, but I'm fetching 200 contacts almost instantly in my app, but I'm not requesting the thumbnail image.Gritty
Have you tried fetch them in batch?Weirdie
Not sure but I think WhatsApp is start syncing contacts as soon as open app first time . Read this quora.com/How-does-the-contacts-sync-work-in-WhatsApp/answer/…Punchball
Did the almost same except performing all this on main thread and thumb image. And it works very fast, like in Telegram or whatsoever (2000 + contacts).Ellene
@Gritty I tried removing the CNContactThumbnailImageDataKey and CNContactOrganizationNameKey as CNKeyDescriptor. The fetching is faster, but still not the level expected. previously it was taking 65 seconds, but it takes 60-62 secs.Chondro
@FahriAzimov I tried performing on mainThread, The process has become fast, but it's blocking the thread till the time contacts are loaded. So, I don't think it is a wise choice to make because the loading of conversations is blocked till the contacts are fetched.Chondro
Did you try to create your own dispatch queue and running the code in that queue? I think, here problem is using global queue on background quality. Try .userInitiated or other mode, or create your own dispatch queue.Ellene
G
2

My guess is that the number of contacts you're sending to the backend is huge. 3000 Contacts are too much and I think one of the following is happening:

  1. Either the request is too big and it takes time to deliver for the backend.
  2. It's too heavy for the backend and it takes time to process and return to the client, and this is what is causing the delay for you.

The least likely problem is:

  1. Your parsing method is very heavy on the CPU. But this is very unlikely.

Did you measure the duration between the parsing starts and ends?

I think you should measure the durations between all actions you're doing, for example:

  1. Measure how long it takes to fetch the contacts from the device.
  2. Measure how long it takes to parse the contacts.
  3. Measure how long it takes to get a response from the backend.

This will help you pinpoint exactly what is taking too long.

I hope this helps in solving your problem.

Gritty answered 26/10, 2017 at 9:55 Comment(2)
Thanks @TawaNicolas. Your solution really helped me find out the issue. It is the 3, that you have mentioned that is causing the problem. We are using phoneNumberKit to parse numbers and add country code, which is super slow and taking all the time. I moved to libPhoneNumber-iOS which is working perfectly. For fetching and parsing 2900 contacts the time has come down to 3 seconds from 65 seconds, including the network callChondro
Also, the UI freezing is caused because of filtering the arrays. Once we get response from server, I store all the phone numbers in an array and compare it with the array of all contacts and make a new array with all contacts that are not on our platform. Since it is happening on main-thread the UI is blocked shifting that to global utility thread has prevented the UI blockage.Also, the contacts fetching has been shifted to utility thread to make it fetch faster.Chondro
T
4

The other solution is to actually use the right method in PhoneNumberKit :-)

I had the same issue as you and then realized that PhoneNumberKit has two methods and that I was using the wrong one:

  • A first one, that is used to parse an individual phone number (which is the one you use in your code above). It takes a single object as input.
  • Another one that allows to parse an array of phone number at once. It takes an array of phone number as input.

The naming of these two methods is confusing as they are identical except for their input, but the difference in performance is staggering:

  • Using the individual phone number parsing method (with a for loop like you) took ~60 sec
  • Using the array parsing method parsed ~500 phone numbers in < 2 sec.

So if anyone out there wants to use a Swift native library I would encourage you to use Phone Number Kit as it works great and has a lot of convenient methods (like auto-formatting TextFields).

Turnstile answered 13/6, 2018 at 0:47 Comment(0)
G
2

My guess is that the number of contacts you're sending to the backend is huge. 3000 Contacts are too much and I think one of the following is happening:

  1. Either the request is too big and it takes time to deliver for the backend.
  2. It's too heavy for the backend and it takes time to process and return to the client, and this is what is causing the delay for you.

The least likely problem is:

  1. Your parsing method is very heavy on the CPU. But this is very unlikely.

Did you measure the duration between the parsing starts and ends?

I think you should measure the durations between all actions you're doing, for example:

  1. Measure how long it takes to fetch the contacts from the device.
  2. Measure how long it takes to parse the contacts.
  3. Measure how long it takes to get a response from the backend.

This will help you pinpoint exactly what is taking too long.

I hope this helps in solving your problem.

Gritty answered 26/10, 2017 at 9:55 Comment(2)
Thanks @TawaNicolas. Your solution really helped me find out the issue. It is the 3, that you have mentioned that is causing the problem. We are using phoneNumberKit to parse numbers and add country code, which is super slow and taking all the time. I moved to libPhoneNumber-iOS which is working perfectly. For fetching and parsing 2900 contacts the time has come down to 3 seconds from 65 seconds, including the network callChondro
Also, the UI freezing is caused because of filtering the arrays. Once we get response from server, I store all the phone numbers in an array and compare it with the array of all contacts and make a new array with all contacts that are not on our platform. Since it is happening on main-thread the UI is blocked shifting that to global utility thread has prevented the UI blockage.Also, the contacts fetching has been shifted to utility thread to make it fetch faster.Chondro

© 2022 - 2024 — McMap. All rights reserved.