27、XML解析:SAX、DOM、Pull解析方式对比、解析网络XML数据。
说到XML解析,这几乎是每个Android开发都绕不开的坎儿。我记得刚入行那会儿,项目里要对接一个老旧的接口,返回的数据格式就是XML。那时候我心想,不就是解析个XML嘛,能有多难?结果一上手,发现坑还真不少。
今天咱们就把Android里三种主流的XML解析方式——SAX、DOM、Pull,掰开揉碎了聊一聊。我会结合自己踩过的坑,帮你理清什么时候该用谁。
先说说XML解析到底在干嘛
说白了,就是把XML这种结构化的文本,转换成咱们程序里能用的对象或者数据。你想想看,网络请求回来的是一串字符串,但你的App需要的是User对象、News列表,这中间就需要一个“翻译官”。
Android SDK里给我们提供了三种“翻译官”:SAX、DOM和Pull。它们各有各的脾气。
核心要点:选择哪种解析方式,取决于你的数据量大小、内存敏感度以及开发效率需求。没有银弹,只有最合适的。
SAX解析:流式处理的“节俭派”
SAX的全称是Simple API for XML。它采用事件驱动模型,说白了就是一边读一边处理,不把整个文档都加载到内存里。
工作原理:它像一个流水线工人,从头到尾扫描XML文档。每遇到一个开始标签、结束标签、文本内容,就会触发一个回调事件。你在回调里写自己的处理逻辑。
优点:
- 内存占用极低,适合解析超大XML文件
- 解析速度快,因为是顺序读取
缺点:
- 代码写起来比较繁琐,需要实现多个回调方法
- 不支持随机访问,你不能跳到文档的某个特定位置
- 只能读,不能写
我在项目中遇到过一个大文件解析的场景,一个离线地图数据包,XML文件有几十兆。用DOM直接内存溢出,换成SAX后,问题迎刃而解。嗯,这里要注意,SAX的回调里千万别做耗时操作,否则会阻塞解析流程。
DOM解析:内存里的“完整地图”
DOM(Document Object Model)的方式就豪放多了。它会把整个XML文档读入内存,构建成一棵完整的树结构。你可以在这棵树上任意穿梭、修改、删除节点。
工作原理:一次性加载,生成Document对象。然后你可以通过getElementsByTagName、getChildNodes等方法随意访问任何节点。
优点:
- 开发效率高,API非常直观,符合直觉
- 支持随机访问和修改,可以增删改查
缺点:
- 内存占用巨大,文件越大越吃内存
- 解析速度相对较慢,因为要构建完整的树
我建议,如果你的XML文件很小(比如配置文件、简单的响应数据),用DOM最省事。但要是文件超过1MB,你就得掂量掂量了。我曾经在低端机上用DOM解析一个3MB的XML,直接导致App被系统杀掉——血的教训。
Pull解析:Android官方推荐的“平衡派”
Pull解析是Android专门为移动端设计的。它和SAX有点像,也是流式处理,但它的控制权在你手里。
工作原理:你主动调用next()方法,让解析器“拉”取下一个事件。你可以在循环里判断当前事件类型,然后做相应的处理。想停就停,想跳就跳。
优点:
- 内存占用低,和SAX差不多
- 代码结构清晰,比SAX好写,比DOM更省内存
- 解析过程可控,你可以随时停止解析
缺点:
- 不支持修改文档,只能读取
- 需要手动管理解析状态,稍微有点啰嗦
我个人习惯在大部分网络请求的XML解析中使用Pull。为什么?因为Android系统内部很多地方都在用Pull,比如XmlPullParser就是系统自带的,不需要引入任何第三方库。而且它的性能在移动端表现非常均衡。
三种方式对比一览
| 特性 | SAX | DOM | Pull |
|---|---|---|---|
| 读取方式 | 事件驱动,顺序读取 | 加载到内存,构建树 | 主动拉取,顺序读取 |
| 内存占用 | 低 | 高 | 低 |
| 解析速度 | 快 | 慢(大文件) | 快 |
| 随机访问 | 不支持 | 支持 | 不支持 |
| 修改文档 | 不支持 | 支持 | 不支持 |
| 代码复杂度 | 高 | 低 | 中 |
| Android推荐 | 不推荐 | 小文件可用 | 强烈推荐 |
实战:用Pull解析网络XML数据
光说不练假把式。咱们直接写一段代码,看看怎么用Pull解析从网络获取的XML数据。假设服务器返回了这样一个XML:
<?xml version="1.0" encoding="utf-8"?>
<books>
<book id="1">
<title>Android开发艺术探索</title>
<author>任玉刚</author>
<price>89.00</price>
</book>
<book id="2">
<title>第一行代码</title>
<author>郭霖</author>
<price>79.00</price>
</book>
</books>
我们要把它解析成List<Book>。核心代码如下:
public List<Book> parseBooks(InputStream inputStream) throws Exception {
List<Book> bookList = null;
Book currentBook = null;
String currentTag = null;
// 获取Android系统自带的Pull解析器
XmlPullParser parser = Xml.newPullParser();
parser.setInput(inputStream, "UTF-8");
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
String tagName = parser.getName();
switch (eventType) {
case XmlPullParser.START_TAG:
currentTag = tagName;
if ("books".equals(tagName)) {
bookList = new ArrayList<>();
} else if ("book".equals(tagName)) {
currentBook = new Book();
// 读取属性
String id = parser.getAttributeValue(null, "id");
currentBook.setId(Integer.parseInt(id));
}
break;
case XmlPullParser.TEXT:
// 获取当前节点的文本内容
String text = parser.getText();
if (currentBook != null && currentTag != null) {
switch (currentTag) {
case "title":
currentBook.setTitle(text);
break;
case "author":
currentBook.setAuthor(text);
break;
case "price":
currentBook.setPrice(Double.parseDouble(text));
break;
}
}
break;
case XmlPullParser.END_TAG:
if ("book".equals(tagName) && bookList != null) {
bookList.add(currentBook);
currentBook = null;
}
currentTag = null;
break;
}
// 主动拉取下一个事件
eventType = parser.next();
}
return bookList;
}
这段代码看起来有点长,但逻辑很清晰。你想想看,我们就是在循环里判断当前是什么事件,然后做对应的事。START_TAG时创建对象,TEXT时赋值,END_TAG时把对象加入列表。
小技巧:在TEXT事件中,文本内容可能包含空白符(换行、空格)。建议用text.trim()处理一下,避免出现空字符串赋值的情况。我曾经因为这个bug排查了半天,最后发现是换行符搞的鬼。
网络请求与解析的整合
在实际项目中,我们通常会用OkHttp或Retrofit发起网络请求,拿到ResponseBody的字节流,然后直接传给解析器。注意,千万不要把整个响应体先转成字符串再解析,那样会多一次内存拷贝,对大文件来说很浪费。
// 使用OkHttp示例
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.example.com/books.xml")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 处理网络错误
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
// 直接从response.body().byteStream()获取输入流
InputStream inputStream = response.body().byteStream();
try {
List<Book> books = parseBooks(inputStream);
// 回到主线程更新UI
} catch (Exception e) {
e.printStackTrace();
} finally {
inputStream.close();
}
}
}
});
注意事项:网络请求和解析操作一定要放在子线程中执行。Android 4.0之后不允许在主线程进行网络请求,否则会抛出NetworkOnMainThreadException。另外,记得在finally块中关闭InputStream,防止内存泄漏。
三种解析方式的选择建议
说了这么多,到底该怎么选?我根据自己的经验,给你一个简单的决策思路:
- 文件小于100KB,且需要修改XML:用DOM,省事。
- 文件较大(超过1MB),只需要读取:用Pull,Android官方推荐,性能好。
- 文件超大(几十MB),且对内存极度敏感:用SAX,虽然代码难写点,但内存控制最好。
- 大部分网络API返回的XML:无脑用Pull,准没错。
我个人现在几乎只用Pull。为什么?因为它的代码可读性比SAX好太多,内存又比DOM省。你想想看,在移动端,内存就是命根子,能省一点是一点。
知识体系总览
下面这张图帮你梳理了本章的核心逻辑,一看就明白:
这张图把三种方式的核心特点都标出来了。你可以看到,Pull在内存和代码复杂度之间取得了很好的平衡,这也是为什么它成为Android官方首选的原因。
好了,关于XML解析的内容就聊到这儿。记住,技术选型没有绝对的对错,关键是要理解每种方式的优缺点,然后根据你的实际场景做选择。下次遇到XML解析的需求,你应该心里有数了吧?
公众号:蓝海资料掘金营,微信deep3321