这节课是 Android 开发(入门)课程 的第三部分《访问网络》的第二节课,导师是 Chris Lei 和 Joe Lewis。因为上节课的 JSON 是硬编码的占位符,并非真正从网络获取的数据,因此按照计划的开发步骤,要实现从网络获取数据,这节课先经过一个叫做 Soonami 的示例应用 (Sample App) 来验证网络 (Networking) 相关的代码。由于网络命题的内容很庞大,因此课程中会从实用性出发,仅对用到的部分提供相应的资讯,不做深刻讨论。html
Soonami App 一样使用 USGS API 显示是否有地震引发的海啸预警,分四个步骤完成:java
关键词:Android Permissions、Android System Architecture、Exception、try/catch/finally block、HTTP Request、URL Class、HttpURLConnection、HTTP Verb、HTTP Status Code、StringBuilder、InputStream、InputStreamReader、BufferedReader、Method Chainingnode
在进行 Android 中的网络操做前,先了解一下 Android 权限的相关知识。默认状况下 Android 应用不具有任何权限,当应用须要使用设备的蓝牙、网络链接、指纹识别,或者访问用户的日历、地址、联系人等操做时,应用就须要请求权限,完整的 Android 权限列表能够到 Android Developers 网站 查看。android
Android 权限按保护等级分为几种类型,其中最重要的两种是正常权限 (Normal Permissions) 和危险权限 (Dangerous Permissions)。git
正常权限:容许的操做对用户信息和其它应用的数据无影响,例如使用设备的蓝牙、网络链接、指纹识别等,完整列表能够到 Android Developers 网站 查看。当应用请求正常权限时,Android 会自动授予应用该权限,无需用户介入。github
危险权限:容许访问用户的我的信息,可能会对其它应用的数据产生影响,例如访问用户的日历、地址、联系人等。当应用请求危险权限时,须要由用户手动处理该请求。危险权限是经过 权限组 (Permission Groups) 来管理的(正常权限也可能包含在权限组内,不过权限组对其无影响,因此无需考虑权限组内的正常权限)。
(1)当设备运行在 Android 6.0 (API Level 23) 以及应用的 targetSdkVersion
为 23 或以上时,Android 会在应用运行时 (Runtime),弹出对话框,显示应用请求的危险权限所在的权限组。若是用户拒绝权限请求,应用未能得到该权限,那么它就没法提供对应的功能,但仍能正常运行;若是用户赞成该请求,就至关于授予应用整个权限组的权限。例如应用请求 READ_CONTACTS
权限时,这个权限属于 CONTACTS
权限组,系统就会在应用运行时弹出对话框,显示应用请求 CONTACTS
权限组,若是用户赞成该请求,此时应用只得到 READ_CONTACTS
权限,可是在这个基础上,若是应用再请求同一权限组的 WRITE_CONTACTS
权限,Android 会自动授予应用该权限。
(2)当设备运行在 Android 5.1 (API Level 22) 或应用的 targetSdkVersion
为 22 或如下时,Android 会在应用安装时 (Install Time),弹出对话框,显示应用请求的全部权限组列表,用户必须赞成全部的权限请求,不然没法安装应用。json
为应用请求 Android 权限的方法是在 AndroidManifest 中添加 <uses-permission>
标签以及对应的属性,注意标签名不是 <user-permission>
,例如在 Soonami App 中请求网络访问的权限:api
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.soonami">
<uses-permission android:name="android.permission.INTERNET"/>
...
</manifest>
复制代码
正如上面描述的,网络访问属于 Android 的正常权限,系统会自动授予应用该权限,无需用户介入。浏览器
Tips:
1. 虽然应用得到一个危险权限就意味着得到了整个权限组的权限,可是在未来的 Android SDK 中一些权限可能会从一个权限组移动到另外一个权限组,因此不该该根据权限组来假定应用是否得到某些权限,最佳作法 (Best Practice) 是在 AndroidManifest 中明确请求每一个权限。
2. 当应用要用到拍照、地图等功能时,能够经过 Intent 调用相应的应用来实现,从而避免请求过多的权限。过多的权限请求会引发用户的怀疑,因此应用应该尽量少地请求权限,同时确保具备充分的理由向用户解释须要请求权限的缘由。bash
之因此 Android 有系统权限的概念,是由于 Android 是一种权限分离 (privilege-separated) 的操做系统,应用以惟一的身份标识运行。也就是说,每一个 Android 应用都运行在一个进程沙盒 (Process Sandbox) 中,应用须要明确请求沙盒外的资源和数据。这种模式是由 Android 系统框架决定的,应用与设备之间的交互经过一系列的层抽象 (Layer Abstraction) 实现,每一层实现一部分功能,越底层实现的功能越小。
上图是简化的 Android 系统框架,完整的图表能够到 Android Developers 网站 查看。
在 Soonami App 中,应用经过 Android Framework 的 HttpURLCOnnection 类使用设备上的蜂窝或 Wi-Fi 硬件设备,以从网络上获取数据,而不是直接操做 Android 系统,更不是直接控制设备硬件。
若是 Soonami App 在没有得到网络访问权限的状况下进行相关的操做,应用会产生 SecurityException 致使应用崩溃。事实上,一些应用崩溃的缘由每每是没有正确处理 Exception(例外/异常)。Exception 是 Throwable class 的一个扩展类(另外一个是 Error),当代码运行失败或遇到意外状态时会触发异常(Throw an Exception),称为异常事件 (Exception Event)。Exception class 的子类定义了许多异常事件的类型,例如 IllegalStateException 表示有 method 被非法状态下调用;NullPointerException 表示对空对象进行非法操做。因此异常能够理解为错误 (error),但它能够被捕获 (catch) 处理或包含到 Exception 类的实例中;与其它类同样,开发者也能够建立自定义的 Exception class,例以下面的 InvalidPurchaseException。
public void completePurchase() throws InvalidPurchaseException {
...
...
throw new InvalidPurchaseException();
...
}
复制代码
throw
。异常可分为检查异常和非检查异常 (Checked and Unchecked Exception)。
completePurchase()
method 时必须处理 InvalidPurchaseException 异常。这种作法在一个类内的辅助方法 (Helper Method) 很经常使用,能够将异常处理转移到调用 method 的地方。处理 Exception 的方法一般是将可能触发异常的 method 放到 try/catch/finally 区块中,例以下面的 openFile
method:
public void openFile() {
FileReader reader = null;
try {
// constructor may throw FileNotFoundException
reader = new FileReader("someFile");
int i = 0;
while (i != -1) {
//reader.read() may throw IOException
i = reader.read();
System.out.println((char) i);
}
} catch (FileNotFoundException e) {
Log.e(LOG_TAG, "Problem reading the file.", e);
} catch (IOException e) {
Log.e(LOG_TAG, "Problem opening the file.", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Problem closing the file.", e);
}
}
System.out.println("--- File End ---");
}
}
复制代码
FileReader()
和 reader.read()
两个可能触发异常的 method 放进 try
区块,并用两个 catch
区块分别处理不一样的异常,在这里是经过 Log 日志记录错误信息。FileReader()
触发异常,那么就再也不执行 try
区块内的代码,而是跳到 catch (FileNotFoundException e)
处理异常,而后跳到 finally
执行该区块内的代码,最后跳出 try/catch 区块,从上至下继续执行下面的代码。若是 reader.read()
触发异常,则会跳到 catch (IOException e)
处理异常,接下来的步骤与上面相同。所以,try
区块内的代码没法保证老是会执行,代码不会同时进入两个 catch
区块。finally
区块内的代码都会执行。reader
变量是在 try/catch 区块外声明的,若是在 try
区块内声明变量,那么变量的做用域仅在 try/catch 区块内。Tips:
1. 在 Android Studio 中打开 Java 文件,选中左侧的 "7:Structure" 标签,能够按照嵌套结构清晰地选择浏览文件中的 Java 变量、类、对象。
2. 在 Android Studio 中选中被识别出错误的代码(有波浪下划线)使用快捷键 opt(alt)+enter 能够选择 Android Studio 提供的解决方案。例如选择 "Surround with try/catch" 能够快速添加 try/catch 区块,在 catch
区块内还会自动添加 e.printStackTrace();
表示打印错误堆栈。
网络是计算机(包括手机、笔记本电脑、服务器等)之间交换信息的概念,它的基本原理是一台计算机向另外一台计算机发送 HTTP 请求,发送端一般称为客户端,接收端为 Web 服务器;服务器做出响应后,客户端可以获取响应并从中提取信息。例如使用浏览器打开 Google 搜索主页时,浏览器做为客户端向 Google 服务器发送 HTTP 请求,浏览器接收到 Google 服务器的响应后解析数据,最后刷新页面显示一个完整的网页。利用 Chrome 浏览器的开发者工具(在空白处右键选择 Inspect),在 Network 界面能够看到浏览器已加载的资源 (HTML, CSS, JavaScript),选中其中一项,在 Headers 标签页下能够看到浏览器向 Google 服务器发送的请求的相关信息,如 URL、method、响应代码等。
HTTP 请求 (HTTP Request) 是网络交换信息的基础部分,HTTP(超文本传输协议,Hypertext Transfer Protocol)是其中的核心技术。相似在餐厅点披萨,顾客须要明确告诉服务员披萨的尺寸和配料等信息,客户端发送 HTTP 请求也须要明确指出请求内容以及提供方式,其中一项重要指标是 URL(统一资源定位器,Uniform Resource Locator),它决定了数据源的地址或位置,在 API 中称为端点 (Endpoints)。一个 URL 示例以下,它包含了五个基本元素:
https://example.com/animal/mammal/primate?diet=omnivore&active=night#tarsier
复制代码
//
标记符。?
为开始,每一个参数用 &
分隔。#
为开始,指页面中的某些资源 ID,表示页面会从该资源开始显示。在 Android 中利用 URL class 来生成访问 API 端点的 URL,例如在 Soonami App 中新建一个名为 createUrl
的 URL 对象,在 try/catch 区块内经过字符串构造 URL 对象,同时可捕获 MalformedURLException 并经过 Log 日志记录错误信息。
/**
* Returns new URL object from the given string URL.
*/
private URL createUrl(String stringUrl) {
URL url = null;
try {
url = new URL(stringUrl);
} catch (MalformedURLException exception) {
Log.e(LOG_TAG, "Error making the HTTP request.", exception);
return null;
}
return url;
}
复制代码
建立 URL 对象后,经过调用 url.openConnection()
建立一个 HttpURLConnection 对象,经过调用其中的 method 就能够在 Android 中生成 HTTP 请求了。这种模式是有 Android 系统框架决定的,经过层抽象使 App 仅用几行代码就可以完成复杂的工做。例如在这里就经过 Android Framework 的 HttpURLCOnnection 类使用设备上的蜂窝或 Wi-Fi 硬件设备,以从网络上获取数据,而不是直接操做 Android 系统,更不是直接控制设备硬件。
Tip: OkHttp 是一个开源的 HTTP 客户端第三方库,它也能够实现 Android 的网络操做。
/**
* Make an HTTP request to the given URL and return a String as the response.
*/
private String makeHttpRequest(URL url) throws IOException {
String jsonResponse = "";
// If the URL is null, then return early.
if (url == null) {
return jsonResponse;
}
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
try {
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setReadTimeout(10000 /* milliseconds */);
urlConnection.setConnectTimeout(15000 /* milliseconds */);
urlConnection.connect();
// If the request is successful (response code 200),
// then read the input stream and parse the response.
if (urlConnection.getResponseCode() == 200) {
inputStream = urlConnection.getInputStream();
jsonResponse = readFromStream(inputStream);
} else {
Log.e(LOG_TAG, "Error response code: " + urlConnection.getResponseCode());
}
} catch (IOException e) {
Log.e(LOG_TAG, "Problem retrieving the earthquake JSON results.", e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (inputStream != null) {
// function must handle java.io.IOException here
inputStream.close();
}
}
return jsonResponse;
}
复制代码
urlConnection = (HttpURLConnection) url.openConnection();
url.openConnection()
建立一个 HttpURLConnection 对象,默认返回的数据类型为 URLConnection,不过 HttpURLConnection 是 URLConnection 的子类,因此这里能够转换数据类型。urlConnection.setRequestMethod("GET");
setRequestMethod
method 来设置 HTTP 动词。HTTP 方法或动词 (Method/Verb) 是客户端发送 HTTP 请求的另外一项重要指标,经过它来完成客户端与服务器之间的交互,一般是四种操做,建立 (Create)、读取 (Read)、更新 (Update)、删除 (Delete),简写 CRUD)。经常使用的 HTTP 动词有:
(1)GET: 客户端从服务器获取或检索数据。
(2)POST: 客户端向服务器发送一些数据。
(3)PUT: 客户端更新服务器上的数据。
(4)DELETE: 客户端删除服务器上的数据。
在 Soonami App 中,客户端要从服务器中获取地震信息,属于读取操做,因此这里设置 HTTP 动词为 GET。HTTP 动词的详细信息能够到这个网站查看,里面详细叙述了每一个 HTTP 动词的用法,以及对应的服务器响应。尽管 HTTP 动词的用法遵循必定的规则,可是对于不一样 API 而言会有差别,最终应用要以 API 文档为准。
urlConnection.setReadTimeout(10000 /* milliseconds */);
调用 HttpURLConnection 的 setReadTimeout
method 来设置读取数据的延时为 10000 毫秒。
urlConnection.setConnectTimeout(15000 /* milliseconds */);
调用 HttpURLConnection 的 setConnectTimeout
method 来设置链接延时为 15000 毫秒。
urlConnection.connect();
打包 HTTP 请求并将其发送到服务器。这行代码是客户端与服务器创建 HTTP 链接的位置,在此以前的内容属于设置 HTTP 请求,在此以后的属于接收响应并解析数据的内容。
urlConnection.getResponseCode() == 200
调用 HttpURLConnection 的 getResponseCode
method 来获取 HTTP 响应代码。
服务器对 HTTP 请求的响应经过 HTTP 响应代码 (HTTP Status Code) 表示,响应代码为三位数字,按首位数字分为五类响应,完整的 HTTP 响应代码列表能够到 Wikipedia 查看。
(1)1xx Informational Responses
信息状态码,表示请求已被服务器接收,但仍需继续处理。
(2)2xx Success
成功状态码,表示请求已成功被服务器接收、理解、并接受。常见 "200 OK" 表示请求已成功,请求的数据返回客户端。
(3)3xx Redirection
重定向状态码,表示客户端须要采起进一步的操做才能完成请求。常见 "301 Moved Permanently" 表示请求的资源已永久移动到新位置。
(4)4xx Client Errors
客户端错误状态码,表示客户端可能发生了错误,妨碍服务器的处理。常见 "400 Bad Request" 表示因为明显的客户端错误(请求语法错误,欺骗性路由请求等),服务器不会处理该请求;"403 Forbidden" 表示服务器已经理解请求,可是拒绝执行它;"404 Not Found" 表示请求的资源未在服务器上找到。
(5)5xx Server Errors
服务器错误状态码,表示服务器在处理请求的过程当中有错误或者异常状态发生,没法完成有效的请求。常见 "502 Bad Gateway" 表示做为网关或代理工做的服务器尝试执行请求时,从上游服务器接收到无效的响应。
根据 HTTP 响应代码,针对服务器的不一样响应做出对应的处理方案,使代码可以正常运行,同时增长代码的鲁棒性。例如在 Soonami App 中,USGS 服务器对 GET 动词的响应多是 200 表示 OK,也多是 404 表示未找到资源,应用经过 if-else 语句实现仅在 USGS 服务器响应代码为 "200 OK" 时接收响应并解析数据,收到其它响应代码时经过 Log 日志记录错误信息。
inputStream = urlConnection.getInputStream();
将服务器返回的数据存放在 InputStream 中。对于计算机而言,每一段数据,不管是文本仍是图片,都是存放在字节大小的块中,应用在接收数据时数据以数据流 (InputStream) 的形式输入。数据流是抽象的,以二进制 (0/1) 保存。
jsonResponse = readFromStream(inputStream);
经过 readFromStream
辅助方法来解析 InputStream 数据流,最终传给 jsonResponse
字符串。因为数据流是二进制 (0/1) 保存的原始数据,因此应用在使用前须要解析成有意义的内容。例如这里须要将服务器返回的 GeoJSON 原始二进制数据转换成字符串,readFromStream
辅助方法的代码以下:
/**
* Convert the {@link InputStream} into a String which contains the
* whole JSON response from the server.
*/
private String readFromStream(InputStream inputStream) throws IOException {
StringBuilder output = new StringBuilder();
if (inputStream != null) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
BufferedReader reader = new BufferedReader(inputStreamReader);
String line = reader.readLine();
while (line != null) {
output.append(line);
line = reader.readLine();
}
}
return output.toString();
}
复制代码
reader.readLine();
使 BufferedReader 在收到对某个字符的请求后会读取并保存该字符先后的一整行字符,当请求另外一个字符时就能利用 BufferedReader 提早读取的字符来实现请求,无需再调用 InputStreamReader。StringBuilder output = new StringBuilder();
output.append(line);
append
method 添加字符序列。append
method 能够在一行内屡次调用,如 output.append(line1).append(line2);
,这种方法叫作方法链 (Method Chaining)。output.deleteCharAt(3)
表示删除索引号 3 的字符;output.toString()
表示将 StringBuilder 保存到一个不可变 (Immutable) 的字符串中。