通常情况下,每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器上获取数据。不过这个时候就出现了一个问题,这些数据到底要以什么样的格式在网络上传输呢?随便传递一段文本肯定是不行的,因为另一方根本就不会知道这段文本的用途是什么。因此,一般我们都会在网络上传输一些格式化后的数据,这种数据会有一定的结构规格和语义,当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而取出他想要的那部分内容。
在网络上传输数据时最常用的格式有两种:XML 和 JSON,下面我们就来一个一个地进行学习,本节首先学习ー下如何解析 XML 格式的数据。
在开始之前我们还需要先解决一个问题,就是从哪儿才能获取一段 XML 格式的数据呢?这里我准备教你搭建一个最简单的 Web 服务器,在这个服务器上提供一段 XML 文本,然后我们在程序里去访问这个服务器,再对得到的 XML 文本进行解析。
搭建 Web 服务器其实非常简单,有很多的服务器类型可供选择,这里我准备使用 Apache 服务器。另外,这里只会演示Windows 系统下的搭建过程,因为Mac 和 Ubuntu 系统都是默认安装好Apache 服务器的,只需要启动一下即可。如果你使用的是这两种系统,可以自行搜索一下具体的操作方法。
下面我们来看Windows 系统下的搭建过程。首先你需要去下载一个 Apache 服务器的安装包,官方下载地址是:http: /httpd. apache.org。如果你在这个网址中找不到 Windows 版的安装包,也可以直接在百度上搜索“Apache 服务器下载”,将会找到很多下载链接。下载完成后双击就可以进行安装了。一直Next 即可,中途会提示让输入自己的域名,我们随便输入一个域名就可以了,还有一个就是提示安装路径,我们自己选择,然后就可以完成安装了。安装成功之后服务器会自动启动,你可以打开的浏览器来验证一下。在地址栏输入 127.0.0.1,如果出现了如图所示的界面,就说明服务器已经启动成功了。
接下来进入到 C:\ Apache\htdocs 目录下,在这里新建一个名为 get_data.xml 的文件,然后编辑这个文件,并加入如下 XML 格式的内容。
<app>
<app>
<id>1</id>
<name>Google</name>
<version>1.0</version>
</app>
<app>
<id>2</id>
<name>Google</name>
<version>2.1</version>
</app>
<app>
<id>3</id>
<name>Google</name>
<version>2.3</version>
</app>
</app>
这时在浏览器中访问http://127.0.0.1/get_data.xml 这个网址,就应该出现下图所示的内容。
好了,准备工作到此结束,接下来就让我们在Android 程序里去获取并解压这段XML 数据吧。
11.3.1 Pull 解析方式
解析 XML 格式的数据其实也有挺多种方式的,本节中我们学习比较常用的两种,Pull 解析和 SAX 解析。那么简单起见,这里仍然是在 NetworkTest 项目的基础上继续开发,这样我们就可以重用之前网络通信部分的代码,从而把工作的重心放在 XML 数据解析上。
既然 XML 格式的数据已经提供好了,现在要做的就是从中解析出我们想要得到的那部分内容。修改 MainActivity 中的代码,如下所示:
private fun sendRequestWithOkHttp() {
// 开启线程发起网络请求
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
// 访问的服务器地址是计算机本机
.url("http://10.0.2.2/get_data.xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null){
parseXMLWithPull(responseData)
}
}catch (e:Exception){
e.printStackTrace()
}
}
}
private fun parseXMLWithPull(xmlData: String) {
try {
val factory = XmlPullParserFactory.newInstance()
val xmlPullParser = factory.newPullParser()
xmlPullParser.setInput(StringReader(xmlData))
var eventType = xmlPullParser.eventType
var id = ""
var name = ""
var version = ""
while (eventType != XmlPullParser.END_DOCUMENT){
val nodeName = xmlPullParser.name
when(eventType){
// 开始解析某个节点
XmlPullParser.START_TAG -> {
when(nodeName){
"id" -> id = xmlPullParser.nextText()
"name" -> name = xmlPullParser.nextText()
"version" -> version = xmlPullParser.nextText()
}
}
// 完成解析某个节点
XmlPullParser.END_TAG -> {
if ("app" == nodeName){
Log.d("MainActivity","id is $id")
Log.d("MainActivity","name is $name")
Log.d("MainActivity","version is $version")
}
}
}
eventType = xmlPullParser.next()
}
}catch (e:Exception){
e.printStackTrace()
}
}
可以看到,这里首先将Http 请求的地址改成了http://10.0.2.2/get_data.xml,10.0.2.2 对于模拟器来说就是计算机甄姬的IP 地址。在得到了服务器的返回数据后,我们不再直接将其展示,而是调用了parseXMLWithPull() 方法来解析服务器返回的数据。
下面就来仔细看下,parseXMLWithPull() 方法中的代码吧。这里首先要创建一个XMLPullParserFactory 的实例,并借助这个实例得到XmlPullParser 对象,然后调用XmlPullParser 的setInput() 方法将服务器返回的XML 数据设置进去,之后就可以开始解析了。解析的过程也非常简单,通过getEventType() 方法可以得当当前的解析事件,然后在一个while 循环中不断第进行解析,如果当前的解析事件不等于 XmlPullParser.END_DOCUMENT ,说明解析的工作还没完成,调用next() 方法后可以获取下一个解析事件。
在 while 循环中,我们通过 getName()方法得到当前节点的名字,如果发现节点名等于 id、name 或 version,就调用 nextText() 方法来获取节点内具体的内容,每当解析完一个 app 节点后就将获取到的内容打印出来。
好了,整体的过程就是这么简单,不过在程序运行之前还得再进行一项额外的配置。从Android 9.0 系统开始,应用程序默认只允许使用HTTPS 类型的网络请求,HTTP 类型的网络请求因为有安全隐患默认不再被支持,而我们搭建的Apache 服务器现在使用的就是HTTP。
那么为了能让程序使用HTTP,我们还需要进行如下配置才可以。右击res 目录 → New → File ,创建一个network_config.xml 文件。然后修改newwork_config.xml 文件中的内容,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted = "true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
这段配置文件的意思就是允许我们以明文的方式在网络上传输数据,而且HTTP 使用的就是明文传输方式。
接下来修改Androidmanifest.xml 中的代码来启动我们刚才配置的文件(android:networkSecurityConfig = "@xml/network_config"):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.networktest">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_config"
>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这就可以在程序中使用HTTP了,下面就让我们来测试一下吧。运行 NetworkTest 项目,然后点击 Send Request 按钮,观察 logcat 中的打印日志,如图所示:
可以看到,我们已经将XML 数据中的指定内容成功解析出来了。
11.3.2 SAX 解析方式
Pull 解析方式虽然非常好用,但它并不是我们唯一的选择。SAX 解析也是一种特别常用的 XML 解析方式,虽然它的用法比 Pul 解析要复杂一些,但在语义方面会更加清楚。
通常情况下我们都会新建一个类继承自 DefaultHandler,并重写父类的 5 个方法,如下所示:
class MyHandler : DefaultHandler() {
override fun startDocument() {
super.startDocument()
}
override fun startElement(
uri: String?,
localName: String?,
qName: String?,
attributes: Attributes?
) {
super.startElement(uri, localName, qName, attributes)
}
override fun characters(ch: CharArray?, start: Int, length: Int) {
super.characters(ch, start, length)
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
super.endElement(uri, localName, qName)
}
override fun endDocument() {
super.endDocument()
}
}
这 5 个方法一看就很清楚吧?startDocument() 方法会在开始 XML 解析的时候调用,startElement() 方法会在开始解析某个节点的时候调用,characters() 方法会在获取节点中内容的时候调用,endElement() 方法会在完成解析某个节点的时候调用,endDocument() 方法会在完成整个 XML 解析的时候调用。其中,startElement() 、characters() 和 endElement() 这 3 个方法是有参数的,从 XML 中解析出的数据就会以参数的形式传入到这些方法中。需要注意的是,在获取节点中的内容时,characters() 方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。
那么下面就让我们尝试用 SAX 解析的方式来实现和上一小节中同样的功能吧。新建一个 Contenthandler 类继承自 Defaulthandler,并重写父类的 5 个方法,如下所示:
class MyHandler : DefaultHandler() {
private var nodeName = ""
private lateinit var id : StringBuilder
private lateinit var name: StringBuilder
private lateinit var version : StringBuilder
override fun startDocument() {
id = StringBuilder()
name = StringBuilder()
version = StringBuilder()
}
override fun startElement(
uri: String,
localName: String,
qName: String?,
attributes: Attributes?
) {
// 记录当前节点名
nodeName = localName
Log.d("ContentHandler","uri is $uri")
Log.d("ContentHandler","localName is $localName")
Log.d("ContentHandler","qName is $qName")
Log.d("ContentHandler","attributes is $attributes")
}
override fun characters(ch: CharArray?, start: Int, length: Int) {
// 根据当前节点名判断将内容添加到哪一个StringBuilder 对象中
when(nodeName){
"id" -> id.append(ch,start,length)
"name" -> name.append(ch,start,length)
"version" -> version.append(ch,start,length)
}
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
if ("app" == localName){
Log.d("ContentHandler","id is ${id.toString().trim()}")
Log.d("ContentHandler","name is ${name.toString().trim()}")
Log.d("ContentHandler","version is ${version.toString().trim()}")
// 最后来将StringBuilder 清空
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}
override fun endDocument() {
}
}
可以看到,我们首先给 id、name 和 verslon 节点分别定义了一个 StringBuilder 对象并在 startDocument() 方法里对它们进行了初始化。每当开始解析某个节点的时候,start Element() 方法就会得到调用,其中 localName 参数记录着当前节点的名字,这里我们把它记录下来。接着在解析节点中具体内容的时候就会调用 characters() 方法,我们会根据当前的节点名进行判断,将解析出的内容添加到哪一个 StringBuilder 对象中。最后在 endElement() 方法中进行判断,如果 app 节点已经解析完成,就打印出 id、name 和 verslon 的内容。需要注意的是,目前 id、name 和 version 中都可能是包括回车或换行符的,因此在打印之前我们还需要调用一下 trim() 方法,并且打印完成后还要将 StringBuilder 的内容清空掉,不然的话会影响下一次内容的读取。
接下来的工作就非常简单了,修改 MainActivity 中的代码,如下所示:
private fun sendRequestWithOkHttp() {
// 开启线程发起网络请求
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
// 访问的服务器地址是计算机本机
.url("http://10.0.2.2/get_data.xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null){
parseXMLWithSAX(responseData)
}
}catch (e:Exception){
e.printStackTrace()
}
}
}
private fun parseXMLWithSAX(xmlData: String) {
try {
val factory = SAXParserFactory.newInstance()
val xmlReader = factory.newSAXParser().xmlReader
val handler = MyHandler()
// 将ContentHandler 的实例设置到 XMLReader 中
xmlReader.contentHandler = handler
// 开始执行解析
xmlReader.parse(InputSource(StringReader(xmlData)))
}catch (e:Exception){
e.printStackTrace()
}
}
在得到了服务器返回的数据后,我们这次去调用 parseXMLWithSAX() 方法来解析 XML 数据。parseXMLWithSAX() 方法中先是创建了一个 SaxParserFactory 的对象,然后再获取到 XMLReader 对象,接着将我们编写的 ContentHandler 的实例设置到 XMLReader 中,最后调用 parse() 方法开始执行解析就好了。
现在重新运行一下程序,点击 Send Request 按钮后观察 Logcat I中的打印日志,你会看到和Pull 解析一样的结果。
除了 Pull 解析和 SAX 解析之外,其实还有一种 DOM 解析方式也算挺常用的,不过这里我们就不再展开进行讲解了,感兴趣的话你可以自己去査阅一下相关资料。