尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android 短信备份实战:用 ContentResolver + XmlSerializer 把短信导出成 XML 文件

Android 短信备份实战:用 ContentResolver + XmlSerializer 把短信导出成 XML 文件 1. 从 mmssms.db 到 XMLAndroid 短信备份到底在备份什么Android 短信备份这件事很多人第一反应是去翻/data/data/com.android.providers.telephony/databases/mmssms.db。这个思路本身没错那个 SQLite 文件确实是短信的最终落盘位置但真机没有 root 权限根本读不到它。所以实际开发里更稳的做法是走ContentResolver把系统暴露出来的content://sms/当成一个标准数据源来查再用XmlSerializer把结果序列化成 XML 文件。这套链路能做什么简单说它能把手机里收件箱、发件箱、草稿箱里的短信按你指定的字段号码、时间、类型、正文读出来写成一个结构化的 XML。适合谁适合需要做本地数据迁移、换机前留档、或者单纯想研究 Android 四大组件里 ContentProvider 怎么用的开发者。它不依赖 root不依赖第三方云服务全程在设备本地完成。我试过直接拿模拟器发几条短信然后用这套代码导出文件确实能生成但中间踩了几个坑权限没声明导致query直接抛SecurityExceptiongetColumnIndex写错字段名返回 -1getString(-1)又抛异常还有短信一多边读边写如果不及时关 Cursor内存会涨得很快。所以这篇不打算只贴一段能跑的代码而是把权限、查询、序列化、校验、排错整条链路拆开讲让你自己独立完成一次备份。核心检索词先摆出来Android 短信备份、ContentResolver 查询 sms Uri、XmlSerializer 序列化落盘。这三个词贯穿全文你按这个顺序理解就不会乱。先明确数据模型。content://sms/这张表里我们关心的字段有四个address是手机号date是毫秒时间戳type区分收发1 接收、2 发送还有 3 草稿等body是短信正文。查询时用 projection 数组指定这四个字段比SELECT *更可控也避免拿到一些你不需要的列。为什么不用直接读 db 文件因为从 Android 4.4 开始非系统应用访问其他应用私有目录被严格限制mmssms.db属于 TelephonyProvider 的私有数据普通应用没有读权限。而 ContentProvider 是系统专门开的口子配合READ_SMS权限就能合法查询。这也是为什么标题里强调 ContentResolver而不是直接文件操作。XML 作为导出格式的好处是自描述、跨平台、易校验。你导出的文件拿到电脑上用浏览器或任意文本编辑器都能看字段名一目了然。相比 CSVXML 对嵌套和特殊字符比如短信正文里的换行、引号处理更自然XmlSerializer会自动做转义省得你自己拼字符串时被注入问题坑到。下面从工程配置开始一步步把这条链路搭起来。你会看到完整的权限声明、查询投影、序列化代码以及导出后怎么校验文件结构、怎么在真机上验证读取结果。每一段都可以直接复制进你的项目改改用。2. 前置准备权限、依赖与 TaoToken 接入配置动手之前先把环境和权限理清楚。短信备份涉及两个敏感权限读短信和写外部存储。从 Android 6.0 开始这两个都属于危险权限除了在清单里声明运行时还得动态申请。很多人代码写得没问题一跑就崩八成是漏了运行时申请这一步。先看清单文件AndroidManifest.xml里要加的内容uses-permission android:nameandroid.permission.READ_SMS / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion28 /注意WRITE_EXTERNAL_STORAGE我加了maxSdkVersion28。因为从 Android 10API 29开始分区存储生效应用往公共目录写文件要走 MediaStore 或者用应用专属目录直接Environment.getExternalStorageDirectory()在 29 以上会受限。如果你只打算在旧设备或模拟器上跑这样写没问题如果要兼容新系统导出路径建议换成getExternalFilesDir()那个目录不需要存储权限。运行时申请用ActivityCompat.requestPermissions读短信和写存储一起申请if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.READ_SMS, Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1001); }依赖方面XmlSerializer来自org.xmlpull.v1Android SDK 自带不用额外引库。ContentResolver、Cursor、Uri也都是 framework 里的零依赖。这也是这套方案轻量的原因不需要引入任何第三方库就能完成。如果你在开发过程中需要调用大模型来辅助生成或审查这段序列化代码可以用 TaoToken 的 API 做接入。它的 Base URL 是https://taotoken.net/api兼容 OpenAI 风格的接口。配置方式很简单在请求里带上 Key 和模型 ID 即可。比如用 curl 验证一下连通性curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 帮我检查这段 XmlSerializer 代码有没有漏掉 endDocument}] }Key 在控制台的 API Keys 页面生成地址是https://taotoken.net/console/api-keys。模型 ID 按你实际要用的填对话调试可以在模型对话页面试。这套配置和短信备份本身是解耦的你完全可以离线写完代码只在需要辅助时接一下。再强调一个容易忽略的点READ_SMS在 Google Play 上属于受限权限上架审核很严个人练手或内部工具无所谓商业应用要慎重。本文聚焦本地备份的技术实现不涉及上架合规讨论。环境准备好后下一节进入真正的查询和序列化代码。我会把投影、Cursor 遍历、XmlSerializer 的每个调用都写清楚包括参数含义和常见写错的地方。3. 可复制配置ContentResolver 查询与 XmlSerializer 序列化完整代码这一节是全文的核心代码可以直接复制。我把它拆成三块查询短信、封装数据、序列化落盘。每块都给出完整方法你按顺序拼进项目即可。先定义数据实体SmsInfo字段和数据库列对应public class SmsInfo { private String address; private long date; private int type; private String body; public String getAddress() { return address; } public void setAddress(String address) { this.address address; } public long getDate() { return date; } public void setDate(long date) { this.date date; } public int getType() { return type; } public void setType(int type) { this.type type; } public String getBody() { return body; } public void setBody(String body) { this.body body; } }查询部分用ContentResolver.query拿到 Cursor投影只取需要的四列public ListSmsInfo querySms(Context context) { ListSmsInfo list new ArrayList(); Uri uri Uri.parse(content://sms/); ContentResolver resolver context.getContentResolver(); String[] projection new String[]{address, date, type, body}; Cursor cursor null; try { cursor resolver.query(uri, projection, null, null, date ASC); if (cursor ! null) { int idxAddress cursor.getColumnIndex(address); int idxDate cursor.getColumnIndex(date); int idxType cursor.getColumnIndex(type); int idxBody cursor.getColumnIndex(body); while (cursor.moveToNext()) { SmsInfo info new SmsInfo(); info.setAddress(cursor.getString(idxAddress)); info.setDate(cursor.getLong(idxDate)); info.setType(cursor.getInt(idxType)); info.setBody(cursor.getString(idxBody)); list.add(info); } } } finally { if (cursor ! null) cursor.close(); } return list; }这里有个关键优化getColumnIndex在循环外调用一次而不是每次moveToNext都调。原示例里在循环内反复调getColumnIndex短信一多就是明显的性能浪费。另外sortOrder传了date ASC让导出结果按时间排序方便后续阅读。序列化部分用XmlSerializer把 List 写成 XMLpublic static File backupSmsToXml(ListSmsInfo list, Context context) { XmlSerializer serializer Xml.newSerializer(); File file new File(context.getExternalFilesDir(null), sms_backup.xml); FileOutputStream os null; try { os new FileOutputStream(file); serializer.setOutput(os, UTF-8); serializer.startDocument(UTF-8, true); serializer.startTag(null, smss); serializer.attribute(null, count, String.valueOf(list.size())); serializer.attribute(null, exportTime, String.valueOf(System.currentTimeMillis())); for (SmsInfo sms : list) { serializer.startTag(null, sms); serializer.attribute(null, address, sms.getAddress() null ? : sms.getAddress()); serializer.attribute(null, date, String.valueOf(sms.getDate())); serializer.attribute(null, type, String.valueOf(sms.getType())); serializer.text(sms.getBody() null ? : sms.getBody()); serializer.endTag(null, sms); } serializer.endTag(null, smss); serializer.endDocument(); serializer.flush(); return file; } catch (Exception e) { Log.e(SmsBackup, serialize failed, e); return null; } finally { if (os ! null) { try { os.close(); } catch (IOException ignored) {} } } }几个细节值得说。setOutput的编码用UTF-8和startDocument保持一致否则中文短信会乱码。startDocument第二个参数传true表示输出独立的 XML 声明。serializer.flush()在endDocument之后调用确保缓冲区数据全部写入文件漏了这步可能文件不完整。address和body做了 null 判断因为草稿短信的号码可能为空直接attribute传 null 会抛异常。如果你用 TaoToken 的 coding plan 来辅助生成这类模板代码可以把上面的实体和查询逻辑贴进去让它补全序列化部分模型 ID 选你常用的即可。接入文档在https://taotoken.net/doc里面有完整的参数说明。调用入口放在 Activity 里一个按钮触发public void onBackupClick(View v) { new Thread(() - { ListSmsInfo list querySms(this); File out backupSmsToXml(list, this); runOnUiThread(() - { if (out ! null) { Toast.makeText(this, 导出成功: out.getAbsolutePath(), Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, 导出失败, Toast.LENGTH_SHORT).show(); } }); }).start(); }查询和写文件都放在子线程避免主线程 IO 导致 ANR。这是原示例里没处理好的地方短信量大时主线程直接卡死。4. 验证请求与成功结果导出文件结构校验与真机读取代码跑通不代表结果正确。导出后必须校验两件事XML 结构是否合法、内容是否和手机里的短信对得上。这一节给出具体的校验方法。先看导出的文件长什么样。用adb pull把文件拉到电脑adb pull /sdcard/Android/data/com.example.smsbackup/files/sms_backup.xml ./sms_backup.xml如果你用的是Environment.getExternalStorageDirectory()路径换成/sdcard/sms_backup.xml。拉下来后用xmllint校验格式xmllint --noout sms_backup.xml echo XML 格式合法合法的话会输出提示不合法会打印具体行号和错误。常见的格式错误是标签没闭合比如漏了endTagxmllint会直接指出来。再看内容结构。一个正常的导出文件应该像这样?xml version1.0 encodingUTF-8 standaloneyes ? smss count3 exportTime1730000000000 sms address8613800138000 date1729999000000 type1你好这是一条测试短信/sms sms address10086 date1729999100000 type2话费余额查询/sms sms address8613900139000 date1729999200000 type1收到稍后回复/sms /smss校验要点根标签是smsscount属性等于实际短信条数每条sms有address、date、type三个属性正文在标签内type为 1 表示接收、2 表示发送和数据库定义一致。真机读取验证我一般用两种方式。第一种是直接在手机上用文件管理器打开 XML看能不能正常显示中文乱码说明编码没对上。第二种是写个简单的解析回读把 XML 再读回 List和查询结果比对条数XmlPullParser parser Xml.newPullParser(); parser.setInput(new FileInputStream(file), UTF-8); int eventType parser.getEventType(); int parsedCount 0; while (eventType ! XmlPullParser.END_DOCUMENT) { if (eventType XmlPullParser.START_TAG sms.equals(parser.getName())) { parsedCount; } eventType parser.next(); } Log.i(SmsBackup, 解析条数 parsedCount 原始条数 list.size());两个数字相等说明序列化和解析闭环没问题。如果解析条数偏少多半是某条短信正文里含有特殊字符导致标签提前闭合检查serializer.text是否对内容做了转义XmlSerializer 默认会转义、、一般不用手动处理。还有一种验证是拿模拟器发短信。用 DDMS 或者adb emu sms send命令adb emu sms send 10086 测试短信内容发几条后重新导出对比条数是否增加。这个方式适合快速验证查询逻辑有没有漏掉某些类型的短信。校验通过后你手上就有一个结构清晰、可读可解析的备份文件了。整个过程不依赖网络不依赖 root纯本地完成。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错对照写这类代码最容易在几个固定位置翻车。我把真实遇到过的报错和原因列出来你对照着查。java.lang.SecurityException: Permission Denial: reading com.android.providers.telephony.SmsProvider。这是最典型的权限问题。原因有两个清单里没声明READ_SMS或者声明了但没做运行时申请。Android 6.0 以上必须动态申请只写清单不生效。检查checkSelfPermission的返回值确认用户点了允许。android.database.CursorIndexOutOfBoundsException: Index -1 requested。这个报错指向getColumnIndex返回了 -1。说明你投影里的字段名和实际列名对不上比如把body写成了content或者把address写成了number。解决方法是打印cursor.getColumnNames()看真实列名再修正 projection。java.io.FileNotFoundException: /storage/emulated/0/sms_backup.xml: open failed: EACCES。写文件权限问题。Android 10 以上用Environment.getExternalStorageDirectory()会失败换成getExternalFilesDir(null)即可那个路径不需要存储权限。如果坚持用公共目录得走 MediaStore。XmlPullParserException: Unexpected token或者导出的 XML 打不开。多半是漏了endTag或endDocument标签没闭合。检查每个startTag是否都有对应的endTagstartDocument是否配了endDocument。如果你在接入 TaoToken 辅助调试时遇到401 Unauthorized那是 Key 没带对或者过期了。检查请求头Authorization: Bearer key里的 key 是否和控制台生成的一致注意别把Bearer拼错。local proxy failed一般是本地网络配置问题检查你的请求地址是不是https://taotoken.net/api别多加或少加路径段。reading choices这类报错通常出现在流式响应解析时模型返回的 chunk 结构和你预期的不一致检查choices[0].delta.content的取值路径。OAuth 相关报错则多见于用第三方客户端登录的场景确认回调地址和授权范围配置正确。还有一个隐蔽的坑Cursor 没关导致内存泄漏。查询完一定要在finally里cursor.close()否则短信多了之后 CursorWindow 会占满内存后续查询直接失败。原示例里在方法末尾关了但如果中间抛异常就关不掉所以放finally最稳。最后提醒一句content://sms/查的是全部短信包括收件箱、发件箱、草稿。如果你只想导收件箱可以在 selection 里加type ?参数传1。这个过滤条件按需加不影响整体链路。6. 把备份链路接进你的工程从查询到落盘的复用建议代码跑通之后怎么把它变成一个可复用的模块而不是一次性脚本是下一步要考虑的。我的做法是把查询和序列化拆成独立工具类Activity 只负责权限申请和触发这样换到别的项目里直接拷两个文件就行。工具类SmsBackupUtil暴露两个静态方法queryAll(Context)返回 ListexportToXml(List, File)返回是否成功。导出路径由调用方传入不写死在工具类里方便你根据系统版本决定用公共目录还是应用专属目录。这样职责清晰测试也好写。如果你要定期备份可以配合WorkManager做周期任务但注意后台读短信在 Android 10 以上限制更多实际能不能跑取决于系统版本和厂商策略。练手阶段手动触发就够了。导出文件建议加时间戳命名比如sms_backup_20250101_120000.xml避免多次备份互相覆盖。文件头部的exportTime属性也保留方便追溯。这套链路的价值在于它把 ContentProvider、Cursor、XmlSerializer 三个知识点串成了一个完整场景。你理解了短信备份换成联系人、通话记录套路是一样的换 Uri、换 projection、换实体字段序列化部分几乎不用改。所以别只盯着短信把它当成 ContentResolver 查询加 XML 落盘的通用模板来掌握。需要查接入文档或者生成 Key 的时候接入文档在https://taotoken.net/docAPI Keys 在https://taotoken.net/console/api-keys。长期做编码和 Agent 相关开发的话Coding Plan 页面有更完整的方案说明。这些是辅助工具核心的查询和序列化逻辑还是得你自己跑一遍才算真会。
返回列表