如果你在玩手机的时候不能上网,那你一定会感到特别地枯燥乏味。没错,现在早已不是玩 单机的时代了,无论是PC、手机、平板,还是电视,几乎都会具备上网的功能,在可预见的未 来,手表、眼镜、汽车等设备也会逐个加入到这个行列,21世纪的确是互联网的时代。

当然,Android手机肯定也是可以上网的,所以作为开发者,我们就需要考虑如何利用网络来编写出更加出色的应用程序,像QQ、微博、微信等常见的应用都会大量使用网络技术。本章主要会讲述如何在手机端使用HTTP协议和服务器端进行网络交互,并对服务器返回的数据进行解析,这也是Android中最常使用到的网络技术,下面就让我们一起来学习一下吧。

9.1 WebView 的用法

        有时候我们可能会碰到一些比较特殊的需求,比如说要求在应用程序里展示一些网页。相信每个人都知道,加载和显示网页通常都是浏览器的任务,但是需求里又明确指出,不允许打开系统浏览器,而我们当然也不可能自己去编写一个浏览器出来,这时应该怎么办呢?

        不用担心,Android早就已经考虑到了这种需求,并提供了一个WebView控件,借助它我们就可以在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页。

      WebView的用法也是相当简单,下面我们就通过一个例子来学习一下吧。新建一个 WebViewTest项目,然后修改activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android: layout_width=,,match_parent" android:layout_height=,,match_parent" ><WebViewandroid: id=,,(a+id/web_view"android:layout_width="match_parent"android:layout_height="match_parent" />
</LinearLayout>

        可以看到,我们在布局文件中使用到了一个新的控件:WebViewo这个控件当然也就是用来 显示网页的了,这里的写法很简单,给它设置了一个id,并让它充满整个屏幕。

然后修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedlnstanceState) (         super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);WebView webView = (WebView) findViewByld(R.id.webview);     webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient());webView.loadllrl("http://www.baidu.com");
}

        MainActivity中的代码也很短,首先使用findViewByld ()方法获取到了 WebView的实例, 然后调用WebView的getSettings ()方法可以去设置一些浏览器的属性,这里我们并不去设置 过多的属性,只是调用了 setJavaScriptEnabled()方法来让WebView支持JavaScript脚本。

        接下来是非常重要的一个部分,我们调用了 WebView的setWebViewClient ()方法,并传 入了一个WebViewClient的实例。这段代码的作用是,当需要从一个网页跳转到另一个网页时, 我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器。

        最后一步就非常简单了,调用WebView的loadU「l()方法,并将网址传入,即可展示相应 网页的内容,这里就让我们看一看百度的首页长什么样吧。

        另外还需要注意,由于本程序使用到了网络功能,而访问网络是需要声明权限的,因此我们 还得修改AndroidManifest.xml文件,并加入权限声明,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"         package="com.example.webviewtest"><uses-permission android:name="android・permission・INTERNET" />
</manifest>

        在开始运行之前,首先需要保证你的手机或模拟器是联网的,如果你使用的是模拟器,只需 保证电脑能正常上网即可。然后就可以运行一下程序了,效果如图9.1所示。

        可以看到,WebViewTest这个程序现在已经具备了一个简易浏览器的功能,不仅成功将百度 的首页展示了出来,还可以通过点击链接浏览更多的网页。

        当然,WebView还有很多更加高级的使用技巧,我们就不再继续进行探讨了,因为那不是本 章的重点。这里先介绍了一下WebView的用法,只是希望你能对HTTP协议的使用有一个最基 本的认识,接下来我们就要利用这个协议来做一些真正的网络开发工作了。

9.2使用HTTP协议访问网络

        如果说真的要去深入分析HTTP协议,可能需要花费整整一本书的篇幅。这里我当然不会这 么干,因为毕竟你是跟着我学习Android开发的,而不是网站开发。对于HTTP协议,你只需要 稍微了解一些就足够了,它的工作原理特别简单,就是客户端向服务器发出一条HTTP请求,服 务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以 了。是不是非常简单? 一个浏览器的基本工作原理也就是如此了。比如说上一节中使用到的 WebView控件,其实也就是我们向百度的服务器发起了一条HTTP请求,接着服务器分析出我们 想要访问的是百度的首页,于是会把该网页的HTML代码进行返回,然后WebView再调用手机 浏览器的内核对返回的HTML代码进行解析,最终将页面展示出来。

        简单来说,WebView已经在后台帮我们处理好了发送HTTP请求、接收服务响应、解析返回 数据,以及最终的页面展示这几步工作,不过由于它封装得实在是太好了,反而使得我们不能那 么直观地看出HTTP协议到底是如何工作的。因此,接下来就让我们通过手动发送HTTP请求的 方式,来更加深入地理解一下这个过程。

  1. 使用 HttpURLConnection

        在过去,Android ±发送HTTP请求一般有两种方式:HttpURLConnection和HttpClient。不 过由于HttpClient存在API数量过多、扩展困难等缺点,Android团队越来越不建议我们使用这 种方式。终于在Android 6.0系统中,HttpClient的功能被完全移除了,标志着此功能被正式弃用, 因此本小节我们就学习一下现在官方建议使用的HttpURLConnection的用法。

        首先需要获取到HttpURLConnection的实例,一般只需new出一个URL对象,并传入目标 的网络地址,然后调用一下openConnection ()方法即可,如下所示:

​
URL url = new URL("百度一下,你就知道");HttpURLConnection connection = (HttpURLConnection) url.openConnection();​

        在得到了 HttpURLConnection的实例之后,我们可以设置一下HTTP请求所使用的方法。常 用的方法主要有两个:GET和POST。GET表示希望从服务器那里获取数据,而POST则表示希望 提交数据给服务器。写法如下:

connection.setRequestMethod("GET");

        接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服务器 希望得到的一些消息头等。这部分内容根据自己的实际情况进行编写,示例写法如下:

connection.setConnectTimeout(8000);connection.setReadTimeout(8000);

        之后再调用getlnputStreamO方法就可以获取到服务器返回的输入流了,剩下的任务就是 对输入流进行读取,如下所示:

Inputstream in = connection.getInputStream();

        最后可以调用disconnect ()方法将这个HTTP连接关闭掉,如下所示:

connection.disconnect();

        下面就让我们通过一个具体的例子来真正体验一下HttpURLConnection的用法。新建一个 NetworkTest项目,首先修改activity main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:orientation="vertical" android:layout_width="match_parent"     android:layout_height="match_parent" ><Buttonandroid:id="@+id/send_request"android: layout_width=,,match_parent"android:layout_height="wrap_content" android:text="Send Request" /><ScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent" ><TextViewandroid:id="@+id/response_text"android: layout_width="niatch_pa「ent” android:layout_height=,,wrap_content" /></ScrollView>
</LinearLayout>

        注意这里我们使用了一个新的控件:ScrollView,它是用来做什么的呢?由于手机屏幕的空 间一般都比较小,有些时候过多的内容一屏是显示不下的,借助ScrollView控件的话,我们就可 以以滚动的形式查看屏幕外的那部分内容。另外,布局中还放置了一个Button和一个TextView, Button用于发送HTTP请求,TextView用于将服务器返回的数据显示出来。

        接着修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
TextView responseText;
^Override
protected void onCreate(Bundle savedlnstanceState) (
super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain); Button sendRequest = (Button) findViewById(R.id.sendrequest); responseText = (TextView) findViewById(R.id.responsetext); sendRequest.setOnClickListener(this);
}
^Override
public void onClick(View v) {
if (v.getld() == R.id.sendrequest) ( sendRequestWithHttpURLConnection();
}
}
private void sendRequestWithHttpURLConnection() {
//开启线程来发起网络请求
new Thread(new Runnable() {
^Override public void run() { HttpURLConnection connection = null; BufferedReader reader = null;
try (
URL url = new URL("http://www.baidu.com");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection. 5etReaofTifneout(80OO); Inputstream in = connection.getInputStream();
//下面对获取到的输入流进行读取
reader = new BufferedReader(new InputStreamReader(in)); StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) { response.append(line);
} showResponse(response.toString());
} catch (Exception e) {
e.printStackTrace();
} finally { if (reader != null) ( try {
reader.close();
} catch (lOException e) {
e.printStackTrace();
}
}
if (connection != null) (
connection.disconnect();
}
}
}
}).start();
}
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
^Override
public void run() {
//在这里进行UI操作,将结果显示到界面上 responseText.setText(response);
}
});
}
}

        可以看到,我们在Send Request按钮的点击事件里调用了 sendRequestWithHttpURL- ConnectionO方法,在这个方法中先是开启了一个子线程,然后在子线程里使用HttpURL- Connection发出一条HTTP请求,请求的目标地址就是百度的首页。接着利用BufferedReader对 服务器返回的流进行读取,并将结果传入到了 showResponse()方法中。而在showResponse() 方法里则是调用了一个runOnUiThreadO方法,然后在这个方法的匿名类参数中进行操作,将 返回的数据显示到界面上。那么这里为什么要用这个runOnUiThreadO方法呢?这是因为 Android是不允许在子线程中进行UI操作的,我们需要通过这个方法将线程切换到主线程,然后 再更新UI元素。关于这部分内容,我们将会在下一章中进行详细讲解,现在你只需要记得必须 这么写就可以了。

        完整的一套流程就是这样,不过在开始运行之前,仍然别忘了要声明一下网络权限。修改

AndroidManifest.xml中的代码,如下所示:

AndroidManifest.xml中的代码,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.networktest">
<uses-permission android:name="android.permission.INTERNET1' />
</manifest>

好了,现在运行一下程序,并点击Send Request按钮,结果如图9.2所示。

        是不是看得头晕眼花?没错,服务器返回给我们的就是这种HTML代码,只是通常情况下浏览器都会将这些代码解析成漂亮的网页后再展示出来。

        那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将HTTP请求的方法改成POST,并在获取输入流之前把要提交的数据写岀即可。注意每条数据都要以键值对的形式存在,数据与数据之间用“&”符号隔开,比如说我们想要向服务器提交用户名和密码,就可以这样写:

connection.setRequestMethod("POST");DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out .writeBytes(,,username=admin&password=123456,');

        好了,相信你已经将HttpURLConnection的用法很好地掌握了

  1. 使用 OkHttp

当然我们并不是只能使用HttpURLConnection,完全没有任何其他选择,事实上在开源盛行 的今天,有许多岀色的网络通信库都可以替代原生的HttpURLConnection,而其中OkHttp无疑是 做得最出色的一个。

OkHttp是由鼎鼎大名的Square公司开发的,这个公司在开源事业上面贡献良多,除了 OkHttp 之外,还开发了像Picasso, Retrofit等著名的开源项目。OkHttp不仅在接口封装上面做得简单易 用,就连在底层实现上也是自成一派,比起原生的HttpURLConnection,可以说是有过之而无不 及,现在已经成了广大Android开发者首选的网络通信库。那么本小节我们就来学习一下OkHttp 的用法,OkHttp 的项目主页地址是:https://github.com/square/okhttpo

在使用OkHttp之前,我们需要先在项目中添加OkHttp库的依赖。编辑app/build.gradle文件, 在dependencies闭包中添加如下内容:

dependencies {compile fileTree(dir: 'libs', include: [jar'])compile 1 com.android.support:appcompat-v7:24.2.1' testCompile 'junit:junit:4.12'compile 'com・squareup・okhttp3:okhttp:3.4.1'}

        添加上述依赖会自动下载两个库,一个是OkHttp库,一个是Okio库,后者是前者的通信基 础。其中3.4.1是我写本书时OkHttp的最新版本,你可以访问OkHttp的项目主页来查看当前最 新的版本是多少。

        下面我们来看一下OkHttp的具体用法,首先需要创建一个OkHttpClient的实例,如下所示:

OkHttpClient client = new OkHttpClient();

        接下来如果想要发起一条HTTP请求,就需要创建一个Request对象:

Request request = new Request.Builder().build();

        当然,上述代码只是创建了一个空的Request对象,并没有什么实际作用,我们可以在最 终的build()方法之前连缀很多其他方法来丰富这个Request对象。比如可以通过url()方法 来设置目标的网络地址,如下所示:

Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();

        之后调用OkHttpClient的newCall()方法来创建一个Call对象,并调用它的execute()方 法来发送请求并获取服务器返回的数据,写法如下:

Response response = client.newCall(request).execute():

        其中Response对象就是服务器返回的数据了,我们可以使用如下写法来得到返回的具体内容:

String responseData = response.body().st ring();

        如果是发起一条POST请求会比GET请求稍微复杂一点,我们需要先构建出一个Request Body对象来存放待提交的参数,如下所示:

RequestBody requestBody = new FormBody.Builder().add("username", "admin").add(“password”, h123456") .buildO;

        然后在Request.Builder中调用一下post ()方法,并将RequestBody对象传入:

RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add(“password”, h123456") .buildO;

        接下来的操作就和GET请求一样了,调用executeO方法来发送请求并获取服务器返回的 数据即可。

        好了,OkHttp的基本用法就先学到这里,本书中后面所有网络相关的功能我们都将会使用 OkHttp来实现,到时候再进行进一步的学习。那么现在我们先把NetworkTest这个项目改用OkHttp 的方式再实现一遍吧。

        由于布局部分完全不用改动,所以现在直接修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
(QOverride
public void onClick(View v) (
if (v.getld() == R.id.sendrequest) ( sendRequestWithOkHttpd;
}
}
private void sendRequestWithOkHttp() { new Thread(new Runnable() {
@0verride
public void run() { try {
OkHttpClient client = new OkHttpClientf); Request request = new Request.Builder() .url("http://www.baidu.com") .buildO;
Response response = client.newCall(request).execute(); String responseData = response.body().st ring(); showResponse(responseData);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}

        这里我们并没有做太多的改动,只是添加了一个sendRequestWithOkHttp()方法,并在 Send Request按钮的点击事件里去调用这个方法。在这个方法中同样还是先开启了一个子线程, 然后在子线程里使用OkHttp发出一条HTTP请求,请求的目标地址还是百度的首页,OkHttp的 用法也正如前面所介绍的一样。最后仍然还是调用了 showResponseO方法来将服务器返回的数 据显示到界面上。

        仅仅是改了这么多代码,现在我们就可以重新运行一下程序了。点击Send Request按钮后, 你会看到和上一小节中同样的运行结果,由此证明,使用OkHttp来发送HTTP请求的功能也已 经成功实现了。

        这样的话,相信你就已经把HttpURLConnection和OkHttp的基本用法都掌握得差不多了。

9.3解析XML格式数据

        通常情况下,每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提 交数据,也可以从服务器上获取数据。不过这个时候就出现了一个问题,这些数据到底要以什么 样的格式在网络上传输呢?随便传递一段文本肯定是不行的,因为另一方根本就不会知道这段文 本的用途是什么。因此,一般我们都会在网络上传输一些格式化后的数据,这种数据会有一定的 结构规格和语义,当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而取出他 想要的那部分内容。

        在网络上传输数据时最常用的格式有两种:XML和JSON,下面我们就来一个一个地进行学 习,本节首先学习一下如何解析XML格式的数据。

        在开始之前我们还需要先解决一个问题,就是从哪儿才能获取一段XML格式的数据呢?这 里我准备教你搭建一个最简单的Web服务器,在这个服务器上提供一段XML文本,然后我们在 程序里去访问这个服务器,再对得到的XML文本进行解析。

        搭建Web服务器其实非常简单,有很多的服务器类型可供选择,这里我准备使用Apache服 务器。首先你需要去下载一个Apache服务器的安装包,官方下载地址是:http://httpd.apache.org/ download.cgio如果你在这个网址中找不到Windows版的安装包,也可以直接在百度上搜索 uApache服务器下载”,将会找到很多下载链接。

下载完成后双击就可以进行安装了,如图9.3所示。

然后一直点击Next,会提示让你输入自己的域名,我们随便填一个域名就可以了,如图9.4 所示。

 

        接着继续一直点击Next,会提示让你选择程序安装的路径,这里我选择安装到C:\Apache S 录下,之后再继续点击Next就可以完成安装了。安装成功后服务器会自动启动起来,你可以打 开电脑的浏览器来验证一下。在地址栏输入127.0.0.1,如果出现了如图9.5所示的界面,就说明 服务器已经启动成功了。

        接下来进入到C:\Apache\htdocs目录下,在这里新建一个名为get_data.xml的文件,然后编 辑这个文件,并加入如下XML格式的内容。

<apps><app><id>l</id><name>Google Maps</name> <version>l.0</version> </app><app><id>2</id><name>Chrome</name><version>2.l</version> </app><app><id>3</id><name>Google Play</name> <version>2.3</version> </app></apps>

        这时在浏览器中访问http://127.0Ol/get_data.xml这个网址,就应该出现如图9.6所示的内容。

         好了,准备工作到此结束,接下来就让我们在Android程序里去获取并解析这段XML数 据吧。

9.3.1 Pull解析方式

        解析XML格式的数据其实也有挺多种方式的,本节中我们学习比较常用的两种,Pull解析 和SAX解析。那么简单起见,这里仍然是在NetworkTest项目的基础上继续开发,这样我们就可 以重用之前网络通信部分的代码,从而把工作的重心放在XML数据解析上。

        既然XML格式的数据已经提供好了,现在要做的就是从中解析出我们想要得到的那部分内 容。修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private void sendRequestWithOkHttp() (
new Thread(new Runnable() (
(QOverride
public void run() { try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//指定访问的服务器地址是电脑本机
.url("http://10.0.2.2/get_data.xml")
.build();
Response response = client.newCall(request).execute(); String responseData = response.body().st ring(); parseXMLWithPull(responseData);
} catch (Exception e) {
e.printStackT race();
}
}
}).start();
}
private void parseXMLWithPull(String xmlData) { try {
XmlPullParserFactory factory = XmlPullParserFactory.newlnstance(); XmlPullParser xmlPullParser = factory.newPullParser(); xmlPullParser.setinput(new St ringReader(xmlData)); int eventType = xmlPullParser.getEventType();
String id = ""j
String name =
String version =; while (eventType != XmlPullParser.END_DOCUMENT) { String nodeName = xmlPullParser.getName(); switch (eventType) {
//开始解析某个节点
case XmlPuIlParser.START_TAG: { if ("id".equals(nodeName)) { id = xmlPullParser.nextText();
} else if ("name1* .equals(nodeName)) { name = xmlPulLParser.nextText();
} else if ("version".equals(nodeName)) { version = xmlPullParser.nextText();
} break;
}
//完成解析某个节点
case XmlPullParser.END_TAG: {
if ("app".equals(nodeName)) {
Log.dC^ainActivity**, “id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
} break;
}
default:
break;
} eventType = xmlPullParser.next();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

        可以看到,这里首先是将HTTP请求的地址改成了 http://10.022/get_data.xml, 10.0.2.2对于 模拟器来说就是电脑本机的IP地址。在得到了服务器返回的数据后,我们并不再直接将其展示, 而是调用了 parseXMLWithPuUO方法来解析服务器返回的数据。

        下面就来仔细看下parseXMLWithPuUO方法中的代码吧。这里首先要获取到一个 XmlPullParserFactory的实例,并借助这个实例得到XmlPullParser对象,然后调用 XmlPuUParser的setlnput()方法将服务器返回的XML数据设置进去就可以开始解析了。解 析的过程也非常简单,通过getEventTypeO可以得到当前的解析事件,然后在一个while循环 中不断地进行解析,如果当前的解析事件不等于XmlPullParser.END DOCUMENT,说明解析工 作还没完成,调用next()方法后可以获取下一个解析事件。

        在while循环中,我们通过getName()方法得到当前节点的名字,如果发现节点名等于id、 name或version,就调用nextText ()方法来获取节点内具体的内容,每当解析完一个app节点后 就将获取到的内容打印出来。

        好了,整体的过程就是这么简单,下面就让我们来测试一下吧。运行NetworkTest项目,然 后点击Send Request按钮,观察logcat中的打印日志,如图9.7所示。

         可以看到,我们已经将XML数据中的指定内容成功解析出来了。

  1. SAX解析方式

        Pull解析方式虽然非常好用,但它并不是我们唯一的选择。SAX解析也是一种特别常用的 XML解析方式,虽然它的用法比Pull解析要复杂一些,但在语义方面会更加清楚。

        通常情况下我们都会新建一个类继承自DefaultHandler,并重写父类的5个方法,如下所示:

public class MyHandler extends DefaultHandler {(QOverridepublic void startDocument() throws SAXException {}@0verridepublic void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {}©Overridepublic void characters(char[] ch, int start, int length) throws SAXException { }(aOverridepublic void endElement(String uri, String localName, String qName) throws SAXException {}@0verridepublic void endDocument() throws SAXException { }}

        这5个方法一看就很清楚吧? startDocument()方法会在开始XML解析的时候调用, startElement ()方法会在开始解析某个节点的时候调用,characters ()方法会在获取节点中内 容的时候调用,endElement ()方法会在完成解析某个节点的时候调用,endDocument ()方法会 在完成整个XML解析的时候调用。其中,startElement (). characters()和endElement() 这3个方法是有参数的,从XML中解析出的数据就会以参数的形式传入到这些方法中。需要注 意的是,在获取节点中的内容时,cha「acters()方法可能会被调用多次,一些换行符也被当作 内容解析出来,我们需要针对这种情况在代码中做好控制。

        那么下面就让我们尝试用SAX解析的方式来实现和上一小节中同样的功能吧。新建一个

        ContentHandler类继承自DefaultHandler,并重写父类的5个方法,如下所示:

public class ContentHandler extends DefaultHandler {private String nodeName;private StringBuilder id;private StringBuilder name;private StringBuilder version;(QOverridepublic void startDocument() throws SAXException {id = new StringBuilder();name = new StringBuilder();version = new StringBuilder。;}©Overridepublic void sta rtElement(St ring uri, String localName, String qName, Attributes attributes) throws SAXException {//记录当前节点名nodeName = localName;}^Overridepublic void characters(char[] ch, int start, int length) throws SAXException { //根据当前的节点名判断将内容添加到哪一个StringBuilder对象中 if ("id".equals(nodeName)) {id.append(ch, start, length);} else if ("name".equals(nodeName)) {name.append(ch, start, length);} else if ("version".equals(nodeName)) { version.append(ch, start, length);}}(QOverridepublic void endElement(String uri, String localName, String qName) throws SAXException { if ("app".equals(localName)) {Log.d("ContentHandler", "id is " + id.toString().trim());Log.d("ContentHandler", "name is " + name.toString().trim());Log.d("ContentHandler"z "version is " + version.toString(),trim());//最后要将StringBuilder清空掉id.setLength(G);name.setLength(O); version.setLength(O);}}@0verridepublic void endDocument() throws SAXException {super.endDocumentf);}}

        可以看到,我们首先给id. name和version节点分别定义了一个StringBuilder对象, 并在startDocumentO方法里对它们进行了初始化。每当开始解析某个节点的时候,Start- Element ()^ 法就会得到调用,其中localName参数记录着当前节点的名字,这里我们把它记 录下来。接着在解析节点中具体内容的时候就会调用charactersO方法,我们会根据当前的节 点名进行判断,将解析出的内容添加到哪一个StringBuilder对象中。最后在endElementO 方法中进行判断,如果app节点已经解析完成,就打印岀id、name和version的内容。需要注 意的是,目前id、name和version中都可能是包括回车或换行符的,因此在打印之前我们还需 要调用一下trim()方法,并且打印完成后还要将StringBuilder的内容清空掉,不然的话会影 响下一次内容的读取。

        接下来的工作就非常简单了,修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private void sendRequestWithOkHttp() {
new Th read(new Runnable() (
(BOverride
public void run() { try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builderf)
//指定访问的服务器地址是电脑本机
.urlChttp://10.0.2.2/get_data.xml")
.buildO;    "
Response response = client.newCall(request).execute(); String responseData = response.body().st ring(); parseXMLWithSAX(responseData);
) catch (Exception e) (
e.printStackTrace();
}
}
}).start();
private void parseXMLWithSAX(String xmLData) { try {
SAXParserFactory factory = SAXParserFactory.newInstance(); XMLReader xmlReader = factory.newSAXParser().getXMLReader(); ContentHandler handler = new ContentHandLer();
// 将 ContentHandler 的实例设置到 XMLReader 中 xmlReader.setContentHandler(handler);
//开始执行解析
xmlReader.parse(new InputSource(new StringReader(xmLData)));
} catch (Exception e) { e.printStackTrace();
}
}
}

        在得到了服务器返回的数据后,我们这次去调用parseXMLWithSAXO方法来解析XML数 据。parseXMLWithSAX()方法中先是创建了一个SAXParserFactory的对象,然后再获取到 XMLReader对象,接着将我们编写的ContentHandler的实例设置到XMLReader中,最后调用 pa rse ()方法开始执行解析就好了。

        现在重新运行一下程序,点击Send Request按钮后观察logcat中的打印日志,你会看到和图 9.7中一样的结果。

        除了 Pull解析和SAX解析之外,其实还有一种DOM解析方式也算挺常用的,不过这里我 们就不再展开进行讲解了,感兴趣的话你可以自己去查阅一下相关资料。

9.4解析JSON格式数据

        现在你已经掌握了 XML格式数据的解析方式,那么接下来我们要去学习一下如何解析JSON 格式的数据了。比起XML, JSON的主要优势在于它的体积更小,在网络上传输的时候可以更省 流量。但缺点在于,它的语义性较差,看起来不如XML直观。

        在开始之前,我们还需要在C:\Apache\htdocs目录中新建一个get data.json的文件,然后编 辑这个文件,并加入如下JSON格式的内容:

[{"id":”5”,“version”:”5.5”,"name":"Clash of Clans"}, {" id ” 6 ”,"version" H name":" Boom Beach"},{“id”:“7”,"version":”3.5”,”name”:"Clash Royale"}]

        这时在浏览器中访问http://l27.0.0.l/get_data.json这个网址,就应该出现如图9.8所示的内容。

        好了,这样我们把JSON格式的数据也准备好了,下面就开始学习如何在Android程序中解 析这些数据吧。

9.4.1 使用 JSONObject

        类似地,解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用谷 歌的开源库GSONo另外,一些第三方的开源库如Jackson. FastJSON等也非常不错。本节中我 们就来学习一下前两种解析方式的用法。

        修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private void sendRequestWithOkHttp() (
new Th read(new Runnable() {
(QOverride
public void run() { try {
OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder()
//指定访问的服务器地址是电脑本机
.url("http://10.0.2.2/get_data. jsonH)
.build();
Response response = client.newCall(request).executef); String responseData = response.body().st ring(); parseJSONWithJSONObject(responseData);
} catch (Exception e) { e.printStackTrace();
}
}
}) -startO ;
private void pa rseJSONWithJSONObj ect(St ring jsonData) { try {
JSONArray jsonArray = new JSONArray(jsonData); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); String id = jsonObject.getString("id"); String name = jsonObject.getString("name"); String version = jsonObject.getString("version"); Log.d("MainActivity", "id is " + id); Log.d ("MainActivity*', "name is " + name); Log.d("MainActivity11, "version is " + version);
}
} catch (Exception e) { e.printStackTraceO;
}
}
}

        首先记得要将HTTP请求的地址改成http://l0.0.2.2/get_data.json,然后在得到了服务器返回 的数据后调用parseJSONWithJSONObject()方法来解析数据。可以看到,解析JSON的代码真 的非常简单,由于我们在服务器中定义的是一个JSON数组,因此这里首先是将服务器返回的数 据传入到了一个JSONArray对象中。然后循环遍历这个JSONArray,从中取出的每一个元素都 是一个JSONObject对象,每个JSONObject对象中又会包含id、name和version这些数据。 接下来只需要调用getStringO方法将这些数据取出,并打印岀来即可。

好了,就是这么简单!现在重新运行一下程序,并点击Send Request按钮,结果如图9.9 所示。

  1. 使用 GSON

        如何你认为使用JSONObject来解析JSON数据已经非常简单了,那你就太容易满足了。谷 歌提供的GSON开源库可以让解析JSON数据的工作简单到让你不敢想象的地步,那我们肯定是 不能错过这个学习机会的。

不过GSON并没有被添加到Android官方的API中,因此如果想要使用这个功能的话,就必 须要在项目中添加GSON库的依赖。编辑app/build.gradle文件,在dependencies闭包中添加如下 内容:

dependencies (compile fileTree(dir: 'libs', include: ['*.jar*]) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:24.2.1' compile 'com.squareup.okhttp3:okhttp:3.4.1* compile * com・ google.code.gson:gson:2.7'}

        那么GSON库究竟是神奇在哪里呢?其实它主要就是可以将一段JSON格式的字符串自动映 射成一个对象,从而不需要我们再手动去编写代码进行解析了。

        比如说一段JSON格式的数据如下所示:

{,,name,,:,,Tom,,z,,age,,:20}

        那我们就可以定义一个Person类,并加入name和age这两个字段,然后只需简单地调用 如下代码就可以将JSON数据自动解析成一个Person对象了:

Gson gson = new Gson();Person person = gson.fromJson(jsonData, Person.class);

        如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TypeToken将期望解析成 的数据类型传入到f romJson()方法中,如下所示:

List<Person> people = gson.fromJson(jsonData,
new TypeToken<List<Person»() {}.getType());

        好了,基本的用法就是这样,下面就让我们来真正地尝试一下吧。首先新增一个App类,并

加入id、name和version这3个字段,如下所示:

public class App {private String id;private String name;private String version;public String getld() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) ( this.name = name;}public String getVersion() { return version;}public void setVersion(String version) ( this.version = version;

然后修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private void sendRequestWithOkHttp() {
new Th read(new Runnable() {
(QOverride
public void run() { try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//指定访问的服务器地址是电脑本机
.url("http://10.0.2.2/get_data.j son") .buildO;   ~
Response response = client.newCall(request).execute(); String responseData = response.body().st ring(); parseJSONWithGSON(responseData);
} catch (Exception e) {
e.printStackTrace();
}
}
}),start();
private void pa rseJSONWithGSON(St ring jsonData) { Gson gson = new Gson();
List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App»() {}.getType());
for (App app : appList) {
Log.d("MainActivity", "id is " + app.getld());
Log.d("MainActivity", "name is u + app.getName());
Log.d("MainActivity", "version is H + app.getVersion());

        现在重新运行程序,点击Send Request按钮后观察logcat中的打印日志,你会看到和图9.9 中一样的结果。

        好了,这样我们就算是把XML和JSON这两种数据格式最常用的几种解析方法都学习完了, 在网络数据的解析方面,你已经成功毕业了。

9.5网络编程的最佳实践

        目前你已经掌握了 HttpURLConnection和OkHttp的用法,知道了如何发起HTTP请求,以 及解析服务器返回的数据,但也许你还没有发现,之前我们的写法其实是很有问题的。因为一个 应用程序很可能会在许多地方都使用到网络功能,而发送HTTP请求的代码基本都是相同的,如 果我们每次都去编写一遍发送HTTP请求的代码,这显然是非常差劲的做法。

        没错,通常情况下我们都应该将这些通用的网络操作提取到一个公共的类里,并提供一个静 态方法,当想要发起网络请求的时候,只需简单地调用一下这个方法即可。比如使用如下的写法:

public class Httplltil {public static String sendHttpRequest(St ring address) {HttpURLConnection connection = null;try {URL url = new URL(address);connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");connection.setConnectTimeout(8000);connection.setReadTimeout(8000);connection.setDoInput(true);connection.setDoOutput(true); Inputstream in = connection.getlnputstream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder response = new StringBuilder();String line;while ((line = reader.readLine()) != null) { response.append(line);}return response.toString();} catch (Exception e) {e.printStackTrace(); return e.getMessage();} finally {if (connection != null) {connection.disconnect();}}}}

        以后每当需要发起一条HTTP请求的时候就可以这样写:

​
String address = "百度一下,你就知道";String response = HttpUtil.sendHttpRequest(address);​

        在获取到服务器响应的数据后,我们就可以对它进行解析和处理了。但是需要注意,网络请 求通常都是属于耗时操作,而sendHttpRequest ()方法的内部并没有开启线程,这样就有可能 导致在调用sendHttpRequest ()方法的时候使得主线程被阻塞住。

        你可能会说,很简单嘛,在sendHttpRequest ()方法内部开启一个线程不就解决这个问题 了吗?其实没有你想象中的那么容易,因为如果我们在sendHttpRequest()方法中开启了一个 线程来发起HTTP请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线 程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然 也就无法返回响应的数据了。

        那么遇到这种情况时应该怎么办呢?其实解决方法并不难,只需要使用Java的回调机制就 可以了,下面就让我们来学习一下回调机制到底是如何使用的。

        首先需要定义一个接口,比如将它命名成HttpCallbackListener,代码如下所示:

public interface HttpCallbackListener {void onFinish(String response);void onError(Exception e);}

        可以看到,我们在接口中定义了两个方法,onFinishO方法表示当服务器成功响应我们请 求的时候调用,。花「「0「()表示当进行网络操作出现错误的时候调用。这两个方法都带有参数, onFinishO方法中的参数代表着服务器返回的数据,而onEr「or()方法中的参数记录着错误的 详细信息。

        接着修改HttpUtil中的代码,如下所示:

public class HttpUtil {public static void sendHttpRequest(final String address, finalHttpCallbackListener listener) {new Thread(new Runnable() {^Override public void run() {HttpURLConnection connection = null;try {URL url = new URL(address);connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET");connection.setConnectTimeout(8000);connection.setReadTimeout(8000);connection.setDoInput(true);connection.setDoOutput(true); Inputstream in = connection.getlnputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(in));StringBuilder response = new StringBuilder();String line;while ((line = reader.readLine()) != null) { response.append(line);}if (listener != null) {//回调onFinish()方法 listener.onFinish(response.toString());}} catch (Exception e) (if (listener != null) {//回调onError()方法 listener.onError(e);}) finally {if (connection != null) {connection.disconnect();}}} }).start();}}

        我们首先给sendHttpRequest()方法添加了一个HttpCallbackListener参数,并在方法 的内部开启了一个子线程,然后在子线程里去执行具体的网络操作。注意,子线程中是无法通过 return语句来返回数据的,因此这里我们将服务器响应的数据传入了 HttpCallbackListener的 onFinishO方法中,如果出现了异常就将异常原因传入到onError()方法中。

        现在sendHttpRequest()方法接收两个参数了,因此我们在调用它的时候还需要将 HttpCallbackListener的实例传入,如下所示:

HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {^Overridepublic void onFinish(String response) {//在这里根据返回内容执行具体的逻辑}^Overridepublic void onError(Exception e) {//在这里对异常情况进行处理}});

        这样的话,当服务器成功响应的时候,我们就可以在onFinishO方法里对响应数据进行处 理了。类似地,如果出现了异常,就可以在。nErrorO方法里对异常情况进行处理。如此一来, 我们就巧妙地利用回调机制将响应数据成功返回给调用方了。

        不过你会发现,上述使用HttpURLConnection的写法总体来说还是比较复杂的,那么使用 OkHttp会变得简单吗?答案是肯定的,而且要简单得多,下面我们来具体看一下。在HttpUtil 中加入一个sendOkHttpRequest ()方法,如下所示:

public class HttpUtil {public static void sendOkHttpRequest(St ring address, okhttp3.Callback callback) { OkHttpClient client = new OkHttpClient();Request request = new Request.Builder().url(address) .buildO;client. newCall(request).enqueue(callback);}}

        可以看到,sendOkHttpRequest。方法中有一个 okhttp3.Callback 参数,这个是 0kHttp 库中自带的一个回调接口,类似于我们刚才自己编写的HttpCallbackListenero然后在client. newCall()之后没有像之前那样一直调用execute()方法,而是调用了一个enqueue()方法,并 把okhttp3. Callback参数传入。相信聪明的你已经猜到了,OkHttp在enqueue()方法的内部 已经帮我们开好子线程了,然后会在子线程中去执行HTTP请求,并将最终的请求结果回调到 okhttp3.Callback 当中。

        那么我们在调用sendOkHttpRequest ()方法的时候就可以这样写:

HttpUtil.sendOkHttpRequest("http://www.baidu.com", new okhttp3.Callback() {
(QOverride
public void onResponse(Call call, Response response) throws lOException {
//得到服务器返回的具体内容
String responseData = response.body().st ring();
}
(aOverride
public void onFailure(Call call, lOException e) {
//在这里对异常情况进行处理
}
});

        由此可以看出,OkHttp的接口设计得确实非常人性化,它将一些常用的功能进行了很好的 封装,使得我们只需编写少量的代码就能完成较为复杂的网络操作。当然这并不是OkHttp的全 部,后面我们还会继续学习它的其他相关知识。

        另外需要注意的是,不管是使用HttpURLConnection还是0kHttp,最终的回调接口都还是在 子线程中运行的,因此我们不可以在这里执行任何的UI操作,除非借助runOnUiThreadO方法 来进行线程转换。至于具体的原因,我们很快就会在下一章屮学习到了。

9.6小结与点评

        本章中我们主要学习了在Android中使用HTTP协议来进行网络交互的知识,虽然Android 中支持的网络通信协议有很多种,但HTTP协议无疑是最常用的一种。通常我们有两种方式来发 送HTTP请求,分别是HttpURLConnection和OkHttp,相信这两种方式你都已经很好地掌握了。

        接着我们又学习了 XML和JSON格式数据的解析方式,因为服务器响应给我们的数据一般 都是属于这两种格式的。无论是XML还是JSON,它们各自又拥有多种解析方式,这里我们只 是学习了最常用的几种,如果以后你的工作中还需要用到其他的解析方式,可以自行去学习。

        本章的最后同样是最佳实践环节,在这次的最佳实践中,我们主要学习了如何利用Java的 回调机制来将服务器响应的数据进行返回。其实除此之外,还有很多地方都可以使用到Java的回 调机制,希望你能举一反三,以后在其他地方需要用到回调机制时都能够灵活地使用。

        在进行了一章多媒体和一章网络的相关知识学习后,你是否想起来Android四大组件中还剩 一个没有学过呢!那么下面就让我们进入到Android服务的学习旅程之中。

第009天:APP的网络连接相关推荐

  1. 三星手机显示app无网络连接到服务器,三星应用商店发生错误?三星应用商店无法连接网络怎么办?[图]...

    最近不少用户在用三星应用商店的时候出现了问题!一般是提示发生错误或者无法连接网络!三星应用商店发生错误?三星应用商店无法连接网络怎么办?来看看解决办法吧! 三星应用商店发生错误?三星应用商店无法连接网 ...

  2. Fiddler抓部分app时网络连接失败

    通过fiddler抓app时,在安装手机安装fiddler证书后,发现有的app可以正常联网,有的app连不上网,如知乎.微博. 在Options-HTTPS 取消勾选Decrypt HTTPS tr ...

  3. android 开发,app设置网络连接的代理地址

    //先设置代理Properties prop = System.getProperties();//proxyhostIPaddressString proxyHost = "localho ...

  4. android 判断是否有网络连接,判断网络连接类型

    当App需要进行网络连接获取数据时,先进行判断是否有网络连接,并且对网络类型进行判断,可以有效地避免对用户造成不必要的困惑和损失. /** 判断是否有网络连接* @author tianjie* @r ...

  5. 一个软件网络连接异常_手机也能玩PC大作了,串流软件Steam Link登陆iOS App Store...

    去年V社发布消息宣布,将会推出一款全新的针对智能手机的Steam串流APP,而在近日这个名为Steam Link App终于正式登陆了iOS App Store.玩家可以通过手机和电脑连接,畅玩电脑上 ...

  6. 苹果youtube无法连接网络_苹果App Store无法连接?你只错在这一步

    [IceTiger 原创资讯] iPhone手机相信小伙伴们肯定都非常熟悉不过了,毕竟其占据着苹果重要的地位.而下载软件我们必须要用到的App Store小伙伴们你们真的会用?遇到无法连接我们该怎么办 ...

  7. ios显示wifi无网络连接到服务器,iOS APP没有联网权限解决办法

    APP发布后,部分用户反馈手机可以连上WiFi,但是APP通信不上,有的手机甚至在设置界面就找不到APP,证书中加入了网络和WiFi服务,工程TARGETS 的 Capabilities中的Netwo ...

  8. 托利多bcom怎么查看连接网络_遇到手机APP提示无网络连接,应该怎么办?

    是不是有时候打开某些app后会提示"无网络连接"或者"当前网络不可用"提示,但是此提示很快会自己消失或者手动刷新后消失.怀疑过网络问题?手机问题?~~~ 想必有 ...

  9. app显示服务器连接超时,APP网络请求超时反馈设计与思考

    最近我负责了一个网络请求超时的反馈设计,借此机会我也顺便通过此文记录了一下整个思考过程,整理一下自己的思路. 当我们在使用APP的时候,偶尔会碰到网络状态不好的情况.那么对于网络状态不好的情况有哪些分 ...

最新文章

  1. dev gridcontrol summaryitem如何加条件_如何一次清洗1000根核磁管
  2. centos7部署两个mysql_centos7 安装mysql5.7主从复制主写分离
  3. 用C++实现不能被继承的类
  4. mysql索引如何做_5分钟,告诉你MySQL字符串怎么做索引
  5. Java操作oracle数据库
  6. 几款流行的HTML5 UI 框架比较
  7. ssh首次连接时提示yes/no
  8. 澳大利亚新南威尔士大学 巩东博士 招收若干名计算机视觉和机器学习方向PhD...
  9. 通过例子理解 k8s 架构 - 每天5分钟玩转 Docker 容器技术(122)
  10. eurekaAutoServiceRegistration 异常
  11. leetcode - Recover Binary Search Tree
  12. python模块heapq之简单学习使用
  13. Visual Studio中的项目属性--生成--配置
  14. [id: * L:/* ! R:/*] onUncaughtException(SimpleConnection{channel=[id: *
  15. 首次登陆系统强制修改密码
  16. invalid byte sequence for encoding utf8 0xcb 0xef
  17. bcdedit添加linux引导,强大的BCDEdit工具-启动项等相关问题-设置默认开机启动项
  18. 近两日学的Linux系统基础命令总结
  19. 哭了,谁还会心疼?累了,谁让我依靠?
  20. tesla p4 linux驱动,Ubuntu 16.04. 装tesla p4 显卡驱动+cuda9.0+docker+nvidia-docker 详细方法,这里是服务器为主...

热门文章

  1. 用OpenCV实现Photoshop算法(五): 亮度对比度调整
  2. Python版中秋佳节月饼抢购脚本
  3. 手把手带你从零基础抓取A站短视频,并且制作从动态壁纸,这些小姐姐我全都要!
  4. 微信小程序去除button边框底色
  5. (基于matlab自写代码)语音信号增强
  6. 【从零单排HBase 02】全面认识HBase架构(建议收藏)
  7. 虚拟机开机出现dracut_安装CentOS 7的时候出现dracut:/#求解决方法!
  8. 如何判断Windows程序是32位还是64位
  9. 良心博客滴滴开源框架VirtualAPK插件化介绍加教程加DEMO加投入项目
  10. 审稿人应该无偿为期刊审稿吗?