如何提升Android ContactsContract批量查询联系人数据的性能
Hey there! I see you're dealing with a brutal performance bottleneck when fetching contacts on Android—looping through each contact ID and making separate queries for name, phone, email, and address is killing speed for 1000+ contacts. Let's fix this by leveraging the ContactsContract.Data table properly, so we only make one single query instead of thousands.
Why Your Current Code Is Slow
Your current flow does this for every contact:
- Grab a contact ID from
ContactsContract.Contacts - Run a separate query for name data
- Run another query for phone numbers
- A third query for emails
- A fourth query for addresses
That's 4 extra queries per contact—1000 contacts = 4000 total queries (plus the initial one)! Each query involves disk I/O and content provider overhead, which adds up fast and makes the process feel sluggish.
The Better Approach: Query ContactsContract.Data Once
All contact-related data (names, phones, emails, addresses) lives in the ContactsContract.Data table. Each row has a MIMETYPE tag that tells you exactly what kind of data it represents. We can query all the fields we need in one go, then group the results by CONTACT_ID in memory to build each contact's full profile.
Required Columns to Query
We'll pull these columns from ContactsContract.Data to cover all your needs:
CONTACT_ID: To group entries by individual contactMIMETYPE: To identify if the row is a name, phone, email, or address- Name field:
StructuredName.GIVEN_NAME - Phone fields:
Phone.NUMBER,Phone.TYPE,Phone.LABEL - Email fields:
Email.DATA,Email.LABEL - Address field:
StructuredPostal.CITY
Optimized Code Implementation
Here's the refactored code that does everything in one query, with efficient in-memory grouping:
val resolver: ContentResolver = contentResolver // Define all columns we need in a single query val projection = arrayOf( ContactsContract.Data.CONTACT_ID, ContactsContract.Data.MIMETYPE, // Name columns ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, // Phone columns ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.LABEL, // Email columns ContactsContract.CommonDataKinds.Email.DATA, ContactsContract.CommonDataKinds.Email.LABEL, // Address columns ContactsContract.CommonDataKinds.StructuredPostal.CITY ) // Query all relevant data in one go, filtered to only the MIMETYPES we care about val cursor = resolver.query( ContactsContract.Data.CONTENT_URI, projection, "${ContactsContract.Data.MIMETYPE} IN (?, ?, ?, ?)", arrayOf( ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ), // Sort by CONTACT_ID to make grouping consecutive and easier "${ContactsContract.Data.CONTACT_ID} ASC" ) if (cursor != null && cursor.count > 0) { // Show progress UI myProgressBar?.progress = 0 myProgressBarCircleText?.text = getString(R.string.processing_contacts) myProgressBar?.visibility = View.VISIBLE myProgressBarCircle?.visibility = View.VISIBLE myProgressBarCircleText?.visibility = View.VISIBLE val jsonAllContacts = JSONObject() jsonAllContacts.put("source", "2") // Process on background thread to avoid blocking UI Thread(Runnable { // Map to hold contact JSON objects, keyed by CONTACT_ID val contactMap = mutableMapOf<String, JSONObject>() val totalRows = cursor.count var currentProgress = 0 while (cursor.moveToNext()) { // Update progress bar safely on UI thread currentProgress = ((cursor.position.toFloat() / totalRows.toFloat()) * 100).toInt() runOnUiThread { myProgressBar?.progress = currentProgress } val contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)) // Get existing contact JSON or create a new one val currentContact = contactMap.getOrPut(contactId) { JSONObject() } val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)) when (mimeType) { // Handle name data ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { val givenName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)) ?: "" currentContact.put("given", givenName) // Add family/middle name handling here if needed } // Handle phone numbers ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { val phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))?.replace(" ", "") ?: "" val phoneType = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)) ?: "" val phoneLabel = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LABEL)) ?: "" val label = if (phoneType.isNotEmpty()) phoneType else phoneLabel val jsonPhone = JSONObject().apply { put("label", label) put("number", phoneNumber) } currentContact.accumulate("phone", jsonPhone) } // Handle emails ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> { val email = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)) ?: "" val emailLabel = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.LABEL)) ?: "" val jsonEmail = JSONObject().apply { put("label", emailLabel) put("email", email) } currentContact.accumulate("email", jsonEmail) } // Handle addresses (focused on CITY as requested) ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> { val city = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.CITY)) ?: "" val jsonAddress = JSONObject().apply { put("city", city) // Add other address fields (street, postal code) here if needed } currentContact.accumulate("address", jsonAddress) } } } // Convert contact map values to a JSON array val contactsArray = JSONArray() contactMap.values.forEach { contactsArray.put(it) } jsonAllContacts.put("contacts", contactsArray) // Log or use the final JSON as needed Log.e("JSON", "Final contacts JSON: $jsonAllContacts") // Clean up resources cursor.close() // Hide progress UI safely on UI thread runOnUiThread { myProgressBar?.visibility = View.GONE myProgressBarCircle?.visibility = View.GONE myProgressBarCircleText?.visibility = View.GONE } }).start() } else { cursor?.close() // Handle case where no contacts are found }
Key Improvements
- Single Query: Only one trip to the contacts provider, eliminating hundreds/thousands of unnecessary I/O operations
- In-Memory Grouping: Use a
MutableMapto build each contact's data as we iterate through the cursor, avoiding repeated lookups - Sorted Results: Sorting by
CONTACT_IDensures all entries for a contact are processed consecutively, making grouping smoother - Safe UI Updates: Use
runOnUiThreadto update the progress bar without risking UI thread blocking
Notes
- If you need additional fields (like family name, street address, or postal code), just add them to the
projectionarray and handle them in the correspondingwhenblock - Always close cursors to avoid memory leaks
- Ensure you've declared the
READ_CONTACTSpermission in your app manifest
内容的提问来源于stack exchange,提问作者Mike Casan Ballester

