12.02 数据解析-XML解析

2025/5/12 Android 开发基础

XML 在 Android 里有两个身份:一是系统资源格式(layout、manifest、strings 全是 XML——但那些由系统解析,不需要你写解析代码),二是服务端接口格式(老接口、RSS、部分政企系统仍在用)。本节讲第二种场景的三种解析器与选型,并延伸到数据模型(DTO)的设计原则——这部分与格式无关,是解析工作的"上层建筑"。

# 一、三种 XML 解析器对比

解析器 模式 内存 速度 适用场景
DOM(Document) 树模型:整棵读进内存 高(文档大易 OOM) 快(随机访问方便) 小文档、需要反复随机访问节点
SAX(Simple API) 流式推送:边读边回调 最低 快 只读一遍的大文档
Pull(XmlPullParser) 流式拉取:主动 next() 推进 最低 快 Android 推荐,可控性强

Push 与 Pull 的区别(面试常问):SAX 是"解析器推给你"——注册监听器,事件发生时被动回调,逻辑分散在回调里;Pull 是"你主动拉"——循环调用 next() 取事件,用普通循环和分支控制整个解析流程,可随时中断、跳过,代码更直观。Android 内置 Pull(org.xmlpull),也是系统资源解析采用的方案。

# 二、XmlPullParser 实战

以下面这段 RSS/接口数据为例:

<?xml version="1.0" encoding="utf-8"?>
<articles>
    <article id="1">
        <title>Handler 消息机制原理</title>
        <author>JiangMing</author>
        <tags>
            <tag>Android</tag>
            <tag>Handler</tag>
        </tags>
    </article>
    <article id="2">
        <title>数据存储</title>
        <author>JiangMing</author>
        <tags>
            <tag>SQLite</tag>
        </tags>
    </article>
</articles>

# 1. 数据实体

public class Article {
    public long id;
    public String title;
    public String author;
    public List<String> tags = new ArrayList<>();
}

# 2. Pull 解析实现

public class ArticleXmlParser {

    public static List<Article> parse(InputStream in) throws Exception {
        List<Article> articles = new ArrayList<>();
        XmlPullParser parser = Xml.newPullParser();
        parser.setInput(in, "UTF-8");

        int event = parser.getEventType();          // 第一个事件:START_DOCUMENT
        Article current = null;
        String currentTag = null;

        while (event != XmlPullParser.END_DOCUMENT) { // 循环推进,直到文档结束
            switch (event) {
                case XmlPullParser.START_TAG:
                    switch (parser.getName()) {
                        case "article":
                            current = new Article();
                            // 读取属性:id="1"是article的attribute而非文本
                            current.id = Long.parseLong(parser.getAttributeValue(0));
                            break;
                        case "title":
                            currentTag = "title";
                            break;
                        case "author":
                            currentTag = "author";
                            break;
                        case "tag":
                            currentTag = "tag";
                            break;
                        default:
                            break;
                    }
                    break;

                case XmlPullParser.TEXT:
                    // TEXT事件:当前标签内的文本(注意可能包含空白换行)
                    if (current != null && currentTag != null) {
                        String text = parser.getText().trim();
                        if (!text.isEmpty()) {
                            switch (currentTag) {
                                case "title":
                                    current.title = text;
                                    break;
                                case "author":
                                    current.author = text;
                                    break;
                                case "tag":
                                    current.tags.add(text);
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                    break;

                case XmlPullParser.END_TAG:
                    if ("article".equals(parser.getName())) {
                        articles.add(current);       // 一个article闭合:收集
                        current = null;
                    }
                    currentTag = null;
                    break;

                default:
                    break;
            }
            event = parser.next();                   // 主动拉取下一个事件
        }
        return articles;
    }
}
// 使用(配合11章网络请求)
ThreadPoolManager.execute(() -> {
    Response response = OkHttpHolder.getClient()
            .newCall(new Request.Builder().url("https://example.com/feed.xml").build())
            .execute();
    if (response.body() != null) {
        List<Article> articles = ArticleXmlParser.parse(response.body().byteStream());
        // 切主线程更新UI(10章范式)
    }
});

Pull 解析的四个关键事件:START_DOCUMENT → 循环处理 START_TAG / TEXT / END_TAG → END_DOCUMENT。写法的固定套路:用 currentTag 记住"我正在处理哪个标签的文本",在 TEXT 事件里按它分发赋值。

# 3. DOM 解析参考(对比用)

// DOM:一次加载成树,适合小文档
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document doc = factory.newDocumentBuilder().parse(inputStream);
NodeList nodes = doc.getElementsByTagName("article");
for (int i = 0; i < nodes.getLength(); i++) {
    Element article = (Element) nodes.item(i);
    String id = article.getAttribute("id");
    String title = article.getElementsByTagName("title").item(0).getTextContent();
}

# 三、数据模型(DTO)设计原则

解析质量一半取决于解析器,另一半取决于模型设计。这部分内容较多,已单独成篇:12.03 数据模型(DTO)设计——包含 DTO/VO/Mapper 三层架构、七条设计原则(可空语义、集合预初始化、映射统一、嵌套建模、UI 语义进 VO、不可变倾向、版本兼容)与端到端优秀实践案例、反例对照表。本节只留两条与 XML 直接相关的提醒:

  1. XML 解析结果同样落到 DTO/VO:上面的 Article 类就是 DTO,Pull 解析循环本身兼任了 Mapper 的角色
  2. 属性与文本的建模差异:<article id="1"> 的 id 建模为字段 long id,<title> 文本建模为 String title——解析方式不同,但模型形态一致

# 四、常见问题与陷阱

  1. TEXT 事件被空白打断:标签内换行缩进会产生多个 TEXT 事件,getText() 后必须 trim() + 判空(上面代码已处理)。
  2. 属性 vs 文本:<article id="1"> 的 id 是属性(getAttributeValue),<title> 内容是文本事件,别混。
  3. DOM 解析大文档 OOM:树模型内存 ≈ 文档体积的数倍,RSS/大配置用 Pull。
  4. XML 实体转义:内容含 &lt; &amp; 时 Pull 自动解码为 <、&;反向生成 XML 时要转义。
  5. 命名空间:XML 带 xmlns 前缀时用 getNamespace()/getAttributeValue(ns, name),普通 getName() 可能拿到带前缀的名字。

# 结束

本节讲解了 XML 解析的三种解析器对比(DOM 树模型/SAX 推模型/Pull 拉模型——Android 推荐)、XmlPullParser 完整实战(四类事件 + currentTag 套路),并延伸到数据模型设计原则(DTO/VO 分层、包装类型、集合预初始化、版本兼容思维)。到这里数据解析整章完成,下一节处理 UI 耗资源大户——图片:12.04 图片加载。更多内容请参考:XML 解析 (opens new window)。