从 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不能如期工作。