《第一行代码 第二版》第九章
《第一行代码 第二版》
本博客是对第一行代码的精简总结,仅供个人学习使用。如需系统学习请购买正版或者电子书籍。
链接附上 🔗图灵社区:https://www.ituring.com.cn/book/2744/
第 9 章 看看精彩的世界——使用网络技术
若玩手机不能上网,那有什么用,微博、微信、QQ用了大量的网络技术。下面讲到了常用的网络技术:
9.1 WebView的用法
(1)适用范围:应用内展示网页,但却不允许打开系统浏览器,借助WebView可以在自己应用程序中嵌入一个浏览器。目的是应用程序内显示网页。
(2)实现方法:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("http://www.baidu.com");
}
}
首先使用findViewById()
方法获取到了WebView的实例,然后调用WebView的 getSettings()
方法可以去设置一些浏览器的属性,这里我们并不去设置过多的属性,只是调用了setJavaScriptEnabled()
方法来让WebView支持JavaScript脚本。
接下来是非常重要的一个部分,我们调用了WebView的setWebViewClient()
方法,并传入了一个WebViewClient的实例。这段代码的作用是,当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器。
最后一步就非常简单了,调用WebView的loadUrl()
方法,并将网址传入,即可展示相应网页的内容,这里就让我们看一看百度的首页长什么样吧。
另外还需要注意,由于本程序使用到了网络功能,而访问网络是需要声明权限的,因此我们还得修改AndroidManifest.xml文件
<uses-permission android:name="android.permission.INTERNET" />
9.2 使用Http协议访问网络
WebView封装的太好,发送Http请求,接受服务响应、解析返回数据为一体,因此不能直观看出Http协议到底如何工作的。手动发送Http请求。
9.2.1 使用HttpURLConnnection
Android上发送Http请求:HttpURLConnection和HttpClient。Android 6.0之后HttpClient移除,因此只用HttpURLConnection。
首先需要获取到HttpURLConnection的实例,一般只需new出一个URL对象,并传入目标的网络地址,然后调用一下openConnection()
方法即可,如下所示:
URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
在得到了HttpURLConnection的实例之后,我们可以设置一下HTTP请求所使用的方法。常用的方法主要有两个:GET
和POST
。GET
表示希望从服务器那里获取数据,而POST
则表示希望提交数据给服务器。写法如下:
connection.setRequestMethod("GET");
接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服务器希望得到的一些消息头等。这部分内容根据自己的实际情况进行编写,示例写法如下:
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
之后再调用getInputStream()
方法就可以获取到服务器返回的输入流了,剩下的任务就是对输入流进行读取,如下所示:
InputStream in = connection.getInputStream();
最后可以调用disconnect()
方法将这个HTTP连接关闭掉,如下所示:
connection.disconnect();
开启线程来发起网络请求,利用connection.getInputStream()获取输入流,利用BufferReader对获取到的输入流进行读取
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);
}
}
4.利用ShowResponse方法显示结果,Android不允许在子线程更改UI,必须切换主线程,进行更改元素。
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//在这里显示UI操作,并将结果显示到界面上。
responseText.setText(response);
}
});
}
5.记得使用 reader.close();connection.disconnect();关闭Reader流和Connection连接。若发送的话:
那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将HTTP请求的方法改成POST
,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键值对的形式存在,数据与数据之间用“&”符号隔开,比如说我们想要向服务器提交用户名和密码,就可以这样写:
connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");
9.2.2 使用OKHttp
由Square公司开发,开源库,可以替代HttpURLConnection。接口封装简单,底层实现好。首选网络通信库。
1.添加依赖关系,compile 'com.squareup.okhttp3:okhttp:3.4.1',自动下载OKHttp和Okio库
get:
下面我们来看一下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().string();
post
如果是发起一条POST
请求会比GET
请求稍微复杂一点,我们需要先构建出一个Request Body
对象来存放待提交的参数,如下所示:
RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
后在Request.Builder中调用一下post()
方法,并将RequestBody
对象传入:
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBody)
.build();
接下来的操作就和GET
请求一样了,调用execute()
方法来发送请求并获取服务器返回的数据即可。
9.3 解析XML格式数据
网络传输向服务器提交的数据或者从服务器获取的数据一般都有相应的结构格式和语义。常见格式有:XML和JSON。首先需要搭建一个服务器:
1.Apache服务器下载及搭建。参考链接:
https://blog.csdn.net/qq_34804120/article/details/78862290(很恶心啊,浪费了大半个早上,才搞好。http://127.0.0.1:8080/get_data.xml。127.0.0.1就是localHost,8080是端口号。根本就没有霖神书上的玩意好么)
2.写相应的XML文件。Get_data.xml。对该xml进行解析。
<apps>
<app>
<id>1</id>
<name>Google Maps</name>
<version>1.0</version>
</app>
<app>
<id>2</id>
<name>Chrome</name>
<version>2.1</version>
</app>
<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>
输入网址,可以显示该xml网页,开始对其进行解析。
9.3.1 Paul解析方式
1.将请求地址修改为http://10.0.2.2:8080/get_data.xml。
2.利用自定义parseXMLWITHPull方法对respnoseData进行解析
3.获取XmlPullParserFactory实例,以此构建XmlPullParser对象,利用setInput方法将数据设置进去开始解析。
4.getEventType可以得到当前的解析事件。随后在while循环里不停解析。若等于XmlPullParser.END_DOCUMENT,则结束解析,否则执行调用next循环解析。
5.getName获取当前节点的名字,若等于id,name或version,则利用nextText获取其中内容,每当解析结束一个app节点便将其内容打印出来。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
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().string();
parseXMLWithPull(responseData);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
private void parseXMLWithPull(String xmlData) {
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlData));
int eventType = xmlPullParser.getEventType();
String id = "";
String name = "";
String version = "";
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();
}
}
}
9.4 解析JSON格式数据
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// 指定访问的服务器地址是电脑本机
.url("http://10.0.2.2/get_data.json")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
parseJSONWithJSONObject(responseData);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
private void parseJSONWithJSONObject(String 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("MainActivity", "version is " + version);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
解析JSON的代码真的非常简单,由于我们在服务器中定义的是一个JSON数组,因此这里首先是将服务器返回的数据传入到了一个JSONArray
对象中。然后循环遍历这个JSONArray
,从中取出的每一个元素都是一个JSONObject
对象,每个JSONObject
对象中又会包含id
、name
和version
这些数据。接下来只需要调用getString()
方法将这些数据取出,并打印出来即可。
解析JSON格式数据:、Json体积更小、更省流量。但语义较差、不够直观。可以官方提供的JSONObject进行解析,也可以利用谷歌的开源库Gson库来解析JSON格式数据。当然第三方JackSon和FastJSON也不错,最后是一个实践活动说明了OKHttp相较于Http协议通信,强大太多,有相应的回调接口,建议更多使用OKhttp。
9.4.2 使用GSON
比如说一段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<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 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() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// 指定访问的服务器地址是电脑本机
.url("http://10.0.2.2/get_data.json")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
parseJSONWithGSON(responseData);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
private void parseJSONWithGSON(String 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.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
}
回调机制
首先需要定义一个接口,比如将它命名成HttpCallbackListener,代码如下所示:
public interface HttpCallbackListener {
void onFinish(String response);
void onError(Exception e);
}
可以看到,我们在接口中定义了两个方法,onFinish()
方法表示当服务器成功响应我们请求的时候调用,onError()
表示当进行网络操作出现错误的时候调用。这两个方法都带有参数,onFinish()
方法中的参数代表着服务器返回的数据,而onError()
方法中的参数记录着错误的详细信息。
接着修改HttpUtil中的代码,如下所示:
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) {
if (listener != null) {
// 回调onError()方法
listener.onError(e);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
}
我们首先给sendHttpRequest()
方法添加了一个HttpCallbackListener
参数,并在方法的内部开启了一个子线程,然后在子线程里去执行具体的网络操作。注意,子线程中是无法通过return
语句来返回数据的,因此这里我们将服务器响应的数据传入了HttpCallbackListener的onFinish()
方法中,如果出现了异常就将异常原因传入到onError()
方法中。
现在sendHttpRequest()
方法接收两个参数了,因此我们在调用它的时候还需要将HttpCallbackListener的实例传入,如下所示:
HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
@Override
public void onFinish(String response) {
// 在这里根据返回内容执行具体的逻辑
}
@Override
public void onError(Exception e) {
// 在这里对异常情况进行处理
}
});
这样的话,当服务器成功响应的时候,我们就可以在onFinish()
方法里对响应数据进行处理了。类似地,如果出现了异常,就可以在onError()
方法里对异常情况进行处理。如此一来,我们就巧妙地利用回调机制将响应数据成功返回给调用方了。
不过你会发现,上述使用HttpURLConnection的写法总体来说还是比较复杂的,那么使用OkHttp会变得简单吗?答案是肯定的,而且要简单得多,下面我们来具体看一下。在HttpUtil中加入一个sendOkHttpRequest()
方法,如下所示:
public class HttpUtil {
...
public static void sendOkHttpRequest(String address, okhttp3.Callback callback) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(address)
.build();
client.newCall(request).enqueue(callback);
}
}
可以看到,sendOkHttpRequest()
方法中有一个okhttp3.Callback
参数,这个是OkHttp库中自带的一个回调接口,类似于我们刚才自己编写的HttpCallbackListener。然后在client.newCall()
之后没有像之前那样一直调用execute()
方法,而是调用了一个enqueue()
方法,并把okhttp3.Callback
参数传入。相信聪明的你已经猜到了,OkHttp在enqueue()
方法的内部已经帮我们开好子线程了,然后会在子线程中去执行HTTP请求,并将最终的请求结果回调到okhttp3.Callback
当中。
那么我们在调用sendOkHttpRequest()
方法的时候就可以这样写:
HttpUtil.sendOkHttpRequest("http://www.baidu.com", new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// 得到服务器返回的具体内容
String responseData = response.body().string();
}
@Override
public void onFailure(Call call, IOException e) {
// 在这里对异常情况进行处理
}
});
由此可以看出,OkHttp的接口设计得确实非常人性化,它将一些常用的功能进行了很好的封装,使得我们只需编写少量的代码就能完成较为复杂的网络操作。当然这并不是OkHttp的全部,后面我们还会继续学习它的其他相关知识。
另外需要注意的是,不管是使用HttpURLConnection还是OkHttp,最终的回调接口都还是在子线程中运行的,因此我们不可以在这里执行任何的UI操作,除非借助runOnUiThread()
方法来进行线程转换。