鸿蒙常见问题分析十六:调用联系人服务queryContacts接口导致应用闪退
摘要:本文详细分析了HarmonyOS应用中查询大量联系人导致闪退的问题。当用户通讯录联系人超过十万条时,queryContacts接口在主线程执行会触发系统AppFreeze保护机制,导致应用崩溃。解决方案包括:1)使用TaskPool在子线程执行查询;2)分批处理大数据量;3)添加进度反馈和错误处理机制。文章提供了完整示例代码,包含权限申请、子线程查询、进度显示等功能模块,并对比了不同优化方案
引言:那个让十万联系人"崩溃"的夜晚
上周,团队里的小王正在开发一个社交类应用。应用需要读取用户的通讯录,实现快速添加好友的功能。测试时一切正常,小王信心满满地将应用发布到了测试环境。
然而,第二天一早,产品经理急匆匆地跑过来:"小王,有个用户反馈说应用一打开就闪退!"
小王心里一紧,赶紧查看崩溃日志。发现崩溃的用户有一个共同特点——他们的通讯录联系人数量都异常庞大,有的甚至接近十万条。
"这不可能啊,"小王皱着眉头,"我在自己手机上测试得好好的,通讯录才几百个联系人。"
更糟糕的是,这个闪退问题不是每次都出现,而是在特定条件下才会触发:当用户通讯录联系人数量超过一定阈值,应用在调用queryContacts接口查询所有联系人时,就会突然崩溃,连错误提示都没有。
这个问题不仅影响用户体验,更让应用在应用商店的评分直线下降。今天,我们就来彻底解决这个让无数HarmonyOS开发者头疼的"联系人查询闪退"问题。
一、问题现象:神秘的"瞬间消失"
1.1 典型问题场景
在实际开发中,开发者可能会遇到以下问题:
-
大规模联系人闪退:用户通讯录联系人数量接近十万条时,应用调用
queryContacts接口后突然闪退 -
主线程卡死:应用界面完全无响应,超过6秒后系统强制关闭应用
-
无错误提示:崩溃前没有任何错误信息或异常提示
-
特定设备重现:在内存较小或性能较低的设备上更容易出现
1.2 具体表现
// 开发者期望:正常查询所有联系人
// 实际现象:应用突然闪退,无任何错误信息
import { contact } from '@kit.ContactsKit';
async function queryAllContacts(context: Context) {
try {
// 当联系人数量巨大时,这里可能导致应用闪退
const contactsData: contact.Contact[] = await contact.queryContacts(context);
console.log(`查询到 ${contactsData.length} 个联系人`);
return contactsData;
} catch (err) {
// 有时甚至无法进入catch块
console.error(`查询联系人失败: ${err.code} ${err.message}`);
return [];
}
}
// 用户反馈日志:
// "应用一打开就闪退,重新安装也没用"
// "只有清空通讯录才能正常使用"
// "其他应用读取通讯录都正常,就这个应用会崩溃"
二、背景知识:queryContacts与AppFreeze的"恩怨"
2.1 什么是queryContacts接口?
queryContacts是HarmonyOS联系人服务(ContactKit)提供的核心接口,用于查询设备上的所有联系人。它返回一个包含完整联系人信息的数组,包括姓名、电话号码、邮箱等。
接口特性:
-
同步阻塞:在主线程执行时,会阻塞UI渲染
-
内存密集型:返回所有联系人数据,内存占用与联系人数量成正比
-
权限依赖:需要
ohos.permission.READ_CONTACTS权限
2.2 什么是AppFreeze?
AppFreeze(应用无响应)是HarmonyOS系统的一种保护机制。当应用主线程被阻塞超过一定时间(通常是6秒),系统会判定应用无响应,并采取以下措施:
-
弹出无响应对话框:提示用户等待或强制关闭
-
生成AppFreeze日志:记录卡死时的线程状态和堆栈信息
-
强制终止应用:如果用户选择"强制关闭"或超时未响应
2.3 系统监控机制
HarmonyOS通过以下机制监控应用响应:
|
监控维度 |
阈值 |
触发条件 |
系统动作 |
|---|---|---|---|
|
主线程阻塞 |
6秒 |
主线程连续6秒无响应 |
标记为AppFreeze |
|
输入事件超时 |
5秒 |
用户输入5秒无响应 |
提示应用无响应 |
|
服务响应超时 |
10秒 |
服务调用10秒未返回 |
终止服务调用 |
三、问题定位:十万条联系人的"压力测试"
3.1 崩溃日志分析
通过分析AppFreeze日志,我们可以定位问题的根本原因:
# AppFreeze日志示例
Timestamp: 2024-03-10 14:30:25
Process: com.example.contactsapp
PID: 12345
Reason: THREAD_BLOCK_6S
# 线程堆栈信息
Main Thread Stack:
at ohos.contact.ContactManager.queryContacts(Native Method)
at ohos.contact.ContactManager.queryContacts(ContactManager.java:256)
at com.example.contactsapp.MainAbility.queryAllContacts(MainAbility.ets:89)
at com.example.contactsapp.MainAbility.onWindowStageCreate(MainAbility.ets:45)
# 系统诊断信息
Diagnostic Info:
- CPU Usage: 95%
- Memory Usage: 85%
- Thread State: BLOCKED
- Blocked Time: 6324ms (超过6秒阈值)
3.2 根本原因分析
通过对大量崩溃案例的分析,queryContacts接口导致应用闪退的根本原因是:
-
主线程阻塞:
queryContacts在主线程执行,查询大量联系人时耗时过长 -
内存压力:十万条联系人数据可能占用数百MB内存,导致内存紧张
-
CPU竞争:设备在高压情况下,CPU需要同时处理查询和其他系统任务
-
超时触发:执行时间超过6秒,触发系统的AppFreeze保护机制
3.3 复现条件
问题通常在以下条件下复现:
|
条件 |
阈值 |
影响程度 |
|---|---|---|
|
联系人数量 |
> 50000条 |
高 |
|
设备内存 |
< 4GB |
高 |
|
CPU负载 |
> 70% |
中 |
|
并发任务 |
有其他后台任务 |
中 |
|
系统版本 |
HarmonyOS 3.0+ |
低 |
四、分析结论:主线程的"不能承受之重"
4.1 核心发现
经过深入分析,我们得出以下关键结论:
-
单线程瓶颈:HarmonyOS应用主线程为单线程,负责UI渲染和事件处理
-
同步操作风险:
queryContacts是同步操作,会阻塞主线程 -
数据量敏感:查询时间与联系人数量成正比,十万条联系人可能耗时数秒
-
系统保护机制:超过6秒无响应,系统会强制终止应用
4.2 技术原理分析
// 问题代码示例
@Entry
@Component
struct ContactsApp {
@State contacts: contact.Contact[] = [];
build() {
Column() {
// UI组件...
}
.onClick(() => {
// 在主线程执行耗时操作
this.queryContactsOnMainThread(); // 危险操作!
})
}
// 在主线程查询联系人
async queryContactsOnMainThread() {
// 获取权限...
// 查询联系人(可能耗时数秒)
this.contacts = await contact.queryContacts(context);
// 在此期间,UI完全无响应
// 超过6秒会导致AppFreeze
}
}
4.3 影响范围评估
这个问题不仅影响queryContacts接口,还可能影响其他类似的同步耗时操作:
|
接口类别 |
风险等级 |
建议处理方式 |
|---|---|---|
|
联系人查询 |
高风险 |
必须使用子线程 |
|
媒体文件扫描 |
高风险 |
必须使用子线程 |
|
数据库大量查询 |
中风险 |
建议使用子线程 |
|
网络请求 |
低风险 |
异步处理即可 |
|
文件读写 |
中风险 |
大文件需子线程 |
五、解决方案:让查询"飞"起来
5.1 核心思路
解决queryContacts闪退问题的核心思路是:
-
异步执行:将耗时操作移到子线程
-
分批处理:大量数据分批查询
-
进度反馈:向用户显示查询进度
-
错误处理:完善的异常处理机制
5.2 完整示例代码(使用TaskPool)
import { abilityAccessCtrl, common, Permissions, sendableContextManager } from '@kit.AbilityKit';
import { taskpool } from '@kit.ArkTS';
import { contact } from '@kit.ContactsKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 需要的权限
const permissions: Array<Permissions> = ['ohos.permission.READ_CONTACTS'];
/**
* 向用户申请通讯录读取权限
* @param context UIAbility上下文
*/
function requestContactsPermission(context: common.UIAbilityContext): Promise<boolean> {
return new Promise((resolve, reject) => {
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(context, permissions)
.then((data) => {
const grantStatus: Array<number> = data.authResults;
const allGranted = grantStatus.every(status => status === 0);
if (allGranted) {
console.info('通讯录权限已授权');
resolve(true);
} else {
console.error('用户拒绝了通讯录权限');
resolve(false);
}
})
.catch((err: BusinessError) => {
console.error(`申请权限失败: ${err.code}, ${err.message}`);
reject(err);
});
});
}
/**
* 可发送对象,用于在任务间传递上下文
*/
@Sendable
class ContactsQueryTask {
constructor(
sendableContext: sendableContextManager.SendableContext,
options?: {
batchSize?: number; // 分批大小
timeout?: number; // 超时时间
fields?: string[]; // 查询字段
}
) {
this.sendableContext = sendableContext;
this.batchSize = options?.batchSize || 1000;
this.timeout = options?.timeout || 30000; // 30秒超时
this.fields = options?.fields || ['name', 'phone', 'email'];
}
sendableContext: sendableContextManager.SendableContext;
batchSize: number;
timeout: number;
fields: string[];
}
/**
* 在子线程中查询联系人的并发函数
*/
@Concurrent
async function queryContactsInBackground(task: ContactsQueryTask): Promise<{
success: boolean;
contacts?: contact.Contact[];
count?: number;
error?: string;
duration?: number;
}> {
const startTime = Date.now();
try {
// 转换上下文
const context: Context = sendableContextManager.convertToContext(task.sendableContext);
// 设置查询选项
const queryOptions: contact.ContactQueryOptions = {
// 可以添加过滤条件
// predicates: ...
// 可以指定返回字段
// attributes: task.fields
};
console.info(`开始查询联系人,分批大小: ${task.batchSize}`);
// 查询所有联系人
const contactsData: contact.Contact[] = await contact.queryContacts(context, queryOptions);
const contactCount = contactsData.length;
const endTime = Date.now();
const duration = endTime - startTime;
console.info(`联系人查询完成,数量: ${contactCount}, 耗时: ${duration}ms`);
return {
success: true,
contacts: contactsData,
count: contactCount,
duration: duration
};
} catch (err) {
console.error(`联系人查询失败: ${JSON.stringify(err)}`);
return {
success: false,
error: `查询失败: ${err?.code} ${err?.message}`,
duration: Date.now() - startTime
};
}
}
/**
* 分批查询联系人的并发函数(适用于超大数据量)
*/
@Concurrent
async function queryContactsInBatches(task: ContactsQueryTask): Promise<{
success: boolean;
batches?: contact.Contact[][];
totalCount?: number;
error?: string;
duration?: number;
}> {
const startTime = Date.now();
try {
const context: Context = sendableContextManager.convertToContext(task.sendableContext);
const allContacts: contact.Contact[][] = [];
let offset = 0;
let totalCount = 0;
console.info(`开始分批查询联系人,每批: ${task.batchSize}条`);
// 分批查询直到没有更多数据
while (true) {
try {
const queryOptions: contact.ContactQueryOptions = {
// 这里可以添加分页逻辑
// 注意:contact.queryContacts本身不支持分页参数
// 需要根据实际情况调整分批策略
};
const batchContacts: contact.Contact[] = await contact.queryContacts(context, queryOptions);
if (batchContacts.length === 0) {
break; // 没有更多数据
}
allContacts.push(batchContacts);
totalCount += batchContacts.length;
offset += task.batchSize;
console.info(`已查询 ${totalCount} 条联系人`);
// 检查是否超时
if (Date.now() - startTime > task.timeout) {
console.warn('查询超时,提前结束');
break;
}
} catch (batchError) {
console.error(`分批查询失败: ${JSON.stringify(batchError)}`);
// 继续尝试下一批
}
}
const duration = Date.now() - startTime;
return {
success: true,
batches: allContacts,
totalCount: totalCount,
duration: duration
};
} catch (err) {
console.error(`分批查询失败: ${JSON.stringify(err)}`);
return {
success: false,
error: `分批查询失败: ${err?.code} ${err?.message}`,
duration: Date.now() - startTime
};
}
}
@Entry
@Component
struct SafeContactsQueryDemo {
@State contactCount: number = 0;
@State queryStatus: 'idle' | 'querying' | 'success' | 'error' = 'idle';
@State queryProgress: number = 0;
@State queryDuration: number = 0;
@State errorMessage: string = '';
@State contactsPreview: contact.Contact[] = [];
// 查询配置
private queryConfig = {
useBatchMode: false, // 是否使用分批模式
batchSize: 500, // 每批大小
timeout: 30000, // 超时时间(毫秒)
previewCount: 10 // 预览显示数量
};
build() {
Column({ space: 20 }) {
// 标题
Text('安全查询联系人示例')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#007DFF')
.margin({ top: 30, bottom: 20 })
// 状态显示
Row() {
switch (this.queryStatus) {
case 'idle':
Text('准备查询')
.fontColor('#666666')
break;
case 'querying':
Text('正在查询中...')
.fontColor('#FF9500')
break;
case 'success':
Text(`查询成功: ${this.contactCount} 个联系人`)
.fontColor('#34C759')
break;
case 'error':
Text('查询失败')
.fontColor('#FF3B30')
break;
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(10)
.backgroundColor('#F5F5F5')
.borderRadius(8)
// 进度条
if (this.queryStatus === 'querying') {
Row() {
Progress({ value: this.queryProgress, total: 100 })
.width('80%')
.height(8)
.color('#007DFF')
Text(`${this.queryProgress}%`)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 10 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(10)
}
// 查询结果预览
if (this.contactsPreview.length > 0) {
Column({ space: 10 }) {
Text('联系人预览')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 10 })
ForEach(this.contactsPreview, (contact: contact.Contact) => {
Row({ space: 12 }) {
// 头像占位
Circle({ width: 40, height: 40 })
.fill('#E8E8E8')
.overlay(
Text(contact.name?.firstName?.[0] || '?')
.fontSize(16)
.fontColor('#666666')
)
Column({ space: 4 }) {
Text(contact.name?.fullName || '未知姓名')
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
if (contact.phoneNumbers && contact.phoneNumbers.length > 0) {
Text(contact.phoneNumbers[0].phoneNumber || '')
.fontSize(12)
.fontColor('#666666')
}
}
.layoutWeight(1)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.shadow({ radius: 2, color: '#1A000000', offsetX: 0, offsetY: 1 })
})
}
.width('100%')
.padding(16)
.backgroundColor('#F8F9FA')
.borderRadius(12)
.margin({ top: 20 })
}
// 错误信息
if (this.errorMessage) {
Text(this.errorMessage)
.fontSize(12)
.fontColor('#FF3B30')
.width('90%')
.padding(10)
.backgroundColor('#FFEBEE')
.borderRadius(8)
.margin({ top: 10 })
}
// 查询耗时
if (this.queryDuration > 0) {
Text(`查询耗时: ${this.queryDuration}ms`)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 10 })
}
// 操作按钮
Column({ space: 12 }) {
Button('安全查询联系人(子线程)')
.width('90%')
.height(44)
.backgroundColor(this.queryStatus === 'querying' ? '#CCCCCC' : '#007DFF')
.fontColor(Color.White)
.enabled(this.queryStatus !== 'querying')
.onClick(() => this.safeQueryContacts())
Button('分批查询联系人(大数据量)')
.width('90%')
.height(44)
.backgroundColor(this.queryStatus === 'querying' ? '#CCCCCC' : '#34C759')
.fontColor(Color.White)
.enabled(this.queryStatus !== 'querying')
.onClick(() => this.batchQueryContacts())
Button('重置查询')
.width('90%')
.height(44)
.type(ButtonType.Normal)
.enabled(this.queryStatus !== 'querying')
.onClick(() => this.resetQuery())
}
.width('100%')
.margin({ top: 30 })
// 配置选项
Column({ space: 12 }) {
Text('查询配置')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 8 })
Row({ space: 20 }) {
Text('分批大小:')
.fontSize(12)
.fontColor('#666666')
TextInput({ text: this.queryConfig.batchSize.toString() })
.width(80)
.height(32)
.type(InputType.Number)
.onChange((value: string) => {
const num = parseInt(value);
if (!isNaN(num) && num > 0) {
this.queryConfig.batchSize = num;
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row({ space: 20 }) {
Text('超时时间(ms):')
.fontSize(12)
.fontColor('#666666')
TextInput({ text: this.queryConfig.timeout.toString() })
.width(100)
.height(32)
.type(InputType.Number)
.onChange((value: string) => {
const num = parseInt(value);
if (!isNaN(num) && num > 0) {
this.queryConfig.timeout = num;
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('90%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 20 })
// 使用说明
Column({ space: 8 }) {
Text('使用说明')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 8 })
Text('1. 点击"安全查询联系人"在子线程中执行查询')
.fontSize(12)
.fontColor('#666666')
Text('2. 大数据量建议使用"分批查询"')
.fontSize(12)
.fontColor('#666666')
Text('3. 查询期间UI保持响应,不会闪退')
.fontSize(12)
.fontColor('#666666')
Text('4. 需要通讯录读取权限')
.fontSize(12)
.fontColor('#666666')
}
.width('90%')
.padding(16)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.margin({ top: 20, bottom: 30 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding(20)
}
/**
* 安全查询联系人(使用子线程)
*/
async safeQueryContacts() {
// 检查权限
const hasPermission = await this.checkPermission();
if (!hasPermission) {
prompt.showToast({ message: '需要通讯录权限' });
return;
}
// 更新状态
this.queryStatus = 'querying';
this.queryProgress = 0;
this.errorMessage = '';
this.contactsPreview = [];
try {
// 创建可发送上下文
const context = sendableContextManager.convertFromContext(
this.getUIContext().getHostContext() as Context
);
// 创建查询任务
const task = new ContactsQueryTask(context, {
batchSize: this.queryConfig.batchSize,
timeout: this.queryConfig.timeout
});
// 模拟进度更新
const progressInterval = setInterval(() => {
if (this.queryProgress < 90) {
this.queryProgress += 10;
}
}, 300);
// 在子线程中执行查询
const startTime = Date.now();
const result = await taskpool.execute(queryContactsInBackground, task);
// 清理进度定时器
clearInterval(progressInterval);
this.queryProgress = 100;
const endTime = Date.now();
this.queryDuration = endTime - startTime;
if (result.success && result.contacts) {
this.queryStatus = 'success';
this.contactCount = result.count || 0;
// 显示前N个联系人作为预览
this.contactsPreview = result.contacts.slice(0, this.queryConfig.previewCount);
prompt.showToast({
message: `查询成功: ${this.contactCount} 个联系人`
});
} else {
this.queryStatus = 'error';
this.errorMessage = result.error || '查询失败';
prompt.showToast({ message: '查询失败' });
}
} catch (error) {
this.queryStatus = 'error';
this.errorMessage = `查询异常: ${JSON.stringify(error)}`;
console.error('查询联系人异常:', error);
prompt.showToast({ message: '查询异常' });
}
}
/**
* 分批查询联系人(适用于超大数据量)
*/
async batchQueryContacts() {
const hasPermission = await this.checkPermission();
if (!hasPermission) {
prompt.showToast({ message: '需要通讯录权限' });
return;
}
this.queryStatus = 'querying';
this.queryProgress = 0;
this.errorMessage = '';
this.contactsPreview = [];
try {
const context = sendableContextManager.convertFromContext(
this.getUIContext().getHostContext() as Context
);
const task = new ContactsQueryTask(context, {
batchSize: this.queryConfig.batchSize,
timeout: this.queryConfig.timeout
});
// 模拟进度更新
const progressInterval = setInterval(() => {
if (this.queryProgress < 90) {
this.queryProgress += 5;
}
}, 500);
const startTime = Date.now();
const result = await taskpool.execute(queryContactsInBatches, task);
clearInterval(progressInterval);
this.queryProgress = 100;
const endTime = Date.now();
this.queryDuration = endTime - startTime;
if (result.success && result.batches) {
this.queryStatus = 'success';
this.contactCount = result.totalCount || 0;
// 合并第一批数据作为预览
const allContacts = result.batches.flat();
this.contactsPreview = allContacts.slice(0, this.queryConfig.previewCount);
prompt.showToast({
message: `分批查询成功: ${this.contactCount} 个联系人`
});
} else {
this.queryStatus = 'error';
this.errorMessage = result.error || '分批查询失败';
prompt.showToast({ message: '分批查询失败' });
}
} catch (error) {
this.queryStatus = 'error';
this.errorMessage = `分批查询异常: ${JSON.stringify(error)}`;
console.error('分批查询异常:', error);
prompt.showToast({ message: '分批查询异常' });
}
}
/**
* 检查并申请权限
*/
async checkPermission(): Promise<boolean> {
try {
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
return await requestContactsPermission(context);
} catch (error) {
console.error('检查权限失败:', error);
return false;
}
}
/**
* 重置查询状态
*/
resetQuery() {
this.queryStatus = 'idle';
this.queryProgress = 0;
this.contactCount = 0;
this.queryDuration = 0;
this.errorMessage = '';
this.contactsPreview = [];
}
}
5.3 权限配置
在module.json5中添加必要的权限声明:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.READ_CONTACTS",
"reason": "需要读取通讯录联系人",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
}
],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
]
}
}
5.4 优化方案对比
|
方案 |
优点 |
缺点 |
适用场景 |
|---|---|---|---|
|
主线程查询 |
实现简单,代码直观 |
容易导致AppFreeze,用户体验差 |
联系人数量少(<1000) |
|
TaskPool子线程 |
避免主线程阻塞,响应性好 |
需要处理线程间通信 |
大多数场景推荐使用 |
|
Worker线程 |
独立运行环境,更稳定 |
启动开销较大,通信复杂 |
需要长时间运行的任务 |
|
分批查询 |
内存占用可控,支持大数据量 |
实现复杂,需要处理分页逻辑 |
联系人数量极大(>50000) |
|
延迟加载 |
首次加载快,按需加载 |
实现复杂,需要缓存机制 |
列表显示联系人场景 |
六、进阶方案:智能联系人查询管理器
6.1 完整的联系人查询管理器
对于需要频繁查询联系人的应用,可以创建一个专门的查询管理器:
import { taskpool } from '@kit.ArkTS';
import { contact } from '@kit.ContactsKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class ContactsQueryManager {
private static instance: ContactsQueryManager;
private queryCache: Map<string, contact.Contact[]> = new Map();
private isQuerying: boolean = false;
private queryQueue: Array<{
resolve: (contacts: contact.Contact[]) => void;
reject: (error: any) => void;
context: Context;
options?: contact.ContactQueryOptions;
}> = [];
// 单例模式
static getInstance(): ContactsQueryManager {
if (!ContactsQueryManager.instance) {
ContactsQueryManager.instance = new ContactsQueryManager();
}
return ContactsQueryManager.instance;
}
/**
* 安全查询联系人(带缓存)
*/
async queryContactsSafely(
context: Context,
options?: contact.ContactQueryOptions,
useCache: boolean = true
): Promise<contact.Contact[]> {
// 生成缓存键
const cacheKey = this.generateCacheKey(options);
// 检查缓存
if (useCache && this.queryCache.has(cacheKey)) {
console.info('从缓存中获取联系人数据');
return this.queryCache.get(cacheKey)!;
}
// 如果正在查询,加入队列
if (this.isQuerying) {
console.info('已有查询在进行中,加入队列');
return new Promise((resolve, reject) => {
this.queryQueue.push({ resolve, reject, context, options });
});
}
// 开始查询
this.isQuerying = true;
try {
// 在子线程中执行查询
const contacts = await this.executeQueryInBackground(context, options);
// 更新缓存
if (useCache) {
this.queryCache.set(cacheKey, contacts);
}
// 处理队列中的等待请求
this.processQueryQueue();
return contacts;
} catch (error) {
// 查询失败,清理状态并抛出错误
this.isQuerying = false;
throw error;
}
}
/**
* 在子线程中执行查询
*/
@Concurrent
private async executeQueryInBackground(
context: Context,
options?: contact.ContactQueryOptions
): Promise<contact.Contact[]> {
try {
console.info('在子线程中查询联系人');
const contacts = await contact.queryContacts(context, options);
console.info(`查询到 ${contacts.length} 个联系人`);
return contacts;
} catch (error) {
console.error(`联系人查询失败: ${JSON.stringify(error)}`);
throw error;
}
}
/**
* 处理查询队列
*/
private async processQueryQueue() {
this.isQuerying = false;
if (this.queryQueue.length === 0) {
return;
}
// 处理下一个请求
const nextRequest = this.queryQueue.shift();
if (nextRequest) {
try {
const contacts = await this.queryContactsSafely(
nextRequest.context,
nextRequest.options,
true
);
nextRequest.resolve(contacts);
} catch (error) {
nextRequest.reject(error);
}
// 继续处理队列
this.processQueryQueue();
}
}
/**
* 清空缓存
*/
clearCache(): void {
this.queryCache.clear();
console.info('联系人缓存已清空');
}
/**
* 获取缓存统计信息
*/
getCacheStats(): { size: number; keys: string[] } {
return {
size: this.queryCache.size,
keys: Array.from(this.queryCache.keys())
};
}
/**
* 生成缓存键
*/
private generateCacheKey(options?: contact.ContactQueryOptions): string {
if (!options) {
return 'all_contacts';
}
const keyParts: string[] = [];
if (options.attributes) {
keyParts.push(`attrs:${options.attributes.sort().join(',')}`);
}
if (options.predicates) {
// 简化predicates为字符串表示
keyParts.push(`predicates:${JSON.stringify(options.predicates)}`);
}
return keyParts.length > 0 ? keyParts.join('|') : 'all_contacts';
}
/**
* 分批查询联系人(适用于超大数据量)
*/
async queryContactsInBatches(
context: Context,
batchCallback?: (batch: contact.Contact[], index: number) => void,
batchSize: number = 1000
): Promise<{ total: number; batches: number }> {
let allContacts: contact.Contact[] = [];
let batchIndex = 0;
let totalCount = 0;
try {
// 第一次查询获取总数(如果支持)
const firstBatch = await contact.queryContacts(context);
totalCount = firstBatch.length;
if (totalCount <= batchSize) {
// 数据量小,直接返回
if (batchCallback) {
batchCallback(firstBatch, 0);
}
return { total: totalCount, batches: 1 };
}
// 分批处理
for (let i = 0; i < totalCount; i += batchSize) {
batchIndex++;
// 实际项目中可能需要支持分页的查询接口
// 这里简化处理,截取当前批次
const startIndex = i;
const endIndex = Math.min(i + batchSize, totalCount);
const currentBatch = firstBatch.slice(startIndex, endIndex);
allContacts = allContacts.concat(currentBatch);
// 回调通知批次处理
if (batchCallback) {
batchCallback(currentBatch, batchIndex);
}
// 避免阻塞主线程,每批处理后让出控制权
await this.delay(10);
}
return { total: totalCount, batches: batchIndex };
} catch (error) {
console.error('分批查询失败:', error);
throw error;
}
}
/**
* 延迟函数
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
6.2 使用示例
@Entry
@Component
struct ContactsApp {
private contactsManager = ContactsQueryManager.getInstance();
@State contacts: contact.Contact[] = [];
@State isLoading: boolean = false;
@State errorMessage: string = '';
build() {
Column() {
if (this.isLoading) {
LoadingProgress()
.color('#007DFF')
.width(50)
.height(50)
Text('正在查询联系人...')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 10 })
} else if (this.errorMessage) {
Text(this.errorMessage)
.fontSize(14)
.fontColor('#FF3B30')
.multilineTextAlignment(TextAlign.Center)
} else {
List() {
ForEach(this.contacts, (contact: contact.Contact) => {
ListItem() {
ContactItem({ contact: contact })
}
})
}
.width('100%')
.height('100%')
}
}
.onClick(() => {
this.loadContacts();
})
}
async loadContacts() {
this.isLoading = true;
this.errorMessage = '';
try {
const context = getContext(this) as common.UIAbilityContext;
// 使用联系人管理器安全查询
this.contacts = await this.contactsManager.queryContactsSafely(
context,
undefined, // 查询选项
true // 使用缓存
);
} catch (error) {
this.errorMessage = `加载联系人失败: ${error.message}`;
console.error('加载联系人失败:', error);
} finally {
this.isLoading = false;
}
}
}
七、常见FAQ
Q1:应用审核被驳回,提示"应用在读取通讯录时闪退",该怎么办?
A:首先需要确认闪退的具体原因。可以通过以下步骤排查:
-
检查权限:确保已正确声明并申请
ohos.permission.READ_CONTACTS权限 -
使用子线程:确保
queryContacts在子线程中执行 -
添加超时机制:设置合理的查询超时时间
-
分批处理:对于大数据量,使用分批查询策略
-
错误处理:添加完善的try-catch错误处理
Q2:用户通讯录有十几万联系人,查询时间太长怎么办?
A:针对大数据量场景,可以采取以下优化策略:
-
分批查询:将查询分成多个小批次进行
-
延迟加载:先显示部分数据,滚动时再加载更多
-
后台查询:在后台线程执行,不影响主线程响应
-
缓存结果:缓存查询结果,避免重复查询
-
进度提示:向用户显示查询进度,提升体验
// 分批查询示例
async function queryLargeContactsInBatches(context: Context): Promise<contact.Contact[]> {
const allContacts: contact.Contact[] = [];
const batchSize = 1000;
let offset = 0;
while (true) {
try {
// 实际项目中可能需要支持分页的查询接口
const batch = await contact.queryContacts(context);
if (batch.length === 0) {
break;
}
allContacts.push(...batch);
offset += batchSize;
// 更新进度
updateProgress(offset);
// 避免阻塞,每批处理后让出控制权
await delay(10);
} catch (error) {
console.error(`分批查询失败: ${error}`);
break;
}
}
return allContacts;
}
Q3:如何在不阻塞UI的情况下显示查询进度?
A:使用TaskPool配合状态管理实现进度更新:
@Component
struct ContactsQueryWithProgress {
@State progress: number = 0;
@State status: string = '准备查询';
async queryWithProgress(context: Context) {
this.status = '开始查询';
// 创建进度更新函数
const updateProgress = (value: number) => {
this.progress = value;
this.status = `查询中... ${value}%`;
};
// 在子线程中查询
const task = new QueryTask(context, updateProgress);
const result = await taskpool.execute(queryContactsTask, task);
if (result.success) {
this.status = '查询完成';
this.progress = 100;
} else {
this.status = '查询失败';
}
}
}
@Concurrent
async function queryContactsTask(task: QueryTask) {
// 模拟进度更新
for (let i = 0; i <= 100; i += 10) {
task.updateProgress(i);
await delay(100);
}
// 实际查询逻辑
return await contact.queryContacts(task.context);
}
Q4:查询联系人时内存占用过高怎么办?
A:优化内存使用的策略:
-
分批处理:不要一次性加载所有联系人
-
选择性加载:只加载需要的字段
-
及时释放:使用完的数据及时置空
-
内存监控:监控应用内存使用情况
-
使用弱引用:对于缓存数据使用弱引用
import { abilityAccessCtrl, common, Permissions, sendableContextManager } from '@kit.AbilityKit';
import { taskpool, TaskPoolInterface } from '@kit.ArkTS';
import { contact } from '@kit.ContactsKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 完整的联系人查询管理器
*/
export class SafeContactsQueryManager {
// TaskPool实例
private taskPool: TaskPoolInterface = taskpool.getTaskPool('contacts-query-pool');
// 查询状态
private isQuerying: boolean = false;
// 查询队列
private queryQueue: Array<{
resolve: (result: QueryResult) => void;
reject: (error: Error) => void;
context: common.UIAbilityContext;
options?: contact.ContactQueryOptions;
}> = [];
/**
* 安全查询联系人(完整的实现)
*/
async safeQueryContacts(
context: common.UIAbilityContext,
options?: contact.ContactQueryOptions,
progressCallback?: (progress: number, message: string) => void
): Promise<QueryResult> {
// 1. 检查权限
const hasPermission = await this.checkPermission(context);
if (!hasPermission) {
return {
success: false,
error: '没有通讯录读取权限',
contacts: []
};
}
// 2. 如果正在查询,加入队列
if (this.isQuerying) {
return new Promise((resolve, reject) => {
this.queryQueue.push({ resolve, reject, context, options });
if (progressCallback) {
progressCallback(0, '等待其他查询完成...');
}
});
}
// 3. 开始查询
this.isQuerying = true;
try {
// 4. 创建可发送上下文
const sendableContext = sendableContextManager.convertFromContext(context);
// 5. 创建查询任务
const queryTask = {
context: sendableContext,
options: options,
timeout: 30000, // 30秒超时
progressCallback: progressCallback
};
// 6. 在子线程中执行查询
const result = await this.executeInTaskPool(queryTask);
// 7. 处理队列中的下一个请求
this.processNextQuery();
return result;
} catch (error) {
// 8. 查询失败,清理状态
this.isQuerying = false;
// 9. 处理队列中的下一个请求
this.processNextQuery();
return {
success: false,
error: error instanceof Error ? error.message : '未知错误',
contacts: [],
duration: 0
};
}
}
/**
* 在TaskPool中执行查询
*/
private async executeInTaskPool(task: QueryTask): Promise<QueryResult> {
const startTime = Date.now();
try {
// 1. 提交任务到TaskPool
const taskResult = await this.taskPool.execute(queryContactsConcurrent, task);
const endTime = Date.now();
const duration = endTime - startTime;
if (taskResult.success && taskResult.contacts) {
return {
success: true,
contacts: taskResult.contacts,
count: taskResult.contacts.length,
duration: duration
};
} else {
return {
success: false,
error: taskResult.error || '查询失败',
contacts: [],
duration: duration
};
}
} catch (error) {
const endTime = Date.now();
return {
success: false,
error: error instanceof Error ? error.message : '任务执行失败',
contacts: [],
duration: endTime - startTime
};
}
}
/**
* 检查权限
*/
private async checkPermission(context: common.UIAbilityContext): Promise<boolean> {
try {
const atManager = abilityAccessCtrl.createAtManager();
const permissions: Array<Permissions> = ['ohos.permission.READ_CONTACTS'];
const result = await atManager.requestPermissionsFromUser(context, permissions);
return result.authResults.every(status => status === 0);
} catch (error) {
console.error('检查权限失败:', error);
return false;
}
}
/**
* 处理下一个查询请求
*/
private async processNextQuery(): Promise<void> {
this.isQuerying = false;
if (this.queryQueue.length === 0) {
return;
}
const nextQuery = this.queryQueue.shift();
if (nextQuery) {
try {
const result = await this.safeQueryContacts(
nextQuery.context,
nextQuery.options
);
nextQuery.resolve(result);
} catch (error) {
nextQuery.reject(error as Error);
}
}
}
/**
* 取消所有查询
*/
cancelAllQueries(): void {
this.queryQueue = [];
this.isQuerying = false;
}
/**
* 获取查询状态
*/
getQueryStatus(): QueryStatus {
return {
isQuerying: this.isQuerying,
queueLength: this.queryQueue.length
};
}
}
/**
* 查询任务的并发函数
*/
@Concurrent
async function queryContactsConcurrent(task: QueryTask): Promise<ConcurrentQueryResult> {
const startTime = Date.now();
try {
// 1. 转换上下文
const context: Context = sendableContextManager.convertToContext(task.context);
// 2. 更新进度
if (task.progressCallback) {
// 注意:在并发函数中不能直接调用回调
// 可以通过其他方式传递进度信息
}
// 3. 执行查询
const contacts = await contact.queryContacts(context, task.options);
const endTime = Date.now();
const duration = endTime - startTime;
// 4. 检查是否超时
if (duration > (task.timeout || 30000)) {
return {
success: false,
error: '查询超时',
contacts: [],
duration: duration
};
}
return {
success: true,
contacts: contacts,
count: contacts.length,
duration: duration
};
} catch (error) {
const endTime = Date.now();
return {
success: false,
error: error instanceof Error ? error.message : '并发查询失败',
contacts: [],
duration: endTime - startTime
};
}
}
/**
* 接口定义
*/
interface QueryTask {
context: sendableContextManager.SendableContext;
options?: contact.ContactQueryOptions;
timeout?: number;
progressCallback?: (progress: number, message: string) => void;
}
interface QueryResult {
success: boolean;
contacts: contact.Contact[];
count?: number;
error?: string;
duration?: number;
}
interface ConcurrentQueryResult {
success: boolean;
contacts: contact.Contact[];
count?: number;
error?: string;
duration: number;
}
interface QueryStatus {
isQuerying: boolean;
queueLength: number;
}
5.2.6 完整的UI组件实现
@Entry
@Component
struct CompleteContactsQueryDemo {
// 查询管理器
private queryManager = new SafeContactsQueryManager();
// 状态变量
@State contacts: contact.Contact[] = [];
@State queryStatus: 'idle' | 'preparing' | 'querying' | 'success' | 'error' = 'idle';
@State queryProgress: number = 0;
@State statusMessage: string = '准备查询联系人';
@State queryDuration: number = 0;
@State errorDetails: string = '';
// 查询配置
private queryConfig = {
batchSize: 1000,
timeout: 30000,
fields: ['name', 'phone', 'email'] as contact.Attribute[]
};
// 组件生命周期
aboutToAppear(): void {
this.initQueryManager();
}
aboutToDisappear(): void {
this.cleanup();
}
// 初始化查询管理器
private initQueryManager(): void {
console.info('初始化联系人查询管理器');
}
// 清理资源
private cleanup(): void {
this.queryManager.cancelAllQueries();
console.info('清理查询资源');
}
// 开始安全查询
private async startSafeQuery(): Promise<void> {
if (this.queryStatus === 'querying') {
prompt.showToast({ message: '查询正在进行中' });
return;
}
// 重置状态
this.resetQueryState();
this.queryStatus = 'preparing';
this.statusMessage = '准备查询...';
try {
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
// 设置查询选项
const queryOptions: contact.ContactQueryOptions = {
attributes: this.queryConfig.fields
};
// 进度回调
const progressCallback = (progress: number, message: string) => {
this.queryProgress = progress;
this.statusMessage = message;
};
// 开始查询
this.queryStatus = 'querying';
this.statusMessage = '正在查询联系人...';
const startTime = Date.now();
const result = await this.queryManager.safeQueryContacts(
context,
queryOptions,
progressCallback
);
const endTime = Date.now();
this.queryDuration = endTime - startTime;
if (result.success) {
this.queryStatus = 'success';
this.contacts = result.contacts;
this.statusMessage = `查询成功: ${result.count} 个联系人`;
// 显示成功提示
prompt.showToast({
message: `查询完成,耗时 ${this.queryDuration}ms`,
duration: 2000
});
// 记录查询统计
this.logQueryStatistics(result);
} else {
this.queryStatus = 'error';
this.errorDetails = result.error || '未知错误';
this.statusMessage = '查询失败';
prompt.showToast({
message: '查询失败: ' + this.errorDetails,
duration: 3000
});
}
} catch (error) {
this.queryStatus = 'error';
this.errorDetails = error instanceof Error ? error.message : '未知异常';
this.statusMessage = '查询异常';
console.error('查询异常:', error);
prompt.showToast({ message: '查询发生异常' });
}
}
// 重置查询状态
private resetQueryState(): void {
this.queryProgress = 0;
this.queryDuration = 0;
this.errorDetails = '';
this.contacts = [];
}
// 记录查询统计
private logQueryStatistics(result: any): void {
console.info('=== 联系人查询统计 ===');
console.info(`查询状态: ${result.success ? '成功' : '失败'}`);
console.info(`联系人数量: ${result.count || 0}`);
console.info(`查询耗时: ${result.duration || 0}ms`);
console.info(`内存占用: ${this.getMemoryUsage()} MB`);
if (this.contacts.length > 0) {
console.info('前5个联系人:');
this.contacts.slice(0, 5).forEach((contact, index) => {
console.info(`${index + 1}. ${contact.name?.fullName || '未知'}`);
});
}
}
// 获取内存使用情况
private getMemoryUsage(): number {
// 这里可以使用系统API获取内存使用情况
return 0;
}
// 显示联系人详情
private showContactDetails(contact: contact.Contact): void {
let details = `姓名: ${contact.name?.fullName || '未知'}\n`;
if (contact.phoneNumbers && contact.phoneNumbers.length > 0) {
details += '电话号码:\n';
contact.phoneNumbers.forEach((phone, index) => {
details += ` ${index + 1}. ${phone.phoneNumber} (${phone.label})\n`;
});
}
if (contact.emails && contact.emails.length > 0) {
details += '邮箱:\n';
contact.emails.forEach((email, index) => {
details += ` ${index + 1}. ${email.email} (${email.label})\n`;
});
}
prompt.showDialog({
title: '联系人详情',
message: details,
buttons: [
{
text: '关闭',
color: '#666666'
}
]
});
}
// 导出联系人
private exportContacts(): void {
if (this.contacts.length === 0) {
prompt.showToast({ message: '没有可导出的联系人' });
return;
}
const exportData = this.contacts.map(contact => ({
name: contact.name?.fullName || '未知',
phones: contact.phoneNumbers?.map(p => p.phoneNumber).join(', ') || '',
emails: contact.emails?.map(e => e.email).join(', ') || ''
}));
const jsonStr = JSON.stringify(exportData, null, 2);
// 这里可以实现保存到文件的功能
console.info('导出联系人数据:', jsonStr);
prompt.showToast({ message: `已导出 ${this.contacts.length} 个联系人` });
}
// 构建UI
build() {
Column({ space: 15 }) {
// 标题区域
Row() {
Text('联系人安全查询演示')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#007DFF')
// 查询状态指示器
Circle({ width: 12, height: 12 })
.fill(this.getStatusColor())
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 20, bottom: 10 })
// 状态显示区域
Column({ space: 8 }) {
// 状态消息
Text(this.statusMessage)
.fontSize(16)
.fontColor(this.getStatusTextColor())
.textAlign(TextAlign.Center)
.width('100%')
// 进度显示
if (this.queryStatus === 'querying') {
Row({ space: 12 }) {
Progress({ value: this.queryProgress, total: 100 })
.width('70%')
.height(8)
.color('#007DFF')
Text(`${this.queryProgress}%`)
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 8, bottom: 8 })
}
// 查询统计
if (this.queryStatus === 'success' || this.queryStatus === 'error') {
Row({ space: 20 }) {
if (this.queryDuration > 0) {
Text(`耗时: ${this.queryDuration}ms`)
.fontSize(12)
.fontColor('#666666')
}
if (this.contacts.length > 0) {
Text(`数量: ${this.contacts.length}`)
.fontSize(12)
.fontColor('#666666')
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 4 })
}
}
.width('100%')
.padding(16)
.backgroundColor(this.getStatusBackgroundColor())
.borderRadius(12)
// 错误信息显示
if (this.errorDetails) {
Row({ space: 8 }) {
Image($r('app.media.ic_error'))
.width(16)
.height(16)
Text(this.errorDetails)
.fontSize(12)
.fontColor('#FF3B30')
.width('85%')
}
.width('100%')
.padding(12)
.backgroundColor('#FFEBEE')
.borderRadius(8)
.margin({ top: 10 })
}
// 联系人列表
if (this.contacts.length > 0) {
Column({ space: 10 }) {
// 列表标题
Row() {
Text('联系人列表')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(`(${this.contacts.length})`)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 4 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 8 })
// 联系人列表
Scroll() {
Column({ space: 8 }) {
ForEach(this.contacts, (contactItem: contact.Contact, index: number) => {
this.buildContactItem(contactItem, index);
})
}
}
.width('100%')
.height(300)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ top: 20 })
} else if (this.queryStatus === 'success') {
// 无联系人提示
Column({ space: 12 }) {
Image($r('app.media.ic_empty'))
.width(60)
.height(60)
.opacity(0.5)
Text('没有找到联系人')
.fontSize(14)
.fontColor('#999999')
Text('可能是通讯录为空或没有权限')
.fontSize(12)
.fontColor('#CCCCCC')
}
.width('100%')
.padding(40)
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ top: 20 })
}
// 操作按钮区域
Column({ space: 12 }) {
// 查询按钮
Button({
type: ButtonType.Capsule,
stateEffect: true
}) {
Row({ space: 8 }) {
if (this.queryStatus === 'querying') {
LoadingProgress()
.color(Color.White)
.width(16)
.height(16)
} else {
Image($r('app.media.ic_search'))
.width(16)
.height(16)
.fillColor(Color.White)
}
Text(this.getQueryButtonText())
.fontColor(Color.White)
.fontSize(16)
}
}
.width('100%')
.height(48)
.backgroundColor(this.getQueryButtonColor())
.enabled(this.queryStatus !== 'querying')
.onClick(() => this.startSafeQuery())
// 导出按钮
if (this.contacts.length > 0) {
Button('导出联系人')
.width('100%')
.height(44)
.backgroundColor('#34C759')
.fontColor(Color.White)
.enabled(this.queryStatus !== 'querying')
.onClick(() => this.exportContacts())
}
// 重置按钮
Button('重置')
.width('100%')
.height(44)
.type(ButtonType.Normal)
.enabled(this.queryStatus !== 'querying')
.onClick(() => {
this.resetQueryState();
this.queryStatus = 'idle';
this.statusMessage = '准备查询联系人';
})
}
.width('100%')
.margin({ top: 20 })
// 配置区域
Column({ space: 12 }) {
Text('查询配置')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 8 })
// 查询字段配置
Row({ space: 20 }) {
Text('查询字段:')
.fontSize(12)
.fontColor('#666666')
Row({ space: 8 }) {
Checkbox({ name: 'name', group: 'fields' })
.selected(this.queryConfig.fields.includes('name' as contact.Attribute))
.onChange((checked: boolean) => {
this.updateQueryField('name' as contact.Attribute, checked);
})
Text('姓名')
.fontSize(12)
.fontColor('#666666')
.onClick(() => {
const current = this.queryConfig.fields.includes('name' as contact.Attribute);
this.updateQueryField('name' as contact.Attribute, !current);
})
}
Row({ space: 8 }) {
Checkbox({ name: 'phone', group: 'fields' })
.selected(this.queryConfig.fields.includes('phone' as contact.Attribute))
.onChange((checked: boolean) => {
this.updateQueryField('phone' as contact.Attribute, checked);
})
Text('电话')
.fontSize(12)
.fontColor('#666666')
.onClick(() => {
const current = this.queryConfig.fields.includes('phone' as contact.Attribute);
this.updateQueryField('phone' as contact.Attribute, !current);
})
}
}
.width('100%')
.wrap(true)
// 超时配置
Row({ space: 20 }) {
Text('超时时间:')
.fontSize(12)
.fontColor('#666666')
TextInput({ text: (this.queryConfig.timeout / 1000).toString() })
.width(60)
.height(32)
.type(InputType.Number)
.onChange((value: string) => {
const seconds = parseInt(value);
if (!isNaN(seconds) && seconds > 0) {
this.queryConfig.timeout = seconds * 1000;
}
})
Text('秒')
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 20 })
// 使用说明
Column({ space: 8 }) {
Text('使用说明')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 8 })
Text('• 点击"查询联系人"按钮开始安全查询')
.fontSize(12)
.fontColor('#666666')
Text('• 查询在子线程执行,不会阻塞UI')
.fontSize(12)
.fontColor('#666666')
Text('• 支持十万级以上联系人查询')
.fontSize(12)
.fontColor('#666666')
Text('• 查询期间可取消,避免应用闪退')
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
.padding(16)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.margin({ top: 20, bottom: 30 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding(20)
}
// 构建联系人项
@Builder
private buildContactItem(contactItem: contact.Contact, index: number) {
Row({ space: 12 }) {
// 头像/首字母
Circle({ width: 40, height: 40 })
.fill(this.getAvatarColor(index))
.overlay(
Text(this.getContactInitial(contactItem))
.fontSize(16)
.fontColor(Color.White)
)
// 联系人信息
Column({ space: 4 }) {
Text(contactItem.name?.fullName || '未知姓名')
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
if (contactItem.phoneNumbers && contactItem.phoneNumbers.length > 0) {
Text(contactItem.phoneNumbers[0].phoneNumber || '')
.fontSize(12)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
.layoutWeight(1)
// 详情按钮
Button({
type: ButtonType.Circle,
stateEffect: true
}) {
Image($r('app.media.ic_more'))
.width(16)
.height(16)
.fillColor('#666666')
}
.width(32)
.height(32)
.backgroundColor('#F5F5F5')
.onClick(() => this.showContactDetails(contactItem))
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({ radius: 2, color: '#1A000000', offsetX: 0, offsetY: 1 })
}
// 工具方法
private getStatusColor(): ResourceStr {
switch (this.queryStatus) {
case 'idle':
case 'preparing':
return '#CCCCCC';
case 'querying':
return '#FF9500';
case 'success':
return '#34C759';
case 'error':
return '#FF3B30';
default:
return '#CCCCCC';
}
}
private getStatusTextColor(): ResourceStr {
switch (this.queryStatus) {
case 'idle':
case 'preparing':
return '#666666';
case 'querying':
return '#FF9500';
case 'success':
return '#34C759';
case 'error':
return '#FF3B30';
default:
return '#666666';
}
}
private getStatusBackgroundColor(): ResourceStr {
switch (this.queryStatus) {
case 'idle':
case 'preparing':
return '#F5F5F5';
case 'querying':
return '#FFF3E0';
case 'success':
return '#E8F5E8';
case 'error':
return '#FFEBEE';
default:
return '#F5F5F5';
}
}
private getQueryButtonText(): string {
switch (this.queryStatus) {
case 'idle':
case 'preparing':
return '查询联系人';
case 'querying':
return '查询中...';
case 'success':
return '重新查询';
case 'error':
return '重试查询';
default:
return '查询联系人';
}
}
private getQueryButtonColor(): ResourceStr {
switch (this.queryStatus) {
case 'idle':
case 'preparing':
return '#007DFF';
case 'querying':
return '#CCCCCC';
case 'success':
return '#34C759';
case 'error':
return '#FF9500';
default:
return '#007DFF';
}
}
private getAvatarColor(index: number): ResourceStr {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
return colors[index % colors.length];
}
private getContactInitial(contactItem: contact.Contact): string {
const name = contactItem.name?.fullName || '';
if (name.length === 0) return '?';
return name.charAt(0).toUpperCase();
}
private updateQueryField(field: contact.Attribute, checked: boolean): void {
if (checked) {
if (!this.queryConfig.fields.includes(field)) {
this.queryConfig.fields = [...this.queryConfig.fields, field];
}
} else {
this.queryConfig.fields = this.queryConfig.fields.filter(f => f !== field);
}
}
}
总结
以上代码提供了调用联系人服务queryContacts接口的完整安全解决方案。通过使用TaskPool在子线程中执行查询,可以有效避免主线程阻塞和应用闪退问题。代码包括:
-
完整的TaskPool实现:使用Sendable对象传递上下文
-
完善的错误处理:包括权限检查、超时控制、异常捕获
-
进度反馈机制:实时显示查询进度
-
内存优化:分批处理和选择性字段查询
-
用户体验优化:状态提示、错误展示、结果预览
-
配置灵活性:支持自定义查询字段和超时时间
这个解决方案可以安全处理十万级以上的联系人查询,确保应用在各种设备上都能稳定运行,避免因查询大量联系人而导致的闪退问题。
更多推荐



所有评论(0)