8、删除联系人:删除RawContacts记录、级联删除Data表数据、软删除与硬删除、回收站机制设计思路

删除联系人,听起来很简单对吧?不就是调个delete方法嘛。但如果你真这么想,那坑可就大了。Android的联系人数据库,说白了是一张巨大的关系网。你删一条RawContacts记录,如果不小心,可能会把别人的数据也带走了。

我当年第一次做联系人模块时,就犯过这个错。测试反馈说「删了A的联系人,B的电话号码也丢了」。排查了半天,发现是级联删除把共享的数据给清了。嗯,从那以后,我对删除逻辑就格外小心。

8.1 删除的本质:标记还是清除?

先想清楚一个问题:用户点了删除,是真的要彻底消失,还是说「先放回收站,万一后悔了还能找回来」?

这两种思路,对应着两种删除方式:

  • 硬删除(Hard Delete):直接从数据库里抹掉记录。不可恢复。
  • 软删除(Soft Delete):给记录打一个标记,比如把DELETED字段置为1。查询时过滤掉。

我个人习惯,在正式产品里优先用软删除。为什么?因为用户误操作的概率比你想象的高得多。你想想看,一个联系人存了好几年,突然手滑删了,找不回来,那体验得多糟糕。

核心原则: 删除操作,能软删就别硬删。除非你确定用户真的想「彻底清除」。

8.2 删除RawContacts:级联删除的连锁反应

RawContacts表是联系人的「身份凭证」。你删一条RawContacts记录,系统会自动级联删除它下面的Data表数据。这是ContentProvider帮我们做的。

但问题来了——如果多个RawContacts属于同一个Contact(聚合后的联系人),你删掉其中一个,系统会重新聚合剩下的RawContacts。如果只剩一个了,那这个Contact ID可能也会被回收。

我建议你这样做:

// 删除指定RawContacts,系统会自动级联删除Data
ContentResolver resolver = context.getContentResolver();
Uri rawContactUri = ContentUris.withAppendedId(
    RawContacts.CONTENT_URI, rawContactId);
int deleted = resolver.delete(rawContactUri, null, null);

你看,代码就三行。但背后的逻辑远不止这些。我曾经在项目里遇到一个问题:删了一个RawContacts后,另一个RawContacts的显示名称变了。排查后发现,是因为聚合规则重新计算了显示名称的来源。

注意: 删除RawContacts时,不要手动去删Data表。让系统级联删除更安全。你手动删,可能会破坏引用完整性。

8.3 软删除的实现:标记法

软删除的核心,就是给RawContacts表加一个标记字段。Android原生的RawContacts表里其实有一个DELETED字段(INTEGER类型,0表示未删除,1表示已删除)。

具体做法:

// 软删除:标记为已删除
ContentValues values = new ContentValues();
values.put(RawContacts.DELETED, 1);
Uri uri = ContentUris.withAppendedId(
    RawContacts.CONTENT_URI, rawContactId);
context.getContentResolver().update(uri, values, null, null);

查询时,记得过滤掉已删除的记录:

String selection = RawContacts.DELETED + " = 0";
Cursor cursor = resolver.query(
    RawContacts.CONTENT_URI,
    null, selection, null, null);

说白了,软删除就是「假装看不见」。数据还在,只是不展示给用户。

小技巧: 如果你需要做回收站功能,软删除是基础。你可以在DELETED=1的基础上,再加一个DELETED_AT时间戳字段,方便做自动清理。

8.4 硬删除:什么时候用?

硬删除,就是真的从数据库里把记录抹掉。我一般只在以下场景用:

  • 用户主动选择「永久删除」
  • 回收站里的记录超过保留期限(比如30天)
  • 测试环境清理数据

硬删除的代码和软删除很像,只是把update换成了delete:

// 硬删除:彻底清除
ContentResolver resolver = context.getContentResolver();
Uri uri = ContentUris.withAppendedId(
    RawContacts.CONTENT_URI, rawContactId);
resolver.delete(uri, null, null);

但你要记住,硬删除是不可逆的。我曾经在测试环境里手滑执行了批量硬删除,结果整个联系人表空了。还好是测试库,不然就等着挨骂吧。

警告: 硬删除前,最好先备份数据。或者至少弹个确认对话框,让用户二次确认。

8.5 回收站机制设计思路

回收站,说白了就是「软删除 + 时间管理 + 恢复功能」。我设计过几版回收站,踩了不少坑,这里分享一个比较成熟的思路:

8.5.1 数据结构设计

你不需要新建一张表。直接在RawContacts表上做文章就行:

字段 类型 说明
DELETED INTEGER 0=正常,1=已删除(回收站)
DELETED_AT INTEGER 删除时间戳(毫秒)
DELETED_FROM TEXT 删除来源(比如「手动删除」「同步删除」)

嗯,这里要注意:DELETED_AT字段原生表里没有,你需要自己维护。我建议在ContentProvider里拦截删除操作,自动填充这个字段。

8.5.2 回收站的核心流程

我画了一张流程图,帮你理清思路:

用户点击删除 是否选择「永久删除」? 硬删除:直接DELETE 软删除:标记DELETED=1 数据彻底清除 数据进入回收站 用户操作? 恢复:DELETED=0 永久删除:硬删除 超30天自动清理

8.5.3 恢复联系人

恢复操作其实就是软删除的逆过程:

// 恢复联系人:把DELETED改回0
ContentValues values = new ContentValues();
values.put(RawContacts.DELETED, 0);
values.putNull("DELETED_AT");  // 清除删除时间
Uri uri = ContentUris.withAppendedId(
    RawContacts.CONTENT_URI, rawContactId);
context.getContentResolver().update(uri, values, null, null);

这里有个细节:恢复后,系统会自动重新聚合联系人。如果原来的聚合组已经变了,恢复后的联系人可能会出现在不同的分组里。我建议你在恢复后,主动触发一次联系人同步或刷新。

8.5.4 自动清理过期数据

回收站不能无限期保留数据。我一般设置30天自动清理。实现方式有两种:

  • 定时任务:用WorkManager每天跑一次,删除DELETED_AT超过30天的记录。
  • 懒清理:每次查询回收站时,顺便清理过期的数据。

我个人更推荐懒清理。为什么呢?因为定时任务如果没跑成功,数据就永远留在那了。懒清理虽然每次查询多花几毫秒,但胜在可靠。

// 懒清理:查询回收站时,顺便删除过期数据
long thirtyDaysAgo = System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000;
String where = RawContacts.DELETED + "=1 AND DELETED_AT < " + thirtyDaysAgo;
resolver.delete(RawContacts.CONTENT_URI, where, null);
提示: 懒清理的删除操作,建议放在异步线程里执行。别在主线程搞,万一数据量大,会卡UI。

8.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要直接操作Data表:删除RawContacts时,让系统级联删除Data。你手动删,可能会破坏聚合关系。
  • 软删除后查询要加过滤:所有查询联系人的地方,都要加上DELETED=0的条件。否则回收站的数据会出现在正常列表里。
  • 注意同步冲突:如果你的应用支持多设备同步,软删除的记录可能会被同步到其他设备。我建议在同步协议里也加上DELETED标记。
  • 回收站数据也要备份:用户可能以为删了就没了,但回收站里的数据其实还在。如果用户卸载重装,回收站数据就丢了。我建议定期把回收站数据导出到云端。

嗯,删除联系人这块,说白了就是「怎么删、删多深、能不能恢复」这三个问题。想清楚了,代码写起来就顺了。


公众号:蓝海资料掘金营,微信deep3321