如何在查询 Android ContactsContract.Contacts 的所有联系人的许多数据时提高性能?

How to improve performance when query many data on ALL contacts from Android ContactsContract.Contacts?

OBJECTIVE

我想为用户phone中的每个联系人获取以下数据

StructuredName.GIVEN_NAME|Phone.NUMBER|Email.DATA|StructuredPostal.CITY

我很确定我使用纯 SQL 查询查询 ContactsContract.Data table,但没有关于如何执行此操作的明确文档。 好像可以inject SQL in the contentResolver.query,但好像不可持续。

问题

我以后的代码可以完美运行,但速度很慢。

基本上,

  1. 我通过它从 ContactsContract.Contacts 和 LOOP 获取所有联系人的 ID,对于每个联系人,
  2. SELECT CommonDataKinds.StructuredName、
  3. 上的命名数据
  4. SELECT 和 LOOP phone 数据 CommonDataKinds.Phone,
  5. SELECT 和 LOOP 电子邮件数据 CommonDataKinds.Email,
  6. SELECT 和 LOOP 地址数据 CommonDataKinds.StructuredPostal

然而,许多循环在性能方面显然适得其反。

对于 1000 个联系人,它会进行大约 3000 个查询。

代码

// CREATE Content resolver
val resolver: ContentResolver = contentResolver
val cursor = resolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        arrayOf(
                ContactsContract.Contacts._ID
        ),
        null,
        null,
        null
)

if ( cursor != null && cursor.count > 0) {
    // PROGRESSBAR Process
    myProgressBar?.progress = 0
    myProgressBarCircleText?.text = getString(R.string.processing_contacts)
    myProgressBar?.visibility = View.VISIBLE
    myProgressBarCircle?.visibility = View.VISIBLE
    myProgressBarCircleText?.visibility = View.VISIBLE



    // PUT BASIC REQUIRED INFO
    val jsonAllContacts = JSONObject()
    jsonAllContacts.put("source", "2")



    // EXECUTE CODE on another thread to prevent blocking UI
    Thread(Runnable {
        var cursorPosition = 0
        var currentProgress: Int


        Log.e("JSON", "cursor.count: ${cursor.count}")


        // CODE TO EXEC LOOP
        while (cursor.moveToNext()) {

            // Increment cursor for progressBar
            cursorPosition += 1
            currentProgress = ((cursorPosition.toFloat() / cursor.count.toFloat()) * 100).toInt()


            // INIT of jsonObjects
            val jsonEmail = JSONObject()
            val jsonPhone = JSONObject()
            val jsonAddress = JSONObject()
            val jsonCurrentContact = JSONObject()



            /**
             * NAME DETAILS
             */
            val contactID = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID))



            val nameCur = contentResolver.query(
                    ContactsContract.Data.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
                            ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
                            ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME
                    ),
                    ContactsContract.Data.CONTACT_ID + " = ?" + " AND " + ContactsContract.Data.MIMETYPE + " = ?",
                    arrayOf(
                            contactID,
                            ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
                    ),
                    null
            )
            var givenName = ""
            var familyName: String
            var middleName: String
            var fullName = ""

            if ( nameCur != null ) {
                while (nameCur.moveToNext()) {

                    givenName = nameCur.getString(nameCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)) ?: ""
                    middleName = nameCur.getString(nameCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME)) ?: ""
                    familyName = nameCur.getString(nameCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)) ?: ""


                    fullName = if ( middleName != "" && (middleName != familyName) ) {
                        "$middleName $familyName"
                    } else {
                        familyName
                    }
                }

                jsonCurrentContact.put("given", givenName)
                jsonCurrentContact.put("family", fullName)
            }
            nameCur?.close()


            /**
             * PHONE NUMBER
             */
            val phoneCur = contentResolver.query(
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.Phone.TYPE,
                            ContactsContract.CommonDataKinds.Phone.LABEL,
                            ContactsContract.CommonDataKinds.Phone.NUMBER
                    ),
                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=?",
                    arrayOf( contactID ),
                    null
            )

            if ( phoneCur != null && phoneCur.count > 0 ) {
                while (phoneCur.moveToNext()) {
                    val phoneNumType = phoneCur.getString( phoneCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE) ) ?: ""
                    val phoneNumLabel = phoneCur.getString( phoneCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LABEL) ) ?: ""
                    var label: String
                    val phoneNumber = phoneCur.getString( phoneCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) ).replace(" ", "") ?: ""

                    //Log.e("JSON", "JSON phoneNum: $phoneNumLabel $phoneNumber")


                    // TRY to get label info
                    label = if ( phoneNumType == "" ) {
                        phoneNumLabel
                    } else {
                        phoneNumType
                    }

                    jsonPhone.put("label", label)
                    jsonPhone.put("number", phoneNumber)
                    jsonCurrentContact.accumulate("phone", jsonPhone)

                }


            }
            phoneCur?.close()


            /**
             * EMAIL
             */
            val emailCur = contentResolver.query(
                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.Email.LABEL,
                            ContactsContract.CommonDataKinds.Email.DATA
                    ),
                    ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?",
                    arrayOf(contactID),
                    null
            )
            if ( emailCur != null ) {

                while (emailCur.moveToNext()) {
                    val emailLabel = emailCur.getString(emailCur.getColumnIndex(ContactsContract.CommonDataKinds.Email.LABEL)) ?: ""
                    val email = emailCur.getString(emailCur.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)) ?: ""

                    jsonEmail.put("label", emailLabel)
                    jsonEmail.put("email", email)
                    jsonCurrentContact.accumulate("email", jsonEmail)

                }

            }
            emailCur?.close()




            /**
             * ADDRESS
             */
            var street: String
            var city: String
            var postalCode: String
            var state: String
            var country: String
            var label: String
            val addressCur = contentResolver.query(
                    ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
                            ContactsContract.CommonDataKinds.StructuredPostal.STREET,
                            ContactsContract.CommonDataKinds.StructuredPostal.CITY,
                            ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE,
                            ContactsContract.CommonDataKinds.StructuredPostal.REGION,
                            ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY
                    ),
                    ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID + "=" + contactID,
                    null,
                    null
            )

            if ( addressCur != null ) {

                while (addressCur.moveToNext()) {
                    label         = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.TYPE)) ?: ""
                    street          = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.STREET)) ?: ""
                    city            = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.CITY)) ?: ""
                    postalCode      = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE)) ?: ""
                    state           = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.REGION)) ?: ""
                    country         = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)) ?: ""

                    jsonAddress.put("label", label)
                    jsonAddress.put("street", street)
                    jsonAddress.put("city", city)
                    jsonAddress.put("postalcode", postalCode)
                    jsonAddress.put("state", state)
                    jsonAddress.put("country", country)
                    jsonCurrentContact.accumulate("address", jsonAddress)

                }

            }
            addressCur?.close()


            Log.e("", "jsonCurrentContact: $jsonCurrentContact")



            // PUT the current JSON object info into an array
            jsonAllContacts.accumulate("contacts", jsonCurrentContact)
        }
        cursor.close()

    }).start()

} else {
    cursor?.close()
}        

您提到的 3000 个查询可以减少到一个,而且应该会很快完成。

我们将在改进代码时利用两件事:

  1. 存储在 ContactsContract.CommonDataKinds.XXX table 中的所有数据实际上存储在一个名为 Data.
  2. 的大 table 中
  3. ContactsContract 中有一个隐式连接,允许我们在查询 Data
  4. 时从 ContactsContract.Contacts 中获取 select 列

为了简化代码,我建议您定义一个 Contact 对象来存储我们为单个联系人找到的信息,并使用 HashMap 将联系人 ID 映射到 Contact 对象

在此处阅读更多相关信息:https://developer.android.com/reference/android/provider/ContactsContract.Data.html

这里有一些代码可以帮助您入门:

Map<Long, Contact> contacts = new HashMap<>();

// If you need item type / label, add Data.DATA2 & Data.DATA3 to the projection
String[] projection = {Data.CONTACT_ID, Data.DISPLAY_NAME, Data.MIMETYPE, Data.DATA1};
// Add more types to the selection if needed, e.g. StructuredName
String selection = Data.MIMETYPE + " IN ('" + Phone.CONTENT_ITEM_TYPE + "', '" + Email.CONTENT_ITEM_TYPE + "', '" + StructuredPostal.CONTENT_ITEM_TYPE + "')"; 
Cursor cur = cr.query(Data.CONTENT_URI, projection, selection, null, null);

// Loop through the data
while (cur.moveToNext()) {
    long id = cur.getLong(0);
    String name = cur.getString(1);
    String mime = cur.getString(2); // email / phone / postal
    String data = cur.getString(3); // the actual info, e.g. +1-212-555-1234

    // get the Contact class from the HashMap, or create a new one and add it to the Hash
    Contact contact;
    if (contacts.containsKey(id)) {
        contact = contacts.get(id);
    } else {
        contact = new Contact(id);
        contact.setDisplayName(name);
        // start with empty Sets for phones and emails
        // instead of HashSets you can use some object to retain more info about the data item (e.g. label)
        contact.setPhoneNumbers(new HashSet<>()); 
        contact.setEmails(new HashSet<>());
        contact.setAddresses(new HashSet<>());
        contacts.put(id, contact);
    } 

    switch (mime) {
        case Phone.CONTENT_ITEM_TYPE: 
            contact.getPhoneNumbers().add(data);
            break;
        case Email.CONTENT_ITEM_TYPE: 
            contact.getEmails().add(data);
            break;
        case StructuredPostal.CONTENT_ITEM_TYPE: 
            contact.getAddresses().add(data);
            break;
    }
}
cur.close();

跟进 成绩进步很大! 现在这里有一些小的调整,可以使您的新代码更进一步:

  1. 尽量避免排序,为排序传递 null,并调整代码以按数据出现的任何顺序处理数据,SQLite 排序有时会显着降低查询速度
  2. 将你的投影减少到你真正需要的字段,你放在投影上的东西越多,需要在设备上的进程之间转移的数据就越大,这会导致更多的块,每个块中的行更少
  3. 不要 getString 投影中的所有字段,如果某些字段仅由 StructuredPostal 使用,则只读取 StructuredPostal 行而不是每次迭代。

在评论中报告您使用上述技巧取得的成果...

这是我的 Kotlin 代码,基于 @marmor 答案的逻辑。

此优化代码在 500 毫秒内检索三星 S7 中的 1500 个联系人,而不是问题代码 中的 3 分钟。

为了完整起见,我将把数据聚合到 JSON 对象中的那段代码和 运行 进程的所有细节留在单独的线程中。

Thread(Runnable {
    val resolver: ContentResolver = contentResolver
    var jsonToSend = JSONObject()
    var jsonAllContacts = JSONObject()
    val jsonName = JSONObject()
    val jsonEmail = JSONObject()
    val jsonPhone = JSONObject()
    val jsonAddress = JSONObject()

    var cursorPosition = 0
    var currentProgress: Int

    val projection = arrayOf(
            ContactsContract.Data.CONTACT_ID,
            ContactsContract.Data.DISPLAY_NAME,
            ContactsContract.Data.MIMETYPE,
            ContactsContract.Data.DATA1,
            ContactsContract.Data.DATA2,
            ContactsContract.Data.DATA3,
            ContactsContract.Data.DATA4,
            ContactsContract.Data.DATA5,
            ContactsContract.Data.DATA6,
            ContactsContract.Data.DATA7,
            ContactsContract.Data.DATA8,
            ContactsContract.Data.DATA9,
            ContactsContract.Data.DATA10,
            ContactsContract.Data.DATA11,
            ContactsContract.Data.DATA12,
            ContactsContract.Data.DATA13,
            ContactsContract.Data.DATA14
    )
    val selection = ContactsContract.Data.MIMETYPE + " IN ('" + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "', '" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "', '" + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "', '" + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE + "')"

    val order = "CASE WHEN " + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' THEN 0 ELSE 1 END ASC, '" + ContactsContract.Data.CONTACT_ID + "'"
    val cursor = resolver.query(
            ContactsContract.Data.CONTENT_URI,
            projection,
            selection,
            null,
            order
    )

    Log.e("cursor", "cursor STARTED")

    if (cursor != null) {
        while (cursor.moveToNext())
        {
            cursorPosition++
            currentProgress = ((cursorPosition.toFloat() / cursor.count.toFloat()) * 100).toInt()


            // SELECT the data needed from the standardized table
            val id = cursor.getLong(0).toString()
            val name = cursor.getString(1)
            val mime = cursor.getString(2) // email / phone / postal
            val data1 = cursor.getString(3) // the actual info, e.g. +1-212-555-1234
            val data2 = cursor.getString(4)
            val data3 = cursor.getString(5) 
            val data4 = cursor.getString(6) 
            val data5 = cursor.getString(7) 
            val data6 = cursor.getString(8) 
            val data7 = cursor.getString(9) 
            val data8 = cursor.getString(10) 
            val data9 = cursor.getString(11) 
            val data10 = cursor.getString(12) 
            val data11 = cursor.getString(13) 
            val data12 = cursor.getString(14) 
            val data13 = cursor.getString(15) 
            val data14 = cursor.getString(16) 
            // get the Contact class from the HashMap, or create a new one and add it to the Hash




            when (mime) {
                ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
                    /**
                     * Type     Alias           Data column
                     * String   DISPLAY_NAME    DATA1
                     * String   GIVEN_NAME  DATA2
                     * String   FAMILY_NAME DATA3
                     * String   PREFIX  DATA4   Common prefixes in English names are "Mr", "Ms", "Dr" etc.
                     * String   MIDDLE_NAME DATA5
                     * String   SUFFIX  DATA6   Common suffixes in English names are "Sr", "Jr", "III" etc.
                     * String   PHONETIC_GIVEN_NAME DATA7   Used for phonetic spelling of the name, e.g. Pinyin, Katakana, Hiragana
                     * String   PHONETIC_MIDDLE_NAME    DATA8
                     * String   PHONETIC_FAMILY_NAME    DATA9
                     */
                    //Log.e("contact", "Name-- name:$name -- id:$id == 1:$data1 // 2:$data2 // 3:$data3 // 4: $data4 // 5:$data5 // 6:$data6 // 7:$data7 // 8:$data8 // 9:$data9 // 10:$data10 // 11:$data11 // 12:$data12 // 13:$data13 // 14:$data14")

                    val currentJSON = JSONObject()
                    val fullName = if ( data5 != null && (data5 != data3) ) {
                        "$data5 $data3"
                    } else {
                        data3 ?: data1 // PUT data1 as last resort because a contact with no names inputted will return something else (e.g. email address)
                    }

                    currentJSON.put("given", data2)
                    currentJSON.put("family", fullName)


                    jsonName.put( id, currentJSON)



                }
                ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> {
                    /**
                     * IMPROVE - check that valueForJSON / data1 is a properly formatted email. It sometimes saves in the email the name of the person instead of the email.
                     * e.g. "aze qsd" instead of "aze.qsd@gmail.com"
                     */
                    //Log.e("contact", "Email-- name:$name -- id:$id == 1:$data1 // 2:$data2 // 3:$data3 // 4: $data4 // 5:$data5 // 6:$data6 // 7:$data7 // 8:$data8 // 9:$data9 // 10:$data10 // 11:$data11 // 12:$data12 // 13:$data13 // 14:$data14")

                    val valueForJSON = data1


                    if ( jsonEmail.has(id) ) {
                        val indexString = jsonEmail[id].toString()
                        val indexArray = JSONObject(indexString)

                        if ( !indexArray.has(valueForJSON) ) {
                            jsonEmail.put( id, indexArray.put( valueForJSON, data2 ) )
                        }
                    } else {
                        jsonEmail.put( id, JSONObject().put( valueForJSON, data2 ) )
                    }



                }
                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
                    /**
                     * Type Alias   Data column
                    String  NUMBER  ContactsContract.DataColumns.DATA1
                    int ContactsContract.CommonDataKinds.CommonColumns.TYPE ContactsContract.DataColumns.DATA2  Allowed values are:
                    ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM. Put the actual type in ContactsContract.CommonDataKinds.CommonColumns.LABEL.
                    TYPE_HOME
                    TYPE_MOBILE
                    TYPE_WORK
                    etc..
                    String  ContactsContract.CommonDataKinds.CommonColumns.LABEL    ContactsContract.DataColumns.DATA3
                    String  ContactsContract.CommonDataKinds.CommonColumns.NORMALIZED_NUMBER    ContactsContract.DataColumns.DATA4

                     */
                    //Log.e("contact", "Phone-- name:$name -- id:$id == 1:$data1 // 2:$data2 // 3:$data3 // 4: $data4 // 5:$data5 // 6:$data6 // 7:$data7 // 8:$data8 // 9:$data9 // 10:$data10 // 11:$data11 // 12:$data12 // 13:$data13 // 14:$data14")

                    val valueForJSON = data4 ?: data1


                    if ( jsonPhone.has(id) ) {
                        val indexString = jsonPhone[id].toString()
                        val indexArray = JSONObject(indexString)

                        if ( !indexArray.has(valueForJSON) ) {
                            jsonPhone.put( id, indexArray.put( valueForJSON, data2 ) )
                        }
                    } else {
                        jsonPhone.put( id, JSONObject().put( valueForJSON, data2 ) )
                    }

                }
                ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> {
                    /**
                     * Type Alias   Data column
                    String  FORMATTED_ADDRESS   ContactsContract.DataColumns.DATA1
                    int ContactsContract.CommonDataKinds.CommonColumns.TYPE ContactsContract.DataColumns.DATA2  Allowed values are:
                    ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM. Put the actual type in ContactsContract.CommonDataKinds.CommonColumns.LABEL.
                    TYPE_HOME
                    TYPE_WORK
                    TYPE_OTHER
                    String  ContactsContract.CommonDataKinds.CommonColumns.LABEL    ContactsContract.DataColumns.DATA3
                    String  STREET  ContactsContract.DataColumns.DATA4
                    String  POBOX   ContactsContract.DataColumns.DATA5  Post Office Box number
                    String  NEIGHBORHOOD    ContactsContract.DataColumns.DATA6
                    String  CITY    ContactsContract.DataColumns.DATA7
                    String  REGION  ContactsContract.DataColumns.DATA8
                    String  POSTCODE    ContactsContract.DataColumns.DATA9
                    String  COUNTRY ContactsContract.DataColumns.DATA10
                     */
                    //Log.e("contact", "Address-- name:$name -- id:$id == 1:$data1 // 2:$data2 // 3:$data3 // 4: $data4 // 5:$data5 // 6:$data6 // 7:$data7 // 8:$data8 // 9:$data9 // 10:$data10 // 11:$data11 // 12:$data12 // 13:$data13 // 14:$data14")


                    val currentJSON = JSONObject()
                    val valueForJSON = data1
                    currentJSON.put("street", data4)
                    currentJSON.put("city", data7)
                    currentJSON.put("postcode", data9)
                    currentJSON.put("state", data8)
                    currentJSON.put("country", data10)



                    if ( jsonAddress.has(id) ) {
                        val indexString = jsonAddress[id].toString()
                        val indexArray = JSONObject(indexString)

                        if ( !indexArray.has(valueForJSON) ) {
                            jsonAddress.put( id, indexArray.put( valueForJSON, currentJSON ) )
                        }
                    } else {
                        jsonAddress.put( id, JSONObject().put( valueForJSON, currentJSON ) )
                    }

                }
            }



            runOnUiThread {
                //PROGRESS HERE
                //myProgressBar?.visibility = View.VISIBLE
                myProgressBar?.progress = currentProgress
            }

        }

    }
    cursor?.close()



    // PUT some data to the first level of the nested JSON object
    jsonToSend.put("source", "2")



    /**
     * ADD the type of contact info to the main jsonAllContacts Object
     */
    jsonAllContacts = addNameToCurrentObject(jsonAllContacts, jsonName)
    jsonAllContacts = addNewKeyToCurrentObject(jsonAllContacts, jsonPhone, "email")
    jsonAllContacts = addNewKeyToCurrentObject(jsonAllContacts, jsonPhone, "phone")
    jsonAllContacts = addNewKeyToCurrentObject(jsonAllContacts, jsonAddress, "address")



    /**
     * Remove the IDs that was used for aggregation of data
     */
    jsonToSend = removeIDofObject(jsonToSend, jsonAllContacts)

}).start()