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

Posted

技术标签:

【中文标题】从 Android ContactsContract.Contacts 查询所有联系人的许多数据时如何提高性能?【英文标题】:How to improve performance when query many data on ALL contacts from Android ContactsContract.Contacts? 【发布时间】:2020-01-20 16:41:10 【问题描述】:

目标

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

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

我很确定我使用纯 SQL 查询查询了 ContactsContract.Data 表,但没有关于如何执行此操作的明确文档。 看来你可以inject SQL in the contentResolver.query 但它似乎不可持续。

问题

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

基本上,

    我从 ContactsContract.Contacts 和 LOOP 获取所有联系人的 ID,并通过它获取每个联系人的 ID, 在 CommonDataKinds.StructuredName 上选择命名数据, 在 CommonDataKinds.Phone 上选择和 LOOP 电话数据, 在 CommonDataKinds.Email 上选择和 LOOP 电子邮件数据, 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()
        

【问题讨论】:

【参考方案1】:

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

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

    ContactsContract.CommonDataKinds.XXX 表中存储的所有数据实际上都存储在一个名为 Data 的大表中。 ContactsContract 中有一个隐式连接,允许我们在查询 Data 时从 ContactsContract.Contacts 中选择列

为了使代码更简单,我建议您定义一个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();

跟进 性能进步很大! 现在这里有一些小的调整可以让你的新代码更进一步:

    尽量避免排序,传递 null 进行排序,并调整代码以按任何顺序处理数据,SQLite 排序有时会显着减慢查询速度 将您的投影减少到您实际需要的字段,您在投影上放置的内容越多,需要在设备上的进程之间转移的数据量就越大,这会导致更多的块,每个块中的行更少李> 不要在投影中的所有字段上使用getString,如果某些字段仅供StructuredPostal 使用,请仅读取StructuredPostal 行而不是每次迭代的字段。

在 cmets 中报告您通过上述技巧可以实现的目标...

【讨论】:

我在下面发布了Kotlin 答案。由于您的逻辑,获取联系人列表并处理它从 3 分钟缩短到 500 毫秒。 我认为你可以通过一些调整获得更好的性能,我会在我的答案中发布我的 cmets【参考方案2】:

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

此优化代码可在 500 毫秒内检索 S7 Samsung 中的 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()

【讨论】:

以上是关于从 Android ContactsContract.Contacts 查询所有联系人的许多数据时如何提高性能?的主要内容,如果未能解决你的问题,请参考以下文章

如何导入特定联系人的电话号码?

ContactsContract.CommonDataKinds.Phone.NUMBER不能如期工作。

如何从 Android 版本 Marshmallow 开始请求从 Android 拨打电话的权限?

从0到1实现一个Android路由——APT收集路由

从0到1实现一个Android路由——初探路由

从0到1实现一个Android路由——URL解析器