21世纪的确是互联网时代。
Android手机必须可以上网的,作为开发者的我们,需要考虑如何利用网络来编写更加出色的应用程序(大量使用网络技术)。接下来我们学习在手机端使用HTTP协议和服务器进行交互,并对服务器返回的数据进行解析(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">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent">WebView>
LinearLayout>
接下来修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity {
private WebView web_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
web_view= (WebView) findViewById(R.id.web_view);
web_view.getSettings().setJavaScriptEnabled(true); //支持JavaScript脚本
web_view.setWebViewClient(new WebViewClient()); //是否跳转网页也都在WebView中显示
web_view.loadUrl("http://www.baidu.com"); //加载需要展示的网址
}
}
声明网络权限,代码如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.webviewtest">
<uses-permission android:name="android.permission.INTERNET"/>
...
manifest>
可以看到,成功的将百度展示在WebView上,还可以通过点击链接浏览更多的网页。
HTTP协议的工作原理:就是客户端向服务端发送一条HTTP请求,服务器接收请求之后返回一些数据给客户端,然后客户端在对这些数据解析和处理。浏览器的基本工作原理就是这样。比如上一节我们学习的WebView控件。
在过去,Android上发送HTTP请求一般有两种方式HttpURLConnection和HttpClient(由于缺点过多,在Android6.0系统,HttpClient已经正式废弃使用)。接下来我们来学习HttpURLConnection的用法。
首先获取HttpURLConnection的实例,new 出一个URL对象,传入网络地址,然后调用openConnection()方法,代码如下:
URL url=new URL("http://www.baidu.com");
HttpURLConnection connection=(HttpURLConnection)url.openConnection();
得到HttpURLConnection实例之后,设置HTTP请求的方式,一般为GET(从服务器获取数据)和POST(提交数据给服务器)。代码如下:
connection.setRequestMethod("GET");
自由定制,比如设置连接超时,读取超时的毫秒数,以及服务器希望得到一些消息头等。示例写法如下:
connection.setConnectionTimeout(8000);
connection.setReadTimeout(8000);
之后调用getInputStream()获取到服务器返回的输入流,读取输入流,代码如下:
InputStream in=connection.getInputStream();
最后调用disconnect()关闭HTTP连接资源,代码如下:
connection.disconnect();
新建NetWorkTest项目,修改activity_main.xml中的代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_send_request"
android:text="Send Request"
android:textAllCaps="false"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_response"
android:layout_width="match_parent"
android:layout_height="match_parent" />
ScrollView>
LinearLayout>
接下来修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_send_request;
private TextView tv_response;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_send_request= (Button) findViewById(R.id.btn_send_request);
tv_response= (TextView) findViewById(R.id.tv_response);
btn_send_request.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId()==R.id.btn_send_request){
sendRequestWithHttpURLConnection();
}
}
private void sendRequestWithHttpURLConnection() {
//开启线程发送网络请求
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection=null;
BufferedReader reader=null;
try {
URL url=new URL("https://www.baidu.com/");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
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 (IOException e) {
e.printStackTrace();
}
}
if (connection!=null){
connection.disconnect();
}
}
}
}).start();
}
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//进行UI操作,显示结果
tv_response.setText(response);
}
});
}
}
声明网络权限,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hjw.networktest">
<uses-permission android:name="android.permission.INTERNET"/>
...
manifest>
服务器返回给我们的代码。
提交数据给服务器,请求方式改为”POST”,并在获取输入流之前提交数据。每条数据都要以键值对的形式存在,数据与数据之间用“&”隔开。比如我们提交用户名和密码,示例如下:
connection.setRequestMethod("POST");
DataOutputStream out=new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");
OkHttp网络通信库代替原生的HttpURLConnection,是由Square公司开发的(贡献:OkHttp,Picasso,Retrofit等开源项目)。
OkHttp项目的主要地址:https://github.com/square/okhttp
使用OkHttp,添加依赖会,自动下载OkHttp库和Okio库(后者是前者的通信基础),添加内容如下:
compile 'com.squareup.okhttp3:okhttp:3.8.0'
OkHttp的具体用法,首先创建OkHttpClient实例,如下所示:
OkHttpClient client=new OkHttpClient();
发送HTTP请求,就必须创建Request对象:
Request request=new Request.Builder().build();
通过在build()方法之前连缀来丰富Request对象,通过url()方法来设置目标的网络地址,如下所示:
Request request=new Request.Builder()
.url("https://www.baidu.com/")
.build();
之后调用OkHttpClient的newCall()方法创建一个Call对象,并调用execute()方法发送请求并获取服务器返回的数据(Response对象),如下所示:
Response response = client.newCall(request).execute();
通过以下方式得到返回具体内容:
String responseData = response.body().string();
如果发送一条POST请求,首先需构建出一个RequestBody对象来存放待提交的数据的参数,如下所示:
RequestBody requestBody=new FormBody.Builder()
.add("username","admin")
.add("password","123456")
.build();
在调用Request.Builder调用post()方法,传入RequestBody对象:
Request request=new Request.Builder()
.url("https://www.baidu.com/")
.post(requestBody)
.build();
接下来的操作就和GET请求一样了,调用execute()方法来发送请求并获取服务器返回数据。
使用OkHttp再实现,在NetWorkTest项目基础修改,修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
@Override
public void onClick(View v) {
if (v.getId()==R.id.btn_send_request){
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp() {
//开启线程发送网络请求
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder().url("https://www.baidu.com/").build();
Response response = client.newCall(request).execute();
String responseData=response.body().string();
showResponse(responseData);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
}
现在运行程序。点击Send Request按钮,同上一小节运行的效果相同。
通常情况下,每个访问网络的情况下,都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器上获取数据。
在网络上传输数据时最常用的格式有两种:XML和JSON。
获取XML格式的数据,搭建一个最简单的Web服务器。
下载安装http://www.wampserver.com/
接下来我们进入到C:\app\Apache\htdocs 目录下,在这里创建名为get_data.xml文件,编写XML格式如下:
<apps>
<app>
<id>1id>
<name>Google Mapsname>
<version>1.0version>
app>
<app>
<id>2id>
<name>Chromename>
<version>2.1version>
app>
<app>
<id>3id>
<name>Google Playname>
<version>2.3version>
app>
apps>
访问http://localhost:81/get_data.xml这个网址,显示如下:
接下来我们学习比较常用的XML解析这段XML数据:Pull解析个SAX解析。
在NetworkTest项目的基础上继续开发,解析XMl数据。
修改MainActivity中的代码如下,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://192.168.56.1:81/get_data.xml").build();
try {
Response response = client.newCall(request).execute();
String xmlData = response.body().string();
parseXMLWithPull(xmlData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
...
private void parseXMLWithPull(String xmlData) {
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); //获取XmlPullParserFactory实例
XmlPullParser xmlPullParser = factory.newPullParser(); //借助实例获取XmlPullParser对象
xmlPullParser.setInput(new StringReader(xmlData));//设置解析数据
int eventType = xmlPullParser.getEventType();//通过getEventType()的到解析当前事件
String id = "";
String name = "";
String version = "";
/*
* while循环,解析事件不等于 XmlPullParse.END_DOCUMENT,通过next方法获取下一个实例,
* 通过getName方法获取当前子节点的名字,调用nextText()获取子节点的内容。
*/
while (eventType != XmlPullParser.END_DOCUMENT) {
String nodeName = xmlPullParser.getName();
switch (eventType) {
case XmlPullParser.START_TAG:
if ("id".equals(nodeName)) {
id = xmlPullParser.nextText();
} else if ("name".equals(nodeName)) {
name = xmlPullParser.nextText();
} else if ("version".equals(nodeName)) {
version = xmlPullParser.nextText();
}
break;
case XmlPullParser.END_TAG:
if ("app".equals(nodeName)) {
Log.d("MainActivity", "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();
}
}
}
运行程序,点击Send Request按钮,观察打印日志如下:
SAX解析也是一种常用的XML解析方式,用法比Pull复杂以一些,语义方面更加清楚。
通常情况下我们都会新建一个类继承自DefaultHandler,并重写父类的5个方法,如下所示:
public class MyHandler extends DefaultHandler {
/**
* 开始解析XMl
* @throws SAXException
*/
@Override
public void startDocument() throws SAXException {
super.startDocument();
}
/**
* 开始解析某个节点的
* @param uri
* @param localName
* @param qName
* @param attributes
* @throws SAXException
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes);
}
/**
* 获取节点中的内容
* @param ch
* @param start
* @param length
* @throws SAXException
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
super.characters(ch, start, length);
}
/**
* 完成解析某个节点
* @param uri
* @param localName
* @param qName
* @throws SAXException
*/
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
super.endElement(uri, localName, qName);
}
/**
* 完成整个XML解析
* @throws SAXException
*/
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
接下来我们使用SAX解析实现和使用Pull解析实现上一个同样的功能,新建ContentHandler 类继承自 DefaultHandler,重写5个方法,如下所示:
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
@Override
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();
version = new StringBuilder();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
//记录当前节点
nodeName = localName;
}
@Override
public 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);
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
super.endElement(uri, localName, qName);
if ("app".equals(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
public void endDocument() throws SAXException {
super.endDocument();
}
}
接下来修改MainActivity中的代码,如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://192.168.56.1:81/get_data.xml").build();
try {
Response response = client.newCall(request).execute();
String xmlData = response.body().string();
parseXMLWithSAX(xmlData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private void parseXMLWithSAX(String xmlData) {
try {
SAXParserFactory factory=SAXParserFactory.newInstance(); //创建SAXParserFactory实例对象
XMLReader xmlReader = factory.newSAXParser().getXMLReader(); //获取XMLReader对象
ContentHandler handler=new ContentHandler();
//给xmlReader设置ContentHandler
xmlReader.setContentHandler(handler);
//开始解析执行
xmlReader.parse(new InputSource(new StringReader(xmlData)));
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行程序,点击Send Request按钮,观察logcat打印日志,就会看到可Pull解析一样的效果。
比起XML,JSON主要的优势在于:体积更小,网络传输更省流量。
缺点在于:语义性较差,不如XML直观。
接下来我们在 C:\app\Apache\htdocs 目录下,新建一个 get_data.json 的文件,编写JSON格式内容如下:
[{"id":"5","version":"5.5","name":"Clash of Clans"},
{"id":"6","version":"7.0","name":"Boom Beach"},
{"id":"7","version":"3.5","name":"Clash Royale"}]
接下来我们在浏览器访问http://localhost:81/get_data.json地址,如图:
这样我们就准备好JSON数据了。
官方提供的JSONObject解析的用法。
接下来,我们修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://192.168.200.109:81/get_data.json").build();
try {
Response response = client.newCall(request).execute();
String jsonData = response.body().string();
parseJSONWithJSONObject(jsonData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
...
private void parseJSONWithJSONObject(String jsonData) {
try {
JSONArray jsonArray = new JSONArray(jsonData); //返回数据到JSONArray中
for (int i = 0; i < jsonArray.length(); i++) { //循环遍历JSONArray,取出每一个JSONObject对象,获取数据。
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("MainActivity", "version is " + version);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
Google提供的GOSN开源库,更加简单的解析JSON数据。
添加GSON库的依赖:
compile 'com.google.code.gson:gson:2.8.0'
GSON库的优势在于可以将一段JSON格式的字符串自动映射成一个对象,从而不需要我们再手动去编写代码了。
比如解析一段如下JSON格式的数据如下所示:
{"name":"Tom","age":"20"}
那么我们就可以定义一个Person类,加入name和age字段,调用如下代码就可以将JSON数据解析成一个Person对象了。
Gson gson = new Gson();
Person person=gson.fromJson(jsonData, Person.class);
如果需要解析一段JSON数组,借助TypeToken将期望解析成数据类型传入fromJson()方法中,如下所示:
List personList = gson.fromJson(jsonData, new TypeToken<List>(){}.getType());
以上就是GSON的基本用法了,接下来我们来新建一个App类,并加入id,name和version字段,如下:
public class App {
private String id;
private String name;
private String version;
public String getId() {
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 Thread(new Runnable() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://192.168.200.109:81/get_data.json").build();
try {
Response response = client.newCall(request).execute();
String jsonData = response.body().string();
parseJSONWithGOSN(jsonData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private void parseJSONWithGOSN(String jsonData) {
Gson gson = new Gson();
List appList = gson.fromJson(jsonData, new TypeToken>() {
}.getType());
for (App app : appList) {
Log.d("MainActivity", "id is " + app.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
}
重新运行程序,点击Send Request按钮,观察logcat中的日志,与上节JSONObject运行的结果一模一样。
通过封装网络操作的公共类,提供一个静态方法,每当我们发送网络请求,就只需要简单的调用一下这个方法即可。如下所示:
public class HttpUtil {
public static String sendHttpRequest(String 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.getInputStream();
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 = "https://www.baidu.com/";
String reponse= HttpUtil.sendHttpRequest(address);
因为网络请求属于耗时操作,而sendHttpRequest()方法的内部没有开启线程,就会导致主线程被阻塞住。
解决无法返回响应数据的问题,使用Java的回调机制。
首先定义HttpCallbackListener,代码如下:
public interface HttpCallbackListener {
void onFinish(String response); //服务器成功返回数据
void onError(Exception e); //网络操作出现异常(记录错误详情信息)
}
接下来我们修改MainActivity中的代码如下:
public class HttpUtil {
public static void sendHttpRequest(final String address, final HttpCallbackListener 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.getInputStream();
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) {
e.printStackTrace();
if (listener != null) {
//回调onError()方法
listener.onError(e);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
}
现在我们调用 sendHttpRequest() 方法接受两个参数,因此,代码如下:
HttpUtil.sendHttpRequest("https://www.baidu.com/", new HttpCallbackListener() {
@Override
public void onFinish(String response) {
//在这里根据返回内容执行具体的操作
}
@Override
public void onError(Exception e) {
// 在这里对异常情况进行处理
}
});
封装OkHttp,在HttpUtil中加入sendOkHttpRequest()方法,代码如下:
public class HttpUtil {
public static void sendOkHttpRequest(String address, Callback callback) {
...
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(address).build();
client.newCall(request).enqueue(callback); //enqueue()内部开启子线程
}
}
我们在sendOkHttpRequest()方法的时候可以写成:
HttpUtil.sendOkHttpRequest("https://www.baidu.com", new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 在这里对异常情况进行处理
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//在这里根据返回内容执行具体的操作
final String responseData = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
tv_response.setText(responseData);
}
});
}
});
无论是HttpURLConnection还是OkHttp,都不可以在这里执行任何的UI操作,借助runOnUiThread()方法来进行线程转换。
本章学习了在Android中使用HTTP协议来进行网络交互的知识。
解析XML数据格式常用的方式:Pull解析,SAX解析,DOM解析。
解析JSON数据常用的方式:JSONObject和GSON。
学习了利用Java的回调机制来将服务器响应的数据进行返回。