安卓学习日志 Day12 — HTTP 网络

概述

学习网络相关的概念,便于将获得的信息运用到 Android 应用中。例如 Android 中的权限以及在后台线程中执行任务。

这些都将通过一个现有的 Soonami 项目(简版 QuakeReport) 来体现。

Android 权限

权限的作用是保护 Android 用户的隐私。Android 应用必须请求权限才能访问敏感的用户数据(例如联系人和短信)以及某些系统功能(例如相机和互联网)。系统可能会自动授予权限,也可能会提示用户批准请求,具体取决于访问的功能。

Android 安全架构的设计主旨是:在默认情况下,任何应用都没有权限执行会对其他应用、操作系统或用户带来不利影响的任何操作。这包括读取或写入用户的私有数据(例如联系人或电子邮件)、读取或写入其他应用的文件、执行网络访问、使设备保持唤醒状态等。

权限分为几个保护级别。保护级别影响着是否需要运行时权限请求。

有三种保护级别会影响第三方应用:普通、签名和危险权限。如需查看特定权限所拥有的保护级别,请访问权限 API 参考页面

请求 Internet 权限

下面将在 Soonami 项目中练习权限的请求,首先克隆项目的初始版本到本地,命令行中使用 Git 命令:

git clone -b starting-point https://github.com/HEY-BLOOD/Soonami.git

然后导入Android Studio 中导入并运行,这时候应用会闪退,同时日志记录如下所示:

在这里插入图片描述

从日志记录中可以看出 应用崩溃的原因是没有网络权限。

修复方法

声明应用可能请求的权限,在应用的清单文件中添加相应的 <uses-permission>元素。例如,需要访问网络的应用应在清单中添加以下代码行:

AndroidManifest.xml

<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    <application ...>
        ...
    </application>

更改后重新运行应用,运行结果如下(代码更改前后对比参考 此链接):

HTTP 请求 URL

超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。

下面将通过更改 Soonami 应用中的 请求URL,来改变 Soonami应用 显示的数据。

更改 Soonami应用的请求URL为 此链接,在 MainActivity 中找到请求URL 常量 USGS_REQUEST_URL,更改如下:

    /** URL to query the USGS dataset for earthquake information */
    private static final String USGS_REQUEST_URL =
            "https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&starttime=2014-01-01&endtime=2014-12-01&minmagnitude=7";

更改完成 运行应用,运行结果如下(代码更改前后对比参考 此链接):

在这里插入图片描述

URL 对象

URL 是统一资源定位符的缩写,用于识别 Internet 资源的所在位置(这里指地震数据列表)。

下面 谈谈 Soonami 应用是如何 使用 URL 对象的。

首先,在 MainActivity 的 onCreate() 方法中启动了一个 TsunamiAsyncTask 自定义异步任务 来执行网络请求。 TsunamiAsyncTaskdoBackground() 方法定义如下:

        @Override
        protected Event doInBackground(URL... urls) {
            // Create URL object
            URL url = createUrl(USGS_REQUEST_URL);

            // Perform HTTP request to the URL and receive a JSON response back
            String jsonResponse = "";
            try {
                jsonResponse = makeHttpRequest(url);
            } catch (IOException e) {
                // TODO Handle the IOException
            }

            // Extract relevant fields from the JSON response and create an {@link Event} object
            Event earthquake = extractFeatureFromJson(jsonResponse);

            // Return the {@link Event} object as the result fo the {@link TsunamiAsyncTask}
            return earthquake;
        }
  • 1、2 行,表示重写父类的 doBackground() 方法
  • 4 行,通过辅助方法,将 URL 字符串窗常量 USGS_REQUEST_URL 转换为实际的 URL 对象
  • 7 行,预备的 JSON 响应字符串
  • 9 行,将 URL对象作为参数 在辅助方法 makeHttpRequest(url) 中 发起 网络请求 并获得 JSON 字符串响应
  • 剩余代码暂不关心

现在问题是 辅助方法 makeHttpRequest(url) 是如何 使用传入的 URL对象发起网络请求的呢?

辅助方法 makeHttpRequest(url) 定义如下:

        /**
         * Make an HTTP request to the given URL and return a String as the response.
         */
        private String makeHttpRequest(URL url) throws IOException {
            String 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();
                inputStream = urlConnection.getInputStream();
                jsonResponse = readFromStream(inputStream);
            } 
            …………
                
            return jsonResponse;
        }

在辅助方法的第 9 行 调用 了 URL 对象的 openConnection() 方法,并将返回的对象 强制转换为 HttpURLConnecton 对象。

从字面上看 HttpURLConnection 对象能够连接到 URL 对象所指向的地址,还可以执行响应的操作。

HttpURLConnection

HttpURLConnection 是一个 HTTP 客户端,可以用来执行 HTTP 请求。该类可使用 HTTP连接在 web 中发送和接收数据。

继续 分析 makeHttpRequest() 辅助方法中 HttpURLConnection 的应用:

        /**
         * Make an HTTP request to the given URL and return a String as the response.
         */
        private String makeHttpRequest(URL url) throws IOException {
            String 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();
                inputStream = urlConnection.getInputStream();
                jsonResponse = readFromStream(inputStream);
            } 
            …………
                
            return jsonResponse;
        }
  • 9 行,已经有 URL 对象时直接调用其 openConnection() 方法创建一个 HTTP 客户端

  • 10 ~ 12 行,使用创建的 HTTP 连接继续指定 连接详情,如请求的方法

  • 13 行,继续调用 connect() 方法打开链接

  • 14 行,最后 调用 getInputStream() 获得返回结果的输入流

  • 15 行,使用辅助方法 readFromStream() 将响应的数据流转换为 JSON格式的字符串

这就是使用 HttpURLConnection 发送请求并获得响应的基本过程,但 Android 平台实际上都在为我们处理许多复杂的联网细节,换句话说 Android 作为一个操作系统其实是与设备硬件协作来建立链接,并处理一些进行正常通信所需的物理步骤。

还可以使用那些非 Google 创建的第三方库,例如 OKHttp 是一个由 Square 创建的大众开源 HTTP客户端。

InputStream

在 调用 HttpURLConnection 对象的 getInputStream() 方法后 成功获得了从服务器返回的响应数据。

        /**
         * Make an HTTP request to the given URL and return a String as the response.
         */
        private String makeHttpRequest(URL url) throws IOException {
            String 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();
                inputStream = urlConnection.getInputStream();
                jsonResponse = readFromStream(inputStream);
            } 
            …………
            return jsonResponse;
        }

但每一段数据 无论文本或是图像实际上都存储在字节大小的块中,在应用接收数据时,数据以输入流的形式进入,即由字节组成的流。

输入流是抽象的,也就是说输入流会隐藏详细信息,例如 这些字节是否表示文件或网页 甚至是媒体内容。

所以在接收输入流后需要把字节重组为应用可以使用的有意义的内容,对于文本数据 需要将字节重新转换为 字符串,而这正是 BufferedReader 类的作用。

下面来看看辅助方法 readFromStream(inputStream); 是如何将文本类型的输入流转换为字符串的,方法定义如下:

        /**
         * 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();
        }
  • 5 行,方法定义,接收一个 输入流对象
  • 6 行,预留的 StringBuilder 可变字符序列(String 不可变),用存储最终的 JSON字符串
  • 7 行,判断输入流是否存在数据,有则进行转换
  • 8 行,创建一个 InputStreamReader 对象用于读取 InputStream 的数据,并指定字符编码为 "UTF-8"(每次只能读取一个字符)
  • 9 行,创建一个 BufferedReader 对象,对 InputStreamReader 对象进行包装,优化读取性能的问题
  • 10 行,读取 第一行字符数据
  • 11~14 行,若不为空则将数据添加到 StringBuilder 对象中,并继续读取后面的数据,直至全部写入 StringBuilder 对象
  • 16 行,返回最终的 JSON 字符串

这里使用了 StringBuilder 类型来存在响应的 JSON 字符串,而没有使用 String 类型。

因为 String 是不可变的,而 StringBuilder 是不可变的。这表示当一个 String 对象创建后就可以固定了,当使用字符串的连接操作时其实并不是真正的连接操作,而是创建了一个新的 String 并赋值。而 StringBuilder 创建之后可以修改。(可以将 String 理解为字符数组,而 StringBuilder 是字符链表)

而响应的数据流 包含一个很长的 JSON 格式的字符串,需要循环很多次才能读取完所有的数据,所有采用可变的字符序列 StringBuilder 更合理,只需一个 StringBuilder 就够了,不必每次循环都创建一个 String 对象,更简洁高效的同时也减少了不必要的内存浪费。

HTTP 状态码

要使代码更可靠,请处理网络代码中的 空情况。例如,如果 url 为空,就不应该尝试 创建 HTTP 请求。

更改 MainActivity 中的 makeHttpRequest 方法如下:

        /**
         * 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();
                inputStream = urlConnection.getInputStream();

                // If the request was successful (response code 200),
                // then read the input stream and parse the response.
                if (urlConnection.getResponseCode() == 200) {
                    inputStream = urlConnection.getInputStream();
                    jsonResponse = readFromStream(inputStream);
                }
            } 
            
            …………
                
            return jsonResponse;
        }

同样的,如果 JSON 响应为空 字符串,不应该再继续对其进行解析。在解析 JSON 字符串之前先判断是否为空:

更改 MainActivity 中的 extractFeatureFromJson() 方法如下:

        /**
         * Return an {@link Event} object by parsing out information
         * about the first earthquake from the input earthquakeJSON string.
         */
        private Event extractFeatureFromJson(String earthquakeJSON) {
            // If the JSON string is empty or null, then return early.
            if (TextUtils.isEmpty(earthquakeJSON)) {
                return null;
            }

            …………
                
            return null;
        }

为了测试 当HTTP 响应失败时情况,将 MainActivity 中的请求 URL字符串常量 USGS_REQUEST_URL 的值 更改为这个 无效地址,然后运行(更改前后代码对比参考此链接):

由于已经将 请求 URL字符串常量 USGS_REQUEST_URL 的值改为了无效的地址,因此 将无法得到正确的响应,同时 HTTP状态码为 400,因此 被解析的 JSON 字符串默认为空,页面不会显示任何 数据。

在这里插入图片描述

日志错误

当程序中可能出现异常时应该对其进行 捕捉和处理。

分别 更改 MainActivity 中两处 TODO 标记的代码如下:

…………
	try {
	jsonResponse = makeHttpRequest(url);
} catch (IOException e) {
	Log.e(LOG_TAG, "Problem making the HTTP request.", e);
}
…………
………………
catch (IOException e) {
    Log.e(LOG_TAG, "Problem retrieving the earthquake JSON results.", e);
} finally {
    if (urlConnection != null) {
        urlConnection.disconnect();
    }
}
………………

以及当 HTTP 请求失败(即状态码不为 200)最好将其输出到日志,以便查找错误。

………………
// If the request was 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());
}
…………

最后继续使用无效的请求地址运行应用,在 Android Studio 中打开 Logcat,可以找到 Tag 为 MainActivity 的日志错误(代码更改前后对比参考 此链接):

在这里插入图片描述

Android 系统架构

现在先回退一步,谈一下 Android 系统结构,Android Framework 为了帮助我们与系统进行交互提供很多有用的类,如 Acticity 的生命周期和 设备权限,或帮助我们访问设备的一些功能如 相机、收音机、Wi-Fi 和蓝牙 以及螺旋仪之类的传感器。

访问设备硬件的路径在 Framework 中是通过 一系列抽象层来实现的,每一层都负责解决问题的一部分,相应的 下级的层负责解决下层更小的问题,最后将这些层合并 就能实现所需的功能。

可以将 Android 设备的每一个功能都看成是由不同的层编制的(自上往下,从高到低):

在这里插入图片描述

  • App(应用层),开发人员建立的所有应用程序所在的层,在应用中开发人员使用一些 Android Framework 中的类(如 TextView 和 Activity),仅仅通过几行代码就可以完成很多复杂、重要的事情(例如,展示文本 或打开一个新的页面 等)。
  • Framework(Android 框架),Android框架通过调用 Android 低级层来避免很多不必要的复杂事物,并把低级层作为起重设备。这在控制设备硬件的过程中是一项艰苦且复杂的工作,最后一切得以运行,这就是所说的层阻碍
  • Android Operating System(安卓操作系统,简称 OS),有一套自己的复杂的代码来规范应用和系统进程如何访问设备硬件的有限资源。为了保护系统整体的有效性和响应性在运行应用的同时必须共享设备资源。
  • Physical Device Hardware(物理设备硬件),对底层为实际的物理硬件设备,如 Wi-Fi、蓝牙 或是显卡、CPU、内存条和 GPU等装配在设备上的物理电器元件。

我们可以将 Framework 看作是连接应用和设备的绳索,展示了当用户使用应用时设备是如何表现 的。

例如,在应用的代码中,将 Framework 称为 API。音频应用 Miwok 使用 Framework 从设备的扬声器播放声音。在 Soonami 应用中使用 HttpURLConnection 来建立网络请求,使用设备上的无线设备在 Internet 上发送信息数据包。

我们不需要直接操作 Android 操作系统,Framework 是 阻碍发生在硬件上实现的详细内容的 抽象模型。

这是一个过期的系统构架图, 但它可以帮助更好地理解图层。

总结

通过本次学习,逐步 掌握了从 Internet 中获取数据 并将数据显示在应用中所涉及的所有相关部分,其中包括了 如何创建并配置HTTP请求 以及 使用 HTTP URL连接类发送请求,然后学习了 如何接收并解析响应,在学习 StringBuidler类的同时了解了响应会以数据流的形式出现 并利用 BufferedReader 将 inputstream 转换为 String。

参考

危险权限

正常权限

其他资源:

所有权限的参考文档

Soonami 应用项目,通过此 Github 链接

HTTP / REST 教程

Java 教程:使用 URL

Spotify API 端点

URL 类文档

使用 URL 的文章

Using the OkHttp library for HTTP requests - Tutorial - Tutorial

过期的系统构架图

HTTP 请求类型

HTTP 方法:GET vs POST

HTTP 状态代码

How do I read / convert an InputStream into a String in Java?

如果你有兴趣,请观看此教程了解异常! 通过按方法指定抛出异常检查或非检查异常

错误日志:使用 Log.e 方法调用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值