安卓软件内部原理快速参考-全-
安卓软件内部原理快速参考(全)
一、简介
2016 年,全球有超过 500 万安卓开发者。三年后的 2019 年,全球有 25 亿部安卓设备在流通。毫无疑问,Android 是一个被广泛采用的操作系统,而且不会很快普及。
这本书是为新老 Android 软件工程师设计的,关注点远离典型的 UI 设计。相反,这本书关注的是 Java 和 Android 系统中在学习构建应用时通常会被遗忘的部分。这本书将作为技术的参考指南,从设备和用户唯一标识符到 ProGuard 混淆,再到长期运行的服务,反过来挖掘一些从一开始就不明显的原则。
顾名思义,这本书不会涉及 Android UI 开发,也不会涉及低级 C 语言或内核技术。相反,这本书将关注 Java 和 Android 系统中容易理解、有用和有趣的技术。
这是什么书
-
非 UI 元素的 Android Java 编程参考指南。
-
这本书涵盖了从 Android 4.4 到 11.0 的 Android 操作系统。
这本书不是什么
-
这本书不包括 Java 的入门教程;如果你正在寻找这个,有许多其他伟大的资源。
-
本书不涉及任何低级 C 编程、内核交互或漏洞研究。
-
虽然本书可能会涉及 Android UI 的一两个方面,但这不是重点。
二、Android 版本
2008 年 9 月 23 日发布 Android 1.0 从那以后,操作系统经历了无数次变化。表 2-1 表示 Android 的不同版本(从 1.5 版本 Cupcake 开始)。
Android 中的 API 级别定义了该设备上支持的 API 功能。如果使用设备的 shell 提示符(例如,通过命令adb shell
,它是 Android 平台工具的一部分),您可以通过运行getprop | grep sdk
返回与当前 API 级别相关的系统属性。这也可以通过 adb (Android 调试桥)从连接的机器上完成。
下面显示了在运行 Android 11 的 Google Pixel 4a 上运行该命令的输出示例:
表 2-1
Android 发布版本
|版本
|
代码名称
|
API 级别
|
出厂日期
|
Linux 内核版本
|
| --- | --- | --- | --- | --- |
| 1.5 | 纸杯蛋糕 | 3 | 【2009 年 4 月 | 2.6.27 |
| 1.6 | 甜甜圈 | 4 | 2009 年 9 月 | 2.6.29 |
| 2.0 | 艾克蕾尔 | 5 | 2009 年 10 月 | 2.6.29 |
| 2.0.1 | 艾克蕾尔 | six | | |
| Two point one | 艾克蕾尔 | seven | | |
| 2.2.x | 弗罗约 | 8 | 【2010 年 5 月 | 2.6.32 |
| 2.3 - > 2.3.2 | 姜饼 | 9 | 【2010 年 12 月 | 2.6.35 |
| 2.3.3 -> 2.3.7 | 姜饼 | Ten | | |
| 3.0 | 蜂巢 | 11 | 【2011 年 2 月 | 2.6.36 |
| Three point one | 蜂窝 | Twelve | | |
| 3.2.x | 蜂窝 | Thirteen | | |
| 4.0.1 - > 4.0.2 | 冰淇淋三明治 | 14 | 【2011 年 10 月 | 3.0.1 |
| 4.0.3 -> 4.0.4 | 冰淇淋三明治 | Fifteen | .. | .. |
| 4.1.x | 糖豆 | 16 | 【2012 年 7 月 | 3.0.31 |
| 4.2.x | 果冻豆 | Seventeen | | 3.4.0 |
| 4.3.x | 果冻豆 | Eighteen | | 3.4.39 |
| 4.4 - > 4.4.4 | 奇巧 | 19 | 【2013 年 10 月 | 3.10 |
| 5.0 | 棒棒糖 | 21 | 【2014 年 6 月 | 3.16.1 |
| Five point one | 棒棒糖 | Twenty-two | | |
| 6.0 | 棉花糖 | 23 | 【2015 年 10 月 | 3.18.10 |
| 7.0 | 牛轧糖 | 24 | 【2016 年 8 月 | 3.18.48/4.4.0 |
| Seven point one | 牛轧糖 | Twenty-five | | |
| 8.0.0 | 奥利奥 | 26 | 【2017 年 8 月 | 3.18.72/4.4.83/4.9.44 |
| 8.1.0 | 奥利欧 | Twenty-seven | | 3.18.70/4.4.88/4.9.56 |
| 9 | 馅饼 | 28 | 【2018 年 8 月 | 4.4.146/4.9.118/4.14.61 |
| 10 | Android 10 (Q) | 29 | 【2019 年 9 月 | 4.9.191/4.14.142/4.19.71 |
| 11 | Android 11 (R) | 30 | 【2020 年 9 月 | 4.14.y/4.19.y/5.4.y |
[ro.build.version.min_supported_target_sdk]: [23]
[ro.build.version.preview_sdk]: [0]
[ro.build.version.preview_sdk_fingerprint]: [REL]
[ro.build.version.sdk]: [30]
[ro.product.build.version.sdk]: [30]
[ro.qti.sdk.sensors.gestures]: [false]
[ro.system.build.version.sdk]: [30]
[ro.system_ext.build.version.sdk]: [30]
[ro.vendor.build.version.sdk]: [30]
三、基本原则
安卓沙盒
Android 运行在一个多用户的 Linux 系统下,这意味着每个应用及其存储都在一个单独的用户下运行。这意味着在正常情况下,应用不能读取另一个应用的数据或内部存储。每个进程都使用自己的虚拟机(VM)来隔离应用。在 API level 21 (Android 5)之前,这将是一个 Dalvik 虚拟机,在以后的版本中将改为使用 Android 运行时(ART)。两者都以相似的方式运行,在运行应用编译的 Dalvik 字节码时,它们模拟设备的 CPU、寄存器和其他功能。然而,ART 被认为具有许多性能改进。
在这些虚拟机中,应用只能访问运行所需的组件(最低权限策略)。这些单独的流程虚拟机是由合子 1 (zai gowt)创建的。Zygote 在启动时由 Android 运行时启动,拥有 root 权限,拥有第一个虚拟机和所有共享的 Java 类和资源。当一个新的应用想要启动时,一个新的 Zygote 进程被分叉,并且该应用被绑定到新进程的线程,并且它的代码在其中运行,非必需的和非请求的权限被 Zygote 丢弃,以便该应用只拥有必要的权限。
应用组件
活动
活动是安卓应用的主要入口。类似于单个网页,活动是单个屏幕,通常只在前台运行。虽然不是所有的活动都必须可见,但大多数标准的应用活动都是可见的。活动可以通过扩展Activity
类以编程方式实现。
服务
一般来说,服务是 Android 中的一个实用程序,用于在应用当前不在前台运行时在后台提供功能,例如音乐播放器、电子邮件客户端轮询电子邮件或地图应用。多年来,Android 中用于任务服务的首选技术已经从使用Services
变为使用JobSchedulers
。这些将在第十章中详细讨论。
广播接收机
应用的另一个入口点是系统、其他应用和应用本身可以“广播”应用随后接收的事件的地方。广播接收机具有受限的功能(一般来说,在被认为无响应之前,它们只能运行 10 秒 2 ),因此,通常会启动另一种形式的长期运行服务,例如前台Activity
或JobScheduler
。广播接收器是通过扩展BroadcastReceiver
类实现的。这些将在第四章中详细讨论。
内容供应器
内容供应器用于管理应用数据集,以便它们可以与设备上的其他应用共享。使用 URI,即使 URI 所属的应用当前没有运行,其他应用也可以查询或修改数据。示例包括图像、文本文件、SQLite 数据库等。
显示
应用的清单文件 3 是预编译创建的,在运行时不能编辑。这些类似 xml 的文件称为AndroidManifest.xml
,详细描述了单个应用中的所有组件(活动、广播接收器、服务等)。).清单文件还详细说明了应用需要的权限、最低 API 级别,以及应用使用的硬件和软件功能(如摄像头)。虽然应用只有一个清单文件,但导入的库可能有自己的清单文件。因此,在构建期间,Gradle 将合并所有这些单独的清单文件;结果称为合并清单。 4 这在反转 Android 应用(APK
文件)时很明显,因为(如果使用库的话)反转后的清单会比原来的预合并清单大得多,并且有更多的元素。
这里可以看到一个简单的 AndroidManifest.xml
文件的例子。在这个例子中,这个包叫做 simple_app
,它使用 FINGERPRINT
权限(如下所述),它的主要入口点是一个叫做 MainActivity
的活动,它有两个意图过滤器:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simple_app">
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<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>
</application>
</manifest>
许可
Android 中的权限遵循最小特权模型,其中特定类型的功能只给予明确请求它们的应用。有两种主要类型的权限:清单权限和运行时权限(其中通常属于“危险”权限类别)。运行时和清单权限都必须在 Android 清单中声明;然而,除此之外,必须在运行时请求运行时许可,以提示用户是否愿意应用使用所述功能的对话(如图 3-1 所示)。运行时权限在 API 级别 23 中实现;但是,在此之前,用户将在安装之前看到所有运行时权限。
图 3-1
用户看到的运行时权限示例
如前所述,权限类型是基于它们对用户、Android 系统或设备上的其他应用造成的风险而给出的。在表 3-1 中可以看到所有权限类型的汇总。 5
表 3-1
权限类型
|权限类型
|
风险
|
描述
|
| --- | --- | --- |
| 标准 | 低的 | 默认权限类型。为应用提供隔离的应用级特性,如BLUETOOTH
、NFC
和INTERNET
。 |
| 危险的 | 高的 | 为应用提供对私有数据或设备控制方面的访问,例如WRITE_EXTERNAL_STORAGE, ACCESS_FINE_LOCATION
和CAMERA
。与普通权限类型不同,在系统授予权限之前(或在 API 级别 23 之前的设备上安装时),用户必须接受运行时权限请求(在运行时通过单击按钮)。 |
| 签名 | 批评的 | 只有当请求权限的应用与声明该权限的应用由同一证书签名时,才会授予该权限。这通常用于将权限类型限制为系统/预安装的应用。这些权限通常授予对系统和其他应用的大量访问权限,或者规避 Android 安全机制。这包括MANAGE_EXTERNAL_STORAGE
、READ_LOGS
和CAPTURE_AUDIO_OUTPUT
。 |
除了这些类型之外,还可以将其他标志应用于权限,包括privileged
和development
,它们分别表示给予系统和开发应用的权限。 6
最后,在确定哪些应用可以执行哪些操作时,需要考虑另外两种权限类型:
-
硬限制许可 -其中许可不能被设备上的任何应用持有,除非该许可已经被安装应用允许列出
-
软限制权限 -设备上的任何应用都不能以完整形式帮助某个权限,除非该权限已被安装程序应用允许列出
调试设备时,标准应用通常不可用的权限(如签名权限类型的权限)可以使用 adb 授予。以READ_LOGS
权限为例,以下命令可用于授予权限adb shell pm grant <Package ID> android.permission.READ_LOGS
。要检索应用的包 ID,请参见第五章或第十二章。
一个 舱单 权限的例子:
<uses-permission android:name="android.permission. WRITE_EXTERNAL_STORAGE "/>
一个 运行时 权限请求的例子:
if (Build.VERSION.SDK_INT >= 23) {
// Notification will not be shown unless the correct manifest permission is set
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1234);
}
在编写 Android 规范的时候, 7 列出了 166 种不同的清单权限。表 3-2 显示了这些权限中最常见的一部分,以及它们的 API 和权限级别。
表 3-2
Android 权限
|名字
|
同意
|
API 级别
|
描述
|
| --- | --- | --- | --- |
| 访问 _ 粗略 _ 位置 | 危险的 | 29+ | 通过非 GPS 供应器(如网络供应器)访问大致位置。 |
| 访问 _ 精细 _ 位置 | 危险的 | 1+ | 通过 GPS 和网络供应器访问特定位置。 |
| 访问网络状态 | 标准 | 1+ | 访问ConnectivityManager
负责监控网络连接(Wi-Fi-GPRS、UMTS 等)。). |
| 访问 _ WIFI _ 状态 | 标准 | 1+ | 对WifiManager
的访问负责查看已配置网络列表、当前活动网络和接入点扫描结果。 |
| 接听电话 | 危险的 | 26+ | 允许应用接听来电。 |
| 电池状态 | 签名|特权|发展 | 1+ | 允许汇总电池信息和统计数据。 |
| 蓝牙技术 | 标准 | 1+ | 允许与蓝牙设备配对和连接。 |
| 呼叫电话 | 危险的 | 1+ | 允许应用呼叫,而无需向拨号器应用发送意图(从而无需通过 UI 通知用户)。 |
| 通话特权 | 不适用于第三方应用 | 1+ | 一个限制较少的版本的CALL_PHONE
权限,允许应用呼叫任何电话号码,包括紧急号码。 |
| 照相机 | 危险的 | 1+ | 提供对设备摄像头的访问。 |
| 捕获音频输出 | 禁止第三方应用 | 19+ | 允许不受限制地从设备的麦克风录制音频。 |
| 更改 WIFI 状态 | 标准 | 1+ | 允许应用修改设备的网络配置,以及连接和断开 Wi-Fi 接入点。 |
| 删除 _ 包 | 禁止第三方应用 | 1+ | 允许删除应用。对于 Android 7 用户,如果请求删除的应用与安装它的应用不同,则需要进行确认。 |
| 安装软件包 | 禁止第三方应用 | 1+ | 允许一个应用安装另一个。 |
| 因特网 | 标准 | 1+ | 允许应用打开网络套接字。 |
| 管理外部存储 | 签名| appop |预装 | 30+ | 由于在 Android 10 中实现了作用域存储(具有沙箱外部存储的应用),该许可允许对外部存储空间进行广泛的管理。 |
| 国家足球联盟 | 标准 | 9+ | 允许通过 NFC(近场通信)进行 I/O 操作。 |
| 处理 _ 呼出 _ 呼叫 | 危险和严格限制 | 1–29 | 允许应用查看和重定向所有拨出的电话。 |
| 阅读 _ 联系人 | 危险的 | 1+ | 允许读取用户的联系信息。 |
| 读取外部存储 | 危险,软限制 | 16+ | 允许应用从外部存储器读取数据。在 API 19 之后,读取应用的作用域存储不需要该权限。就 API 级别 29 中的作用域存储的强制实现而言,这仅提供只读访问。 |
| 读取日志 | 禁止第三方应用 | 1+ | 允许读取系统日志信息,例如由DropBoxManager
创建的信息。 |
| 阅读电话号码 | 危险的 | 26+ | 允许应用读取所有设备的电话号码。默认情况下,该权限对即时应用公开。 |
| 读取电话状态 | 危险的 | 1+ | 包括READ_PHONE_NUMBERS
权限。允许访问设备/电话信息,如当前的蜂窝网络。 |
| 阅读 _ 短信 | 危险和严格限制 | 1+ | 允许阅读手机已经收到的短信。 |
| 重新启动 | 禁止第三方应用 | 1+ | 允许重新启动设备。 |
| 接收 _ 引导 _ 完成 | 标准 | 1+ | 允许应用接收Intent.ACTION_BOOT_COMPLETED
意图。该意图在系统完成引导后广播。 |
| 接收 _ 彩信 | 危险和严格限制 | 1+ | 允许应用监控收到的彩信。 |
| 接收 _ 短信 | 危险和严格限制 | 1+ | 允许应用监控传入的短信。 |
| 录音 _ 音频 | 危险的 | 1+ | 允许应用录制音频。 |
| 请求 _ 删除 _ 包 | 标准 | 26+ | 允许应用请求删除自己或另一个应用。这将需要用户交互。 |
| 请求安装软件包 | 签名 | 23+ | 允许应用请求安装应用。这将需要用户交互。 |
| 发送 _ 短信 | 危险且严格限制的权限 | 1+ | 允许发送短信。 |
| 设置 _ 首选 _ 应用 | 标准 | 1–15 | 允许应用修改用户的首选(默认)应用,包括 web 浏览器和安装程序。 |
| 设置定时器 | 禁止第三方应用 | 8+ | 允许设置系统时间。 |
| 使用生物特征 | 标准 | 28+ | 允许使用设备支持的硬件。 |
| 使用 _ 指纹 | 标准 | 23–28 | 允许使用设备指纹硬件。取而代之的是 Android API 级中的USE_BIOMETRIC
权限。 |
| 唤醒 _ 锁定 | 标准 | 1+ | 允许应用启动PowerManager
唤醒锁,禁止设备睡眠或屏幕变暗。 |
| 写 _ 外部 _ 存储 | 危险的 | 4+ | 允许写入设备外部存储器。还授予了READ_EXTERNAL_STORAGE
权限。在 API 19 之后,在应用的作用域存储中读/写不再需要该权限。就 API 级别 29 中的作用域存储的强制实现而言,这仅允许应用读取它们自己的存储数据。 |
语境
Android 中的另一个核心组件是Context
。Android 中的 Context 8 被通俗地称为“上帝”类,是一个用于检索应用环境的全局信息的接口。它允许访问特定于应用的资源以及应用级别的操作,如广播、接收意图和启动活动。一些使用案例包括
-
访问应用内部存储的位置
-
发送祝酒词或通知对话框
-
在活动中设置一个
ImageView
-
检索系统软件包管理器
有两种主要类型的上下文:应用上下文和活动上下文。这两种类型的上下文都绑定到它们各自领域的生命周期——其中应用上下文绑定到应用的生命周期,活动上下文绑定到其活动的生命周期。这意味着如果其中一个被销毁,那么它们各自的上下文将被垃圾收集。
除了 Context 的这两个子类,还有ContextWrapper
可以和 Context 方法getBaseContext()
一起使用。上下文包装器允许使用代理上下文,这样就有可能修改对象的行为而不改变原始上下文。
应用上下文
下面返回应用上下文。当应用被销毁时,它将被垃圾回收。
检索应用上下文:
getApplicationContext()
活动上下文
当在Activity
或Activity
的子类中时,使用 this 返回活动上下文。当活动被销毁时,活动上下文被垃圾收集。
从活动内部检索活动上下文:
this
活动生命周期
作为使用设备的一部分,应用的单个活动可以进入许多不同的状态。activity 类提供了有用的回调,当进入这些状态时会触发这些回调,以便应用可以做出适当的响应。完整的活动生命周期可以在图 3-2 中看到。
图 3-2
Android 应用活动生命周期
onCreate()
第一次创建活动时调用此回调。这个方法接受一个参数savedInstanceState
,它是一个包含活动以前保存的状态的包,或者如果以前不存在的话接受一个参数null
。
onStart()
该回调准备活动进入前台,并使其对用户可见。除非恢复,否则每次活动开始时都会调用此回调。
onResume()
当进入这种状态时,活动准备好被用户交互,并进入前台。可能会发生中断事件,例如电话呼叫或用户移动到另一个活动——如果发生这种情况,活动将移动到onPause()
回调。
onpause()
此回调表明该活动不再处于前台;但是,并不一定意味着它即将被摧毁。
onStop()
当活动对用户不再可见时,在活动被销毁之前调用此回调。这是应用应该释放资源的地方。onStop()
回调是活动将接收的最后一个回调。
onRestart()
当活动重新显示给用户时,在onStop()
回调之后调用该活动。接下来是onStart()
和onResume()
。
安卓用户
在 Android 中,有两个截然不同的概念都可以被定义为“用户”。
Linux 用户
Android 是一个多用户 Linux 系统,其中每个应用都是沙箱化的,这意味着每个应用由不同的用户代表。Android 系统为每个唯一的应用证书(apk
文件签名的证书)分配一个唯一的 Linux 用户 id,反过来还设置所有应用文件的权限,以便只有指定的 Linux 用户 ID 才能访问它们。 9 这意味着如果两个应用由同一个证书签名,那么它们被放在同一个沙箱中。当涉及到Signature
权限类型(前面讨论过)时,这也是必需的,如果一个权限具有这种类型,那么它只能由与创建该权限的应用具有相同证书的应用使用(通常被认为是禁止非系统应用访问系统权限)。
要查看应用的 Linux 用户 ID,您可以使用adb
作为 root 来遍历应用的文件系统(例如/data/data/com.android.chrome
,并使用ls -la
命令,如图 3-3 所示。
图 3-3
adb shell 中看到的 Linux 用户 ID 示例
以编程方式查看应用进程 ID:
Log.v("Application Process ID", String.valueOf(android.os.Process.myUid()));
shell 命令 id -u
也可以通过 adb
或运行时环境使用,如下:
id -u
安卓用户
Android 中的第二个用户概念是为一个设备的多个终端用户 10 设计的,旨在允许多个用户使用同一个 Android 设备。这是通过每个帐户拥有不同的应用数据和一些独特的设置来实现的。反过来,这支持多个用户在后台运行,而另一个用户是活动的。
可以在/data/user
目录下(如图 3-4 所示)或通过 UI 用户屏幕(如图 3-5 所示)找到设备上当前活动的用户。由于不同的用户将拥有他们自己的内部和作用域存储,使用相应的方法调用(例如,使用Context
方法getFilesDir()
)来检索正确的文件路径是很重要的,因为这些路径可能会随时间而改变。一个用户不能访问另一个用户的内部存储器,即使它用于相同的应用。
图 3-5
终端用户对设备上不同用户的看法
图 3-4
adb shell 中显示的 Android 用户 id 示例
四、意图
Intents 是 Android 中的核心进程间通信(IPC)机制之一,允许应用与其他 Android 组件(包括应用)进行通信(例如,发送数据或启动操作),即使接收者当前没有运行。Android 中有两个主要的意图类别,它们是:
-
Explicit——显式意图是指定应用或应用和组件的意图,应用和组件将执行请求。
-
隐式 -隐式意图更加模糊,指定了所需的动作类型(例如,打开相机或定位应用)。
除了这两个主要类别的意图(其中消息被直接发送到特定的应用或服务)之外,还可以发送广播意图。这些广播消息可以由 Android 系统或应用发送,同时由设备上先前注册了特定广播动作类型的所有应用接收。在某些情况下,可能需要特殊权限来注册特定的广播(例如,在BOOT_COMPLETE
权限的情况下,当 Android 系统在启动后完成加载时,允许接收ACTION_BOOT_COMPLETED
意图)。
启动组件
除了这两类意图之外,还有三种发送意图的主要方法:
-
启动一个活动——一个活动的实例可以通过将一个初始化的 Intent 对象传递给
startActivity()
或startActivityForResult()
上下文方法来启动。 -
启动后台服务——在 Android 5.0 (API level 21)之前,可以通过将初始化的
Intent
对象传递给startService()
上下文方法来启动后台Service
。API 级以后,这个可以用来启动JobScheduler
组件。 -
发送广播——虽然系统以固定的频率发送许多广播,如
TIME_TICK
和BOOT_COMPLETE
,但普通应用也可以发送自己的广播。一般来说,广播是一种可以被多个应用同时接收的意图。这种广播可以用sendBroadcast()
或sendOrderedBroadcast()
上下文方法发送。
意图属性
意图由几个属性组成。根据发送的意图和接收者,以下一些属性可能是强制的;然而,在某些情况下,这些都不是必需的。所有标准意图属性都作为常量存储在Intent
对象中,例如Intent.FLAG_ACTIVITY_NO_HISTORY
,其常量值为1073741824
。
核心属性
-
动作 -可使用已初始化的
Intent
对象的setAction
方法设置。该操作定义了要执行的高级操作。 -
数据 -可使用初始化的意图对象的
setData
方法设置。数据字段包括该意图正在操作的数据(例如,要在图像编辑应用中打开的文件)。
附加属性
-
类别 -可使用初始化的
Intent
对象的addCategory
方法设置。类别提供了关于意图要执行的动作的附加上下文。只有能够促进所有指定类别的活动才能被选择来接收意图。 -
类型 -可使用初始化的
Intent
对象的setType
方法设置。通常类型是从数据本身推断出来的;然而,该属性可用于设置特定的 MIME(多用途互联网邮件扩展)类型(例如,audio/mpeg or audio/*
),例如,要返回的数据。 -
组件 -可使用初始化的
Intent
对象的setComponent
方法设置。此属性标识用于意图的组件类的名称。这是一个可选属性,因为它通常是基于意图的内容来标识的。 -
额外的 -可使用初始化的
Intent
对象的putExtra
方法设置。extra 是接收者可以使用的捆绑包(一组不同类型的键值对)(例如,当与您的社交媒体应用共享笔记时,笔记文本将作为捆绑包中的字段发送)。这个包可以包含专有密钥或在 Android 系统中设置的密钥,如EXTRA_ALARM_COUNT
。 -
标志 -可使用初始化的意图对象的
setFlag
方法设置。标志表示正在启动的组件的行为(例如,不包括任务堆栈中的已启动活动)。
行动
可使用初始化的Intent
对象的setAction
方法设置。getAction
方法也可以用于检索接收到的意图的动作。该操作定义了要执行的高级操作。
这里可以看到这样一个例子:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
在撰写本文时,Android 文档 1 列出了 135 种以上的标准意图动作类型。表 4-1 列出了最常见意图动作的子集。
表 4-1
意图动作
|名字
|
API 级别
|
描述
|
| --- | --- | --- |
| 操作 _ 所有 _ 应用 | 1+ | 列出所有可用应用。 |
| 动作 _ 应用 _ 错误 | 14+ | 当用户在崩溃对话警告消息中选择“报告”按钮时发生。然后,该意图将被发送到安装出错应用的安装应用。 |
| 动作 _ 摄像机 _ 按钮 | 1+ | 指示设备的“相机按钮”被按下的广播。 |
| 动作选择器 | 1+ | 系统提供的默认标准活动选择器的替代选择。反过来,这个动作将显示一个活动选择器。 |
| 动作 _ 默认动作 _ 视图 | 1+ | 向用户显示数据的简单操作。 |
| 行动 _ 我的 _ 包 _ 暂停 | 28+ | 一种受保护的广播意图,只能由系统发送,并在应用进入暂停状态时发送给应用。 |
| ACTION _ MY _ PACKAGE _ UNSUSPENDED | 28+ | 一种受保护的广播意图,只能由系统发送,并在应用离开挂起状态时发送给应用。 |
| 行动 _ 包 _ 已添加 | 1+ | 一种受保护的广播意图,只能由系统发送,当设备上安装了新的应用时发送,包括两个附加项:EXTRA_UID
和EXTRA_REPLACING
。 |
| 操作 _ 包 _ 已更改 | 1+ | 受保护的广播意图,只能由系统发送,并且在设备上的现有应用发生变化时发送,包括额外的:EXTRA_UID
、EXTRA_CHANGED_COMPONENT_NAME_LIST
和EXTRA_DONT_KILL_APP
。 |
| 动作 _ 包 _ 数据 _ 清除 | 3+ | 受保护的广播意图,只能由系统发送,并且应该以ACTION_PACKAGE_RESTARTED
开头。当应用的持久数据被擦除时,这个广播被发送,记住这个意图不是发送给应用本身,包括额外的:EXTRA_UID
和EXTRA_PACKAGE_NAME
。 |
| 动作 _ 包 _ 第一个 _ 启动 | 12+ | 一种受保护的广播意图,只能由系统发送,并在首次启动应用时发送给应用的安装程序(例如,谷歌 Play 商店)。不包括额外费用;但是,该数据包括启动的包的名称。 |
| 操作 _ 包 _ 重新启动 | 1+ | 一种受保护的广播意图,只能由系统发送,并在用户终止应用及其所有进程时触发。该数据将包含包的名称,还将包含额外的EXTRA_UID
。 |
| 操作 _ 包 _ 已删除 | 1+ | 一种受保护的广播意图,只能由系统发送,并在应用从设备中移除时触发。由于应用已被删除,它将不会收到意向。除了包含应用名称的数据属性之外,还将包含以下附加内容:EXTRA_UID
、EXTRA_DATA_REMOVED
和EXTRA_REPLACING
。 |
| 动作 _ 电源 _ 已连接 | 4+ | 一种受保护的广播意图,只能由系统发送,表示设备已连接到电源。 |
| 动作 _ 电源 _ 断开 | 4+ | 一种受保护的广播意图,只能由系统发送,表示设备已断开电源。 |
| 操作 _ 重新启动 | 1+ | 一种受保护的广播意图,只能由系统发送,并指示设备重新启动。 |
| 动作 _ 运行 | 1+ | 运行已定义数据的高级操作。 |
| 动作 _ 屏幕 _ 关闭 | 1+ | 一种受保护的广播意图,只能由系统发送,并在设备屏幕进入睡眠状态或变为非活动状态时发送。 |
| 动作 _ 关闭 | 4+ | 受保护的广播意图,只能由系统发送,并在设备关闭过程中触发。 |
| 动作 _ 发送 | 1+ | 向收件人发送数据的高级操作。这个动作通常与选择器成对出现。 |
| 动作 _ 布景 _ 壁纸 | 1+ | 用于显示选择壁纸的设置。 |
| 动作 _ 时区 _ 已更改 | 1+ | 一种受保护的广播意图,只能由系统发送,表示系统时区已更改。额外的EXTRA_TIMEZONE
包含在内。 |
| 动作 _ 声音 _ 命令 | 1+ | 用于启动语音命令的意图动作。 |
| 动作 _ 网页 _ 搜索 | 1+ | 用于启动网络搜索的意图动作。 |
种类
可使用初始化的Intent
对象的addCategory
方法设置。getCategories
方法也可以用于检索接收到的意图的类别。类别提供了关于意图要执行的动作的附加上下文。只有能够促进所有指定类别的活动才能被选择来接收意图。
这里可以看到这样一个例子:
Intent intent = new Intent();
intent.addCategory(Intent.APP_CALCULATOR);
在撰写本文时,Android 文档 2 列出了 39 个以上的标准意向类别属性。表 4-2 列出了这些意向类别中最常见的一部分。
表 4-2
意图类别
|名字
|
API 级别
|
描述
|
| --- | --- | --- |
| 类别 _ 备选 | 1+ | 用于标识标准活动的替代活动或用户当前正在查看的数据。 |
| 类别应用浏览器 | 15+ | 用于打开一个应该可以浏览互联网的活动。它可以与ACTION_MAIN
一起使用,以打开首选的浏览器应用。 |
| 类别 _ 应用 _ 计算器 | 15+ | 用于打开应该能够执行标准算术运算的活动。它可以和ACTION_MAIN
一起使用来打开计算器应用。 |
| 类别 _ 应用 _ 日历 | 15+ | 用于打开应该能够执行标准日历操作的活动。它可以和ACTION_MAIN
一起使用来打开日历应用。 |
| 类别 _ 应用 _ 联系人 | 15+ | 用于打开应该能够查看和操作地址簿条目的活动。它可以和ACTION_MAIN
一起使用,打开通讯录应用。 |
| 类别 _ 应用 _ 电子邮件 | 15+ | 用于打开应该能够发送和接收电子邮件的活动。它可以和ACTION_MAIN
一起使用来打开电子邮件应用。 |
| 类别应用文件 | 15+ | 用于打开应该能够管理存储在设备上的文件的活动。它可以和ACTION_MAIN
一起使用来打开文件应用。 |
| 类别 _ 应用 _ 图库 | 15+ | 用于打开一个活动,该活动应该能够查看和操作存储在设备上的图像和视频文件。它可以与ACTION_MAIN
一起使用,打开图库应用。 |
| 类别 _ 应用 _ 地图 | 15+ | 用于打开一个应该能够显示用户当前位置的活动。它可以和ACTION_MAIN
一起使用来打开地图应用。 |
| 类别 _ 应用 _ 市场 | 11+ | 用于打开允许用户浏览和安装新应用的活动。 |
| 类别 _ 应用 _ 消息 | 15+ | 用于打开应该能够发送和接收文本消息的活动。它可以和ACTION_MAIN
一起使用来打开消息应用。 |
| 类别 _ 应用 _ 音乐 | 15+ | 用于打开应该能够在设备上播放音乐的活动。它可以和ACTION_MAIN
一起使用来打开音乐应用。 |
| 类别 _ 可浏览 | 1+ | 指示可以从浏览器直接调用和启动的活动(例如,通过用户选择到谷歌 Play 商店网站的链接并转到谷歌 Play 商店应用)。 |
| 类别 _ 汽车 _ 模式 | 8+ | 表示该活动已经过优化,适合在汽车环境中工作。 |
| 类别 _ 默认 | 1+ | 用于标识可用作默认操作的活动。 |
| 类别 _ 主页 | 1+ | 当设备启动时以及每当用户返回到开始的活动时启动的活动。 |
| 类别 _ 启动器 | 1+ | 用于标识可在设备上用作初始活动的活动(即主屏幕)。 |
| 类别 _ 猴子 | 1+ | 表示可以由 monkey(一个 Android UI fuzzer)或其他自动化工具测试的活动。 |
额外服务
可使用初始化的Intent
对象的putExtra
方法设置。getExtras
方法也可以用于检索接收到的意向的附加内容。extra 是接收者可以使用的捆绑包(一组不同类型的键值对)(例如,当与您的社交媒体应用共享笔记时,笔记文本将作为捆绑包中的字段发送)。这个包可以包含专有密钥或 Android 系统中设置的密钥,如EXTRA_ALARM_COUNT
。
这里可以看到这样一个例子:
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_TEXT, "This is an example text extra");
在撰写本文时,Android 文档 3 列出了 80 个以上的标准意图额外属性。表 4-3 列出了这些额外意图中最常见的一部分。
表 4-3
额外意图
|名字
|
API 级别
|
描述
|
| --- | --- | --- |
| 额外更改组件名称列表 | 7+ | intent action ACTION_PACKAGE_CHANGED
的一部分,包含所有已更改组件的字符串数组。 |
| extra _ which _KILL_APP-额外 _ 包括 _ kill _ app | 1+ | 部分ACTION_PACKAGE_REMOVED
和ACTION_PACKAGE_CHANGED
动作并覆盖重启目标应用的默认行为。 |
| 额外数据已移除 | 3+ | ACTION_PACKAGE_REMOVED
动作的一部分,用于指示移除应该是完全卸载,移除数据和代码,而不是留下数据的部分卸载(后者作为更新的一部分执行)。 |
| EXTRA_HTML_TEXT | 16+ | 可以作为ACTION_SEND
动作的一部分,与EXTRA_TEXT
附加文本一起使用,表示附加文本是 HTML 格式的文本。 |
| 额外 MIME 类型 | 19+ | 用于设置可接受的 MIME 类型(例如,audio/mpeg 或 audio/*)。 |
| 额外 _ 非 _ 未知 _ 来源 | 14+ | ACTION_INSTALL_PACKAGE
动作的一部分,并指示要安装的应用正从发送意图的应用安装,而不是从未知源安装。 |
| EXTRA_PACKAGE_NAME | 24+ | 包含应用名称。 |
| 额外电话号码 | 1+ | ACTION_NEW_OUTGOING_CALL
和ACTION_CALL
动作的一部分,包含呼叫的电话号码。 |
| 额外安静模式 | 24+ | 指示静音模式(在这种模式下,配置文件中的所有应用都被终止)是打开还是关闭。 |
| EXTRA_REFERRER | 17+ | 在意向发起活动时使用,以确定发起者。值以 URI 的形式提供。 |
| 额外替换 | 3+ | 用作ACTION_PACKAGE_REMOVED
动作的一部分,表示包已被替换。 |
| EXTRA_REFERRER_NAME | 22+ | xtra 的替代产品;但是,该值是作为字符串而不是 URI 提供的。 |
| 仅额外关闭用户空间 | 19+ | 用作ACTION_SHUTDOWN
动作的一部分,以确定应执行部分关闭。这种部分关闭只是重新启动用户空间,而不是执行完整的操作系统重新启动。 |
| 额外 _ 主题 | 1+ | 包含所发送数据主题的高级别附加内容。 |
| 额外文本 | 1+ | ACTION_SEND
动作的一部分。包含以CharSequence
(或String
形式接收的数据的高级额外数据,因为字符串实现了CharSequence
接口)。 |
| 额外时间 | 30+ | 一个高级别 extra,其值包含从 Epoch 开始的以毫秒为单位的时间(例如,1608553466)。 |
| EXTRA_TIMEZONE | 30+ | ACTION_TIMEZONE_CHANGED
动作的一部分,指示设备的时区。 |
| 额外标题 | 1+ | ACTION_CHOOSER
的一部分向用户提供一个标题(CharSequence
、String
等)。). |
| EXTRA_UID | 1+ | ACTION_UID_REMOVED
、ACTION_PACKAGE_REMOVED
和ACTION_PACKAGE_CHANGED
的一部分,用于识别正在运行的应用。 |
旗帜
可使用初始化的意图对象的setFlag
方法设置。标志表示正在启动的组件的行为(例如,不包括任务堆栈中的已启动活动)。getFlags
方法也可以用于检索接收到的意向标志。
这里可以看到这样一个例子:
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
在撰写本文时,Android 文档 4 列出了多达 35 个意向标志。表 4-4 显示了这些意向标志中最常见的一部分。
表 4-4
意图标志
|名字
|
API 级别
|
描述
|
| --- | --- | --- |
| 排除 _ 已停止 _ 包 | 12+ | Android 系统的默认行为是在选择要启动的适用组件时考虑设备上的所有应用。如果此标志设置为“已停止”,将不会包括软件包。 |
| 标志 _ 活动 _ 否 _ 历史 | 1+ | 如果设置,则在用户离开活动(例如,用户移动到另一个应用)之后,新活动不会保留在历史堆栈中。 |
| 标志 _ 活动 _ 先前 _ 是 _ 顶部 | 1+ | 当使用这个标志时,前一个活动保持在任务堆栈的顶部,用于决定新的意图被发送到哪里。 |
| FLAG_ACTIVITY_SINGLE_TOP | 1+ | 设置后,如果接收意图的活动已经在任务堆栈的顶部,则不会发生任何操作。 |
| 标志 _ 活动 _ 新 _ 任务 | 11+ | 使用此标志可以创建一组新的活动,用户可以使用单独的任务堆栈遍历这些活动。 |
| 标志 _ 活动 _ 无 _ 动画 | 5+ | 当设置时,没有过渡动画将被应用于特定活动的开始。 |
| 标志 _ 活动 _ 清除 _ 顶部 | 1+ | 如果已设置,并且正在启动的活动已经在当前任务堆栈中,则堆栈中的所有其他活动都将被删除,并由新活动替换。 |
| 标记 _ 活动 _ 排除 _ 来自 _ 最近 | 1+ | 如果设置,则启动的活动不包括在最近启动的应用的系统列表中。 |
任务堆栈
也被称为 back stack,这是一个后进先出的堆栈,它存储了一个活动的逻辑列表,供用户遍历。当一个应用启动并且当前不存在它的任务时,应用清单中带有MAIN action
的活动被启动。当这个活动来到前台时,它在任务堆栈上创建一个新的根(如图 4-1 和 4-2 所示)。启动的每个后续活动都放在任务堆栈中,当用户选择 back 按钮时,这些活动会弹出堆栈。每个应用可以有一个或多个任务堆栈,具体取决于启动活动时设置的特定标志。
图 4-1
Android 任务堆栈示例
当任务不在前台时(例如,主屏幕按钮已经被按下或者用户遍历到另一个应用/任务堆栈),在堆栈顶部的活动进入onPause()
状态并且移动到后台。后台可以同时存在多个任务;但是,如果需要保留内存,系统可能会销毁这些任务。
还需要注意的是,通过使用服务和其他长期运行的实用程序,应用可以在后台运行,而不会出现在任务堆栈中;这将在第九章中进一步描述。
图 4-2
在用户界面中查看多个任务堆栈
意图过滤器
所有组件都应该有一个android:exported tag
。默认情况下,这是 false,如果是 false,那么组件只能从其应用内部接收消息。通过添加任何意图过滤器,这自动为真,并且意味着外部应用可以与组件交互。当系统接收到一个隐含的意图时,这些意图过滤器也用于通告组件,并决定哪些应用可以接收该意图。有三种主要类型的意图过滤器,它们是Action
、Data
和Category
(如前所述)。
下面是一个带有MAIN
动作和LAUNCHER
类别集的应用的典型入口点的例子。主操作定义该组件应该作为应用的入口点启动,并且它不接收任何数据。启动器类别定义了活动应该显示在顶层启动器中。
应用入口点活动的示例意图过滤器:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
开始内部活动,数据:
public void sendIntentToActivityInApp(){
Intent intent= new Intent(this, IntentReceiver.class);
// Update class to be internal class (to the application) to receive the intent
Bundle bundle= new Bundle();
bundle.putString("key", "value");
intent.putExtras(bundle);
startActivity(intent);
}
在活动中接收意向捆绑:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getIntent().hasExtra("key")) {
String value = getIntent().getExtras().getString("key");
Log.v(this.getClass().getSimpleName(), value);
}
}
启动外部应用:
public void startActivityViaIntent(){
Intent launchIntent = getPackageManager().getLaunchIntentForPackage("com.android.chrome");
startActivity(launchIntent);
}
启动外部应用的特定活动:
public void sendIntentToAnotherActivity(){
Intent intent = new Intent();
intent.setClassName("com.android.chrome", "com.google.android.apps.chrome.Main");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ;
startActivity(intent);
}
向另一个活动发送文件。从 API 级别 24 开始,向其他应用发送文件需要一个文件提供程序,这样就可以共享自定义的 URI:
public void sendFileToAnoutherApplication(){
File file =new File(getApplicationContext().getFilesDir(),"/text/test.txt");
writeFileInInternalStorage(file, "Hello World");
Intent intent = new Intent(Intent.ACTION_SEND);
Uri contentUri = getUriForFile(getApplicationContext(), "com.example.intents.fileprovider", file);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_STREAM,contentUri);
startActivity(Intent.createChooser(intent , "File path"));
}
隐含电话号码意图:
public void phoneNumberIntent(){
Uri number = Uri.parse("tel:5551234");
Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
startActivity(callIntent);
}
隐含的 URL 意图:
public void webIntent(){
Uri webpage = Uri.parse("https://www.android.com");
Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);
startActivity(webIntent);
}
五、应用名称、Android 包名称和 ID
Android 使用多个命名约定来唯一标识一个 Android 应用;这些已在下文中进行了总结,并在本章中进行了更详细的分解:
-
Java 包名——设置在代码库中,描述项目的文件夹结构和应用的结构,帮助模块化应用组件。这用于区分多个同名包中的类,也用于反射等技术。它使用反向互联网域约定,比如
com.mywebsite.blog
。 -
包 ID -通过包属性在清单文件中设置。这是谷歌 Play 商店和安卓系统使用的。这必须与 Java 包名相匹配。它使用反向互联网域约定,比如
com.mywebsite.blog
。 -
应用 ID——在 Gradle 构建文件中设置,并在构建后替换清单文件中的包 ID。它不必匹配包 ID 或 Java 包名。它使用反向互联网域约定,比如
com.mywebsite.blog
。 -
应用名称 -不遵循反向互联网域名约定的名称(因此可以是诸如 myappname 的字符串),并显示给最终用户。
这些值都不能以编程方式更改。这是因为对于包 ID、应用 ID 和应用名称,它们或者已经编译过(对于 Java 包名称),或者位于 manifest 或 res 文件夹中(它们是只读的)。
Java 包名
Java 包名 1 是一个独特的小写(为了避免与接口或类名冲突)字符串序列,以反向互联网域约定编写。它在每个类文件中设置(如下所示),描述了项目的文件夹结构和应用的结构,并有助于模块化应用组件。反过来,Java 包名也用于描述反射等 Java 技术如何与应用交互。属于网站jamesstevenson.me
的银行应用的 Java 包名称的例子可以是me.jamesstevenson.banking
。
活动示例:
package me.jamesstevenson.banking;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
由于以下标识符,在 Android 中很少有理由(除了反射之外)在代码库之外共享应用的包名。
包 ID
应用的包 ID 在AndroidManifest.xml package
属性中设置,并且必须与 Java 包名匹配。如果使用 Gradle 构建工具,在构建完成后,包 ID 将被应用 ID 所取代。这被用作生成R.java
类的名称空间(用于资源处理)以及解析相对类名(例如.MainActivity
到com.example.example_app.MainActivity
)的构建过程的一部分。构建完成后,谷歌 Play 商店和 Android 系统将使用它,如 Android ID 部分所述。
Android 清单中的包 ID 示例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android_namings">
如上所述,可以通过更改包元素,在 AndroidManifest.xml 文件中直接编辑包 ID;然而,如果使用 Gradle 进行构建,并且您希望在构建后维护您的包 ID,请确保删除您的build.gradle
文件中的 Application ID 元素,如下所述,否则它将被替换。
正在检索包 ID
虽然这些标识符都不能以编程方式更改,但它们可以被访问。这将检索构建过程中的包 ID 值。
检索包 ID 的示例:
Log.v("PackageID BuildConfig", BuildConfig.APPLICATION_ID);
Log.v("PackageID context", getApplicationContext().getPackageName());
应用 ID
除了包 id,当使用 Gradle 和 Android 构建工具时,您还可以设置一个应用 ID。
有三条规则涵盖了应用 ID 名称的创建:
-
它必须至少有两段(一个或多个点)。
-
每段必须以字母开头。
-
所有字符必须是字母数字或下划线[a-zA-Z0-9_]。
通过applicationId
属性在 Gradle build.gradle
文件中设置应用 ID。应用 ID 可以不同于您的包 ID,也可以不同于您的 Java 包名。
图 5-2
应用的内部存储文件路径
图 5-1
谷歌 Play 商店上市
该应用 ID 在构建后替换应用的包 ID,从而允许包 ID 在构建过程中使用,然后替换它,以便应用 ID 由谷歌 Play 商店(如图 5-1 所示)和 Android 系统(如图 5-2 所示)使用。如前所述,这是为了不在程序代码库之外共享内部 Java 包名。记住这一点,在 Java 包名为com.example.internalname
的例子中,应用名需要在谷歌 Play 商店中显示为com.example.appname
;需要将AndroidManifest.xml
文件中的包 ID 设置为com.example.internalname
,并将build.gradle
文件中的applicationId
属性设置为com.example.appname
。
在不使用 Gradle 的情况下,需要重构 Java 包名以匹配com.example.appname,
,并且AndroidManifest.xml
中的包 ID 属性也需要设置为com.example.appname
。
Setting Application ID in``Gradle build file
defaultConfig {
applicationId "com.example.appname"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
后缀和味道
除了应用 ID 和包 ID 可以不同之外,您还可以为您的 Android 项目创建不同的后缀——命名产品风格。这通常是因为应用的不同版本(如付费和试用版)有不同的应用 ID。后缀可以在应用build.gradle
文件中修改。
在 Gradle build 文件中修改 后缀和**:
android {
defaultConfig {
applicationId "com.example.appname"
}
productFlavors {
free {
applicationIdSuffix ".trial"
}
pro {
applicationIdSuffix ".paid"
}
}
}
应用名称
Android 中的应用名称是最终用户可见的(如图 5-3 图 5-4 所示);这是显示在应用设置页面上的内容,也是默认情况下显示在应用图标下的内容(但是,在某些情况下,应用图标和文本可以通过编程切换2)。
图 5-4
应用图标和标题
图 5-3
应用设置的页面
可以通过编辑AndroidManifest.xml
文件中的label
属性来设置应用名称,如下所示。最佳实践是将它链接到字符串资源中的一个String
;然而,情况并不一定如此。
在 Android 清单文件中设置应用名称:
android:label="My Application Name"
正在检索应用名称
可以通过编程方式检索应用名称:
public static String getApplicationName(Context context) {
ApplicationInfo applicationInfo = context.getApplicationInfo();
int stringId = applicationInfo.labelRes;
return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId);
}
六、仓库
划分
Android(从 Android 2.2 开始)使用 ext4 文件系统,其核心遵循 Linux 文件系统结构——使用由/
表示的单个根。
在 Android root 中,有几个关键分区; 1 ,2这些分区可以在 Android shell 中使用以下命令列出:
cat /proc/mounts
根据设备版本、原始设计制造商、 3 和原始设备制造商的不同,文件系统根目录下安装和存在的特定文件夹可能会有所不同。因此,下面列出了一些最常见的文件夹:
-
/boot——引导分区包括 RAM 磁盘和内核映像。这两者都是通过
mkbootimg
4 捆绑在一起的,这是一个用于打包引导镜像的实用程序。没有这个分区,设备将无法启动。 -
/系统——这个分区覆盖了 Android 框架。这包括 Android GUI 和系统应用——在
/system/app and /system/priv-app
可用。擦除该分区会将 Android 从设备中移除,同时保持可引导性。 -
/厂商——这个分区包含了不包含在股票 AOSP 5 中的二进制文件和资源(主要关注特定于主板的组件的内核模块,特定于主板的守护进程,或者硬件抽象层(HALs)的附加特性)。这个分区主要是给原始设计厂商(比如高通、Exynos、华为)添加定制的二进制和 GUI。从 Android 8 开始,强制执行 6 当 ODM(原始设计制造商)或 OEM(原始设备制造商)将这些组件添加到设备时,它们应该被添加到
/vendor
分区,而不是/system
分区,这是强制执行之前通常使用的位置。 -
/ODM—
/vendor
分区的扩展,为原设计厂商设计。 7 这种分区允许多个设备使用相同的/vendor partition
并允许/odm
分区在它们之间提供微小的变化。类似于这个分区中的/system
分区,也有应用(/odm/app/
)、本地库(/odm/lib[64]
)等等的位置。Android 10 中增加了对该分区的全面支持。 -
/OEM——原始设备制造商 8 (如三星、一加、华为)可能会决定对 AOSP 形象做出改变——例如,添加他们自己的应用、用户界面或功能。对此的一个扩展是 Android 9 中添加的
/product
分区,与 OEM 分区不同,它是可更新的。 -
/recovery -该分区存储恢复镜像。
-
/数据 -称为用户数据分区,该分区包括应用的内部存储(例如
/data/user/0/com.android.chrome
)以及包含/data/local/tmp
目录和其他类型的用户数据。从 Android 5 开始,Android 已经支持在同一个设备上拥有多个用户;这意味着不同用户的内部存储将在不同的位置可用(例如/data/user/0/com.android.chrome
和/data/user/1/com.android.chrome
)。作为工厂重置的一部分,该分区被擦除。 -
/cache -这个分区是 Android 存储常用组件的地方,也是存储一些数据作为无线更新的一部分的地方。
-
/杂项 -杂项系统设置,通常以开/关开关的形式。
-
/元数据 -设备加密时使用元数据分区。
-
/radio -该分区仅在设备包含无线电组件时使用。
-
这个分区包含可信操作系统的二进制映像。
-
/SD card——在 Android 4 之前,这个目录是可以找到连接的 SD 卡的存储位置。从 Android 4 开始,这变成了所谓的“内置 SD 卡”(SD 卡安装移到了
/sdcard_ext or /sdcard/external_sd
)。这也被称为设备的外部存储器(不管它是否指的是实际的 SD 卡),下面将详细讨论。
External and Internal Storage
内存储器
每个应用都有自己的内部存储空间来存储任意数据。这个空间不需要任何写入权限,并且对每个应用都是沙箱化的——这意味着一个应用不能读取另一个应用的内部存储。
应用的内存位于以下路径(用 Android 用户 id 替换0
):
/data/user/0/<Package ID>/
默认情况下,应用的内部存储结构分为几个子目录;但是,可以向该空间添加任意文件夹。虽然这种存储没有限制,但建议将特定的文件类型存储在特定的目录中;但是,它不是强制的。
-
/files-files 目录用于存放任意文件。这些文件可以是文本文件、图像、JSON 之类的序列化文件或任何其他文件类型。要检索该目录的文件路径,请使用
getApplicationContext().getFilesDir()
。 -
/数据库 -数据库目录用于存储数据库。Android 对创建和管理 SQLite 数据库有很多内置的支持;然而,其他数据库类型如领域数据库 9 也可以存储在这里。创建这些类型的数据库时,默认情况下,这是保存它们的目录。
-
/shared_prefs -共享偏好是 Android 对易于访问的键值对的接受。这些以 XML 格式保存。创建共享偏好设置时,这是保存它们的目录。
-
/缓存 -缓存目录 10 是为保存临时文件而设计的,因为当设备上其他地方需要空间时,Android 系统会删除该文件夹中的文件。可以使用应用上下文:
getApplicationContext().getCacheDir()
访问缓存目录。 -
/lib -用于存储共享库文件。
-
/code _ cache——类似于缓存目录,在 API level 21 中增加,这个目录是为存储缓存代码而设计的。和缓存目录一样,如果需要空间,它也会被 Android 系统清除。这个目录的路径可以用应用上下文:
getApplicationContext().getCodeCacheDir()
来检索。 -
/no_backup -类似于/files 目录,但是,存储在该目录中的文件将被排除在自动备份工具之外。 11
可以使用应用上下文来检索应用内部存储的路径。重要的是要记住,这些文件路径可能会随着时间和操作系统版本的不同而改变,因此不建议存储硬编码的路径。
从 API 级别 24 及更高版本开始,可以使用以下内容来检索应用内部存储的根目录的文件路径:
getApplicationContext().getDataDir();
在 API 级之前,可以使用以下:
getApplicationContext().getFilesDir().getParent();
外部存储器
如前所述,在 Android 4 之前,/sdcard
目录是可以找到连接的 SD 卡的存储位置。从 Android 4 开始,这变成了所谓的“内部 SD 卡”或更常见的“外部存储”, 12 ,这是一个跨应用的共享空间。从 Android 10 开始,这个“共享空间”变成了一个术语,称为作用域存储; 13 这意味着即使应用可以访问外部存储(需要READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE
权限),它现在也只能进行只读访问,以及访问应用特定的目录和应用创建的特定类型的媒体(除非它可以访问MANAGE_EXTERNAL_STORAGE permission)
)。除此之外,在有多个用户的设备上,每个用户都有自己的隔离作用域存储。
记住这一点,如果针对 API 级别 28 或更低进行编译,那么您可以使用Environment
类来检索设备外部存储的路径。然而,如果运行在 API 级别 29 及以上,这是不赞成的,返回的文件路径将不可访问,所以应该使用应用上下文(对于作用域存储)。
获取全局外部存储(API 等级 1 –
28):
Environment.getExternalStorageDirectory();
获取应用范围(沙盒)外部存储(API 级别 29+):
getApplicationContext().getExternalFilesDir(null);
与内部存储不同,如果应用想要向全局外部存储写入或读取文件,它需要WRITE_EXTERNAL_STORAGE
或READ_EXTERNAL_STORAGE
权限。写入外部存储是运行时(危险)权限,而读取只是普通/清单权限。作用域存储不需要权限。
以下是设置清单权限的示例:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
由于写入外部存储器是一种危险的权限,它要求用户在应用授予访问权限之前接受运行时权限。这将向用户显示一个通知,如图 6-1 所示。
外部存储器写运行时请求可以用以下代码运行。
if (Build.VERSION.SDK_INT >= 23) {
if (!isStoragePermissionGranted()){
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1234567);
}
}
检查是否已经设置了 WRITE_EXTERNAL_STORAGE
运行时权限的示例方法:
public boolean isStoragePermissionGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
Log.v(TAG,"Permission is granted");
return true;
} else {
Log.v(TAG,"Permission is revoked");
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
return false;
}
}
else { //permission is automatically granted on sdk<23 upon installation
Log.v(TAG,"Permission is granted");
return true;
}
}
权限改变时执行的活动回调 :
图 6-1
运行时权限对话框
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Log.v(TAG,"Permission: "+permissions[0]+ " was "+grantResults[0]);
//write file to external storage
}
}
文本文件
文件可以写入应用内部或外部存储的任何地方(记住从 Android 10 开始的作用域存储)。这些目录的位置可以通过前面提到的方法调用来检索。
要创建一个操作特定文件的 File 对象,使用下面的代码,其中 filename 是一个表示文件路径的 String 对象:
String filename = "myFile.txt";
File file = new File(getApplicationContext().getFilesDir(), filename);
写文件:
public void writeFileInInternalStorage(File fileToWrite, String fileBody){
// If file doesn't exist attempt to make full directory path
if(!fileToWrite.exists()){
fileToWrite.getParentFile().mkdir();
}
// Write to file
try{
FileWriter writer = new FileWriter(fileToWrite);
writer.append(fileBody);
writer.flush();
writer.close();
}catch (Exception e){
e.printStackTrace();
}
}
读取文件:
private String readFromFile(File file) {
String ret = "";
try {
FileInputStream inputStream = new FileInputStream(file);
if ( inputStream != null ) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String receiveString = "";
StringBuilder stringBuilder = new StringBuilder();
while ( (receiveString = bufferedReader.readLine()) != null ) {
stringBuilder.append("\n").append(receiveString);
}
inputStream.close();
ret = stringBuilder.toString();
}
}
catch (FileNotFoundException e) {
Log.e("TAG", "File not found: " + e.toString());
} catch (IOException e) {
Log.e("TAG", "Can not read file: " + e.toString());
}
return ret;
}
删除文件:
private void deleteStorageFile(File file){
file.delete();
}
数据库
下面详细介绍如何在 Android 中创建一个简单的 SQLite 数据库。在 Android 中,默认情况下,数据库是在内存的/databases
目录中创建的。
Define a schema and contract:
public final class databaseTemplate {
// Inner class that represents the table values
public static class databaseData implements BaseColumns {
public static final String TABLE_NAME = "TABLE_NAME";
public static final String COLUMN_ONE = "FIRST_COLUMN_NAME";
public static final String COLUMN_TWO = "SECOND_COLUMN_NAME";
}
}
创建数据库助手:
public class databaseHelper extends SQLiteOpenHelper {
public databaseHelper(Context context) {
super(context, NAME, null, VERSION);
}
private static final String SQL_CREATE_INSTRUCTION =
"CREATE TABLE " + databaseTemplate.databaseData.TABLE_NAME + " (" +
databaseTemplate.databaseData._ID + " INTEGER PRIMARY KEY," +
databaseTemplate.databaseData.COLUMN_ONE + " TEXT," +
databaseTemplate.databaseData.COLUMN_TWO + " TEXT)";
private static final String SQL_DELETE_INSTRUCTION =
"DROP TABLE IF EXISTS " + databaseTemplate.databaseData.TABLE_NAME;
public static final int VERSION = 1; //increment if schema changed
public static final String NAME = "database.db"; // database name
// Standard Database functions
public void onCreate(SQLiteDatabase database) {
database.execSQL(SQL_CREATE_INSTRUCTION);
}
public void onUpgrade(SQLiteDatabase database, int previousVersion, int currentVersion) {
database.execSQL(SQL_DELETE_INSTRUCTION);
onCreate(database);
}
}
向数据库添加行:
private void addDataToDatabase(Context context, String columnOneData, String columnTwoData){
databaseHelper databaseHelper = new databaseHelper(context);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(databaseTemplate.databaseData.COLUMN_ONE, columnOneData);
contentValues.put(databaseTemplate.databaseData.COLUMN_TWO, columnTwoData);
database.insert(databaseTemplate.databaseData.TABLE_NAME, null, contentValues);
}
读取数据库:
private List readFromDatabase(Context context){
final String TABLE_NAME = databaseTemplate.databaseData.TABLE_NAME;
String selectQuery = "SELECT * FROM " + TABLE_NAME;
databaseHelper databaseHelper = new databaseHelper(context);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = database.rawQuery(selectQuery, null);
ArrayList data = new ArrayList();
if (cursor.moveToFirst()) {
do {
int idIndex = cursor.getColumnIndexOrThrow(databaseTemplate.databaseData._ID);
int columnOneIndex = cursor.getColumnIndexOrThrow(databaseTemplate.databaseData.COLUMN_ONE);
int columnTwoIndex = cursor.getColumnIndexOrThrow(databaseTemplate.databaseData.COLUMN_TWO);
String idValue = String.valueOf(cursor.getString(idIndex));
String columnOneValue = String.valueOf(cursor.getString(columnOneIndex));
String columnTwoValue = String.valueOf(cursor.getString(columnTwoIndex));
ArrayList rowData = new ArrayList();
rowData.add(idValue);
rowData.add(columnOneValue);
rowData.add(columnTwoValue);
data.add(rowData);
} while (cursor.moveToNext());
}
cursor.close();
return data;
}
从数据库中删除一行:
private void deleteRowFromDatabase(Context context, int rowIdToRemove){
databaseHelper databaseHelper = new databaseHelper(context);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String selection = databaseTemplate.databaseData._ID + " LIKE ?";
String[] selectionValues = {String.valueOf(rowIdToRemove)};
database.delete(databaseTemplate.databaseData.TABLE_NAME, selection, selectionValues);
}
共享偏好
共享偏好 14 被写入应用内部存储器的/shared_prefs
目录。它们采用 XML 键-值对的形式,其中共享的首选项名称成为 XML 文件的名称(附加。xml)。
从下面可以看出,在这个共享偏好中有两个条目,一个类型为 long
的条目称为 last_cleanup
,另一个类型为 string
的条目称为 webapp_name
:
<?xml version=’1.0’ encoding=’utf-8’ standalone=’yes’ ?>
<map>
<long name="last_cleanup" value="1585307669741" />
<string name="webapp_name" value="webapp"/>
</map>
共享首选项接受以下原始数据类型
-
线
-
(同 Internationalorganizations)国际组织
-
布尔代数学体系的
-
长的
-
浮动
-
字符串集
创建共享首选项时,必须提供共享首选项的名称。这是 XML 文件的名称。除此之外,您还需要提供要保存到共享首选项的键-值对,然后使用.apply()
(在 API level 8 中添加)进行异步保存,或者使用.commit()
进行同步保存(返回一个表示成功的布尔值)。除此之外,您还必须提供模式。模式是一个整数,代表以下内容之一:
-
MODE_PRIVATE -默认模式,文件只能由创建它的应用访问。
-
MODE _ WORLD _ READABLE——在 API 等级 17 中被弃用,从 API 等级 24 开始抛出安全异常。这允许所有应用对创建的文件具有读取权限。
-
MODE _ WORLD _ WRITEABLE——在 API 等级 17 中被弃用,从 API 等级 24 开始抛出安全异常。这允许所有其他应用对创建的文件进行写访问。
-
MODE_MULTI_PROCESS -在 API 级别 23 中已弃用。在 API 级别 10 之前,这是默认行为。此后,默认行为是将共享首选项加载到内存中,并对内存中的共享首选项执行读取和修改。这意味着,如果在应用运行时编辑了原始文件,它将被应用对共享首选项所做的任何更改覆盖。设置此模式后,将检查共享首选项的修改情况,即使它已经加载到内存中。
添加键值对:
SharedPreferences sharedPref
= getApplicationContext().getSharedPreferences("MySharedPref",
MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("key", "value");
editor.apply();
从共享首选项中读取时,必须提供被检索类型的默认值。如果提供的密钥不存在,则返回:
SharedPreferences sharedPref
= getApplicationContext().getSharedPreferences("MySharedPref",
MODE_PRIVATE);
String value = sharedPref.getString("key", "default value");
删除共享偏好类似于编辑偏好;但是,不使用. put 方法。改用 remove()方法:
SharedPreferences sharedPref = getSharedPreferences("MySharedPref", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.remove("key");
editor.apply();
文件供应器
截至 Android API 等级 24,不能直接与其他应用共享文件 URIs 15 。在这之后,你必须使用一个FileProvider
16 来安全地从你的应用 17 提供一个文件给另一个应用。
在设置 FileProvider
,
时,添加一个 provider
标签到应用的清单文件 application
标签中,遵循以下格式,记住要更改包名:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.storage.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
在这之后,在你的应用res
目录下,在名为xml
的子文件夹filepaths.xml.
中创建一个 XML 资源,然后定义共享路径和将在 URI 中显示的名称。下面的例子共享应用的myText
文件夹的整个根目录,其中 URI 是content://com.example.storage.fileprovider/myText
。
RES 中的 filepaths xml 文件应该包括:
<?xml version=”1.0” encoding=”utf-8”?>
<paths>
<files-path path="/" name="myText" />
</paths>
在此之后,文件(在指定的目录内)可以像平常一样通过意向发送:
File file =new File(getApplicationContext().getFilesDir(),"/myText/test.txt");
writeFileInInternalStorage(file, "Hello World"); // This function is referenced in the Text Files section.
Intent intent = new Intent(Intent.ACTION_SEND);
Uri contentUri = getUriForFile(getApplicationContext(), "com.example.storage.fileprovider", file); //replace with package name
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_STREAM,contentUri);
startActivity(Intent.createChooser(intent , "Sharing Text File"));
素材文件夹
assets 文件夹 18 可由应用开发人员用于在编译时间之前存储任意只读数据,这些数据可在运行时读取。与 res (Resources)文件夹不同,任何文件类型都可以存储在这个目录中;但是,必须将其添加到应用预编译中。为此,通过app
➤ New
➤ Folder
➤ Assets Folder
在相对文件路径\app\src\main\assets
或 Android Studio 中创建一个文件夹。Post 编译时,assets 文件夹位于 APK 的根目录下/assets
。
访问素材文件夹中的文件:
private String readFileFromAssets(String filename, String type){
BufferedReader reader = null;
StringBuilder stringBuilder = new StringBuilder();
try {
reader = new BufferedReader(
new InputStreamReader(getAssets().open(filename), type)); //e.g. "utf-8"
String mLine;
while ((mLine = reader.readLine()) != null) {
stringBuilder.append(mLine+"\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return stringBuilder.toString();
}
资源
与素材类似,这些资源是只读的,并且在预编译时提供。与 assets 文件夹不同,存储在 resource 文件夹中的数据(位于 APK 根目录下的/res)
)必须遵循特定的约定,比如文件类型。可以包含在 res 文件夹中的数据示例包括字符串和整数的 XML 文件以及应用图标。
从 res 文件夹:返回一个字符串(字符串应该存储在 strings.xml 文件中)
getApplicationContext().getString(R.string.string_name)
从 res 文件夹中返回图像。请记住,图像存储在 Res drawable 文件夹的根目录下,并且在访问图像时,不要包括图像扩展名:
ContextCompat.getDrawable(getApplicationContext(), R.drawable.image_name)
七、Android 唯一标识符
唯一标识符可用于一系列任务,从允许用户设置广告偏好、唯一标识用户,到标识特定设备。当涉及到由 Android 权限和 Google Play 开发者政策强制执行的唯一标识符的使用时,也存在强烈的隐私问题。
为了解决用户隐私问题,从 Android 10 (API 级别 29)开始,在如何从 Android 应用中访问硬件唯一标识符方面有了很大的变化。也就是说,应用必须是设备或配置文件的所有者,拥有特殊的运营商权限,或拥有READ_PRIVILEGED_PHONE_STATE
特权权限,才能访问不可重置的设备标识符。READ_PRIVILEGED_PHONE_STATE permission
仅适用于使用设备平台(系统应用)密钥 1 签名的应用。
Google Play 广告 ID
截至 Android KitKat (API level 4.4),Google Play 广告 ID 2 可用于唯一识别设备用户。当在设备上可用时,出于广告目的使用任何其他设备唯一标识符都是违反 Google Play 开发者计划政策的。广告 ID 对最终用户的好处是它既可重置又可用于定制个性化广告。返回的 ID 的一个例子是9fdbfa02-7f28-422e-944e-f02393a9360e
的字符串表示。由于广告 ID 由 Google Play 服务 API 提供,这意味着它只能在具有 Google Play 服务的设备上使用。
可以通过将以下库路径添加到 build.gradle 文件的 dependencies 标签来使用广告 ID:
implementation 'com.google.android.gms:play-services-ads:7.0.0'
检索广告 ID(这不能在主线程上完成——这个例子使用了一个 AsyncTask 详见 第九章:
void getAdvertisingID(final Context context){
AsyncTask.execute(new Runnable() {
@Override
public void run() {
Info adInfo = null;
try {
adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context);
} catch (IOException e) {
e.printStackTrace();
} catch (GooglePlayServicesNotAvailableException e) {
e.printStackTrace();
} catch (GooglePlayServicesRepairableException e) {
e.printStackTrace();
}
String adId = adInfo != null ? adInfo.getId() : null;
Log.v("Advertising ID",adId);
}
});
}
Android ID(安全设置 Android ID–SSAID)
这是一个唯一的 64 位数字(例如,ce79870fa5cbfb56
),是 Android 在广告之外的活动中识别设备用户的首选方法。这个唯一标识符在所有版本的 Android 上都可用,不需要任何额外的权限,并且作为设备出厂重置的一部分被重置。
访问 安卓 ID :
Log.v("Android ID",Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID));
SIM 序列号
SIM 序列号用于国际识别,通常分为 19 位数字 3 。这可以分解为两位数字的电信 ID、两位数字的国家代码、两位数字的网络代码、四位数字的生产月份和年份、两位数字的交换机配置代码、六位数字的 SIM 号码和一个校验位。
SIM 序列号在 Android Pie (API 级别 28)及以下版本的 Android 上可用,在 Android 10 及以上版本中受限于READ_PRIVILEGED_PHONE_STATE
权限。使用 Android 10 之前的 SIM 序列号需要READ_PHONE_STATE
运行时权限。
检索 SIM 序列号:
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("SIM Serial Number", telephonyManager.getSimSerialNumber());
电话号码
为使用电话的设备编写应用时,电话号码可以用作唯一标识符。该标识符在所有版本的 Android 上都可用,并与设备上的 SIM 卡(如果有)绑定,需要READ_PHONE_STATE
或READ_PRIVILEGED_PHONE_STATE permission
才能访问。
正在检索 的电话号码 :
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("Phone Number", telephonyManager.getLine1Number());
IMEI 和美国
为了唯一识别设备和防止被盗,所有移动设备都被指定了一个 IMEI 或 MEID 号码。取决于设备的网络将取决于设备具有哪个标识符。如果在 GSM(全球移动系统)系统上,设备将具有 IMEI 号码,如果在 CDMA(码分多址)系统上,设备将具有 MEID 号码。两者的主要区别在于 IMEI 是 14 位数字,而 MEID 是 15 位数字。与其他硬件标识符类似,IMEIs 和 MEIDs 都可以在 Android 10 之前的设备上使用,如果应用具有READ_PHONE_STATE
权限,就可以读取这些标识符。但是,从 Android 10 开始,一个应用需要READ_PRIVILEGED_PHONE_STATE
。
Pre 及包含 Android N (25)访问:
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("Device ID", telephonyManager.getDeviceId());
贴吧(含)安卓 O (26):
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("IMEI", telephonyManager.getImei());
Log.v("MEID", telephonyManager.getMeid());
八、混淆和加密
记录
Android 中的Log
类可用于在 logcat 中创建日志消息(可通过adb logcat
命令访问);这些日志有几种不同的级别,它们是
-
log . wtf--“多么可怕的失败”(被视为极端错误)
-
Log.e -错误
-
Log.w -警告
-
Log.i -信息
-
Log.d -调试
-
Log.v -详细
如上所述,这些日志消息可以通过 logcat 读取。Logcat 是 Android 的日志系统,记录从系统消息到堆栈跟踪的所有内容。应用可以通过使用Log
类写入 logcat,反过来,这些消息可以通过使用adb logcat
命令或在 Android Studio 等程序中查看。
无论您选择哪种级别,所有日志级别都将显示在 logcat 中。例如,以下日志程序代码在指定 debug 的同时,将被记录到 logcat 中,而不考虑构建类型(即,它将被记录在发布构建中)。同样值得记住的是,调试日志消息将被编译到发布应用中。例如,下面我们可以看到 Java 中的日志消息和 Smali(dal vik 字节码的可读表示)中的反汇编发布版本之间的比较。
在 Java 中 :
Log.d(TAG, "I am a normal debug log message");
In Smali:
iget-object p1, p0, Lcom/example/logger/MainActivity;->TAG:Ljava/lang/String;
const-string v0, "A log using is loggable"
invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
标准测井
开发人员不想使用标准日志记录有几个原因。这些归结为安全性和性能,其中日志应该对恶意参与者隐藏,并且应该在最终用户的设备上避免日志泛滥。
标准日志:
Log.d(TAG, "I am a normal debug log Message");
最终常量变量
限制发布代码中日志语句数量的一种支持方式是使用 Gradle prebuild 生成的 Gradle BuildConfig
文件。生成该文件时,如果构建调试版本,将下面一行设置为true
,如果构建发布版本,将下面一行设置为false
。
build config 文件中的调试值:
public static final boolean DEBUG = Boolean.parseBoolean("true");
实现调试常量:
if (BuildConfig.DEBUG){
Log.d(TAG,"This is a log that won't be compiled in a release build.");
}
当为了发布而构建并设置为 false 时,Java 编译器会发现最终变量不可能为 true,因此不会编译 if 语句中的代码。这意味着日志不会显示在 logcat 中,也意味着日志字符串不会像普通的日志消息一样存在于应用的源代码中。
如果不使用 Gradle,也可以实现与使用BuildConfig.DEBUG
类似的效果。这可以通过使用一个最终布尔值来完成,在调试时将其设置为true
,在发布版本中设置为false
。
设置一个 自定义调试常数 :
final boolean SHOULD_LOG = false;
if (SHOULD_LOG){
Log.d(TAG," A log that should never happen...");
}
使用。可记录
检查是否应该显示日志消息的另一种方法是使用内置在Log
类中的.isLoggable
方法。该方法检查为特定标记设置的日志级别(应用的默认设置是INFO
)。日志级别在一个层次结构中工作,如本节顶部所列。这意味着如果日志级别设置为 Verbose,那么它上面的所有级别也将是true
。与使用BuildConfig,
不同的是,这个值可以通过编程来改变,这个字符串将被编译到应用的代码库中。
log . is logtable 示例:
if (Log.isLoggable(TAG,Log.DEBUG)){
Log.d(TAG,"A log using is loggable");
}
这个日志级别可以通过 shell 使用来设置
setprop log.tag.<log_tag> <log_level>
动态检查是否可调试
这里讨论的限制写入 logcat 的日志数量的最后一种技术是通过动态检查应用是否处于调试状态。如上所述,由于该值可以改变,日志和字符串将被编译到构建的发布应用中。
动态检查应用是否处于调试状态的示例:
boolean isDebuggable = ( 0 != ( getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
if (isDebuggable){
Log.d(TAG,"This log will check programmatically if the app is debuggable.");
}
阿帕尔德
最常见的 Android 混淆工具是 ProGuard,DexGuard 1 是高级替代工具。ProGuard 分析和优化的是 Java 字节码,而不是直接的 Java/Kotlin 代码库。ProGuard 实现了一组技术 2 ,它们是:
-
收缩 -识别并删除不可达或未使用的死代码。包括类、字段、方法和属性。
-
优化器——对代码和代码流进行优化,以改变性能。
-
Obfuscator——将代码库的某些方面(例如,类、字段和方法)重命名为故意模糊和无意义的名称。
-
预验证器——对字节码执行预验证检查,如果检查成功,类文件会用预验证信息进行注释。这是 Java Micro Edition 和 Java 6 及更高版本所必需的。
启用 ProGuard
将gradle.build
文件中的buildTypes
标签编辑为minifyEnabled true
。
例如:
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
ProGuard 映射文件
在使用 Gradle 进行发布构建时,遵循上述步骤之后,Java 字节码将已经被 ProGuard 分析过了。ProGuard 将提供所经历阶段的日志文件。它保存在应用项目根目录的以下相对路径中。
映射文件相对位置:
app/build/outputs/mapping/release/mapping.txt
该文件显示了 ProGuard 已经实现的更改。下面是该文件一部分的示例。在这个例子中可以看到,MainActivity
中的函数showString
已经被允许列出,而 MainActivity 中另一个名为 loadMe 的函数还没有被允许列出,现在被重命名为n
。
映射文件示例:
com.example.java_dexloadable.MainActivity -> com.example.java_dexloadable.MainActivity:
java.lang.String loadMe() -> n
1:1:java.lang.String com.example.java_dexloadable.StringsClass.stringGetter(int):0:0 -> showString
1:1:void showString(android.content.Context,int):0 -> showString
2:2:void showString(android.content.Context,int):0:0 -> showString
如果一个函数或类不在这个映射中,那么它已经被缩小了(意味着它没有在代码中被使用),或者它没有被混淆(由于被允许列出)。
ProGuard 允许列表
默认情况下,ProGuard 会收缩、优化和混淆 Java 字节码中的所有内容。这可以通过编辑位于应用目录根目录下app\proguard-rules.pro
的 ProGuard 文件来控制。该文件可以重命名和移动,并在gradle.build
文件中指定。
下面的示例规则允许-列出类 com.example.java_dexloadable.MainActivity
中的 showString 函数。这里你需要指定类和函数的访问级别(公共的,私有的,包私有的,等等)。)以及函数的参数:
-keep class com.example.java_dexloadable.MainActivity {
public showString(android.content.Context, int);
}
下面的例子也是一样;然而,在这个例子中, MainActivity
中的所有函数都被允许列出:
-keep class com.example.java_dexloadable.MainActivity {
public *;
}
不同类型的保留
在前面的两个示例中,使用了 keep 关键字。有几种不同类型的 3 关键字。这些总结在表 8-1 中。
表 8-1
保留的程序类型
| |没有规则
|
-保持
|
-保留类成员
|
-保留姓名
|
| --- | --- | --- | --- | --- |
| 缩班 | -好的 | x | -好的 | -好的 |
| 收缩成员 | -好的 | x | x | -好的 |
| 混淆类 | -好的 | x | -好的 | x |
| 混淆成员 | -好的 | x | x | x |
入口点
ProGuard 自动将允许列表(也称为白名单)入口点指向一个应用(例如,MAIN
或LAUNCHER
类别的活动)。重要的是要记住,作为反射的一部分使用的入口点不会自动允许列出,所以如果使用 refection,这些入口点必须手动允许列出。然而,这将最小化混淆的有效性,因为纯文本组件的频率会更高。由 ProGuard 自动添加到允许列表的入口点通常包括具有 main 方法、applets、MIDlets、activities 等的类。这也包括调用本机 C 代码的类。
示例规则
在下面的例子中,Java 包名是java_dexloadable,
,所有的规则都被添加到了Proguard-rules.pro
文件中。
保留(允许列表)MainActivity 类中的所有方法:
-keep class com.example.java_dexloadable.MainActivity {
public *;
}
保留(允许列出)MainActivity 类中的 showString 函数以及 MainActivity 类本身:
-keep class com.example.java_dexloadable.MainActivity {
public showString(android.content.Context, int);
}
保留(允许列表)顶层包下的所有内容(不应使用):
-keep class com.example.java_dexloadable.** { *; }
保留(允许列出)函数字符串,但不保留类字符串 class 本身:
-keepclassmembers class com.example.java_dexloadable.StringsClass {
public stringGetter(int);
}
将整个包重新打包成一个根:
-repackageclasses
不执行程序收缩步骤:
--dontshrink
公钥/证书锁定
公钥锁定允许应用将特定的加密公钥与给定的 web 服务器相关联。这反过来用于降低中间人攻击的可能性。
执行公钥锁定时,需要所连接的 web 服务器的公钥。
有两种相当简单的方法可以做到这一点——要么使用下面的 openssl
命令,要么使用下面的代码并从错误消息中提取公钥:
openssl x509 -in cert.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
在下面的代码中,使用前面消息中的公钥散列作为您的散列。在 Android 中,联网不能在主线程上执行,因此它需要在长期运行的服务、异步任务或线程中执行(详见第九章)。
将以下依赖项添加到 build.gradle 文件中,因为这个示例使用 OkHTTP 库。还要确保应用具有互联网权限,并且没有在主线程上进行联网:
implementation("com.squareup.okhttp3:okhttp:4.9.0")
证书锁定示例:
String hostname = "google.com";
CertificatePinner certPinner = new CertificatePinner.Builder()
.add(
hostname,
"sha256/MeCugOOsbHh2GNsYG8FO7wO7E4rjtmR7o0LM4iXHJlM="
)
.build();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.certificatePinner(certPinner)
.build();
HttpUrl.Builder urlBuilder = HttpUrl.parse("https://"+hostname).newBuilder();
String url = urlBuilder.build().toString();
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, "{\"test\":\"testvalue\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Log.v(TAG,request.toString());
Response response = null;
try {
response = okHttpClient.newCall(request).execute();
ResponseBody jsonData = response.body();
Log.v(TAG, jsonData.toString());
} catch (IOException e) {
e.printStackTrace();
}
return Result.success();
AES 加密
AES 使用对称算法,这意味着加密和解密使用相同的密钥。下面是一个用 Java 实现 AES-256 加密的轻量级例子。
下面是 Java 中 AES-256 加密方法的一个例子:
try {
Cipher cipher = null;
cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
KeyGenerator keygen = null;
keygen = KeyGenerator.getInstance("AES");
keygen.init(256);
SecretKey key = keygen.generateKey();
String plainTextString = "I am a plain text";
String cipherTextAsString = "N/A";
String newPlainTextAsString = "N/A";
byte[] plainText = plainTextString.getBytes();
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cipherText = new byte[0];
cipherText = cipher.doFinal(plainText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
cipherTextAsString = new String(cipherText, StandardCharsets.UTF_8);
}
IvParameterSpec iv = new IvParameterSpec(cipher.getIV());
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] newPlainText = cipher.doFinal(cipherText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
newPlainTextAsString = new String(newPlainText, StandardCharsets.UTF_8);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.v(getApplicationContext().getPackageName(), "The plaintext '" + plainTextString + "' encrypted is " + Base64.getEncoder().encodeToString(cipherText) + " and decrypted is '" + newPlainTextAsString);
}
}catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
九、服务、启动器和组件
长期运行的服务
在 Android 中,有几种方法可以在当前活动结束后一次性或周期性地运行“作业”。这里讨论的技术将在以下两种类型之间变化:
-
将强绑定到当前活动,比如
AsyncTask
,意思是如果活动结束,那么任务被垃圾回收。 -
没有将强绑定到当前活动,例如
JobScheduler
,其中任务甚至在父活动本身被垃圾收集后仍继续。
扳机
触发器是长期运行的服务如何分配任务和运行的机制,它们是服务的初始入口点。大多数触发器将为服务提供某种形式的持久性。这些触发可以在表 9-1 中看到。
表 9-1
长期运行的服务触发器
|引发
|
描述
|
| --- | --- |
| 目的 | 从代码中直接触发,通常来自用户交互。 |
| 手机闹钟服务 | 在未来的特定时间触发,一次性或重复发生。 |
| 广播接收器 | 收到特定广播消息时触发。例如,BootComplete
、PowerConnected
或自定义接收器。 |
| 传感器回调 | 收到特定传感器值时触发。 |
| 工作管理器 | 根据 API 等级使用JobScheduler
或AlarmManager
和BootComplete
。 |
| 作业调度程序 | 从 API level 21 (Android L)开始,AlarmManager
的更智能实现允许根据网络、空闲和充电状态运行。也瞌睡顺从。 |
服务
有几种类型的“服务”可以运行,为应用提供长期运行的后台工作。这些可以在表 9-2 中看到。
表 9-2
长期运行的服务类型
|服务
|
描述
|
| --- | --- |
| 工作管理器 | 封装启动器和服务元素(考虑到底层实现被抽象时的向后兼容性)。并发工作之间的最小间隔为 15 分钟,每个工人最多只能运行 10 分钟。WorkManagers
重启后也会自动保持(如果许可的话,使用BootComplete
广播接收器)。 |
| 作业调度程序 | 封装了启动器和服务元素。这些都是高度可定制的,可以根据网络、空闲和电池状态等环境因素运行工作。除此之外,它们还可以被定义为以特定的时间间隔或特定的时间段运行,并在重启后持续运行(如果许可可用,使用BootComplete
广播接收器)。工作之间的最小间隔是 15 分钟。 |
| 服务 | 有许多类型的服务;然而,最常见的一种是意向服务,其中工作请求按顺序运行。后续请求(对服务的意图)会一直等到第一个操作完成。 |
| 线 | 主要用于不想在 UI 线程上工作的作业(例如网络),但是,只要它们的父线程没有被杀死,就可以用于长期运行的后台工作。线程被绑定到父应用。 |
| 异步任务 | 绑定到父Activity
的生命,意味着如果活动结束或者被杀死,那么AsyncTask
也是。这样做的好处是更容易将工作从AsyncTask
推回到 UI 线程。 |
| 前台服务 | 从 API 26 开始,后台服务(如意向服务)被限制为仅在应用处于前台时运行。取而代之的是前台服务,在前台服务运行时,它们必须向用户显示一个持续的通知(例如,一个音乐应用在播放时显示一个音乐播放器)。 |
IntentService、AlarmManager 和 BootComplete
如前所述,IntentService
1 是一种不能直接与 UI 交互的服务。IntentService
中的工作请求按顺序运行,请求将一直等待,直到当前操作完成。在IntentService
上运行的操作不能被中断。
一个AlarmManager
2 是 Android 中的一种机制,允许代码在后台线程中延迟和继续运行。一个AlarmManager
可以被配置为在未来的特定时间以预先配置的时间间隔运行。AlarmManager
还有一个setAlarmClock
选项,允许它在设备处于低功耗空闲或打盹模式时触发。
可以设置一个BroadcastReceiver
来监听引导完成意图 3 ,如第四章所述。该意图在设备重启后启动时发送。反过来,在接收到BootComplete
意图后启动AlarmManager
将意味着设备重启后后台服务继续运行。
从 Android Oreo 8(API 26 级)开始,Android 服务不再能从后台进程启动。 4 这意味着在 Android 8+中,要使用JobSchedulers
或前台服务。请记住,虽然该功能仅适用于针对 Android 8+的应用,但它可以由用户在设置 5 页面中启用,这也对服务如何在后台运行提出了许多额外的限制。
下面的函数将设置一个 报警管理器 按照 waitBeforeRepeatInMinutes 参数的定义每 x 分钟重复一次:
public void startPeriodicWork(long waitBeforeRepeatInMinutes){
// Construct an intent that will execute the AlarmReceiver
Intent intent = new Intent(context, AlarmReceiver.class);
// Create a PendingIntent to be triggered when the alarm goes off
final PendingIntent pIntent = PendingIntent.getBroadcast(context, AlarmReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup periodic alarm every every half hour from this point onwards
long firstMillis = System.currentTimeMillis(); // alarm is set right away
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// First parameter is the type: ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC_WAKEUP
// Interval can be INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_DAY
if (alarm != null) {
alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis,
waitBeforeRepeatInMinutes * 60 * 1000, pIntent);
}
}
接下来,创建 AlarmManager BroadcastReceiver 类。在 Android manifest 中设置 process 属性,这样如果应用关闭了 6 ,它将继续保持活动状态。作为其中的一部分,将 BroadcastReceiver 添加到 AndroidManifest.xml 文件中。
<receiver android:name=".receivers.AlarmReceiver"
android:process=":remote" />
在 警报接收者 中增加以下内容。java:
public class AlarmReceiver extends BroadcastReceiver {
public static final int REQUEST_CODE = 12345;
// Triggered by the Alarm periodically (starts the service to run task)
@Override
public void onReceive(Context context, Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Alarm Receiver with tid "+ tid);
TaskManager taskManager = new TaskManager(context);
taskManager.oneOffTask();
}
}
接下来,为 BootComplete 添加 BroadcastReceiver。将以下内容添加到 AndroidManifest.xml 文件中:
<receiver android:name=".receivers.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
然后创建BootReceiver.java
类:
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Boot Complete Receiver with tid "+ tid);
TaskManager taskManager = new TaskManager(context);
taskManager.startPeriodicWork(5);
}
}
最后创造出IntentService
;为此,创建一个名为 ServiceManager.java: 的文件
public class ServiceManager extends IntentService {
public ServiceManager() {
super("ServiceTest"); //Used to name the worker thread, important only for debugging.
}
@Override
protected void onHandleIntent(Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Service with tid "+ tid);
String val = intent.getStringExtra("foo");
//todo Add the work to be performed here.
}
}
将此服务添加到 AndroidManifest.xml
文件:
<service android:name=".managers.ServiceManager"
android:exported="false"/>
前台服务
Android 中有一系列不同类型的服务 7 ,从启动的服务(运行在 UI 线程中)到IntentService
(运行在自己的线程中)再到绑定的服务(只要有一个活动绑定到它就运行)。
截至 Android 8 Oreo (API 26),Android 应用运行后台服务有限制,除非应用本身在前台。在这种情况下,应该使用startForegroundService()
方法,而不是使用context.startService()
方法。在此之后,服务有 5 秒钟的时间向用户显示通知并调用startForeground(1, notification)
方法,该方法在服务期间一直存在,直到stopForeground(true)
和stopSelf()
方法被调用。前台服务像任何其他服务一样扩展了Service
类,并且除了遵循前面的规则和限制之外,以相同的方式进行操作。
应该使用如下代码来标识应该使用后台服务还是前台服务:
Intent intent = new Intent(context, ServiceManager.class); //replace with an appropriate intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
}else{
context.startService(intent);
}
由于通知是前台服务启动的一部分,这意味着它的副产品是建立一个通知通道 8 (针对 Android 8.0 - API 级别 26 及以上)。添加通知通道是为最终用户提供细粒度访问的一种方式,允许他们更改通知设置并决定应用中的哪些通知通道应该可见。
以下显示了设置通知通道的示例:
public static void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance);
channel.setDescription(CHANNEL_DESC);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
发送通知:
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, 0);
Notification notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle("Notification Title")
.setContentText("Notification Text")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build();
startForeground(1, notification);
}
从 Android 9 (API level 28)开始,除了如下将您的服务添加到 Android 清单中,您还需要添加 FOREGROUND_SERVICE
权限:
<service android:name=".managers.ForegroundServiceManager"
android:exported="false"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
作业调度程序
从 Android 5 L (API 21)开始,当设备有更多可用资源时,作业调度器作为一种任务批处理作业的方式被引入。作为一个整体,多项工作可以由JobSchedulers
,
来完成,他们会将这些任务分成几批。这意味着所分配的工作可能不会按预期执行;然而,它将在该时间前后发生(例如,被指示每 15 分钟执行一次的任务可能在一次运行的 14 分钟后执行,而在另一次运行的 16 分钟后执行)。
JobSchedulers 最强大的功能之一是,如果设置或未设置特定的标准,例如,没有网络连接、电池正在充电或设备处于空闲状态,它们允许工作延期。还有一个选项是用setPeriodic
和setPersisted
来运行周期性的工作,并在重启后继续运行(如果应用拥有 RECEIVE_BOOT_COMPLETED 权限)。setOverrideDeadline
选项还允许在运行一次性工作的最大时间内,允许在强制运行工作之前等待一段时间。
添加以下内容。如果不想要周期工作器,则删除 setPeriodic 和 setPersisted 标记:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startJobScheduler(){
ComponentName serviceComponent = new ComponentName(context, JobSchedulerManager.class);
JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent);
//builder.setMinimumLatency(1 * 1000); // wait at least /Can't call setMinimumLatency() on a periodic job/
//builder.setOverrideDeadline(3 * 1000); // maximum delay //Can't call setOverrideDeadline() on a periodic job.
builder.setPeriodic(1000); //runs over time
builder.setPersisted(true); // persists over reboot
//builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); // require unmetered network
//builder.setRequiresDeviceIdle(true); // device should be idle
//builder.setRequiresCharging(false); // we don't care if the device is charging or not
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler != null) {
jobScheduler.schedule(builder.build());
}
}
然后做一个名为 JobSchedulerManager.java 的类:
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerManager extends JobService {
@Override
public boolean onStartJob(JobParameters jobParameters) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Job Scheduler with tid "+ tid);
//todo perform work here
// returning false means the work has been done, return true if the job is being run asynchronously
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
将下面的类添加到 AndroidManifest.xml 文件中:
<service android:name=".managers.JobSchedulerManager"
android:permission="android.permission.BIND_JOB_SERVICE"/>
一个JobScheduler
服务必须用先前的权限来保护,这样只有具有该权限的应用才能给该服务分配任务。如果在清单中声明了作业服务,但未使用此权限进行保护,则该服务将被系统忽略。
工作经理
如 Android 文档所述, 9 WorkManagers
从 API 14 开始向后兼容。API 14-22 上使用了BroadcastReceiver
和AlarmManager
的组合,API 23+上使用了JobScheduler
。使用WorkManager
而不是AlarmManager
的一个主要缺点是对它们的运行时间有限制(这是从在幕后使用JobScheduler
继承而来的);这包括WorkManager
运行时间不得超过 10 分钟,并且在当前工作开始至少 15 分钟后才能执行另一项连续工作。这样做的原因是为了遵守瞌睡限制。
使用工作管理器时,将以下依赖项添加到 gradle.build
文件中:
def work_version = "2.3.3"
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
下面的代码执行任务并启动一个工作管理器:
PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(
com.example.taskscheduler.managers.WorkManager.class, 15, TimeUnit.MINUTES)
.build(); //update path to match your created WorkManager.java class
WorkManager.getInstance().cancelAllWork();
WorkManager.getInstance().enqueue(work);
最后创建一个名为 WorkManager.java 的类:
public class WorkManager extends Worker {
Context context;
public WorkManager(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
this.context = context;
}
@Override
public Result doWork() {
int tid = Process.myTid();
Log.v("TaskScheduler", "Worker started with tid "+ tid);
// Todo run your work here.
return Result.success();
}
}
穿线
在main
应用线程(由系统为每个应用创建)之外,一个应用可以有多个额外的执行线程。然而,线程的设置相当简单,因为AsyncTasks
被绑定到它们的父线程(通常是一个Activity
)的生命中。在这种情况下,如果线程的父线程被破坏(即,被用户从任务堆栈中移除),则该线程受到垃圾收集(其中移除未使用的资源以为其他组件回收内存)。
启动线程:
public void startThread(){
Thread thread = new ThreadManager();
thread.start();
}
制作一个名为 ThreadManager.java 的 java 类:
public class ThreadManager extends Thread{
public ThreadManager() {
super();
}
@Override
public void run() {
long tid = getId();
// Todo do work here.
Log.v("TaskScheduler", "Starting a new thread "+ tid);
while (true){
Log.v("TaskScheduler", "In a thread: " + tid);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
异步任务
在 Android R–API level 30 中不推荐使用(建议使用标准的java.util.concurrent
或 Kotlin 并发实用程序)–
AsyncTasks 允许在单独的线程上短期运行(一次仅运行几秒钟)代码,同时还可以访问 UI 线程。当构造一个AsyncTask
对象时,需要提供三种类型;这些是:
-
传递到 AsyncTask 执行中的参数的类型
-
后台计算期间使用的进度单位的类型
-
后台方法的结果的类型
必须在主线程上加载、创建和执行——从 API 级开始,这是自动完成的。
调用异步任务:
AsyncTask<String, Void, Void> task = new myAsyncTask(getApplicationContext()).execute("example string");
取消异步任务:
task.cancel(true);
创建一个 AsyncTask 类(作为活动类的私有或包私有子类):
class myAsyncTask extends AsyncTask<String, Void, Void> {
private Context mContext;
public myAsyncTask(Context context) {
this.mContext = context;
}
@Override
protected Void doInBackground(final String... strings) {
final Context context = this.mContext;
runOnUiThread(new Runnable() {
@Override
public void run() {
for (String text:strings) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
}
}
});
return null;
}
}
电池和安全对长期运行服务的影响
长期运行的服务可以为用户提供无数有用的功能;然而,由于电池和安全问题,有无数种方法可以提前终止长期运行的服务。这随服务类型的不同而不同;但是,这通常是由电源管理限制引起的。 10 这些包括以下运行时的限制:
-
十二模式【11】
-
App 待机桶 12
-
应用背景限制
-
应用电池优化
瞌睡
Doze 是在 Android 6 (API level 23)中引入的,当设备空闲时(意味着设备最近一段时间没有接收用户交互),Doze 通过将应用运行时划分到维护窗口中来充当电池节省工具。这些应用可以在其中运行后台任务的窗口开始时很频繁,但是,随着时间的推移,设备空闲的时间越长,这些窗口就会变得越来越不同。这意味着虽然一个AlarmManager
可能被分配每 5 分钟运行一次的任务,但是瞌睡限制会阻止它定期运行。
报警管理器的限制:
-
setExact()
和setWindow()
报警管理器报警被推迟到下一个维护窗口。 -
setAndAllowWhileIdle()
和setExactAndAllowWhileIdle()
将在打盹维护窗口期间正常启动。setAlarmClock()
也将在维护窗口期间启动,系统在警报触发前不久退出休眠。
作业计划程序的限制:
- 作业调度程序或工作管理器被挂起。
其他限制:
- 网络访问、唤醒锁、Wi-Fi 扫描和同步适配器会被忽略和暂停。
您可以使用下面的命令测试一个应用如何在 Doze 模式下运行(在 API 级别 23 以上:
adb shell dumpsys deviceidle force-idle
通过运行以下命令可以退出空闲模式:
adb shell dumpsys deviceidle unforce
应用备用桶
Android 9 (API 级别 28)增加了另一个省电功能。此功能将所有应用分配到四个存储桶之一。每个制造商可以为如何将应用放入每个桶中设置自己的标准(Android 文档强调“机器学习”技术可以用于支持这一决策过程)。反过来,为什么应用被分配特定的存储桶的确切原理是未知的。
这些桶是
-
活动 -应用当前正在使用或最近使用过,包括应用是否已启动活动或正在运行前台服务,或者用户已点击应用的通知:
-
工作 -无限制
-
警报 -无限制
-
-
工作集-app 正常使用:
-
作业 -最多延迟 2 小时
-
警报 -最多延迟 6 分钟
-
-
频繁 -经常使用该应用,但不是每天都使用:
-
作业 -最多延迟 8 小时
-
警报 -最多延迟 30 分钟
-
-
稀有 -不常用的应用:
-
作业 -最多延迟 24 小时
-
警报 -最多延迟 2 小时
-
联网 -最多延迟 24 小时
-
-
Never -应用已安装,但从未运行:
- 如果应用从未运行过,组件将被禁用。
修改组件的状态
应用组件是用前面提到的AndroidManifest.xml file. A
编写的,有四种主要类型的组件,它们是
-
活动
-
服务
-
广播接收机
-
内容供应器
静态修改组件状态
可以通过在 Android 清单中编辑组件条目的android:enabled="false"
标签来静态修改组件。以下活动SecondaryActivity
已经通过其在 Android 清单中的条目被默认禁用。
设置组件的启用属性:
<activity android:name=".SecondaryActivity"
android:enabled="false"
/>
动态修改组件
组件可以通过编程设置为三种主要状态,它们是
-
组件 _ 启用 _ 状态 _ 默认
- 将组件设置为清单中定义的默认状态。
-
组件已启用状态已启用
- 显式启用组件。
-
组件 _ 启用 _ 状态 _ 禁用
- 显式禁用组件。禁用的组件不能使用或启动。
有两种其他的组成状态;然而,这些不能用setComponentEnabledSetting
方法设置。这些是
-
组件 _ 启用 _ 状态 _ 禁用 _ 用户
- 显式禁用该组件,并且可以由用户在适当的系统用户界面中重新启用。
-
组件 _ 启用 _ 状态 _ 禁用 _ 直到 _ 使用
- 这种状态意味着组件应该被识别为禁用的(即,在启动器中不显示活动),直到用户明确地试图在它应该被设置为启用的地方使用它。
启用组件 :
PackageManager packageManager = getApplicationContext().getPackageManager();
ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,PackageManager.DONT_KILL_APP);
返回组件的状态:
PackageManager packageManager = getApplicationContext().getPackageManager();
ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class);
int componentState = packageManager.getComponentEnabledSetting(componentName);
禁用一个组件 :
PackageManager packageManager = getApplicationContext().getPackageManager();
ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,PackageManager.DONT_KILL_APP);
创建 Android 启动器
Android Launcher 在 API 级别 1 中实现,是 Android 的一个组件,它允许 Android 应用作为 Android 设备主屏幕上的基本活动(如图 9-2 所示)。这些主屏幕可以由各个原始设备制造商设置;然而,其他著名的发射器包括 Facebook Home。必须在 Android 设备的设置菜单中设置一个启动器,如图 9-1 所示。
图 9-2
示例启动器
图 9-1
启动器设置
创建启动器应用
首先将以下属性添加到 AndroidManifest.xml 文件中的 activities 活动标记:
android:launchMode="singleTask"
然后给同一个活动标签的意图过滤器添加两个类别:
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.HOME" />
在这个阶段,该应用将作为一个启动器,并且可以从 Android 设备的设置中选择作为主屏幕。下面详细介绍了几个在创建启动器时有用的附加技术。
附加功能
检索应用列表:
private List<ResolveInfo> getListOfApplications(Context context){
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> pkgAppsList = context.getPackageManager().queryIntentActivities( mainIntent, 0);
return pkgAppsList;
}
检索应用的图标:
public static Drawable getActivityIcon(Context context, String packageName, String activityName) {
PackageManager pm = context.getPackageManager();
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName, activityName));
ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
return resolveInfo.loadIcon(pm);
}
设置图像视图。将一个 ImageView 对象添加到您的活动中,名称为 imageView:
<ImageView
android:id="@+id/imageView"
android:layout_width="129dp"
android:layout_height="129dp"
android:foregroundGravity="center_vertical"
app:srcCompat="@android:drawable/ic_dialog_alert"
android:layout_gravity="center"
/>
为 ImageView : 创建一个点击监听器
ImageView chromeIcon = (ImageView) findViewById(R.id.imageView);
chromeIcon.setImageDrawable(getActivityIcon(getApplicationContext(),"com.android.chrome", "com.google.android.apps.chrome.Main"));
ImageView img = findViewById(R.id.imageView);
img.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent launchIntent = getPackageManager().getLaunchIntentForPackage("com.android.chrome");
startActivity(launchIntent);
}
});
设置壁纸。将下面的代码添加到 styles.xml 的名称为 AppTheme 的样式标记中:
<item name="android:windowShowWallpaper">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
隐藏系统界面 :
private void hideSystemUI() {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN);
十、反射和类加载
反射
当谈到拆开 Android 应用并让它们在适合你的状态下运行时,反射是许多王牌之一。简单地说,反射是一个 API,可以用来在运行时访问、检查和修改对象——这包括字段、方法、类和接口(如图 10-1 所示)。
图 10-1
Java 反射图
下面列出了这些组件的摘要:
-
类 -类是一个蓝图/模板,当使用时,可以从它们创建单独的对象。例如,可以用一个
installedRam
变量、getRAM()
方法和setRAM()
方法创建一个Computer
类。使用这个类可以创建一个对象,例如,Computer myComputer = new Computer();
然后方法setRAM()
可以用在myComputer
对象上,例如myComputer.setRAM(32);.
-
方法——方法是一段代码,具有特定的用途,在被调用时运行,可以是类的一部分,也可以是独立的。方法可以传递一系列类型化参数,并且可以返回指定类型的变量。当作为类的一部分时,方法可以是静态的或实例的。实例方法需要在使用之前创建其类的对象,而静态方法不依赖于已初始化的对象。例如,类计算机可能有一个将两个数相加并返回结果的
sum
静态方法,以及一个为所创建对象的特定实例设置 ram 变量的setRAM()
实例方法。 -
构造函数 -构造函数是一种特殊类型的方法,作为对象(如类)初始化的一部分,用来设置变量和调用方法。例如,House 类可能有一个构造函数方法,它将三个变量作为参数:
hight
、numberOfRooms
和hasGarden
。然后可以用House myHouse = new House(10, 2, false);.
创建一个房子对象 -
接口——接口是一个抽象类,它包含一组带有空体的方法。例如,拥有一个生物接口可能有像
move()
、speak()
和eat()
这样的方法,这些方法都需要根据实现接口的职业(生物类型)来填充。
在下面的例子中,将使用两个类来帮助显示一系列不同的反射技术。如果不使用这些示例类,请替换代码示例中适用的引用:
一个 助手类 演示反思:
public class Loadable {
private final static String description = "This is a class that contains an assortment of access modifiers to test different types of reflection.";
private Context context;
private long uniqueId = 0;
private long time = 0;
private DeviceData deviceData = new DeviceData();
public void setDeviceInfo() {
deviceData.setDeviceInfo();
}
public long getTime() {
return time;
}
private Loadable(Context context, long uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
private void setTime(){
this.time = System.currentTimeMillis();
}
private static String getDeviceName(){
return android.os.Build.MODEL;
}
protected static Loadable construct(Context context){
final int uniqueId = new Random().nextInt((1000) + 1);
Loadable loadable = new Loadable(context, uniqueId);
loadable.setDeviceInfo();
return loadable;
}
}
助手类 支持加载时呈现一系列功能:
public class DeviceData {
String version = ""; // OS version
String sdkLevel = ""; // API Level
String device = ""; // Device
String model = ""; // Model
String product = ""; // Product
public void setDeviceInfo(){
version = System.getProperty("os.version");
sdkLevel = android.os.Build.VERSION.SDK;
device = android.os.Build.DEVICE;
model = android.os.Build.MODEL;
product = android.os.Build.PRODUCT;
}
@Override
public String toString() {
return "DeviceData{" +
"version='" + version + '\'' +
", sdkLevel='" + sdkLevel + '\'' +
", device='" + device + '\'' +
", model='" + model + '\'' +
", product='" + product + '\'' +
'}';
}
}
创建类的实例
在下面的例子中,使用反射,创建了一个新的DeviceData
类实例,并且在记录这些字段之一的初始化状态之前运行了一个对setDeviceInfo
方法的调用(以填充它的字段)。
初始化一个类 :
try {
Object initialisedDeviceData= DeviceData.class.newInstance();
initialisedDeviceData.getClass().getDeclaredMethod("setDeviceInfo").invoke(initialisedDeviceData);
String model = (String) initialisedDeviceData.getClass().getDeclaredField("model").get(initialisedDeviceData);
Log.v(TAG, model);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
。getDeclaredMethod 与进行了比较。获取方法
在下面的例子中,我们看到了方法getMethods
和getDeclaredMethods
之间的区别——这对于getFields
和getDeclaredFields
也是一样的。getMethods
将返回一个数组,该数组包含类或接口的public
方法,以及任何从超类或超接口继承的方法(超类/超接口是一个可以从中创建多个子对象的对象)。getDeclaredMethods
另一方面,返回类或接口的所有声明的方法(不仅仅是public
)。
这里的主要区别是,如果需要访问私有方法,将使用getDeclaredMethods
方法,然后用.setAccessible
方法设置可访问性,而如果需要访问superclasses
或superinterfaces,
getMethods
的方法,将改为使用。
getMethods()示例:
for (Method method : Loadable.class.getMethods()){
Log.v(TAG, method.getName());
}
getDeclaredMethods()示例:
for (Method method : Loadable.class.getDeclaredMethods()){
method.setAccessible(true);
Log.v(TAG, method.getName());
}
静态方法
在静态方法的情况下,使用反射不需要类的实例。
静态方法示例:
try {
Method getDeviceName = Loadable.class.getDeclaredMethod("getDeviceName");
getDeviceName.setAccessible(true);
Log.v(TAG,(String) getDeviceName.invoke(Loadable.class));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
私有构造函数
在类的构造函数是私有的情况下,反射仍然可以用来构造类和访问它的字段和方法。
谈论构造函数时的一个额外的怪癖是,当一个成员变量在一个类中定义时——比如String myMemberVariable = android.os.Build.VERSION.SDK;
——它被编译器移动到该类的构造函数中。1
下面是一个用私有构造函数构造类的例子:
try {
Constructor<?> constructor = Loadable.class.getDeclaredConstructor(Context.class, long.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(getApplicationContext(), (Object) 12); // constructor takes a context and an id.
Field uniqueIdField = instance.getClass().getDeclaredField("uniqueId");
uniqueIdField.setAccessible(true);
long uniqueId = (long) uniqueIdField.get(instance);
Log.v(TAG, ""+uniqueId);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
将类初始化为其他类的字段
下面的例子使用反射两次:第一次初始化一个类并获得对它的一个字段的访问,第二次在那个字段(它是一个自己的类)上使用反射来访问它的一个字段(它是一个字符串)。
实例类 实例:
try {
// The loadable class has a static method that can be used to construct it in this example, however, if the constructor isn't public,
// this can also be done with the private constructor example.
// and can be done as in the public class example.
Object instance = Loadable.class.getDeclaredMethod("construct", Context.class)
.invoke(Loadable.class, getApplicationContext());
// Retrieve the field device data which is the class we're looking to get the data of.
Field devicdDataField = instance.getClass().getDeclaredField("deviceData");
devicdDataField.setAccessible(true);
Object initialisedDeviceData = devicdDataField.get(instance);
// After accessing the value from the field we're looking to access the filds of we can use the same type of reflection again after getting it's class.
Field modelField = initialisedDeviceData.getClass().getDeclaredField("device");
modelField.setAccessible(true);
String model = (String) modelField.get(initialisedDeviceData);
Log.v(TAG,model);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
类别加载
Java 类加载器 2 是 Java 运行时环境(JRE)的一个组件,它将 Java 类加载到 Java 虚拟机(JVM)/Dalvik 虚拟机(DVM)/Android 运行时(ART)中。不是所有的类都被同时加载,也不是用同一个类加载器。上下文方法getClassLoader()
可以用来获取当前的类加载器。Android 中有几种类型的类加载,它们是:
-
PathClassLoader -这是 Android 系统为其系统和应用类加载器使用的。
-
DexClassLoader -加载包含一个
.dex
文件的文件类型(如.jar
和.apk
或.dex
文件直接加载)。这些.dex
文件(Dalvik 可执行文件)包含 Dalvik 字节码。 -
URL class loader——这是用来通过 URL 路径检索类或资源的。以
/
结尾的路径被假定为目录,否则它们被假定为.jar
文件。
下面使用 Dalvik 可执行文件和DexClassLoader
执行类加载。
检索当前的类加载器:
ClassLoader loader = getApplicationContext().getClassLoader();
从 API 级(Android O)开始,可以直接从内存中读取一个 dex 文件。为此,读取文件的 ByteBuffer,并使用 MemoryDexClassLoader 中的类。下面是一个将文件读入字节数组的帮助函数:
private static byte[] readFileToByteArray(File file){
FileInputStream fis = null;
byte[] bArray = new byte[(int) file.length()];
try{
fis = new FileInputStream(file);
fis.read(bArray);
fis.close();
}catch(IOException ioExp){
ioExp.printStackTrace();
}
return bArray;
}
内存中 dex 类加载 :
dexLoader = new InMemoryDexClassLoader(ByteBuffer.wrap(readFileToByteArray(filePath)), loader);
另一种方法是直接从文件中加载 dex 文件。DexClassLoader 类采用。dex 文件,optimizedDirectory - where。odex(优化的 dex 文件)存储在 Android API level 26 之前,librarySearchPath - a 字符串列表(由 File.pathSeparator 分隔;)声明包含本地库的目录,以及 parent -父类加载器。
dexLoader = new DexClassLoader(filePath, dexCacheDirectory.getAbsolutePath(), null, loader);
创建一个 dex 类加载器后,选择要加载的类,作为一个字符串:
loadedClass = dexLoader.loadClass("me.jamesstevenson.dexloadable.MainActivity"); //alter path for your use case
在这个阶段,未初始化的类可以正常使用,如反射部分所述。下面展示了如何安全地初始化这个类:
initialisedClass = loadedClass != null ? loadedClass.newInstance() : null;
在初始化这个类 之后,可以调用一个特定的方法,作为一个字符串,它的响应可以像前面用标准反射所做的那样返回:
method = loadedClass != null ? loadedClass.getMethod("loadMeAndIllTakeContext", Context.class) : null;
Object methodResponse = method != null ? method.invoke(initialisedClass, getApplicationContext()) : null;
十一、安卓外壳
Android 基于 Linux 构建,这意味着当使用 adb (Android 的专有命令行工具,允许与设备通信)时,您可以发出常见的 Linux 命令(如 ls 、 cd 、 whoami 等)。)以及几个 Android 操作系统特有的命令。
以下是通过外壳进行基本设备输入的几个例子:
input text "Hello World"
input swipe 50 050 450 100 #coordinates for swipe action
input tap 466 17 #coordinates for tap
service call phone 1 s16 098765432
service call statusbar 1
service call statusbar 2
要求 root 以下内容在所有其他活动之上显示引导映像。这不会阻止活动在动画后面的前景中运行。
/system/bin/bootanimation
通过 svc 命令控制系统属性(需要 root):
svc -l
svc bluetooth enable/ disable
svc wifi enable/ disable
svc nfc enable/ disable
svc data enable/ disable
svc power reboot
svc power shutdown
svc power stayon true #[true|false|usb|ac|wireless]
svc usb getFunctions [function] #Possible values of [function] are any of 'mtp', 'ptp', 'rndis', 'midi'
screen cap 命令 拍摄屏幕照片并保存到设备上的某个位置。类似地,screenrecord 命令记录最多 3 分钟的屏幕,并保存到磁盘:
screencap -p /sdcard/screen.png
screenrecord /sdcard/MyVideo.mp4
列出所有正在运行的进程:
top
top | grep chrome
在设备上安装应用,需要 root。g 权限在没有用户交互的情况下接受所有运行时权限(这个选项在 Android 6.0 之前不存在,运行时权限也不存在)。
pm install -g /data/local/tmp/one.apk
返回设备上可用的输入设备列表。这可以包括音频按钮、电源按钮、触摸屏、指纹读取器和鼠标。
uinput-fpc - Finger print sensor
fts - screen
gpio-keys - volume button
qpnp_pon - volume / power buttons
ls /dev/input/ -l
lsof | grep input/event
# or get the name of the inputs and see when an event occurs on that input
getevent -l
# Return feedback if an input is in use. Useful for identifying if the screen is in use.
cat /dev/input/event2
# Send an event to one of these inputs. For example on my device the below sets the volume to 0.
sendevent /dev/input/event0 0 0 0
通过 Monkey 测试工具(一个 UI fuzzer)启动一个应用。将数字 1 替换为随机触摸输入的次数,作为测试的一部分:
monkey -p com.android.chrome 1
如果您知道活动名称,您可以使用活动管理器启动应用:
am start -n com.android.chrome/com.google.android.apps.chrome.Main
以下返回制造商、设备名称、版本、名称和日期,以及用户和释放键:
getprop ro.build.fingerprint # i.e. google/blueline/blueline:9/PQ3A.190605.003/5524043:user/release-keys
# Returns the kernel version
uname -a
# Also returns the kernel version as well as the device architecture.
cat /proc/version
访问应用的内存(需要 root 用户):
#As Root access the locations used by applications as their internal storage.
cd /data/user/0
# For example accessing the saved offline pages in Chrome and storing it in the data/local/tmp directory for it to be pulled off device later.
su
cd /data/user/0
cd com.android.chrome/cache/Offline Pages/archives
cp 91-a05c-b3f3384516f4.mhtml /data/local/tmp/page.mhtml
chmod 777 /data/local/tmp/page.mhtml
重启设备。应用需要 android.permission.REBOOT 权限或成为 root:
/system/bin/reboot
reboot
svc power reboot
svc power shutdown
以 root 身份读写挂载一个文件系统。 在老设备上 这可以用来设置系统应用目录读写。
busybox mount -o remount,rw /system
中断允许接口设备与处理器通信:
cat /proc/interrupts | grep volume
Dumpsys 提供系统服务信息:
dumpsys -l
dumpsys input
dumpsys meminfo
service call procstats 1
以编程方式运行命令
使用 runtime 类,可以以编程方式运行 shell 命令。如果命令要求的权限级别高于应用所拥有的权限级别,命令将会失败,例如,试图在没有 android.permission.REBOOT 权限的情况下重新启动设备。
运行单个命令:
String filesLocation = getApplicationContext().getDataDir().getAbsolutePath();
try {
Runtime.getRuntime().exec("touch "+filesLocation+"/test.txt");
} catch (IOException e) {
e.printStackTrace();
}
```*
# 十二、反编译和反汇编 Android 应用
Android 应用要么用 Java 编写,要么用 Kotlin 编写。当构建一个应用时,它们被编译成 Dalvik 字节码——用`dex` (Dalvik 可执行文件)表示。这个 Dalvik 字节码是二进制的,因此不可读。既然如此,如果逆向工程师想要分析一个已经编译好的 Android 应用,他们只能选择反编译或反汇编 Dalvik 可执行文件。图 12-1 突出显示了创建和逆向工程一个 Android 应用的过程。

图 12-1
软件开发人员和逆向工程流程视图
## 反编译器 java
第一种选择是使用工具将 Dalvik 字节码反编译成人类可读的 Java。这个 Java 比真正的 Java 更像伪代码,因为它是反编译器对 Dalvik 程序集所代表的内容的“最佳猜测”。虽然 Java 开发人员更熟悉这种视图,但它通常不是最佳选择,因为它不仅不代表实际的应用代码,而且也不可运行或重新编译。像`dex2jar`和`jadx`这样的工具可以用来反编译 Dalvik 可执行文件。Jadx 可用于将 Jadx 项目导出到 Gradle 项目,进而允许将项目加载到 Android Studio 中。
APKTool 可用于提取。来自 APK 的 dex 文件:
```java
apktool -s d <apk path>
用 JADX 反编译并查看 APK 或 Dex 文件的反编译 Java:
jadx -e <apk or dex file path>
反汇编的 Dalvik 字节码(Smali)
可以使用反汇编器将 Dalvik 字节码还原为人类可读的自身表示,而不是反编译成伪 Java。Dalvik 字节码更常用的这种形式叫做 Smali。对 Smali 来说,反汇编的好处是一个dex
文件可以被反汇编、读取、修改、重组和提交,并且仍然处于完全运行的状态。
apk tool 等工具可以用来反汇编 dalvik 字节码:
apktool d <path>
由于其性质,Smali 比 Java 或 Kotlin 有更大的代码占用空间。例如,以下 Java 中的 Toast 代码(一个简单的 Android 弹出消息)是 Smali 中相同代码的一半大小。
Java:
Context context = getApplicationContext();
CharSequence text = "I'm a Toast";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
型式:??
.line 13
const-string v0, "I'm a Toast!"
.line 14
.local v0, "text":Ljava/lang/String;
const/4 v1, 0x1
.line 16
.local v1, "duration":I
invoke-virtual {p0}, Lcom/example/simpletoastapp/MainActivity;->getApplicationContext()Landroid/content/Context;
move-result-object v2
move-object v3, v0
check-cast v3, Ljava/lang/CharSequence;
invoke-static {v2, v3, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v2
.line 17
.local v2, "toast":Landroid/widget/Toast;
invoke-virtual {v2}, Landroid/widget/Toast;->show()V
从运行的设备中提取 apk
为了分析(进而反汇编或反编译)一个 Android 应用,您可能需要首先从设备中提取它。ADB shell 可以用来做这件事。
下面使用包管理器列出设备上的所有包 id:
pm list packages
pm list packages | grep chrome
接下来,可以再次使用包管理器列出所需包的基本 APK 的路径(例如包路径是/data/app/com . Android . chrome-6 PIH 3g 1 et 8 uqozatukwptq = =/base . apk):
pm path <Package ID>
查看此命令返回的目录不需要特殊权限。但是,它的父目录(/data/app
)没有非 root 的读取权限,这意味着设备上的应用不能以这种方式枚举。
最后,提取 APK 最简单的方法是使用 adb,如下所示:
adb pull <package base APK path>
还值得记住的是,诸如 APK 囤积者、 1 之类的工具是免费和开源的,可以用于从设备中大量提取 apk。
十三、总结
本书的目的是为您提供一个参考指南,其中包含了对与 Android 操作系统和其他 Android 安全元素密切合作的 Android 软件开发人员有用的信息。这本书涵盖了从应用沙箱和 Dalvik 虚拟机到 Android 应用的存储类型,以及如何对已经编译好的 Android 应用进行逆向工程。
重要的是要记住,尽管本书中的核心原则在未来许多年都将继续适用,但随着新版本 Android 的发布,一些方面可能会发生变化。在这种情况下,在继续使用这本书作为参考指南的同时,也要回顾分散在整本书中的脚注,以建立在所涵盖的领域之上。
还有大量其他令人惊叹的资源来支持你在 Android 编程、内部和逆向工程方面的知识;以下是其中的一部分:
-
麦蒂·斯通 -安卓 App 逆向工程 1011
-
乔纳森·莱文 -安卓内部2
-
克里斯蒂娜·巴兰 -安卓恶意软件分析| YouTube3
-
克里斯蒂娜·巴兰 -安卓恶意软件分析|领英学习4
-
Ira R. Forman 和 Nate Forman - Java 反思在行动|曼宁5
-
安卓文档 |安卓开发者6
除了这些资源,我还想特别提到 JD,他是这个领域的研究员和软件工程师,没有他我不会被鼓励写这本书。
想了解更多关于我的信息和资源,请访问我的网站 https://JamesStevenson.me/
。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库