这节课是 Android 开发(入门)课程 的第三部分《访问网络》的第二节课,导师是 Chris Lei 和 Joe Lewis。由于上节课的 JSON 是硬编码的占位符,并不是真正从网络获取的数据,所以按照计划的开发步骤,要实现从网络获取数据,这节课先通过一个叫作 Soonami 的示例应用 (Sample App) 来验证网络 (Networking) 相关的代码。因为网络命题的内容很庞大,所以课程中会从实用性出发,仅对用到的部分提供相应的资讯,不作深入讨论。
Soonami App 同样使用 USGS API 显示是否有地震引起的海啸预警,分四个步骤完成:
- Form HTTP Request
- Send the Request
- Receive the Response and make sense of it
- Update the UI
关键词: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 Chaining
Android Permissions
在进行 Android 中的网络操作前,先了解一下 Android 权限的相关知识。默认情况下 Android 应用不具备任何权限,当应用需要使用设备的蓝牙、网络连接、指纹识别,或者访问用户的日历、地址、联系人等操作时,应用就需要请求权限,完整的 Android 权限列表可以到 Android Developers 网站 查看。
Android 权限按保护等级分为几种类型,其中最重要的两种是正常权限 (Normal Permissions) 和危险权限 (Dangerous Permissions)。
-
正常权限:允许的操作对用户信息和其它应用的数据无影响,例如使用设备的蓝牙、网络连接、指纹识别等,完整列表可以到 Android Developers 网站 查看。当应用请求正常权限时,Android 会自动授予应用该权限,无需用户介入。
-
危险权限:允许访问用户的个人信息,可能会对其它应用的数据产生影响,例如访问用户的日历、地址、联系人等。当应用请求危险权限时,需要由用户手动处理该请求。危险权限是通过 权限组 (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),弹出对话框,显示应用请求的所有权限组列表,用户必须同意所有的权限请求,否则无法安装应用。
为应用请求 Android 权限的方法是在 AndroidManifest 中添加 <uses-permission>
标签以及对应的属性,注意标签名不是 <user-permission>
,例如在 Soonami App 中请求网络访问的权限:
<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 调用相应的应用来实现,从而避免请求过多的权限。过多的权限请求会引起用户的怀疑,所以应用应该尽可能少地请求权限,同时确保具有充分的理由向用户解释需要请求权限的原因。
Android System Architecture
之所以 Android 有系统权限的概念,是因为 Android 是一种权限分离 (privilege-separated) 的操作系统,应用以唯一的身份标识运行。也就是说,每个 Android 应用都运行在一个进程沙盒 (Process Sandbox) 中,应用需要明确请求沙盒外的资源和数据。这种模式是由 Android 系统框架决定的,应用与设备之间的交互通过一系列的层抽象 (Layer Abstraction) 实现,每一层实现一部分功能,越底层实现的功能越小。
上图是简化的 Android 系统框架,完整的图表可以到 Android Developers 网站 查看。
- 顶层是应用层,开发者写的所有应用程序都在这一层。App 通过调用下一层的 Android Framework class,如 TextView、Activity,使应用仅用几行代码就完成很多复杂的工作,如显示文本、打开一个新页面。
- 次层是框架层,这一层提供了许多 Android Framework class,它们通过调用下一层的代码来避免很多重复的复杂工作,最终达到控制设备硬件的效果。框架层是连接应用和设备的桥梁。
- 次底层是系统层,这一层有一套复杂的控制设备硬件的代码,用来规范应用和系统进程如何访问硬件资源,从而实现设备上的多个应用共享同一套硬件。
- 底层是物理层,指的是设备硬件,如 Wi-Fi、蓝牙,以及 CPU、GPU、内存等电子器件。
在 Soonami App 中,应用通过 Android Framework 的 HttpURLCOnnection 类使用设备上的蜂窝或 Wi-Fi 硬件设备,以从网络上获取数据,而不是直接操作 Android 系统,更不是直接控制设备硬件。
Exception
如果 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();
...
}
- 在任何地方触发异常时都要用到 Java 关键字
throw
。 - 异常触发后下面的代码不会执行。
异常可分为检查异常和非检查异常 (Checked and Unchecked Exception)。
- 所有非 RuntimeException(Exception 的子类)的异常都是检查异常,都必须在方法签名 (Method Signature) 中声明,表示调用该 method 时必须处理异常,例如调用上面的
completePurchase()
method 时必须处理 InvalidPurchaseException 异常。这种做法在一个类内的辅助方法 (Helper Method) 很常用,可以将异常处理转移到调用 method 的地方。 - 所有 RuntimeException 都是非检查异常,编译器不会强制 (check) 代码处理异常。虽然 Java 有使用异常的标准框架,但这并不意味着一定要在发生错误时触发异常。理论上,在出现错误或意外情况时,应该在代码中提供一些合理的默认行为,尽可能地使代码继续运行,这种做法叫静默失败 (Failing Silently);但是如果错误会对接下来的代码造成影响,那么就要触发异常以通知此错误。开发者需要根据实际情况进行权衡。
处理 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();
表示打印错误堆栈。
Networking
网络是计算机(包括手机、笔记本电脑、服务器等)之间交换信息的概念,它的基本原理是一台计算机向另一台计算机发送 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
- 传送协议 (Protocol/Scheme):通常为 http 或 https,后接
//
标记符。 - 服务器 (Host/Domain/Authority):Web 资源的主体,通常是域名,如 google.com,有时是 IP 地址,如 192.168.0.1。后面可以接网络端口号(数字,若为 HTTP 的默认值 ":80" 可省略)。
- 资源路径 (Resource Path):类似目录结构,表示资源在服务器中的位置。
- 查询 (Query):可选,以
?
为开始,每个参数用&
分隔。 - 片段 (Fragment):可选,以
#
为开始,指页面中的某些资源 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");
调用 HttpURLConnection 的setRequestMethod
method 来设置 HTTP 动词。
HTTP 方法或动词 (Method/Verb) 是客户端发送 HTTP 请求的另一项重要指标,通过它来完成客户端与服务器之间的交互,通常是四种操作,创建 (Create)、读取 (Read)、更新 (Update)、删除 (Delete),简写 CRUD)。常用的 HTTP 动词有:
- GET: 客户端从服务器获取或检索数据。
- POST: 客户端向服务器发送一些数据。
- PUT: 客户端更新服务器上的数据。
- 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 查看。
- 1xx Informational Responses
信息状态码,表示请求已被服务器接收,但仍需继续处理。 - 2xx Success
成功状态码,表示请求已成功被服务器接收、理解、并接受。常见 "200 OK" 表示请求已成功,请求的数据返回客户端。 - 3xx Redirection
重定向状态码,表示客户端需要采取进一步的操作才能完成请求。常见
"301 Moved Permanently" 表示请求的资源已永久移动到新位置。 - 4xx Client Errors
客户端错误状态码,表示客户端可能发生了错误,妨碍服务器的处理。常见 "400 Bad Request" 表示由于明显的客户端错误(请求语法错误,欺骗性路由请求等),服务器不会处理该请求;"403 Forbidden" 表示服务器已经理解请求,但是拒绝执行它;"404 Not Found" 表示请求的资源未在服务器上找到。 - 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();
}
- 通过 InputStreamReader 将 InputStream 的二进制数据转换为字符。其中 Charset 指定了如何将原始数据的逐个字节转换成字符,UTF-8 是一种广泛应用的 Unicode 字符编码。
- 由于 InputStreamReader 一次只能转换一个字符,根据 InputStream 实际提供数据的不同方式,这可能会导致严重的性能问题,因此将 InputStreamReader 包装到 BufferedReader 可以避免这个问题。例如上面的
reader.readLine();
使 BufferedReader 在收到对某个字符的请求后会读取并保存该字符前后的一整行字符,当请求另一个字符时就能利用 BufferedReader 提前读取的字符来实现请求,无需再调用 InputStreamReader。 - 由于数据解析过程中会不断生成新的字符,如果将解析的字符存入字符串,那么就要不断地对字符串重新赋值,实际上是对 String 对象反复进行删除和创建操作,因此这里引入一个新的 StringBuilder class。相对 String 而言,StringBuilder 是可变的 (Mutable),在改变字符时能够节省很多系统资源。
(1)StringBuilder output = new StringBuilder();
与其它类一样,通过构造函数创建一个 StringBuilder 对象。
(2)output.append(line);
通过append
method 添加字符序列。append
method 可以在一行内多次调用,如output.append(line1).append(line2);
,这种方法叫做方法链 (Method Chaining)。
(3)StringBuilder 的其它一些常用 method 有output.deleteCharAt(3)
表示删除索引号 3 的字符;output.toString()
表示将 StringBuilder 保存到一个不可变 (Immutable) 的字符串中。
网友评论