You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何提升Android ContactsContract批量查询联系人数据的性能

Fixing Slow Android Contact Fetching: From 3000 Queries to 1

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:

  1. Grab a contact ID from ContactsContract.Contacts
  2. Run a separate query for name data
  3. Run another query for phone numbers
  4. A third query for emails
  5. 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 contact
  • MIMETYPE: 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

  1. Single Query: Only one trip to the contacts provider, eliminating hundreds/thousands of unnecessary I/O operations
  2. In-Memory Grouping: Use a MutableMap to build each contact's data as we iterate through the cursor, avoiding repeated lookups
  3. Sorted Results: Sorting by CONTACT_ID ensures all entries for a contact are processed consecutively, making grouping smoother
  4. Safe UI Updates: Use runOnUiThread to 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 projection array and handle them in the corresponding when block
  • Always close cursors to avoid memory leaks
  • Ensure you've declared the READ_CONTACTS permission in your app manifest

内容的提问来源于stack exchange,提问作者Mike Casan Ballester

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:46:35