AndroidStudio3-和-Kotlin-学习手册-五-

120 阅读1小时+

AndroidStudio3 和 Kotlin 学习手册(五)

原文:Learn Android Studio 3 with Kotlin

协议:CC BY-NC-SA 4.0

十六、排除故障

我们将介绍的内容:

  • 你会遇到的错误种类

  • 记录调试语句

  • 使用交互式调试器遍历代码

很快,您将会超越本书中示例代码的简单结构。你的程序会变得越来越复杂,文件越来越多,组件越来越多。随着这种情况的发生,你将面临的错误数量也会增加;到那时它们可能更难被发现。

在这一章中,我们将看看你可能会遇到的三种主要的错误,以及什么样的工具或技术可以帮助你解决。

句法误差

语法错误正如您所想的那样:语法错误。发生这种情况是因为你在代码中写了一些 Kotlin 编译器的规则中不允许的东西。换句话说,编译器不理解它。这可能是良性的,就像忘记了表达式中的右花括号或右圆括号一样。它也可能稍微复杂一些,比如在使用泛型时将错误类型的参数传递给函数或参数化类。在 Android 开发的早期,您所能做的就是使用裸露的 SDK,您只有在尝试编译代码时才能知道是否有语法错误,这就是为什么其他程序员也将这种错误称为“编译时”错误的原因。当然,自那以后,Android 开发已经走过了漫长的道路。我们有一个非常称职的 IDE,它甚至可以在你试图编译你的代码之前就发现并指出语法错误。这就好像 IDE 一直在读取代码并编译它。

图 16-1 显示了一个内部 AsyncTask 子类的片段。IDE 通过用红色曲线突出显示有问题的代码来引起您的注意。

img/463887_1_En_16_Fig1_HTML.jpg

图 16-1

AsyncTask 类缺少构造函数

将鼠标悬停在出现曲线的区域足够长的时间,您应该会看到 AS3 的气球提示。它说 AsyncTask 类有一个必须初始化的类型构造函数。要修复它,将构造函数调用——一对括号——放在类定义的旁边,如图 16-2 所示。

img/463887_1_En_16_Fig2_HTML.jpg

图 16-2

AsyncTask 类缺少强制实现

弯弯曲曲的线条正在一条一条消失。这是一个好迹象,意味着我们正在修复错误,但还没有完成。你注意到图 16-2 中的第 15 行了吗?我们仍然有一个错误。它说我们的类没有实现基类成员。AsyncTask 类是抽象的;它声明了抽象方法 doInBackground 。我们必须重写这个方法并编写我们的实现,除非我们也将 class Worker 变成一个抽象类——这不是我们的意图。利用 Android Studio 的快速修复功能( option + Enter 在 Mac, alt + Enter 在 Windows 和 Linux)解决问题,如图 16-2 。

图 16-3 显示了快速固定的作用。它为我们如何修复它提供了一些建议。第一个选项是我们想要的——实现并覆盖 AsyncTask 的抽象成员。

img/463887_1_En_16_Fig3_HTML.jpg

图 16-3

AsyncTask 类的快速修复

点击确定。接下来是实现成员的对话框,如图 16-4 所示。AsyncTask 只有一个需要由子类重写的抽象成员。选择做背景并点击确定继续。

img/463887_1_En_16_Fig4_HTML.jpg

图 16-4

实现成员

Android Studio 会给你一个 doInBackground 函数的结构骨架。现在,您可以编写您的实现了。

有时,即使有曲线的帮助,错误也不是很明显。图 16-5 显示了这个问题的一个例子。

img/463887_1_En_16_Fig5_HTML.jpg

图 16-5

嵌套块

图 16-5 中第 14 行和第 27 行之间的代码显示了一个深度嵌套的块。当您使用匿名对象时,有时会发生这种情况,您可以从示例代码的结构中看到这一点。

img/463887_1_En_16_Fig6_HTML.jpg

图 16-6

错误代码

如果你尝试制作项目(从主菜单栏➤ 构建制作),IDE 会给你更多的信息,更多的信息,如图 16-6 所示;但可能不会给你更多的感悟。这是你确实需要做繁重工作的情况之一。您必须手动检查代码结构。请注意,曲线出现在类的末尾(图 16-6 中的第 27 行)以及告诉我们缺少花括号的错误消息;从那里开始,手动检查成对的花括号。这个问题与我们如何构建代码有关。你只需要小心那些大括号——Python 程序员现在可能会幸灾乐祸地说,“这就是你使用大括号的结果,缩进石头。”

运行时错误

当你的代码遇到它没有预料到的情况时,运行时错误就会发生;顾名思义,这种错误情况只有在程序运行时才会出现,而不是你或编译器在编译时能看到的。您的代码可以顺利编译,但是当运行时环境中的某些内容与您的代码不一致时,它可能会停止运行。这些事情有很多例子,例如:

  • 该应用从互联网上获取一些东西——一张图片或一个文件等。—所以它假设互联网可用,并且有网络连接。一直都是。经验应该告诉你,情况并不总是这样。网络连接有时会中断,如果您不在代码中考虑这一点,它可能会崩溃。

  • 该应用需要从文件中读取。就像我们前面的第一个案例一样,您的代码假设文件将一直存在。有时,文件会损坏,可能变得不可读。这也应该在代码中考虑。

  • 该应用执行数学计算。它使用用户输入的值,有时也使用从其他计算中导出的值。如果您的代码碰巧执行了除法,并且其中一个除法的除数为零,这也会导致运行时问题。

这里有一些代码示例,乍看起来可能没问题,并且可以编译,但是当它在运行时遇到没有准备好的情况时,就会出现运行时错误。

清单 16-1 显示了打开一个文件并将其内容读入一个字符串变量的基本代码。如果代码试图打开一个已经存在的文件,没有问题——代码会正常工作,如预期的那样。如果它试图打开一个不存在或由于某种原因无法读取的文件,问题就会出现。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  button.setOnClickListener {
      openFile("doesnotexist.txt")
  }
}

private fun openFile(file: String) {
  val strFile = File(file).readText()
}

Listing 16-1Possible FileNotFoundException or Other IOException

清单 16-2 可能看起来不自然,但是想象一下,如果你从一个用户那里得到输入,或者你从其他地方读取输入,除数变成零。您将遇到一个算术异常错误。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  button.setOnClickListener {
      divide(10, 0)
  }
}

private fun divide(a:Int, b:Int) :  {
  return a / b
}

Listing 16-2Possible ArithmeticException

顺便说一下, ArithmeticException 只对整数值抛出。对于浮点型和双精度型不会发生这种情况。如果你试图将一个浮点数除以零,它只会产生一个无穷大的值,但不会抛出异常。

清单 16-3 显示了另一个会遇到运行时问题的代码示例。它现在看起来是人为的,因为你可以明显地看到,数组中没有第五元素。但是想象一下,如果你从一个 API 中读取数组(你没有创建数组,是别人创建的),并且你没有使用整数来访问数组;取而代之的是使用变量。到时候误差就不会那么明显了。

val arr = arrayOf(1,2,3,4)
println(arr[5])

Listing 16-3ArrayIndexOutOfBounds Exception

解决运行时错误的唯一方法是:

  1. 了解你的代码。您需要知道哪些调用可能会遇到运行时异常;和

  2. 在代码中使用适当的异常处理。

像 Java 一样,Kotlin 也使用 try-catch 结构来处理异常;但是与 Java 不同,Kotlin 的所有异常都是未检查的(??)。异常处理在 Kotlin 中实际上是可选的— throws 在 Kotlin 中甚至不是一个关键字,但是throws关键字仍然是。这可能是好事,也可能是坏事,看你怎么看;在流行的编码论坛上有关于这个主题的热烈讨论。Kotlin 团队关于检查异常的意见可以在 Kotlin 在线文档( https://kotlinlang.org/docs/reference/exceptions.html )中找到。

根据 Kotlin 团队的说法,Kotlin 针对的是大型开发项目,几乎没有证据表明使用检查异常有助于开发人员的生产力;恰恰相反,它减轻了它。

Kotlin 中的异常处理在很大程度上与 Java 中的方式完全一样。你可以用试抓试抓最终来做。在 Java 7 中,引入了try-withresources 的概念。Kotlin 没有 try-with-resources,但是它有 use 扩展;这相当于尝试资源。

为了唤起我们的记忆,try-catch 块的基本形式如清单 16-4 所示。

| -什么 | 这是 **try** 块的主体。这是您应该编写可能抛出异常的调用的地方。 | | ➋ | 您必须在 catch 子句中尽可能多地提供确切的异常类型(例如,如果您正在处理 FileNotFoundException,那么这就是您应该在此处写入的内容,以代替 **theException)。** | | ➌ | 这是 **catch** 子句的主体。这是您应该写下当异常发生时您想要做的事情的地方(例如,记录到文件,要求用户重复输入,等等)。). | | -你好 | 有时,您可能不想处理异常。你可以**把**扔给函数的调用者(调用栈的上一层),让它成为他们的问题。 | | ➎ | **finally** 子句的主体是放置代码的地方,无论是否发生异常,都要执行这些代码。finally 子句的主体是保证总是被执行的*。* |
try {
  ... ➊
}
catch(mexception: theException) { ➋
  ... ➌
  throw mexception ➍
}
finally {
  ... ➎
}

Listing 16-4The Try-Catch-Finally Structure

现在,让我们看看如何使用 try-catch 来防止打开文件时崩溃。参见清单 16-5 。

| -什么 | **文件**构造函数实际上可以抛出一个 FileNotFoundException,所以我们将它们放在一个 try-catch 块中。 | | ➋ | 我们知道 **FileNotFoundException** 可以被 **File** 构造函数抛出,所以这就是我们放在 **catch** 子句中的内容。如果你想匹配一个更一般类型的异常,你也可以在这里使用 **IOException** 。IOException 是 **FileNotFoundException 的父类。** |
private fun openFile(file: String) {
  try {                                     ➊
    File(file).useLines {
      println(it)
    }
  }
  catch (fe: FileNotFoundException) {       ➋
    println("do your error handling here")
  }
}

Listing 16-5How to Handle the FileNotFoundException

清单 16-6 展示了如何在处理整数运算时防止崩溃。

  private fun divideInt(a:Int, b:Int): Int {
    var result = 0
    try {
      result = a /b
    }
    catch (ae: ArithmeticException) {
      println("handle your exception here")
    }
    finally {
      return result
    }
  }

Listing 16-6How to Handle the ArithmeticException

逻辑错误

逻辑错误是最难发现的。顾名思义,这是你逻辑上的错误。当你的代码没有做你认为它应该做的事情时,那就是逻辑错误。有许多方法可以解决这个问题,但是在这一节中,我们将研究两种方法:在代码的某些地方打印调试语句和使用断点进行代码遍历。

当您检查代码时,您会发现某些您非常确定发生了什么的区域,然后还有一些您不太确定的区域—您可以在这些区域中放置调试语句。就像留下面包屑让你跟着走。有几种方法可以打印调试语句。你可以使用 Java 中的 println 、 **Log、**或者 Logger 类。

图 16-7 显示了 Logcat 工具窗口中 println 语句的输出。

img/463887_1_En_16_Fig7_HTML.jpg

图 16-7

在 Logcat 工具窗口中显示 println

println 是您可以用来打印调试语句的最简单、最容易的工具,但是请记住,只有当 Logcat 的模式设置为“verbose”、“info”或“debug”时,您才能在 Logcat 中看到这些语句。如果您将模式设置为其他模式,如 warn、error 或 assert,您将看不到 println 语句。

当您将 Logcat 的模式设置为 verbose、info 或 debug 时,您将看到 Android 运行时生成的所有消息。如果您只想看到警告消息或错误,那么您需要使用 Log 或 Logger 类。

Log 类有五个静态方法;用法如下所示。

Log.v(tag, message) // verbose
Log.d(tag, message) // debug
Log.i(tag, message) // info
Log.w(tag, message) // warning
Log.e(tag, message) // error

在每种情况下,标签是一个字符串或变量。您可以使用标记来过滤 Logcat 窗口中的消息。消息也是字符串或变量,它包含您实际想要在日志中看到的内容。清单 16-7 显示了如何使用 Log 类的示例代码。

| -什么 | 您可以在类中的任何地方定义标签,但是在本例中,它被定义为 class property。 | | ➋ | 我们正在打印调试信息。 | | ➌ | 我们正在打印警告信息。 |
  val TAG = this@MainActivity::class.toString() ➊

  private fun divideInt(a:Int, b:Int): Int {
    var result = 0
    try {
      Log.d(TAG, "Inside the try")              ➋
      result = a /b
    }
    catch (ae: ArithmeticException) {
      Log.w(TAG, "Sample log message")          ➌
    }
    finally {
      return result
    }
  }

Listing 16-7How to Use the Log Class

或者,我们也可以使用 Java 中的 Logger 类;如清单 16-8 所示。

val Log = Logger.getLogger(MainActivity::class.java.name)

private fun divideInt(a:Int, b:Int): Int {
  var result = 0
  try {
     Log.info("inside try")
     result = a /b
    }
  catch (ae: ArithmeticException) {
     Log.warning("Sample log message")
     println("handle your exception here")
  }
  finally {
    return result
  }
}

Listing 16-8How to Use the Logger Class

运行应用时,您可以在 Logcat 工具窗口中看到日志消息。你可以在 AS3 窗口底部的菜单栏点击它的标签,或者从主菜单栏查看➤工具窗口日志目录来启动它。图 16-8 显示了 Logcat 工具窗口。

img/463887_1_En_16_Fig8_HTML.jpg

图 16-8

Logcat 工具窗口

遍历代码

AS3 包括一个交互式调试器,允许您在代码运行时遍历和单步调试代码。使用交互式调试器,我们可以检查应用的快照——变量值、正在运行的线程等。—在代码中的特定位置和特定时间点。代码中的这些特定位置被称为断点;你可以选择这些断点。

若要设置断点,请选择包含可执行语句的一行,然后在装订线中点按其行号。设置断点时,槽内会出现一个粉红色的圆圈图标,整行都是粉红色点亮,如图 16-9 所示。

img/463887_1_En_16_Fig9_HTML.png

图 16-9

调试器窗口

设置断点后,您必须在调试模式下运行应用。如果应用当前正在运行,请将其停止,然后从主菜单栏中点击运行调试应用。

注意

在调试模式下运行应用并不是调试应用的唯一方式。您还可以在当前运行的应用中附加调试器进程。在某些情况下,第二种技术非常有用,例如,当您试图解决的错误发生在非常特定的条件下时,您可能希望运行应用一段时间,当您认为您接近错误点时,您可以连接调试器。

照常使用该应用。当执行到您设置断点的一行时,该行将从粉红色变为蓝色。这就是你如何知道代码执行是在你的断点。此时,调试器窗口打开,执行停止,AS3 进入交互式调试模式。当您在这里时,应用的状态显示在调试工具窗口中。在此期间,您可以检查变量值,甚至看到应用中运行的线程。

您甚至可以通过单击带有眼镜图标的加号,在“监视”窗口中添加变量或表达式。将有一个文本字段,您可以在其中输入任何有效的表达式。当你按下输入时,Android Studio 会对表达式进行求值,并向你显示结果。要删除监视表达式,请选择表达式,然后单击“监视”窗口上的减号图标。

要恢复程序执行,您可以单击调试器工具栏顶部的“恢复程序”按钮—它是指向右侧的绿色箭头。或者,您也可以从主菜单栏运行恢复程序中恢复程序。如果你想在程序自然完成之前暂停它,你可以点击调试器工具栏上的“停止应用”按钮——它是一个红色的正方形图标。或者,您也可以从主菜单栏运行停止应用中执行此操作

其他说明

在 Android 开发的早期,那时还没有 ide,开发人员使用一种叫做“adb”的工具,这是 Android Debug Bridge 的缩写。这是一个漂亮的命令行工具,可以让你与 Android 设备(虚拟的或真实的)进行通信。它可以让您做以下事情:

  • 安装应用

  • 调试应用

  • 获取对外壳终端的访问权限;请记住,Android 是基于 Linux 的,访问终端会非常方便(例如,当您在 sqlite 数据库上进行一些白盒测试时,等等)。).

Android Studio 接管了一些过去由 adb 做的事情(例如,显示日志、安装应用、调试应用等。).但是,如果你需要在 linux 命令行级别做事情,你真的必须使用ADB——你可以在ANDROID _ HOME/SDK/platform-tools文件夹中找到这个工具;其中 ANDROID_HOME 是您安装 Android SDK 的文件夹。

本章中我们没有提到的另一个工具是 Android Profiler ,它是 Android Studio 3.0 中的新功能。它取代了一个名为的安卓设备监控工具。你可以使用这个工具来查看你的应用的实时数据。您可以找出您的应用消耗了多少 CPU、内存、网络和 I/O 资源。您可以捕获堆转储、查看内存分配以及检查网络传输文件的详细信息。

章节总结

  • 您可能遇到的三种错误是编译类型或语法错误、运行时错误和逻辑错误。

  • 语法错误是最容易修复的。Android Studio 本身为您竭尽全力,让您可以快速发现语法错误。用 AS3 有各种方法来修复语法错误,但是大多数时候,快速修复应该可以做到。

  • Kotlin 不像 Java 那样有检查异常。Kotlin 团队这样做是有充分理由的。如果你是 Kotlin 的初学者,但对 Java 却很陌生,那么这应该不会影响你——在处理可能的异常时,使用你对旧 Java APIs 的了解。如果你是 Kotlin 和 Java 的新手,你应该多花一点时间学习单元测试;这样,你就可以看到你的应用的“快乐之路”和“不快乐之路”;然后你就可以采取相应的行动。

  • 逻辑错误是最难发现的,但 Android Studio 使这种活动变得更容易忍受,因为我们可以使用工具——你可以在程序运行时遍历代码并检查事情。

在下一章中,我们将了解以下内容:

  • 如何使用 SharedPreferences 保存数据?

  • 我们将使用 Bundle 对象,这样我们可以将一些基本类型保存到一个文件中。

  • 我们还将看看如何在活动之间传递数据。

十七、共享参数

我们将介绍的内容:

  • 共享首选项简介

  • 如何从首选项文件中放置和获取数据

  • 如何在活动之间共享首选项文件

默认情况下,Android 应用不会保存你的数据。在应用的整个生命周期中,保持数据的持久性和弹性是您的责任。假设您正在从用户那里收集数据,然后在工作流程进行到一半时,应用被另一个应用中断了。不能保证当你的应用返回时,用户已经输入的任何数据都会存在。

使数据持久意味着以某种形式存储数据。你可以用几种方法存储数据。它们列举如下:

  • 共享的首选项。这是最简单的选择。这只是一个字典对象,它使用熟悉的键值对机制。如果您的数据足够简单,能够以键值对的形式进行结构化,这将非常有用。Android 将这些文件作为 XML 文件存储在内部。您只能存储简单的数据类型,如字符串和基本类型。这通常用于存储用户的偏好,比如列表中的排序顺序,你在电子书应用上阅读的最后一页,等等。

  • 内部或外部存储。使用设备中的内部或媒体存储器(例如 sdcard)。您可以使用它来存储结构更复杂的数据(例如,音频或视频文件)。如果您以前使用过文件 I/O,这与那没有什么不同。

  • SQLite 数据库。这个使用关系数据库。如果您以前使用过其他数据库,如 MS SQL server、MySQL、PostgreSQL 或任何其他关系数据库,这在本质上是相同的。数据存储在表中,您需要使用 SQL 语句来创建、读取、更新和删除数据。

  • 网络存储。如果您可以假设您的用户将始终可以访问 internet,并且您有一个托管在 Internet 上的数据库服务器,那么您可以使用此选项。这种设置可能会有点复杂,因为您需要在某个地方(Amazon、Google 或任何其他云提供商)托管数据库,为数据提供 REST 接口,并在 Android 应用中使用 HTTP 库作为客户端。本书不涉及这个话题。

  • 内容提供者。内容提供商是 Android 平台上的另一个组件;它就在活动、服务和广播接收器的上面。该组件使数据对应用可用,而不是对其本身。可以把它想象成一个拥有公共 HTTP API 层的数据库。任何通过 HTTP 通信的应用都可以读写数据。顺便说一下,ContentProviders 在内部使用 SQLite 数据库——他们只是在简洁的 HTTP API 中包装和提供数据。如果你开发过 RESTful 应用,这些应用通过 API 公开了一些底层数据,这就有点像。

在本章中,我们将了解 SharedPreferences。

SharedPreferences 对象允许您以键-值对的形式存储和检索数据,就像字典一样。它使用 XML 文件进行存储。使用 SharedPreferences 对象存储基本数据可以通过以下步骤完成:

  1. 获取 SharedPreferences 对象。您可以通过从活动中调用 getPreferences 方法来实现这一点。

  2. 接下来,我们得到一个shared preferences。编辑器对象,方法是使用 SharedPreferences 对象的工厂方法。

  3. 现在我们可以用编辑器对象插入数据。

  4. 最后,为了永久存储数据,我们在编辑器上使用了提交应用方法。

清单 17-1 展示了所有这些在代码中的样子。

| -什么 | **Activity.getPreferences** 方法给了我们一个 **SharedPreferences** 对象,它是活动的私有对象。我们使用`Context.MODE_PRIVATE`是因为我们希望首选项文件只允许我们的应用访问——其他应用禁止访问。 | | ➋ | 我们需要一个共享的参考。Editor 对象,我们可以通过在 SharedPreferences 对象上调用 **edit** 方法来获得它。 | | ➌ | 现在,我们可以使用各种 **putXXX** 方法来存储键值对。第一个参数是键,这应该是一个字符串。第二个参数可以是任何基本类型,如 Int、Float、Double、String 等。 | | -你好 | 如果我们不调用**应用**,我们的 **putString** 调用都不会被永久存储到文件中。或者,你也可以调用**提交**。**应用**方法异步保存数据*,而**提交**同步保存*。*因此,要持久化数据,调用 apply 或 commit。* |
val pref = getPreferences(Context.MODE_PRIVATE) ➊
val editor = pref.edit()  ➋

editor.putString("lastname", "Breslav") ➌
editor.putString("firstname", "Andrey")
editor.apply() ➍

Listing 17-1Basic Steps to Save Data

如果你想知道其他的上下文模式选项,这里有。

  • MODE_PRIVATE :默认模式,创建的文件只能由调用应用访问。这可能是你大多数时候想要的。

  • MODE_WORLD_READABLE :任何应用都可以读取偏好数据。这可能会导致应用出现安全漏洞。除非你有一个非常好的理由,否则远离这个。如果您想让数据对任何应用可用,可以考虑构建一个 ContentProvider。

  • MODE_WORLD_WRITEABLE :任何应用都可以编辑偏好数据。这可能会导致应用出现安全漏洞。还是那句话,除非你有充分的理由,否则远离这个。

  • MODE_APPEND :这将把已经存在的参数追加到新的参数中。

让我们为此做一个小的演示项目。表 17-1 显示了项目的详细情况。

表 17-1

演示项目的详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | ch17 首选项 | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

我们想做的是:

  1. 让用户输入他的姓和名——我们将为此使用两个编辑文本。

  2. 当用户单击“保存”按钮时,我们将把姓氏和名字存储到首选项文件中。

  3. 当用户单击“Load”按钮时,我们将从首选项文件中读取姓氏和名字。

  4. 我们将在 TextView 对象中显示它们。

图 17-1 为正在运行的 app 截图。

img/463887_1_En_17_Fig1_HTML.jpg

图 17-1

我们的项目快照,正在运行

清单 17-2 包含 XML 布局文件的完整代码,因此您可以看到视图对象的属性设置。清单 17-3 显示了 MainActivity 的完整和带注释的代码。

| -什么 | 获取一个 **SharedPreferences** 对象。 | | ➋ | 获取一个 **SharedPreferences。编辑**对象。 | | ➌ | 保存 EditText 的运行时值(txtlastname);让我们使用“姓氏”作为关键字。 | | -你好 | 通过调用 **apply** 而不是 **commit 来异步保存数据。** | | ➎ | 我们现在在“加载”按钮监听器里面。让我们获取“lastname”键的值,并将其保存到一个临时变量中。 | | ➏ | 串联姓氏和名字变量 | | -好的 | 将 TextView (txtoutput)的**文本**属性设置为串联的 lastname 和 firstname | | -好的 | 在 **onResume** 回调中,我们初始化了 txtlastname、txtfirstname 和 txtoutput 的文本属性。我们还设置了文本字段的提示属性。 |
import android.content.Context
import android.content.SharedPreferences
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val pref = getPreferences(Context.MODE_PRIVATE)                 ➊

    btnsave.setOnClickListener {
      val editor = pref.edit()                                      ➋

      editor.putString("lastname", txtlastname.text.toString())     ➌
      editor.putString("firstname", txtfirstname.text.toString())
      editor.apply()                                                ➍

      Toast.makeText(this, "Saved data", Toast.LENGTH_LONG).show()
    }

    btnload.setOnClickListener {
      val mlastname = pref.getString("lastname", "")                ➎
      val mfirstname = pref.getString("firstname", "")
      val moutput = "$mfirstname $mlastname"                        ➏

      txtoutput.text = moutput                                      ➐
    }
  }

  override fun onResume() {                                         ➑
    super.onResume()

    txtfirstname.setText("")

    txtlastname.setText("")
    txtfirstname.setHint("first name")
    txtlastname.setHint("last name")

    txtoutput.setText("")
  }
}

Listing 17-3MainActivity, Annotated

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:app=http://schemas.android.com/apk/res-auto
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <EditText
    android:id="@+id/txtfirstname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="36dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <EditText
    android:id="@+id/txtlastname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtfirstname" />

  <TextView
    android:id="@+id/txtoutput"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="183dp"
    android:layout_marginStart="16dp"
    android:text="TextView"
    android:textSize="36sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

  <Button
    android:id="@+id/btnsave"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:text="save"
    app:layout_constraintBaseline_toBaselineOf="@+id/btnload"
    app:layout_constraintStart_toStartOf="parent" />

  <Button
    android:id="@+id/btnload"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="11dp"
    android:layout_marginTop="27dp"
    android:text="load"
    app:layout_constraintEnd_toEndOf="@+id/txtlastname"
    app:layout_constraintTop_toBottomOf="@+id/txtlastname" />
</android.support.constraint.ConstraintLayout>

Listing 17-2/app/res/layout/activity_main.xml

Android 将创建一个 XML 文件来存储该首选项,它将以创建它的活动命名;在我们的例子中,它是主要的活动。

如果你想检查文件,可以使用设备文件浏览器(以前叫 Android 设备监视器)下载。进入主菜单栏,然后查看工具窗口 ➤ **设备文件浏览器。**你应该会看到一个类似图 17-2 的屏幕。

img/463887_1_En_17_Fig2_HTML.jpg

图 17-2

设备文件资源管理器中的 MainActivity.xml 文件

接下来,深入到数据数据fullyqualifiednameofproject(在我的例子中是 net . working dev . ch17 preferences;代入自己的项目名称);然后,进一步向下钻取到shared _ prefsmain activity . XML,如图 17-2 所示。

如果双击 MainActivity.xml 文件,Android Studio 将在主编辑器中显示其内容。或者,您也可以将其下载到您的电脑上。使用 MainActivity.xml 上的上下文相关菜单(右键单击),如图 17-3 所示,然后“另存为”然后,您可以用程序编辑器打开 XML 文件。

img/463887_1_En_17_Fig3_HTML.jpg

图 17-3

将 XML 文件保存到计算机

清单 17-4 显示了 MainActivity.xml 首选项文件的内容。

<?xml version='1.0' encoding='utf-8' standalone="yes" ?>
<map>
    <string name="lastname">hagos</string>
    <string name="firstname">ted</string>
</map>

Listing 17-4Contents of MainActivity.xml

请记住,我们在这里创建的首选项文件只能由 MainActivity 类访问。如果您需要与应用中的其他活动共享首选项文件,您需要创建一个应用级别的首选项。

在活动之间共享数据

要使一个首选项文件对 app 中的所有活动都可用,我们只需要在代码中做一点小小的修改。

| -什么 | **packageName** 实际上是对 **getPackageName()** 的调用。我们只是在这行中构造一个文件名。 | | ➋ | **这是我们唯一需要做的改变**;不要调用 **getPreferences** ,让我们使用 **getSharedPreferences** 。这个函数接受两个参数。你已经知道了第二个,很容易猜到第一个参数是干什么的。第一个参数指定首选项文件的文件名。 |
val filename = "$packageName TESTFILE"val pref = getSharedPreferences(filename, Context.MODE_PRIVATE)   ➋
val editor = pref.edit()

editor.putString("lastname", "Breslav")
editor.putString("firstname", "Andrey")
editor.apply()

Listing 17-5How to Create an Application Level Preferences File

实际上, getPreferences (我们上一节的例子)只是对 getSharedPreferences 的包装调用,前者只是将当前活动的名称作为第一个参数传递给后者。

要从共享首选项文件中检索数据,再次使用 getSharedPreferences ,指定要读取哪个文件,然后使用 getString 方法,如清单 17-6 所示。

| -什么 | 获取 SharedPreferences 对象。通过将首选项文件作为第一个参数传递来指定其名称。 | | ➋ | 第一个参数是关键;它是要检索的首选项的名称。第二个参数是默认值,以防键不存在。 |
val pref = getSharedPreferences("$packageName TESTFILE", Context.MODE_PRIVATE)  ➊

val mlastname = pref.getString("lastname", "")  ➋
val mfirstname = pref.getString("firstname", "")

Listing 17-6How to Read From an Application Preferences File

让我们为此做另一个小演示项目。表 17-2 显示了项目详情。

表 17-2

项目详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | CH17SharedPreferences(共享首选项) | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

我们想做的是:

  1. 让用户输入他的姓和名;为此,我们将使用两个版本。

  2. 当用户点击“Go to 2 nd Activity”按钮时,我们将创建一个启动“SecondActivity”的意图

  3. 在 MainActivity 进入暂停状态之前,我们将把姓氏和名字数据保存到指定的首选项文件中。

  4. 当 SecondActivity 进入用户视图时,我们将在 TextView 中显示“Click LOAD DATA”提示。

  5. 当单击“加载数据”按钮时,我们将检索首选项文件,并将其显示为 TextView 的文本属性。

图 17-4 向我们展示了我们应用的基本故事板。

img/463887_1_En_17_Fig4_HTML.png

图 17-4

我们的项目快照,正在运行

清单 17-7 和 17-8 显示了 activity_main.xml 和 activity_second.xml 的完整代码,因此您可以看到视图对象的属性。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:app=http://schemas.android.com/apk/res-auto
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".SecondActivity"
  tools:layout_editor_absoluteY="81dp">

  <Button
    android:id="@+id/btnloaddata"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="34dp"
    android:layout_marginTop="33dp"
    android:text="Load data "
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtoutput" />

  <TextView
    android:id="@+id/txtoutput"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="34dp"
    android:layout_marginTop="87dp"
    android:text="TextView"
    android:textSize="30sp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 17-8/app/res/activity_second.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:app=http://schemas.android.com/apk/res-auto
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity"
  tools:layout_editor_absoluteY="81dp">

  <EditText
    android:id="@+id/txtlastname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="40dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <EditText
    android:id="@+id/txtfirstname"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="15dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtlastname" />

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="24dp"
    android:layout_marginTop="57dp"
    android:text="Go to 2nd Activity"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtfirstname" />

</android.support.constraint.ConstraintLayout>

Listing 17-7/app/res/layout/activity_main.xml

这就解决了布局问题。在 MainActivity 中,我们需要做的事情如下:

  1. 接受来自用户的一些输入,姓氏和名字。

  2. 单击按钮时,使用明确的意图启动 SecondActivity。

  3. 在 MainActivity 进入“暂停”状态之前,让我们保存首选项文件。

清单 17-9 显示了 MainActivity 的完整和带注释的代码。

| -什么 | 我们正在创建一个将启动 SecondActivity 的明确意图。我们不会在这里保存首选项文件——我们将在稍后的 **onPause** 回调中保存。 | | ➋ | 让我们从这里调用 **saveData** 函数。onPause 函数会在 MainActivity 从用户视野中消失之前被 Android 运行时调用,最终进入“暂停”状态。 | | ➌ | **saveData** 函数是我们实际保存首选项文件的地方。这些代码你都见过了,我们就不再注释了。 | | -你好 | 一条简单的消息告诉用户我们已经保存了数据 | | ➎ | Android 运行时将在 MainActivity 再次对用户完全可见之前调用 **onResume** 函数,如果它来自“暂停”状态。我认为最好在这里重新初始化所有的 UI 元素。 |
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
      val intent = Intent(this@MainActivity, SecondActivity::class.java) ➊
      startActivity(intent)
    }
  }

  override fun onPause() {
    super.onPause()
    saveData()     ➋
  }

  private fun saveData() {   ➌

    val filename = "$packageName TESTFILE"
    val pref = getSharedPreferences(filename, Context.MODE_PRIVATE)
    val edit = pref.edit()

    edit.putString("lastname", txtlastname.text.toString())
    edit.putString("firstname", txtfirstname.text.toString())
    edit.apply()

    Toast.makeText(this, "Saved data", Toast.LENGTH_LONG).show() ➍
  }

  override fun onResume() { ➎
    super.onResume()

    txtfirstname.setText("")
    txtlastname.setText("")
    txtfirstname.setHint("first name")
    txtlastname.setHint("last name")
  }

}

Listing 17-9MainActivity, Annotated

这就是我们在主活动中需要做的一切。在 SecondActivity 中,我们需要在单击按钮时读取指定的首选项文件。清单 17-10 显示了 SecondActivity 的完整和带注释的代码。

| -什么 | 当按钮被单击时,让我们读取指定的首选项文件。 | | ➋ | 让我们提取姓氏(以及名字)。 | | ➌ | 连接姓氏和名字数据,并将其显示为 TextView 的文本属性。 |
import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*

class SecondActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    btnloaddata.setOnClickListener {
    val filename = "$packageName TESTFILE"
    val pref = getSharedPreferences(filename, Context.MODE_PRIVATE) ➊

      val mlastname = pref.getString("lastname", "") ➋
      val mfirstname = pref.getString("firstname", "")

      txtoutput.text = "$mfirstname $mlastname " ➌
    }
  }

  override fun onResume() {
    super.onResume()

    txtoutput.text = "Click 'LOAD DATA'"
  }
}

Listing 17-10
SecondActivity

这应该可以让你开始使用 SharedPreferences。在我们结束这一章之前,我想留给你一些关于共享优先权的信息。编辑对象。你已经知道是提交应用函数负责实际保存文件。它还具有其他功能,如清除和删除。他们是这样做的:

  • 删除(字符串参数) **。**这个调用删除一个命名的偏好。String 参数代表键。因此,像remove("lastname")这样的调用将从首选项文件中删除 lastname 键。

  • 清() **。**删除首选项文件中的所有键。

我将让您来试验这两个编辑器功能。

章节总结

  • Android 有几种持久化数据的方法。它的范围从简单的机制 SharedPreferences 一直到健壮的和一些更复杂的 ContentProviders 和 HTTP 数据库,比如 FireBase。

  • SharedPreferences 使用类似字典或地图的习惯用法。它以键值对的形式存储数据。

  • 您可以将偏好设置文件设为某个活动的专用文件,也可以将其提供给应用中的所有活动。

在下一章,我们将会看到另一种将数据保存到文件中的方法;但是,它不会局限于基本类型。您将学习如何在没有强制结构(如键值对)的情况下使用文件。

十八、内部存储

我们将介绍的内容:

  • Android 的文件 I/O 介绍

  • 内部存储与外部存储

  • 如何使用内部存储

在前一章中,我们学习了如何使用首选项文件。SharedPreferences 使用类似字典的结构,可以用键值格式保存数据行;但是你只能在里面保存基本类型。当您需要处理不限于键值对和基本类型的文件结构时,您可以使用 Java I/O(输入/输出)中良好的 ole 文件类。这就是本章的主题。

文件存储概述

当您需要处理视频、音频、json 或纯文本文件时,您可以使用 Java 文件 I/O 来处理本地文件。您将使用相同的文件、 **InputStream、**和 OutputWriter 以及 Java 中的其他 I/O 类——如果您以前使用过它们的话。Android 的不同之处在于你保存它们的位置。在 Java 桌面应用中,你可以把你的文件放在任何你想放的地方。安卓就不是这样了。就像在 Java web 应用中一样,Android 应用不能在任何地方自由地创建和读取文件。在某些地方,您的应用具有读写权限。

如果你以前没有使用过 Java I/O 也不用担心,我们不会使用任何难以遵循的代码。我们将使用的所有 I/O 例程都在初学者的能力范围之内。

内部和外部存储

Android 区分内部和外部存储。内部存储指的是闪存驱动器中由所有已安装的应用共享的部分。外部存储指的是用户可以安装的存储空间——通常是 sd 卡,但不是必须的。只要它能被用户安装,它可以是任何东西;它甚至可以是内部闪存驱动器的一部分。

每个选项都有利弊,所以你需要考虑你的应用的需求和每个选项的限制。下面的列表显示了一些利弊。

内部存储

  • 内存始终可供您的应用使用。不存在用户卸载 sd 卡或任何设备的危险。它保证会一直在那里。

  • 存储空间的大小将小于外部存储,因为您的应用将只分配到一部分闪存存储空间,该存储空间由所有其他应用共享。在早期版本的 Android 中,这是一个问题,但现在不那么担心了。根据 Android 兼容性定义,从 Android 6.0 开始,Android 手机或平板电脑必须为用户空间保留至少 1.5 GB 的非易失性空间(/data 分区)。对于大多数应用来说,这个空间应该足够了。你可以在这里阅读兼容性定义 https://bit.ly/android6compatibilitydefinition

  • 当你的应用在此空间中创建文件时,只有你的应用可以访问这些文件。除非手机是 root 的,但大多数用户不会 root 他们的手机,所以一般来说,这不是什么大问题。

  • 卸载你的应用时,它创建的所有文件都将被删除。

外部存储器

  • 它通常比内部存储空间更大;但是

  • 它可能并不总是可用(例如,当用户移除 SD 卡或将其安装为 USB 驱动器时。

  • 这里的所有文件对所有应用和用户都是可见的。任何人和任何应用都可以在这里创建和保存文件。他们还可以删除文件。

  • 当一个 app 在这个空间创建了一个文件,它可以比 app 活得更久;我的意思是,当你卸载应用时,文件不会被删除。

缓存目录

无论您选择内部存储还是外部存储,您可能仍然需要在文件位置上再做一个决定。你可以把你的文件放在一个缓存目录或更永久的地方。如果需要空间,缓存目录中的文件可能会被 Android 操作系统或第三方应用回收。所有不在缓存目录中的文件都非常安全,除非您手动删除它们。在本章中,我们不会使用缓存目录或外部存储。我们将只使用内部存储,并将文件放在标准位置。

如何使用内部存储

如前所述,在 Android 中使用文件存储就像在 Java I/O 中使用普通的类一样。有几个选项可以使用,如 openFileInput()openFileOutput() 或其他一些可以使用 InputStreamsOutputStreams 的方法。您只需要记住,这些调用不会让您指定文件路径。你可以只提供文件名,如果你不介意的话,继续使用它们——实际上这就是我们将在本章中使用的。另一方面,如果您需要更大的灵活性,您可以使用getfiledir()getCacheDir() 来获取一个指向文件位置根目录的 File 对象——如果您想要使用内部存储的缓存目录,请使用 getCacheDir() 。当你有一个文件对象时,你可以从那里创建你自己的目录和文件结构。

这是 Android 文件存储领域的一般情况。同样,在本章中,我们将只使用标准位置的内部存储(不是缓存)。

写入文件需要几个简单的步骤。您需要:

  1. 决定文件名

  2. 获取文件输出流对象

  3. 将内容转换为 ByteArray

  4. 使用 FileOutputStream 写入 ByteArray

  5. 不要忘记关闭文件

清单 18-1 向我们展示了它在代码中的样子。

| -什么 | **openFileOutput** 返回一个 FileOutputStream 我们需要这个对象,这样我们就可以写入文件。调用的第一个参数是您想要创建的文件的名称。第二个参数是上下文模式;你已经从上一章知道了这一点。我们使用 MODE_PRIVATE 是因为我们希望文件对应用是私有的。 | | ➋ | **使用**扩展名意味着我不必显式或手动关闭文件。一旦我们使用完它,Android 运行时会为我们关闭它。考虑到许多开发人员忘记关闭文件,这是非常方便的。在应用终止前保持文件句柄打开会导致内存泄漏。 **use** 扩展相当于 Java 的 **try-with-resources。** | | ➌ | **write** 方法需要一个 ByteArray。因此,我们需要将 Editable(EditText 的数据类型)转换为 String,然后将其转换为 ByteArray。 |
val filename = "ourfile.txt"
val out = openFileOutput(filename, Context.MODE_PRIVATE) ➊
out.use { ➋
  out.write(txtinput.text.toString().toByteArray()) ➌
}

Listing 18-1How to Save to a File

从文件中读取比写入文件涉及更多的步骤。您通常需要执行以下操作:

  1. 获取文件输入流

  2. 从流中读取,一次一个字节

  3. 坚持读下去,直到没什么可读的了。当你到达文件末尾时,你会知道你所读取的最后一个字节的值是否为 -1 。到时候就该收手了。

  4. 当您到达文件末尾时,您需要将从流中获取的字节存储到一个临时容器中。StringBuilder 或 StringBuffer 应该可以做到这一点。因为字符串是不可变的,所以使用加号运算符构建字符串对象既浪费又低效。每次使用加号运算符时,它都会创建一个新的 String 对象;如果您的文件包含 2000 个字符,您将创建 2000 个 String 对象。

    如果你正在阅读一个文本文件,情况就是这样。如果您正在读取音频或视频文件等其他内容,您将使用不同的数据结构。

  5. 当你到达文件的末尾时,停止阅读。用你读过的东西做你需要做的事情,不要忘记关闭文件。

清单 18-2 向我们展示了这在代码中的样子。

| -什么 | **openFileInput** 返回一个 FileInputStream 这是我们需要的对象,所以可以从文件中读取。它唯一需要的参数是要读取的文件的名称。 | | ➋ | 我们不可能一下子读完整个文件。我们会一段一段地读。当我们得到一些块时,我们将把它们存储在 StringBuilder 对象中。 | | ➌ | **read** 方法从输入流中读取一个字节的数据,并将其作为整数返回。我们需要一次从流中读取一个字节,直到到达文件结尾(EOF)标记。 | | -你好 | 当流中没有更多的字节要读取时,EOF 被标记为 **-1** 。我们将用它作为 **while** 循环的条件。直到 **bytes_read** 不等于 **-1** 为止,继续读取。 | | ➎ | **read** 方法返回一个 int 它是文件中每个字母的 ASCII 值,作为整数返回。在将它放入 StringBuilder 之前,我们必须将其转换成一个**字符**。 | | ➏ | 如果我们还没到 EOF,让我们读另一个字节。 | | -好的 | 当我们用完要读取的字节时,我们将退出循环并打印 StringBuilder 的内容。 |
val filename = "ourfile.txt"
val input = openFileInput(filename) ➊

input.use {
  var buffer = StringBuilder() ➋
  var bytes_read = input.read() ➌
  while(bytes_read != -1) { ➍
    buffer.append(bytes_read.toChar()) ➎
    bytes_read = input.read() ➏
  }
  println(buffer.toString()) ➐
}

Listing 18-2How to Read From a File

当然,我们会做一些小的演示项目。它巩固了我们的学习。表 18-1 显示了演示项目的详细信息。

表 18-1

项目详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | ch18 内部存储 | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

我们想做的是:

  1. 我们将设置两个活动:MainActivity 和 SecondActivity。

  2. 在 MainActivity 中,用户可以在多行编辑文本中自由键入文本。

  3. 当点击按钮“2 Activity”时,我们将启动一个打开 SecondActivity 的明确意图。

  4. 但是在我们离开 MainActivity 之前,我们将创建一个文件并将 EditText 的内容保存到该文件中。这个调用并不十分昂贵,但是我们将在后台线程中运行这个代码,因为它是一个 I/O 调用。你永远无法确定一个 I/O 调用会多于还是少于 16 ms,所以还是小心为妙。

  5. 在 SecondActivity 中,一旦它对用户可见,我们将读取文件的内容(我们刚刚保存在 MainActivity 中的那个)并使用多行 TextEdit 显示给用户。

  6. 仍然在 SecondActivity 中,当用户单击按钮“1 st Activity”时,我们将启动一个返回主 Activity 的明确意图。

  7. 在 MainActivity 的 onResume 中,我们将尝试读取文件并使其可用于编辑。

图 18-1 显示了我们 app 的两个屏幕。

img/463887_1_En_18_Fig1_HTML.png

图 18-1

该应用在运行时

清单 18-3 和 18-4 显示了 activity_main.xml 的完整代码(MainActivity 的 UI。Kt)和 activity _ second . XML(second activity 的 UI。Kt)。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:app=http://schemas.android.com/apk/res-auto
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <EditText
    android:id="@+id/txtinput"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="34dp"
    android:ems="10"
    android:inputType="textMultiLine"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <Button
    android:id="@+id/btnsecondactivity"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="16dp"
    android:layout_marginTop="8dp"
    android:text="2nd Activity"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/txtinput"
    app:layout_constraintVertical_bias="0.963"
    tools:layout_editor_absoluteX="16dp" />
</android.support.constraint.ConstraintLayout>

Listing 18-3/app/res/layout/activity_main.xml

接下来是 activity_second 的 xml 定义。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:app=http://schemas.android.com/apk/res-auto
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".SecondActivity">

  <Button
    android:id="@+id/btnmainactivity"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="18dp"
    android:layout_marginStart="16dp"
    android:text="1st activity"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

  <TextView
    android:id="@+id/txtoutput"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="29dp"
    android:inputType="textMultiLine"
    android:text="TextView"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Listing 18-4/app/res/layout/activity_second.xml

主要活动有几个方面。在我们看完整的代码之前,让我们先看看它的重要部分。

当 MainActivity 打开时,我们运行一些代码来检查“ourfile.txt”(文件的名称)是否已经存在。如果是,我们将读取它并在 EditText 中显示内容,这样用户就可以编辑它。这段代码位于 onResume() 回调函数中,这是放置代码的好地方,因为一旦用户看到活动,运行时就会调用它。

清单 18-5 显示了 onResume 回调和 loadData 函数。我只注释了三点——从哪里调用 loadData 以及与文件输入/输出相关的所有代码的开始/结束行。你已经熟悉了其余的代码,因为它们已经在本章的前面和/或前面的章节中解释过了。

代码很简单,但是对于初学者来说可能在结构上有挑战性。所以,还是一步一步来吧。

| -什么 | 一旦用户看到活动,我们就调用**loadData()**;这发生在 **onResume** 回调中。 | | ➋ | I/O 代码的开始 | | ➌ | I/O 代码结束。其余的是锅炉板穿线和例外。 |
val Log = Logger.getLogger(MainActivity::class.java.name)

override fun onResume() {
  super.onResume()
  loadData()                                    ➊
}

private fun loadData() {

  val filename = "ourfile.txt"
  Thread(Runnable{
    try {
      val input = openFileInput(filename)       ➋
      input.use {
        var buffer = StringBuilder()
        var bytes_read = input.read()

        while(bytes_read != -1) {
          buffer.append(bytes_read.toChar())
          bytes_read = input.read()
        }
        runOnUiThread(Runnable{
          txtinput.setText(buffer.toString())
        })
      }                                         ➌
    }

    catch(fnfe:FileNotFoundException) {
      Log.warning("file not found, occurs only once")
    }
    catch(ioe: IOException) {
      Log.warning("IOException : $ioe")
    }
  }).start()
}

Listing 18-5
loadData Function

重点关注清单 18-5 中➋点和➌点之间的代码。它们是唯一对读取文件重要的文件。线程Runnablerunnonuithread、 **try、**catch 都是内务代码。他们在那里是因为我们试图进行防御性编码。我们在后台运行,因为 I/O 代码可能需要一些时间来完成。我们使用 try-catch 块是因为 I/O 代码可能会抛出异常。我们使用了 runOnUiThread ,因为当我们在后台线程中时,我们不能向 UI 写任何东西。这些就是结构杂技的原因。

清单 18-6 再次显示了 loadData 函数,但是这次没有 I/O 代码。你只能看到管家代码。

| -什么 | 在这里运行你的后台代码。我们所有的文件输入/输出代码都在这里。 | | ➋ | 这是你写代码的地方,这样**可以抛出异常**。Java I/O 调用会抛出异常——这就是为什么我们需要把它们放在这里。 | | ➌ | 如果你需要更新 UI,你必须回到 UI 线程。在后台线程中,您不能更改用户界面。 | | -你好 | 如果确实发生了异常,请在这里做您需要做的任何事情,以便应用可以恢复。至少,在这里记录一些东西,这样当您以后查看日志时就可以看到发生了什么错误。显式处理异常(像这样)的好处是,如果在运行时遇到不利的事情,应用不会崩溃。这样,你就有机会优雅地恢复。 | | ➎ | **start** 方法将螺纹踢入高速档。它得到了线程,嗯,开始。 |
Thread(Runnable {
  ...                         ➊

  try {
    ...                       ➋

    runOnUiThread(Runnable {
      ...                     ➌
    })
  }
  catch(ioe:IOException) {
    ...                       ➍
  }

}).start()                    ➎

Listing 18-6loadData Without the I/O Codes

从➊到➎包含了所有在后台线程中运行的东西。这个过度扩展语句的基本结构是这样的:所有的 I/O 代码和 try-catch 块都被写在省略号的位置。

接下来,当应用对用户完全可见时,它会等待输入。用户可以在多行编辑文本中添加文本。如果用户点击“2 Activity”按钮,我们将启动带有明确意图的 SecondActivity。MainActivity 从“运行中”转换到“暂停”状态,但在此之前,运行时将调用 MainActivity 的 onPause 方法。我们将在这里编写代码,将数据保存到文件中。清单 18-7 显示了带注释的 saveData 函数。

| -什么 | 我们将在后台线程中运行,因为这是一个 I/O 调用。 | | ➋ | 让我们打开一个文件进行输入。这给了我们一个**文件输入流**。将文件名作为第一个参数传递,将上下文模式作为第二个参数传递。 | | ➌ | 现在我们可以写入文件了。请记住,您只能在 FileInputStream 对象中写入字节数组,因此您必须将 EditText 的运行时值转换为 ByteArray。 | | -你好 | 现在我们必须回到 UI 线程,即使我们只显示一条祝酒词。 |
private fun saveData() {
  val filename = "ourfile.txt"
  Thread(Runnable {                                            ➊
    try {
      val out = openFileOutput(filename, Context.MODE_PRIVATE) ➋
      out.use {
        out.write(txtinput.text.toString().toByteArray())      ➌
      }
      runOnUiThread(Runnable {                                 ➍
        Toast.makeText(this, “Saved", Toast.LENGTH_LONG).show()
      })
    }
    catch(ioe:IOException) {
      Log.warning("Error while saving ${filename} : ${ioe}")
    }
  }).start()
}

override fun onPause() {
  super.onPause()
  saveData()
}

Listing 18-7annotated saveData function

希望这能澄清主要活动的结构。SecondActivity 要简单得多,但它也遵循与 MainActivity 相同的结构流程。清单 18-8 和 18-9 分别显示了 MainActivity 和 SecondActivity 的完整和带注释的代码。

| -什么 | 当单击按钮时,我们将简单地启动一个显式的意图来打开 SecondActivity。这里我们不做任何 I/O 代码。 | | ➋ | 函数 **saveData** 包含将 EditText 的运行时内容写入文件的所有 I/O 代码。 | | ➌ | 在 MainActivity 进入“暂停”状态并从用户视野中消失之前,运行时会调用**on pause;**这就是我们称之为 **saveData 的地方。** | | -你好 | 当 MainActivity 第一次出现在用户的视图中时,运行时调用 **onResume** 。这里我们将调用 **loadData** 函数。它将读取文件并在 TextView 对象中显示其内容。 |
import android.content.Context
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import java.io.FileNotFoundException
import java.io.IOException
import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  val Log = Logger.getLogger(MainActivity::class.java.name)
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    btnsecondactivity.setOnClickListener {
      startActivity(Intent(this, SecondActivity::class.java))     ➊
    }
  }

  private fun saveData() {                                        ➋
    val filename = "ourfile.txt"
    Thread(Runnable {
      try {
        val out = openFileOutput(filename, Context.MODE_PRIVATE)
        out.use {
          out.write(txtinput.text.toString().toByteArray())
        }
        runOnUiThread(Runnable {
          Toast.makeText(this, “Saved", Toast.LENGTH_LONG).show()
        })
      }
      catch(ioe:IOException) {
        Log.warning("Error while saving ${filename} : ${ioe}")
      }
    }).start()
  }

  override fun onPause() {                                        ➌
    super.onPause()
    saveData()
  }

  override fun onResume() {                                       ➍
    super.onResume()
    loadData()
  }

  private fun loadData() {

    val filename = "ourfile.txt"
    Thread(Runnable{
      try {
        val input = openFileInput(filename)
        input.use {
          var buffer = StringBuilder()
          var bytes_read = input.read()

          while(bytes_read != -1) {
            buffer.append(bytes_read.toChar())
            bytes_read = input.read()
          }
          runOnUiThread(Runnable{
            txtinput.setText(buffer.toString())
          })
        }
      }
      catch(fnfe:FileNotFoundException) {
        Log.warning("file not found, occurs only once")
      }
      catch(ioe: IOException) {
        Log.warning("IOException : $ioe")
      }
    }).start()
  }
}

Listing 18-8MainActivity, Annotated

让我们继续第二项活动。

| -什么 | 当 SecondActivity 出现在用户视图中时,我们将调用 **loadData。** | | ➋ | 你以前见过这个代码。这与 MainActivity 中的代码相同。它读取文件并使用 TextView 对象显示其内容。我想我们可以重构代码,在某个地方抽象这个函数,这样我们就可以遵循 DRY(不要重复自己)原则,但这意味着我们需要解释更多的代码和概念。为了可读性,我在这里违反了 DRY 原则。请记住,不要在生产代码中这样做。 | | ➌ | 当点击“第一活动”按钮时,我们返回到主活动。我们也可以在这里调用 **finish()** ,但是我不想完全破坏 SecondActivity,所以我使用了一个显式的意图来返回 MainActivity。 |
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*

class SecondActivity : AppCompatActivity() {

  override fun onResume() {                   ➊
    super.onResume()
    loadData()
  }

  private fun loadData() {                    ➋
    val filename = "ourfile.txt"
    Thread(Runnable {
      val input = openFileInput(filename)
      input.use {
        var buffer = StringBuilder()
        var bytes_read = input.read()
        while(bytes_read != -1) {
          buffer.append(bytes_read.toChar())
          bytes_read = input.read()
        }
        runOnUiThread(Runnable{
          txtoutput.setText(buffer.toString())
        })
      }

    }).start()
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    btnmainactivity.setOnClickListener {          ➌
      startActivity(Intent(this, MainActivity::class.java))
    }
  }
}

Listing 18-9SecondActivity, Annotated

这就结束了这一章。这一章我已经说过几次了,但还是值得重复一遍。I/O 代码并不是最难的——是锅炉板代码让程序看起来比实际更复杂。但是你不能逃避,你需要线程和异常处理代码来观察你的代码中良好的内务处理。

章节总结

  • 当您对存储的需求超出了简单的键值对结构和基本数据类型时,请使用 Java I/O 类。

  • 您可以将文件存储在始终可用但有限的内部存储器中,或者存储在更大但可能会被卸载的外部存储器中。

  • 即使您认为 I/O 调用将少于 16 ms,也要在后台线程中运行代码。你永远不知道在 I/O 调用中会发生什么。

  • Java I/O 调用抛出异常;适当地处理它们。

在下一章,我们将看看 Android 应用的另一个重要组成部分:广播接收器。他们实际上做了你认为他们做的事情——接收广播。我们将研究一些类型的广播,像往常一样,我们将做一个小的演示项目。

十九、广播接收器

我们将介绍的内容:

  • 广播接收器简介

  • 自定义和系统广播

  • 清单和上下文注册的接收器

Android 的应用模型在许多方面都是独一无二的,但让它脱颖而出的是它允许你使用不是你自己制作的其他应用的功能来创建一个应用——我不仅仅指库,我指的是完整的应用。你已经了解了意图——它们是什么,它们能做什么。我们已经了解了如何使用 Intents 来启动其他组件,我们甚至使用它在组件之间传递数据。

还有一种方法可以使用意图。我们可以用它向所有组件发送广播。广播是由 Android 运行时或其他应用(包括您自己的应用)发送的意图,以便每个应用或组件都能听到它。大多数应用会忽略广播,但你可以让你的应用听它。你可以收听这条消息,以便对广播做出回应。这就是本章的主题。

广播接收器简介

因此,我们可以启动发送(广播)到所有应用和组件的意图。但是这有什么好处呢?要回答这个问题,我们需要回忆一下,谈谈 Android 在互操作性和可插拔性方面的哲学。还记得在第十二章中,我们第一次谈到意图吗?我们看了图 19-1 中的图片。

img/463887_1_En_19_Fig1_HTML.png

图 19-1

用户如何与“通讯录”应用交互

用户不在乎使用哪个应用来发送电子邮件、短信或打电话。当用户点击电子邮件时,它会启动一个隐含的意图,即“嘿,我想发一封电子邮件。谁感兴趣?”设备中的每个应用都会听到这个消息,但只有那些收听的人才能做出回应。这就是 BroadcastReceivers 的全部理念——向所有人发布一条消息,如果一些应用订阅了这条消息,他们就可以做出回应。它使用一个发布-订阅模型。

系统广播与自定义广播

意向广播可以由操作系统(系统广播)或应用(自定义广播)发送。每当发生有趣的事情时(例如,当 WiFi 打开[或关闭]时,当电池电量下降到指定阈值时,插入耳机时,或设备切换到飞行模式时等),操作系统都会发送系统广播。).来自系统的广播动作的一些例子如下:

  • android.app.action.ACTION_PASSWORD_CHANGED

  • android.app.action.ACTION_PASSWORD_EXPIRING

  • android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED

  • android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED

  • android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED

  • android.intent.action.BATTERY_CHANGED

  • android.intent.action.BATTERY_LOW

  • android.intent.action.BATTERY_OKAY

文档中列出了大约 150 多种。你可以在 BROADCAST_ACTIONS 上找到它们。Android SDK 中的 TXT 文件。

另一方面,定制广播是你编造的。这些是您发送的意图,以通知您的应用的一些组件(或其他应用)发生了一些“有趣”的事情(例如,文件已完成下载或您已完成计算素数,等等)。).

清单注册与上下文注册

如果你想做一些事情作为对广播的响应,你需要监听它,为了做到这一点,你需要注册一个接收器。有两种注册方式:通过清单和通过上下文。

清单中注册的接收者看起来像清单 19-1 。

| -什么 | 就像活动一样,需要在清单中声明一个 **BroadcastReceiver** 。你必须在它自己的节点中声明它。像活动声明一样,它需要是**应用的子节点。** | | ➋ | ".MyReceiver”是 BroadcastReceiver 类的名称。因此,假设您的应用中有一个名为 MyReceiver 的类,它继承了 BroadcastReceiver。我们干脆把它写成”。MyReceiver,“就像它上面的活动一样”。主要活动”。完整的形式其实是**net . working dev . ch 19 broadcast receiver something . my receiver**,但是我们可以使用简写形式,因为**包**名称已经在前面声明了;看看清单的第二行,你会找到包裹的完整名称。任何需要在清单中声明的后续类都可以简单地使用缩写形式,如“.我的收件人“或”。主要活动”。 | | ➌ | **意向过滤器**是我们实际注册的方式。我们告诉操作系统我们对事件感兴趣**com . working dev . do something**。如果意向是以广播形式发送的,此应用会对其做出响应。 |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="net.workingdev.ch19broadcastreceiverdosomething">
<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">
  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
  <receiver                             ➊
    android:name=".MyReceiver"          ➋
    android:enabled="true"
    android:exported="true">
    <intent-filter>                     ➌
      <action android:name="com.workingdev.DOSOMETHING"/>
    </intent-filter>
  </receiver>

</application>

Listing 19-1BroadcastReceiver Declared in AndroidManifest.xml

通过清单注册的接收者不需要为了响应广播而当前正在运行。接收者在清单上注册的事实足以解析意图。

当接收者通过上下文对象以编程方式注册时,它看起来像清单 19-2 。

| -什么 | 这是我们前面看到的``节点的编程等价物。要创建一个 **IntentFilter** 对象,向其构造函数传递一个广播动作。广播操作是您想要订阅的事件。在这种情况下,我们希望在发出动作为**com . working dev . do something**的意向时得到通知;此意图是自定义广播的一个示例,而不是系统广播。 | | ➋ | 使用活动的 **registerReceiver** 方法注册接收者。该方法有两个参数:a.BroadcastReceiver 的实例,以及 b.IntentFilter 的实例 | | ➌ | 当您以编程方式注册接收方时,请确保您也注销了它。这就是我们在这里做的。它在一个 **try-catch** 结构中,因为它可以抛出一个异常。如果你试图注销一个还没有注册的接收者(或者一个已经注销的接收者),运行时将抛出 **IllegalArgumentException** 。我没有为注册部分这样做,因为 **registerReceiver** 不会抛出任何异常,即使你(意外地)不止一次注册了同一个接收者。一旦注册了一个接收者,运行时将忽略任何注册它的进一步尝试。 | | -你好 | 这是 BroadcastReceiver 类的基本定义。 |
val Log = Logger.getLogger(javaClass.name)

override fun onCreate(savedInstanceState: Bundle?) {

  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  val action_filter = IntentFilter("com.workingdev.DOSOMETHING") ➊
  val receiver = MyReceiver()

  btnregister.setOnClickListener {
    registerReceiver(receiver, action_filter)  ➋
  }

  btnunregister.setOnClickListener {
    try {
      unregisterReceiver(receiver)            ➌
    }

    catch(iae:IllegalArgumentException) {
      Log.warning("IllegalArgument\n ${iae}")
    }
    catch(e:Exception) {
      Log.warning("IllegalArgument\n ${e}")
    }
  }
}

inner class MyReceiver : BroadcastReceiver() {                     ➍
  override fun onReceive(context: Context?, intent: Intent?) {
    println("got it");
    Toast.makeText(this@MainActivity, "Got it", Toast.LENGTH_LONG).show()
  }
}

Listing 19-2How to Register and Unregister a BroadcastReceiver

以编程方式注册的接收器只能在应用(用于注册接收器)仍在运行时响应广播。

广播接收器基础

创建广播接收器时,需要遵循几个步骤。它们是:

  1. 决定您想收听哪个 的广播节目。你想听系统广播还是自定义广播?如果您希望促进应用组件之间的一些消息传递,通常会使用自定义广播。使用 BroadcastReceiver 的一个用例是,当您使用 DownloadManager 系统服务下载大文件时,该服务会在下载完成时发出广播—您可能希望听到广播,以便在下载后立即采取行动。

  2. 决定如何注册接收者,通过上下文还是通过清单?您可以通过任何一种方式(清单或上下文)收听自定义广播,但是有些广播操作是受限制的,您不能通过清单注册来收听它们。我们将很快讨论这个问题。

  3. 创建一个继承自 BroadcastReceiver 类的类。

  4. 覆盖并实现新类的 onReceive 方法。当一个广播被发送时,意图过滤器与动作匹配,操作系统将意图解析到你的应用,最终特定的 BroadcastReceiver 类,运行时调用 onReceive 方法。 onReceive 方法是 BroadcastReceiver 类的核心。无论你想在播配的时候做什么,这都是你需要写的地方。

通常,如果您通过 Android 清单或上下文对象注册了 BroadcastReceiver,就可以收听广播。让我们继续一点。前面,我使用了术语“通过上下文注册”和“以编程方式注册”——它们是同一个,意思相同。“通过上下文注册”是指在上下文对象上调用 registerReceiver 方法。所以声明

registerReceiver(receiver, intent_filter)

与语句相同

this.registerReceiver(receiver, intent_filter)

它们都是在当前活动的上下文中调用的——Activity 类实际上继承自 Context 对象,Service 类也是如此。因此,您可以从活动或服务内部调用 registerReceiver 方法。如果你在一个不从上下文继承的类中,你仍然可以通过获取应用的上下文来注册一个接收者。代码看起来像这样:

getApplicationContext().registerReceiver(receiver, intent_filter)   // or
applicationContext.registerReceiver(receiver, intent_filter)

回到清单与上下文注册,有些广播动作不能在清单中注册;但是你可以通过上下文注册它们。一个例子是android.intent.action. TIME_TICK,这是一个受保护的意图,只能由系统发送。它每 60 秒发送一次,你只有通过上下文注册才能收听。

在 Android 的早期版本中,已经有一些广播被限制在清单之外。写这篇文章的时候,Android 9(或者 API 级)出来了。在本书中,我们一直使用 API 级别 23 作为目标,但是您将从阅读每个 Android 版本的行为变化文档中受益。我在下面列出了一些 Android 官方文档的链接。这些文件以这样或那样的方式影响着广播受众。

  • Android 9 (API 28)行为改变http://bit.ly/behaviorchanges9 。谈谈如果我们想瞄准 Android 9,开发者应该知道的 API 的所有变化。这位医生对广播接收器有话要说。

  • 背景执行限制。bit.ly/bgexeclimit。这是关于你的应用在后台运行时能做什么和不能做什么。不要以为因为不在 UI 线程里,就可以到处跑,想干嘛干嘛。这份文件谈到了这些限制;它还谈到了对广播接收器的限制。

  • broadcast receiverexceptions。bit.ly/broadcastexceptions。从 Android 8 开始(继续到 9),除了一些例外,所有隐式广播动作现在都是清单的禁区。这份文件列举了那些被免除的行为。如果您想知道哪些隐式广播动作仍然可以通过清单注册,请阅读本文档。

隐式与显式广播动作

Android 区分了隐式和显式广播动作。它将显式广播定义为只针对一个应用的广播,而不管有多少其他应用在监听它。另一方面,任何注册的应用都可以听到明确的广播。为了我们的目的和使我们的生活更简单,文档告诉我们不要通过清单收听系统广播。从 Android 8 开始,所有隐式广播(除了那些在 http://bit.ly/broadcastexceptions 列出的)都不能被通过清单注册的接收器听到。但是如果您通过上下文注册,您仍然可以收听这些广播操作。

所有这些新限制的主要原因都与性能优化和节能有关。考虑一下:当设备的 WiFi 连接出现问题时,会发送 CONNECTIVITY_ACTION 广播。如果有十几个应用在监听这个广播,它们都会醒来并采取行动。每次 WiFi 掉线重新连上都会这样。请记住,清单注册的接收者不需要活着才能接收广播;事实上,当他们得到广播时,他们会活过来。这种行为会导致大量功耗。如果你的应用在不运行时不需要被告知 WiFi 连接,那么通过上下文来注册更负责任。

演示应用:自定义广播

让我们构建一个小项目,这样您可以自己尝试广播接收器。表 19-1 显示了该项目的详细情况。

表 19-1

项目详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | ch19 上下文记录 | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

想做什么:

  1. 创建一个响应隐式自定义广播的 BroadcastReceiver

  2. 一旦活动对用户可见,我们将立即注册接收者;和

  3. 我们将在活动进入“暂停”状态之前注销接收者。

  4. MainActivity 的 UI 只有一个按钮。当点击该按钮时,它将发送一个自定义的广播意图。

清单 19-3 显示了 UI 的极简代码。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:app=http://schemas.android.com/apk/res-auto
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="26dp"
    android:layout_marginTop="43dp"
    android:text="send broadcast"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 19-3/app/res/layout/activity_main.xml

我们需要添加一个继承自 BroadcastReceiver 的类。方法之一是从主菜单栏文件新建Kotlin 文件/类。或者,我们也可以使用项目工具窗口的 appjava 文件夹中的上下文菜单,如图 19-2 所示。从那里,你可以去其他广播接收器

img/463887_1_En_19_Fig2_HTML.jpg

图 19-2

新广播接收器

你需要填写类的名字。在本例中,我将该类命名为“MyReceiver”

我们不会在接收器中做任何特别的事情。我们将简单地在日志记录器中显示 toast 消息打印内容。清单 19-4 显示了 MyReceiver 的代码。

| -什么 | 当广播意图与接收器匹配时,操作系统调用广播接收器的 **onReceive** 方法。在这里,您应该为接收方编写应用的业务逻辑(例如,保存文件、根据 WiFi 条件路由程序逻辑等)。). |
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import java.util.logging.Logger

class MyReceiver : BroadcastReceiver() {

  val Log = Logger.getLogger(javaClass.name)

  override fun onReceive(context: Context, intent: Intent) {  ➊
    Toast.makeText(context, "Got it", Toast.LENGTH_LONG).show()
    Log.info("Got it")
  }
}

Listing 19-4
MyReceiver.java

在主活动中,我们将执行以下操作:

  1. 创建 MyReceiver 的实例。我们只需要做一次。这就是为什么我们将在 onCreate 回调中创建实例。

  2. 每当接收者对用户可见时注册它。我们将把这段代码放在 MainActivity 的 onResume 回调中。

  3. 当用户不再与 MainActivity 交互时,注销接收者。

  4. 当点击该按钮时,我们将发送一个自定义的广播意图。

清单 19-5 显示了 MainActivity 的完整和带注释的代码。

img/463887_1_En_19_Fig3_HTML.jpg

图 19-3

我们的应用,跑步

| -什么 | **receiver** 变量保存了 **MyReceiver** 类的实例(我们的 BroadcastReceiver)。我们将变量声明为属性,因为我们将在 **onResume** 和 **onPause** 方法中引用它。我们使用了 **lateinit** 关键字,因为我们现在还不会定义它。 | | ➋ | 让我们使用一个基本的日志对象。 | | ➌ | 现在我们在 **onCreate** 中,让我们定义 MyReceiver 对象。 | | -你好 | 当点击按钮时,我们想要创建一个广播意图,并将其*动作*设置为 DOSOMETHING。 | | ➎ | 启动意图。 | | ➏ | 我们正在进行 **onResume** 回调。每当 MainActivity 对用户可见时,操作系统都会调用这个方法。这是注册接收者的好地方。我们只想在使用应用时收到通知。 | | -好的 | 我们在**中,因为**,在 MainActivity 进入“暂停”状态并从用户的视野中消失之前,操作系统调用这个方法。这是注销接收者的好地方。我们不想在不使用应用时收到通知。 |
import android.content.Intent
import android.content.IntentFilter
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  lateinit var receiver:MyReceiver                      ➊
  val Log = Logger.getLogger(javaClass.name)            ➋

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    receiver = MyReceiver()                             ➌

    button.setOnClickListener {
      val intent = Intent("com.workingdev.DOSOMETHING") ➍
      sendBroadcast(intent)                             ➎
    }
  }

  override fun onResume() {                             ➏
    super.onResume()

    val filter = IntentFilter("com.workingdev.DOSOMETHING")
    registerReceiver(receiver, filter)
    Log.info("Registered receiver")
  }

  override fun onPause() {                              ➐
    super.onPause()

    try {
      unregisterReceiver(receiver)
      Log.info("Unregistered receiver")
    }
    catch(iae: IllegalArgumentException) {
      Log.warning(iae.toString())
    }
  }
}

Listing 19-5
MainActivity.java

另一种发送广播意图的方式是通过 Android 调试桥或 adb ,简称。它是一个命令行工具,允许您与设备(物理设备或仿真设备)进行通信。adb 可以做很多事情,比如安装/卸载 apk、显示日志、在设备上运行 Linux 命令、模拟电话呼叫等等。出于我们的目的,我们将使用发送一个广播意图。

adb 在 Android SDK 的平台工具文件夹里。打开命令行窗口,切换到 Android SDK 的目录。如果你忘记了它在哪里,去 Android Studio 的设置 (Windows 和 Linux)或者首选项 (macOS)。对于 Windows 和 Linux,您可以通过按下按键 CTRL + ALT + S ,或者对于 macOS,您可以按下按键 Command +、(逗号)。

在那里,进入外观和行为➤系统设置➤安卓 SDK,如图 19-4 所示。Android SDK 的位置就在那里。

img/463887_1_En_19_Fig4_HTML.jpg

图 19-4

首选项,Android SDK

回到命令行窗口,切换到 Android SDK 文件夹。从那里,切换到平台工具文件夹,然后运行以下命令:

adb shell am broadcast -a com.workingdev.DOSOMETHING

如果您在 macOS 或 Linux 上,您可能需要在命令前面加上点号和正斜杠,就像这样:

./adb shell am broadcast -a com.workingdev.DOSOMETHING

演示应用:系统广播

下一个项目将与上一个项目相似,但我们将收听系统广播。我们将监听系统每 60 秒发出的 ACTION_TIME_TICK。这是一个受保护的意图,所以我们必须在运行时注册接收者。表 19-2 显示了该项目的详细情况。

表 19-2

系统广播的项目详情

|

项目明细

|

| | --- | --- | | 应用名称 | ch19 系统广播 | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

这个应用非常简单。没有要设置的 UI 元素。这是我们想要做的:

  1. 创建一个 BroadcastReceiver 来侦听 ACTION_TIME_TICK 意图。我们将把它实现为一个内部类——这样做的唯一原因是使代码的表示更加简洁。如果您愿意,您完全可以将 receiver 类实现为独立的类。

  2. 我们希望只有当用户与应用交互时才能收听广播。所以我们将在 MainActivity 的 onResume 回调中注册接收者;我们将在中注销它,因为回调。

  3. 每当收到 ACTION_TIME_TICK 时,我们将简单地使用 Toast 对象向控制台和用户屏幕输出一条消息。

| -什么 | 创建 BroadcastReceiver 的实例 | | ➋ | 创建用于监听 ACTION_TIME_TICK 广播的 **intentfilter** | | ➌ | 在 Resume 上注册**内的接收器;当用户看到应用时,运行时会调用此方法。** | | -你好 | 在应用进入“暂停”状态之前注销接收器。通过这种方式,只要我们的应用在用户的视野内,接收器就只收听广播。当应用不再出现在用户的视图中时,我们不想收到任何广播通知。 | | ➎ | 这是 BroadcastReceiver 的类定义。它是作为内部类实现的,同样有效。这对我们有用,因为我们没有试图在 **onReceive** 回调中做任何实质性的事情。如果程序逻辑过于复杂,BroadcastReceiver 最好在 MainActivity 之外实现。 |
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast

import java.util.logging.Logger

class MainActivity : AppCompatActivity() {

  lateinit var intentfilter:IntentFilter
  lateinit var timereceiver:TimeReceiver
  var current_count = 0

  val Log = Logger.getLogger(javaClass.name)

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    timereceiver = TimeReceiver()                         ➊
    intentfilter = IntentFilter(Intent.ACTION_TIME_TICK)  ➋
  }

  override fun onResume() {
    super.onResume()
    Log.info("App is resuming")
    registerReceiver(timereceiver,intentfilter)           ➌
  }

  override fun onPause() {
    super.onPause()
    Log.info("App is paused")
    try {
      unregisterReceiver(timereceiver)                    ➍
    }
    catch(iae:IllegalArgumentException) {
      Log.warning(iae.toString())
    }
  }

  inner class TimeReceiver : BroadcastReceiver() {        ➎
    override fun onReceive(context: Context?, intent: Intent?) {
      current_count += 1
      var message = "Counter:${current_count}"
      Log.info(message)
      Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
    }
  }
}

Listing 19-6
MainActivity

其他说明

BroadcastReceivers 和 Intents 在让解耦的组件互相对话方面做得很有效。如果你想方便应用之间的交流,使用 BroadcastReceivers 是很好的;它们是进程间通信的良好解决方案。但是,如果你自己的应用的组件之间的通信受到限制,广播接收器是一个昂贵的解决方案。用全球广播不合适。

如果您只是想简化应用组件之间的消息传递,您可能需要考虑一个 LocalBroadcastManager 类。当您使用此功能时,广播数据不会离开您的应用。它不是进程间的。遗憾的是,本章不会讨论 LocalBroadcastManager。但是希望你已经对广播接收器的概念和使用有了一些好的基础。

章节总结

  • 您可以使用 BroadcastReceivers 和 Intents 来创建真正解耦的应用。

  • 您可以让您的应用收听特定的广播,并在广播发送时做一些有趣的事情。

  • BroadcastReceivers 可用于在您的应用中路由程序逻辑。您可以让应用以特定方式运行,以响应运行时环境的变化(例如,低电量、无 WiFi 连接)。

  • 可以通过清单或上下文对象注册 BroadcastReceivers。如果你的目标是 Android 9.0,请确保阅读允许通过清单注册的广播动作。Android 团队不鼓励应用通过清单注册,而是使用上下文注册。

在下一章,你将学习如何准备你的应用进行分发。

二十、应用分发

我们将介绍的内容:

  • 清理

  • 准备发布

  • 签署应用

  • Google Play

在某些时候,您可能希望将您的应用分发给更多的人。Android 应用可以相当自由地分发,没有太多限制;你可以在你的网站上下载,甚至直接通过电子邮件发送给用户,但许多开发者选择在谷歌 Play 商店或亚马逊应用商店等市场上发布他们的应用,以最大限度地扩大影响。不管你打算如何发布,在发布应用之前,你需要做一些事情。

发布应用可能是一项非常复杂的活动,它不仅限于应用分发的技术和程序方面,例如在 developer 上创建帐户。,制作打磨好的图标,给你的 app 签名。它还可能涉及创建文案和宣传文本、社交媒体活动以及许多其他与技术完全无关的事情。本章将只关注 app 分发的技术要求。

**通常,发布应用有两个阶段:

  1. 准备 app 发布。这是我们打扫卫生的地方。你需要在发布前清理应用。这是我们删除所有调试信息和其他设置或记录我们在开发过程中使用的内容的地方。你当然不希望你的用户意外地看到你在编码时为自己留下的“明白了”或“我在这里”的痕迹。你可能还想为应用考虑图标和其他视觉素材。在这个阶段投资一个实际的设备并在上面测试你的应用是一个好主意。最重要的是,在这个阶段,我们将建立一个开发者证书。

  2. 发布 app 。你需要宣传这个应用,销售它,并分发它。如果您将在谷歌 Play 商店发布该应用,您需要注册一个发布者帐户,并使用 Google Play 的开发者控制台进行发布。

准备发布应用

这里我们需要做的三件主要事情是:

  1. 准备发布的材质和素材

  2. 配置要发布的应用

  3. 构建一个发布就绪的应用

准备用于发布的材质和素材

无论你的代码多么漂亮或者聪明,用户永远也看不到它。他将看到的是你的视图对象、图标和应用的其他图形素材。确保它们是抛光的。

如果你不考虑应用的图标,那你就是失职。此图标帮助用户识别您的应用,因为它位于主屏幕上。这个图标还会出现在其他几个区域,比如启动窗口、下载部分,更重要的是,如果你在 Google marketplace 上发布你的应用,这个图标也会显示在那里。应用图标可能在给你的潜在用户创造第一印象方面起着重要作用,所以这是一个好主意,你可以在 http://bit.ly/androidreleaseiconguidelines 找到谷歌的应用图标指南。

如果你要在谷歌市场上发布应用,还需要考虑图形素材,比如屏幕截图和促销文本。请务必阅读谷歌的图形素材指南,该指南可在 http://bit.ly/androidreleasegraphicassets 找到

配置要发布的应用

这是你清理和净化应用的部分。我们在这里提到的东西绝不是强制性的,但在构建发布版本之前浏览它们是一个好主意。

检查包名

在前面的章节中,您已经使用了" com.example.myapp" 作为包名。这对于测试或实践应用来说没问题,但当你将应用发布给公众时就不一样了。软件包名称使应用在市场上独一无二,一旦你决定了软件包名称,你就不能再改变它了。所以,考虑一下吧。

删除日志记录和调试信息

调试和日志信息在开发过程中是有用的——甚至是不可或缺的,但是你不能让你的用户看到它们。在发布应用之前,删除应用的所有调试和日志信息。

调试信息很容易处理,您只需删除清单文件的 <应用> 标签中的Android:debuggeable属性。不幸的是,对于日志记录信息,情况就不一样了。

有多种方法可以解决日志问题;这些解决方案可以像手动删除所有日志语句一样简单(但是繁琐),也可以像编写 sed 或 awk 程序来自动删除日志调用一样复杂。有些人通过配置 ProGuard 来处理日志问题(这超出了本书的范围),有些人甚至会使用像 Timber(GitHub 项目)这样的第三方库来替换 Android 的日志类。无论您采用哪种方法,请注意,您需要在构建发布版之前去掉日志语句。

检查应用权限

在开发过程中的某个时候,您可能已经试验了应用的一些特性,并且您可能已经在清单上设置了权限,比如使用网络、写入外部存储等的权限。检查清单上的 < uses-permission > 标记,确保不授予应用不需要的权限。

远程服务器和 URL

如果您的应用依赖于 web APIs 或云服务,请确保应用的发布版本使用的是生产 URL,而不是测试路径。在开发过程中,您可能已经获得了沙盒和测试 URLs 您需要将它们升级到生产版本。

构建发布就绪的应用

我们在本书中所做的所有项目和示例都通过一个简单的过程部署在模拟器中。我们点击了运行按钮。Android Studio 将该应用构建并组装成 APK,部署在目标设备中。之后 app 运行。在整个过程中,有一步是 Android Studio 为我们做的,而你并不知道。你一点都没有意识到。

Android Studio 执行了一项非常重要的任务,在任何设备(仿真或实际设备)上交付或安装任何 APK 之前,都需要执行这项任务。安卓工作室签了那个 APK。

在您可以在任何设备上安装和运行应用之前,应用的 APK 必须经过数字签名。当我们点击运行按钮时,Android Studio 会自动对所有应用进行签名。但是它使用一个调试证书,这个证书只对开发和测试有用。发布应用时,不能使用相同的证书。包括谷歌在内的大多数应用商店都不接受带有调试证书的应用。

在我们发布应用之前,我们必须用正确的证书对其进行签名,而不是调试证书。我们不需要去像 Thawte 或 Verisign 这样的认证机构——自签名证书就可以了。

启动 Android Studio,如果它尚未打开的话。打开您的项目。从主菜单栏进入构建生成签名 APK ,如图 20-1 所示。

img/463887_1_En_20_Fig1_HTML.jpg

图 20-1

生成签名的 APK

单击“下一步”按钮。您应该会看到“密钥库”对话框,如图 20-2 所示。

img/463887_1_En_20_Fig2_HTML.jpg

图 20-2

密钥库对话框

密钥存储路径询问我们的 Java 密钥存储库(JKS)文件在哪里。在这一点上,你还没有。所以,点击新建。你会看到创建新密钥库的对话框,如图 20-3 所示。

img/463887_1_En_20_Fig3_HTML.jpg

图 20-3

新密钥库

注意

在 Java 中,keystore 是安全证书的存储库——授权证书或公钥证书。

表 20-1 显示了对密钥库输入项的描述。

表 20-1

密钥库项目和描述

|

密钥库项目

|

描述

| | --- | --- | | 密钥库路径 | 要保存密钥库的位置。这完全取决于。一定要记住这个地方。 | | 密码 | 这是密钥库的密码。 | | 别名 | 此别名标识密钥。它只是一个友好的名字。 | | (钥匙)密码 | 这是钥匙的密码。这与密钥库的密码不同(但是如果您愿意,也可以使用相同的密码)。 | | 有效期,以年计 | 默认为 25 年;你可以接受默认值。如果在 Google Play 上发布,证书的有效期必须到 2033 年 10 月——所以,25 年应该没问题。 | | 其他信息 | 只有名字和姓氏字段是必需的。 |

填写完“新建密钥库”对话框后,单击“确定”这将把你带回生成签名 APK 窗口,如图 20-4 所示;但是现在,创建了 JKS 文件,并用它填充了密钥库对话框。

img/463887_1_En_20_Fig4_HTML.jpg

图 20-4

生成签名的 APK,已填充

点击“下一步”

img/463887_1_En_20_Fig5_HTML.jpg

图 20-5

签名 APK APK 目的地文件夹

接下来,我们选择签约 APK 的目的地,如图 20-5 所示。你需要记住这个位置。这是 Android Studio 存储签名 APK 的地方。另外,确保构建类型被设置为“发布”

单击“完成”后,Android Studio 将为您的应用生成签名的 APK。这是您将提交给 Google Play 的文件。你甚至可以在你的网站或其他市场上出售这款 APK——它已经准备好发布了。

发布应用

在向 Google Play 提交应用之前,您需要一个开发者帐户。如果你还没有,可以在 https://developer.android.com 报名。我对接下来的活动做了很多假设。我假设:

  1. 你已经有一个谷歌账户(Gmail);

  2. 你在用谷歌浏览器去https://developer.android.com;和

  3. 您的 Google 帐户已登录 Chrome。

如果你的谷歌账户没有登录 Chrome,你可能会看到类似图 20-6 的东西。Chrome 会要求你选择一个账户(或者创建一个)。

img/463887_1_En_20_Fig6_HTML.jpg

图 20-6

选择帐户

当你整理好你的谷歌账户后,你将被带到 developer.android.com 网站,如图 20-7 所示。

注意

这里显示的截图是他们在写作时出现的。谷歌不时对网站进行修改。当你读到这本书的时候,Google Play 网站可能不再像这些截图一样了。

点击 Google Play ,如图 20-7 所示。

img/463887_1_En_20_Fig7_HTML.jpg

图 20-7

developer.android.com

点击启动游戏控制台,如图 20-8 所示。

img/463887_1_En_20_Fig9_HTML.jpg

图 20-9

Google Play 控制台,注册

img/463887_1_En_20_Fig8_HTML.jpg

图 20-8

启动游戏控制台

您需要通过四个步骤来完成注册(如图 20-9 ):

  1. 使用您的 Google 帐户登录。

  2. 接受开发者协议。

  3. 交报名费。

  4. 填写您的帐户详细信息。

一旦完成注册和支付,您现在就可以访问 Google Play 控制台,如图 20-10 所示。

img/463887_1_En_20_Fig10_HTML.jpg

图 20-10

游戏控制台

您可以从这里开始向商店提交应用的流程。单击“创建应用”按钮开始。

章节总结

  • 你的代码可能很棒,但是用户永远看不到它们。也要注意用户会看到的东西,比如图标和其他图形素材。

  • 在发布代码之前清理它们。删除所有日志和调试信息。

  • 对你自己的工作进行代码评审。如果你有伙伴或者其他人可以和你一起审查代码,那就更好了。如果你的应用使用服务器、RESTful URLs 等。,确保它们是生产就绪的,而不是沙盒。

  • 如果你想将你的应用发布到 Google Play 或 Amazon 等市场,你不能使用调试证书。

  • 如果你想在 Google Play 上出售你的应用,你需要一个 Google Play 账户。我一次性支付了 25 美元的费用,但那是几年前的事了。

  • 别忘了在真实设备上测试你的应用。

  • 我们试图提炼和简化将你的应用放入 Play Store 的过程,但是这一章并不能代替 Android 开发者的发布清单。你还是应该读一下。你可以在 https://bit.ly/appstorelaunchchecklist 找到。**