【问题标题】:How to improve performance when query many data on ALL contacts from Android ContactsContract.Contacts?从 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 但它似乎不可持续。

问题

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

基本上,

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

【问题讨论】:

    标签: android kotlin android-contacts query-performance


    【解决方案1】:

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

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

    1. ContactsContract.CommonDataKinds.XXX 表中存储的所有数据实际上都存储在一个名为 Data 的大表中。
    2. 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();
    

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

    1. 尽量避免排序,传递 null 进行排序,并调整代码以按任何顺序处理数据,SQLite 排序有时会显着减慢查询速度
    2. 将您的投影减少到您实际需要的字段,您在投影上放置的内容越多,需要在设备上的进程之间转移的数据量就越大,这会导致更多的块,每个块中的行更少李>
    3. 不要在投影中的所有字段上使用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()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-01-21
      • 2016-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-03
      相关资源
      最近更新 更多