Android 7.0 除了提供诸多新特性和功能外,还对系统和 API 行为做出了各种变更。本文重点介绍您应该了解并在开发应用时加以考虑的一些主要变更。
如果您之前发布过 Android 应用,请注意您的应用可能会受到这些平台变化的影响。
电池和内存
Android 7.0 包括旨在延长设备电池续航时间和减少 RAM 使用的系统行为变更。这些变更可能会影响您的应用访问系统资源,以及您的应用通过特定隐式 intent 与其他应用交互的方式。
低电耗模式
Android 6.0(API 级别 23)引入了低电耗模式,当用户设备未插接电源、处于静止状态且屏幕关闭时,该模式会推迟 CPU 和网络活动,从而延长电池寿命。Android 7.0 则通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持式设备装在口袋里)时应用部分 CPU 和网络限制,进一步增强了低电耗模式。
当设备处于充电状态且屏幕已关闭一定时间后,设备会进入低电耗模式并应用第一部分限制:关闭应用网络访问、推迟作业和同步。如果设备在进入低电耗模式后处于静止状态达到一定时间,系统会对 PowerManager.WakeLock
、AlarmManager
闹钟、GPS 和 WLAN 扫描应用其余的低电耗模式限制。无论是应用部分或全部低电耗模式限制,系统都会唤醒设备以提供简短的维护窗口。在此期间,应用可以访问网络并执行任何延迟的作业/同步。
请注意,激活屏幕或插接设备电源时,系统将退出低电耗模式并移除这些处理限制。此项新增的行为不会影响有关使您的应用适应 Android 6.0(API 级别 23)中所推出的旧版本低电耗模式的建议和最佳实践,如 对低电耗模式和应用待机模式进行针对性优化中所讨论。您仍应遵循这些建议(例如使用 Firebase Cloud Messaging (FCM) 发送和接收消息),同时开始规划更新以适应额外的低电耗模式行为。
Project Svelte:后台优化
Android 7.0 移除了三项隐式广播,以帮助优化内存使用和电量消耗。此项变更很有必要,因为隐式广播会在后台频繁启动已注册侦听这些广播的应用。删除这些广播可以显著提升设备性能和用户体验。
移动设备会经历频繁的连接变更,例如在 WLAN 和移动数据之间切换时。目前,可以通过在应用清单中注册一个接收器来侦听隐式 CONNECTIVITY_ACTION
广播,让应用能够监控这些变更。由于许多应用都会注册接收此广播,因此单次网络切换可能会导致所有应用被唤醒并同时处理此广播。
同样,在之前的 Android 版本中,应用可以注册接收来自其他应用(如相机)的隐式 ACTION_NEW_PICTURE
和 ACTION_NEW_VIDEO
广播。当用户使用相机应用拍摄照片时,这些应用即会被唤醒以处理广播。
为缓解这些问题,Android 7.0 应用了以下优化措施:
- 如果以 Android 7.0(API 级别 24)及更高版本为目标平台的应用在清单中声明其广播接收器,则这些应用不会收到
CONNECTIVITY_ACTION
广播。如果应用使用Context.registerReceiver()
注册BroadcastReceiver
且该 context 仍有效,则它们仍会收到CONNECTIVITY_ACTION
广播。 - 系统不再发送
ACTION_NEW_PICTURE
或ACTION_NEW_VIDEO
广播。此项优化会影响所有应用,而不仅仅是面向 Android 7.0 的应用。
如果您的应用使用了其中任何 intent,您应尽快移除对它们的依赖关系,以便正确以 Android 7.0 设备为目标平台。
Android 框架提供了多种解决方案来缓解对这些隐式广播的需求。例如,JobScheduler
API 提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。您甚至可以使用 JobScheduler
来响应对 content provider 的更改。
如需详细了解 Android 7.0(API 级别 24)中的后台优化以及如何调整您的应用,请参阅后台优化。
权限更改
Android 7.0 做了一些权限更改,这些更改可能会影响您的应用。
系统权限更改
为了提高私有文件的安全性,以 Android 7.0 或更高版本为目标平台的应用对私有目录的访问权限受到限制 (0700
)。此设置可防止私有文件的元数据泄露,例如其大小或是否存在。此权限更改具有多种副作用:
-
私有文件的文件权限不应再由所有者放宽,为使用
MODE_WORLD_READABLE
和/或MODE_WORLD_WRITEABLE
而进行的此类尝试将触发SecurityException
。注意:迄今为止,这种限制尚不能完全执行。 应用仍可能使用原生 API 或
File
API 来修改它们的私有目录权限。但是,我们强烈反对放宽私有目录的权限。 -
传递软件包网域外的
file://
URI 可能会导致接收器出现无法访问的路径。因此,尝试传递file://
URI 会触发FileUriExposedException
。分享私有文件内容的推荐方法是使用FileProvider
。 -
DownloadManager
不再按文件名分享私人存储的文件。旧版应用在访问COLUMN_LOCAL_FILENAME
时可能出现无法访问的路径。以 Android 7.0 或更高版本为目标平台的应用在尝试访问COLUMN_LOCAL_FILENAME
时会触发SecurityException
。通过使用DownloadManager.Request.setDestinationInExternalFilesDir()
或DownloadManager.Request.setDestinationInExternalPublicDir()
将下载位置设置为公共位置的旧版应用仍可以访问COLUMN_LOCAL_FILENAME
中的路径,但是我们强烈反对使用这种方法。访问由DownloadManager
公开的文件的首选方式是使用ContentResolver.openFileDescriptor()
。
在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode
API 政策禁止在您的应用外部公开 file://
URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用会失败并出现 FileUriExposedException
异常。
如需在应用间共享文件,您应发送 content://
URI,并授予 URI 临时访问权限。授予此权限的最简单方法是使用 FileProvider
类。如需详细了解权限和共享文件,请参阅共享文件。
无障碍功能改进
Android 7.0 包含一些更改,旨在提高平台对于弱视或受损用户的易用性。这些更改一般并不要求更改您的应用代码,不过您应仔细检查并使用您的应用测试这些功能,以评估它们对用户体验的潜在影响。
屏幕缩放
Android 7.0 允许用户设置显示大小,以放大或缩小屏幕上的所有元素,从而改善弱视用户的设备无障碍功能。用户无法将屏幕缩放至低于最小屏幕宽度 sw320dp,该宽度是 Nexus 4 的宽度,也是常规中等大小手机的宽度。
当设备密度发生更改时,系统会以如下方式通知正在运行的应用:
- 如果是面向 API 级别 23 或更低版本系统的应用,系统会自动终止其所有后台进程。这意味着,如果用户切换离开此类应用,转而打开设置屏幕并更改显示大小设置,则系统会像处理内存不足的情况一样终止该应用。如果应用具有任何前台进程,则系统会如处理运行时更改中所述将配置更改通知给这些进程,就像设备的屏幕方向发生变化一样。
- 如果应用以 Android 7.0 为目标平台,则其所有进程(前台和后台)都会收到有关配置变更的通知,如处理运行时更改中所述。
大多数应用并不需要进行任何更改即可支持此功能,不过前提是这些应用遵循 Android 最佳实践。具体要检查的事项:
- 在屏幕宽度为
sw320dp
的设备上测试您的应用,并确保其充分运行。 - 当设备配置发生变更时,更新任何与密度相关的缓存信息,例如缓存位图或从网络加载的资源。当应用从暂停状态恢复运行时,检查配置变更。
注意:如果您要缓存与配置相关的数据,则最好也包括相关元数据,例如该数据对应的屏幕尺寸或像素密度。保存这些元数据便于您在配置变更后决定是否需要刷新缓存数据。
- 避免用像素单位指定尺寸,因为像素不会随屏幕密度缩放。应改为使用与密度无关像素 (
dp
) 单位指定尺寸。
设置向导中的视觉设置
Android 7.0 在“欢迎”屏幕上包含“视觉设置”,用户可以在新设备上设置以下无障碍设置:放大手势、字体大小、显示大小和 TalkBack。此项变更让您可以更容易发现与不同屏幕设置有关的错误。如需评估此功能的影响,您应在启用这些设置的状态下测试应用。您可以在设置 > 无障碍下找到这些设置。
NDK 应用链接至平台库
从 Android 7.0 开始,系统会阻止应用动态链接非 NDK 库,这可能会导致应用崩溃。这种行为变更旨在跨平台更新和不同设备打造一致的应用体验。即使您的代码可能不会链接私有库,但您的应用中的第三方静态库可能会这么做。因此,所有开发者都应进行相应检查,确保他们的应用不会在运行 Android 7.0 的设备上崩溃。如果您的应用使用原生代码,则只能使用公开 NDK API。
您的应用可通过以下三种方式尝试访问私有平台 API:
- 您的应用直接访问私有平台库。您应更新您的应用以添加该应用的库副本,或使用公开 NDK API。
- 您的应用使用访问私有平台库的第三方库。即使您确定应用不会直接访问私有库,也应针对这种情况测试您的应用。
- 您的应用引用一个其 APK 中未包含的库。例如,如果您尝试使用您自己的 OpenSSL 副本,但忘记将它与应用的 APK 进行捆绑,则可能会出现此情况。应用可在包含
libcrypto.so
的 Android 平台版本上正常运行。不过,此应用在不包含此库的新版 Android(例如,Android 6.0 及更高版本)上会崩溃。为修复此问题,请确保您的 APK 捆绑您的所有非 NDK 库。
应用不应使用 NDK 中未包含的原生库,因为这些库可能会发生更改或在不同 Android 版本之间的可用性不同。例如,从 OpenSSL 切换至 BoringSSL 即属于此类更改。此外,由于不属于 NDK 中的平台库没有兼容性要求,因此不同的设备可能提供不同级别的兼容性。
为降低此限制可能对当前发布的应用的影响,面向 API 级别 23 或更低级别的应用在 Android 7.0(API 级别 24)上可暂时访问颇为常用的一组库,例如 libandroid_runtime.so
、libcutils.so
、libcrypto.so
和 libssl.so
。如果您的应用加载其中某个库,logcat 会生成一个警告,并在目标设备上显示一个 Toast 来通知您。如果您看到这些警告,您应更新您的应用以添加该应用自己的库副本,或仅使用公开 NDK API。将来发布的 Android 平台可能会完全限制对私有库的使用,并导致您的应用崩溃。
所有应用在调用既不公开也无法暂时访问的 API 时都会生成运行时错误。结果就是 System.loadLibrary
和 dlopen(3)
都返回 NULL
,并且可能会导致您的应用崩溃。您应检查应用代码以移除对私有平台 API 的使用,并使用搭载 Android 7.0(API 级别 24)的设备或模拟器全面测试应用。如果您不确定您的应用是否使用私有库,您可以检查 logcat 以识别运行时错误。
下表描述的是根据应用使用的私有原生库及其目标 API 级别 (android:targetSdkVersion
),应用预期显示的行为。
库 | 目标 API 级别 | 通过动态链接器进行运行时访问 | Android 7.0(API 级别 24)行为 | 未来的 Android 平台行为 |
---|---|---|---|---|
公开 NDK | 任何 | 无障碍 | 运行正常 | 运行正常 |
私有(暂时可访问的私有库) | 23 或更低 | 暂时可访问 | 合乎预期,但您会收到一个 logcat 警告。 | 运行时错误 |
私有(暂时可访问的私有库) | 24 或更高 | 受限 | 运行时错误 | 运行时错误 |
私有(其他) | 不限 | 受限 | 运行时错误 | 运行时错误 |
检查您的应用是否使用私有库
为帮助您识别加载私有库的问题,logcat 可能会生成一个警告或运行时错误。例如,如果您的应用以 API 级别 23 或更低级别为目标平台,并尝试在搭载 Android 7.0 的设备上访问私有库,您可能会看到类似于以下内容的警告:
03-21 17:07:51.502 31234 31234 W linker : library "libandroid_runtime.so" ("/system/lib/libandroid_runtime.so") needed or dlopened by "/data/app/com.popular-app.android-2/lib/arm/libapplib.so" is not accessible for the namespace "classloader-namespace" - the access is temporarily granted as a workaround for http://b/26394120
这些 logcat 警告会告知您哪个库正在尝试访问私有平台 API,但不会导致您的应用崩溃。但是,如果应用以 API 级别 24 或更高级别为目标平台,logcat 会生成以下运行时错误,您的应用可能会崩溃:
java.lang.UnsatisfiedLinkError: dlopen failed: library "libcutils.so" ("/system/lib/libcutils.so") needed or dlopened by "/system/lib/libnativeloader.so" is not accessible for the namespace "classloader-namespace" at java.lang.Runtime.loadLibrary0(Runtime.java:977) at java.lang.System.loadLibrary(System.java:1602)
如果您的应用使用动态链接到私有平台 API 的第三方库,您可能也会看到上述 logcat 输出。利用 Android 7.0DK 中的 readelf 工具,您可以通过运行以下命令生成给定 .so
文件的所有动态链接的共享库列表:
aarch64-linux-android-readelf -dW libMyLibrary.so
更新应用
通过下面的一些步骤,您可以修复上述类型的错误并确保您的应用不会在将来的更新版平台上崩溃:
- 如果您的应用使用私有平台库,您应更新它,以添加该应用自己的库副本或使用公开 NDK API。
- 如果您的应用使用访问私有符号的第三方库,则联系库作者以更新库。
- 请确保将您的所有非 NDK 库与您的 APK 打包在一起。
- 使用标准 JNI 函数而非来自
libandroid_runtime.so
的getJavaVM
和getJNIEnv
:AndroidRuntime::getJavaVM -> GetJavaVM from <jni.h> AndroidRuntime::getJNIEnv -> JavaVM::GetEnv or JavaVM::AttachCurrentThread from <jni.h>.
- 使用
__system_property_get
而非来自libcutils.so
的私有property_get
符号。为此,请使用__system_property_get
及以下 include 函数:#include <sys/system_properties.h>
注意:系统属性的可用性和内容未通过 CTS 进行测试。应执行进一步修复以避免同时使用这些属性。
- 使用
libcrypto.so
中的SSL_ctrl
符号的本地版本。例如,您应在.so
文件中静态链接libcyrpto.a
,或从 BoringSSL/OpenSSL 添加一个动态链接的libcrypto.so
版本,并将其打包到您的 APK 中。
Android for Work
Android 7.0 包含针对以 Android for Work 为目标平台的应用所做的更改,包括对证书安装、密码重置、次要用户管理和设备标识符访问权限的更改。如果您是要针对 Android for Work 环境开发应用,则应仔细检查这些变更并相应地修改您的应用。
- 您必须先安装委派证书安装程序,然后 DPC 才能对其进行设置。对于面向 Android 7.0(API 级别 24)的配置文件和设备所有者应用,您应在设备政策控制器 (DPC) 调用
DevicePolicyManager.setCertInstallerPackage()
之前安装授权证书安装程序。如果尚未安装该安装程序,系统会抛出IllegalArgumentException
。 - 针对设备管理员的重置密码限制现在也适用于个人资料所有者。设备管理员无法再使用
DevicePolicyManager.resetPassword()
来清除或更改已经设置的密码。设备管理员仍然可以设置密码,但只能在设备没有密码、PIN 码或图案时这样做。 - 即使设置了限制,设备所有者和配置文件所有者仍可以管理账号。而且,即使具有
DISALLOW_MODIFY_ACCOUNTS
用户限制,设备所有者和个人资料所有者仍可调用 Account Management API。 - 设备所有者可以更轻松地管理二级用户。当设备在设备所有者模式下运行时,系统会自动设置
DISALLOW_ADD_USER
限制。这样可以防止用户创建非托管二级用户。此外,CreateUser()
和createAndInitializeUser()
方法已废弃,取而代之的是新的DevicePolicyManager.createAndManageUser()
方法。 - 设备所有者可以访问设备标识符。设备所有者可以使用
DevicePolicyManager.getWifiMacAddress()
访问设备的 Wi-Fi MAC 地址。如果设备上从未启用 Wi-Fi,则此方法会返回null
值。 - 工作模式设置控制工作应用访问。当工作模式关闭时,系统启动器通过使工作应用显示为灰色来指示它们不可用。启用工作模式会再次恢复正常行为。
- 从“设置”界面安装包含客户端证书链和对应私钥的 PKCS #12 文件时,系统不再将该证书链中的 CA 证书安装到受信任的凭据存储空间。当应用稍后尝试检索客户端证书链时,这不会影响
KeyChain.getCertificateChain()
的结果。如果需要,使用 .crt 或 .cer 文件扩展名的 DER 编码格式通过 Settings UI 单独将 CA 证书安装到受信任的凭据存储空间。 - 从 Android 7.0 开始,可针对每个用户管理指纹登记和存储空间。如果配置文件所有者的设备政策客户端 (DPC) 面向搭载 Android 7.0(API 级别 24)的设备上的 API 级别 23(或更低级别),则用户仍可以在该设备上设置指纹,但工作应用不能访问设备指纹。当 DPC 以 API 级别 24 及更高版本为目标平台时,用户可以通过依次前往 Settings > Security > Work profile security 专门为工作资料设置指纹。
DevicePolicyManager.getStorageEncryptionStatus()
会返回新的加密状态ENCRYPTION_STATUS_ACTIVE_PER_USER
,以表明加密处于活动状态,且加密密钥与用户关联。仅当 DPC 面向 API 级别 24 和更高级别时才会返回新的状态。 对于面向更早的 API 级别的应用,即使加密密钥是用户或配置文件特有的,系统也会返回ENCRYPTION_STATUS_ACTIVE
。- 在 Android 7.0 中,如果设备通过单独的工作挑战安装了托管配置文件,则原本通常会影响整个设备的多个方法将会改变其行为方式。这些方法将仅应用于工作资料,而不是影响整个设备。(此类方法的完整列表位于
DevicePolicyManager.getParentProfileInstance()
文档中。)例如,DevicePolicyManager.lockNow()
只锁定工作资料,而不是锁定整个设备。对于其中每个方法,您都可以通过对DevicePolicyManager
的父实例调用该方法来获取旧行为;您可以调用DevicePolicyManager.getParentProfileInstance()
获取此父项。例如,如果您调用父实例的lockNow()
方法,则整个设备将被锁定。
注解保留
Android 7.0 修复了一个注解可见性被忽略的错误。 这种问题会导致应用可在运行时访问原本不允许访问的注解。这些注解包括:
VISIBILITY_BUILD
:仅应编译时可见。VISIBILITY_SYSTEM
:运行时应可见,但仅对底层系统可见。
如果您的应用依赖此行为,请在运行时必须可用的注解中添加保留政策。您可以使用 @Retention(RetentionPolicy.RUNTIME)
执行此操作。
TLS/SSL 默认配置变更
Android 7.0 对应用用于 HTTPS 和其他 TLS/SSL 流量的默认 TLS/SSL 配置做出了以下更改:
- RC4 加密套件现已停用。
- CHACHA20-POLY1305 加密套件现已启用。
默认停用 RC4 可能会导致服务器未协商出新型加密套件时,HTTPS 或 TLS/SSL 连接断开。首选的修复方法是改进服务器的配置,以启用更强更现代的加密套件和协议。理想情况下,应启用 TLSv1.2 和 AES-GCM 以及 Forward Secrecy 加密套件(ECDHE),且最好使用后者。
另一种方法是修改应用以使用自定义 SSLSocketFactory
与服务器通信。出厂时应精心设计以创建 SSLSocket
实例,除默认加密套件外,此实例还应启用服务器所需的部分加密套件。
注意:这些更改不适用于 WebView
。
以 Android 7.0 为目标平台的应用
这些行为变更仅适用于以 Android 7.0(API 级别 24)或更高版本为目标平台的应用。针对 Android 7.0 进行编译或将 targetSdkVersion
设为 Android 7.0 或更高版本的应用必须进行修改,以便适当地支持这些行为(如果适用)。
序列化变更
Android 7.0(API 级别 24)修复了计算默认 serialVersionUID 时与规范不符的 bug。
实现 Serializable
且未指定显式 serialVersionUID
字段的类可能会看到其默认 serialVersionUID 发生变化,这会导致在尝试反序列化在较低版本上序列化或由以较低版本为目标平台的应用序列化的类实例时抛出异常。错误消息将如下所示:
local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 4567
若要解决这些问题,需要向任何受影响的类添加一个 serialVersionUID
字段,该字段值为错误消息中的 stream classdesc
serialVersionUID
(例如,在本例中为 1234
)。此更改遵循了编写序列化代码的所有最佳实践建议,并且适用于所有版本的 Android。
修复的具体 bug 与静态初始化程序方法(即 <clinit>
)的存在有关。根据规范,类中是否存在静态初始化程序方法将影响为该类计算的默认 serialVersionUID。
在修复 bug 之前,如果类没有静态初始化程序,则计算还会检查超类是否有静态初始化程序。
需要说明的是,此变更不会影响以 API 级别 23 或更低级别为目标平台的应用、具有 serialVersionUID
字段的类或具有静态初始化程序方法的类。
其他重要说明
- 如果某个应用在 Android 7.0 上运行,但其目标 API 级别较低,并且用户更改显示大小,则系统会终止该应用进程。应用必须能够妥善处理此情景。否则,当用户从最近用过的应用列表中恢复运行应用时,应用将会出现崩溃现象。
您应测试您的应用,以确保不会发生此行为。 为此,您可以通过 DDMS 手动终止应用,造成相同的崩溃。
密度发生更改时,系统不会自动终止以 Android 7.0(API 级别 24)及更高版本为目标平台的应用,但这些应用仍可能对配置更改做出不良响应。
- Android 7.0 上的应用应该能够妥善处理配置更改,并且在后续启动时不会崩溃。您可以通过更改字体大小 (Setting > Display > Font size) 并随后从最近使用记录中恢复运行应用,来验证应用行为。
-
由于之前的 Android 版本中的一项错误,系统未能将对主线程上的一个 TCP 套接字的写入操作举报为违反严格模式。Android 7.0 修复了此 bug。表现出此行为的应用现在会抛出
android.os.NetworkOnMainThreadException
。一般情况下,我们不建议在主线程上执行网络操作,因为这些操作通常都有可能导致 ANR 和卡顿的高延迟。 -
Debug.startMethodTracing()
方法族现在默认在您的共享存储空间上的软件包特定目录中存储输出,而非 SD 卡根目录。这意味着应用不再需要请求WRITE_EXTERNAL_STORAGE
权限即可使用这些 API。 -
许多平台 API 现在开始检查在
Binder
事务间发送的大负载,系统现在会将TransactionTooLargeExceptions
再次作为RuntimeExceptions
引发,而不再只是默默记录或抑制它们。一个常见例子是在Activity.onSaveInstanceState()
中存储过多数据,导致ActivityThread.StopInfo
在您的应用以 Android 7.0 为目标平台时抛出RuntimeException
。 -
如果应用向
View
发布Runnable
任务,并且View
未附加到窗口,系统会用View
为Runnable
任务排队;在View
附加到窗口之前,Runnable
任务不会执行。此行为会修复以下错误: -
如果 Android 7.0 上一项有
DELETE_PACKAGES
权限的应用尝试删除一个软件包,但另一项应用已经安装了这个软件包,则系统需要用户进行确认。在这种情况下,应用在调用PackageInstaller.uninstall()
时预计的返回状态应为STATUS_PENDING_USER_ACTION
。 - 名为 Crypto 的 JCA 提供程序已弃用,因为它仅有的 SHA1PRNG 算法为弱加密。应用无法再使用 SHA1PRNG(以不安全的方式)派生密钥,因为此提供程序不再可用。如需了解详情,请参阅博文 Android N 中已弃用“Crypto”安全提供程序。