Android 7.0 除了提供诸多新特性和功能外,还对系统和 API 行为做出了各种变更。本文重点介绍您应该了解并在开发应用时加以考虑的一些主要变更。
如果您之前发布过 Android 应用,请注意您的应用可能受到这些平台变化的影响。
电池和内存
Android 7.0 包括旨在延长设备电池寿命和减少 RAM 使用的系统行为变更。这些变更可能会影响您的应用访问系统资源,以及您的应用通过特定隐式 intent 与其他应用交互的方式。
低电耗模式
Android 6.0(API 级别 23)引入了低电耗模式,当用户设备未插接电源、处于静止状态且屏幕关闭时,该模式会推迟 CPU 和网络活动,从而延长电池寿命。而 Android 7.0 则通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持式设备装在口袋里)时应用部分 CPU 和网络限制,进一步增强了低电耗模式。
图 1. 低电耗模式如何应用第一级系统活动限制以延长电池寿命的图示。
当设备处于充电状态且屏幕已关闭一定时间后,设备会进入低电耗模式并应用第一部分限制:关闭应用网络访问、推迟作业和同步。如果进入低电耗模式后设备处于静止状态达到一定时间,系统则会对 PowerManager.WakeLock
、AlarmManager
闹铃、GPS 和 WLAN 扫描应用余下的低电耗模式限制。无论是应用部分还是全部低电耗模式限制,系统都会唤醒设备以提供简短的维护时间窗口,在此窗口期间,应用程序可以访问网络并执行任何被推迟的作业/同步。
图 2. 低电耗模式如何在设备处于静止状态达到一定时间后应用第二级系统活动限制的图示。
请注意,激活屏幕或插接设备电源时,系统将退出低电耗模式并移除这些处理限制。此项新增的行为不会影响有关使您的应用适应 Android 6.0(API 级别 23)中所推出的旧版本低电耗模式的建议和最佳做法,如对低电耗模式和应用待机模式进行针对性优化中所讨论。您仍应遵循这些建议(例如使用 Google 云消息传递 (GCM) 发送和接收消息)并开始安排更新计划以适应新增的低电耗模式行为。
Project Svelte:后台优化
Android 7.0 移除了三项隐式广播,以帮助优化内存使用和电量消耗。此项变更很有必要,因为隐式广播会在后台频繁启动已注册侦听这些广播的应用。删除这些广播可以显著提升设备性能和用户体验。
移动设备会经历频繁的连接变更,例如在 WLAN 和移动数据之间切换时。目前,可以通过在应用清单中注册一个接收器来侦听隐式 CONNECTIVITY_ACTION
广播,让应用能够监控这些变更。由于很多应用会注册接收此广播,因此单次网络切换即会导致所有应用被唤醒并同时处理此广播。
同理,在之前版本的 Android 中,应用可以注册接收来自其他应用(例如相机)的隐式 ACTION_NEW_PICTURE
和 ACTION_NEW_VIDEO
广播。当用户使用相机应用拍摄照片时,这些应用即会被唤醒以处理广播。
为缓解这些问题,Android 7.0 应用了以下优化措施:
- 面向 Android 7.0 开发的应用不会收到
CONNECTIVITY_ACTION
广播,即使它们已有清单条目来请求接受这些事件的通知。在前台运行的应用如果使用BroadcastReceiver
请求接收通知,则仍可以在主线程中侦听CONNECTIVITY_CHANGE
。 - 应用无法发送或接收
ACTION_NEW_PICTURE
或ACTION_NEW_VIDEO
广播。此项优化会影响所有应用,而不仅仅是面向 Android 7.0 的应用。
如果您的应用使用任何 intent,您仍需要尽快移除它们的依赖关系,以正确适配 Android 7.0 设备。Android 框架提供多个解决方案来缓解对这些隐式广播的需求。例如,JobScheduler
API 提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。您甚至可以使用 JobScheduler
来适应内容提供程序变化。
如需了解有关 Android N 中后台优化以及如何改写应用的详细信息,请参阅后台优化。
权限更改
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 的宽度,也是常规中等大小手机的宽度。
图 3. 右侧屏幕显示的是一台运行 Android 7.0 系统映像的设备增大显示尺寸后的效果。
当设备密度发生更改时,系统会以如下方式通知正在运行的应用:
- 如果是面向 API 级别 23 或更低版本系统的应用,系统会自动终止其所有后台进程。这意味着如果用户切换离开此类应用,转而打开 Settings 屏幕并更改 Display size 设置,则系统会像处理内存不足的情况一样终止该应用。如果应用具有任何前台进程,则系统会如处理运行时更改中所述将配置变更通知给这些进程,就像对待设备屏幕方向变更一样。
- 如果是面向 Android 7.0 的应用,则其所有进程(前台和后台)都会收到有关配置变更的通知,如处理运行时更改中所述。
大多数应用并不需要进行任何更改即可支持此功能,不过前提是这些应用遵循 Android 最佳做法。具体要检查的事项:
- 在屏幕宽度为
sw320dp
的设备上测试您的应用,并确保其充分运行。 - 当设备配置发生变更时,更新任何与密度相关的缓存信息,例如缓存位图或从网络加载的资源。当应用从暂停状态恢复运行时,检查配置变更。
注:如果您要缓存与配置相关的数据,则最好也包括相关元数据,例如该数据对应的屏幕尺寸或像素密度。保存这些元数据便于您在配置变更后决定是否需要刷新缓存数据。
- 避免用像素单位指定尺寸,因为像素不会随屏幕密度缩放。应改为使用与密度无关像素 (
dp
) 单位指定尺寸。
设置向导中的视觉设置
Android 7.0 在“Welcome”屏幕中加入了“Vision Settings”,用户可以在新设备上设置以下无障碍功能设置:Magnification gesture、Font size、Display size 和话语提示。此项变更让您可以更容易发现与不同屏幕设置有关的错误。要评估此功能的影响,您应在启用这些设置的状态下测试应用。您可以在 Settings > Accessibility 中找到这些设置。
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 N 上可暂时访问颇为常用的一组库,例如 libandroid_runtime.so
、libcutils.so
、libcrypto.so
和 libssl.so
。如果您的应用加载其中某个库,logcat 会生成一个警告,并在目标设备上显示一个 Toast 来通知您。如果您看到这些警告,您应更新您的应用以添加该应用自己的库副本,或仅使用公开 NDK API。将来发布的 Android 平台可能会完全限制对私有库的使用,并导致您的应用崩溃。
所有应用在调用既非公开又不可暂时访问的 API 时都会生成一个运行时错误。结果就是 System.loadLibrary
和 dlopen(3)
同时返回 NULL
,并可能导致您的应用崩溃。您应检查应用代码以移除对私有平台 API 的使用,并使用预览版设备或模拟器全面测试应用。如果您不确定您的应用是否使用私有库,您可以检查 logcat 以识别运行时错误。
下表描述的是根据应用使用的私有原生库及其目标 API 级别 (android:targetSdkVersion
),应用预期显示的行为。
库 | 目标 API 级别 | 通过动态链接器进行运行时访问 | N Developer Preview 行为 | 最终 Android N 版本行为 | 未来的 Android 平台行为 |
---|---|---|---|---|---|
公开 NDK | 任意 | 可访问 | 合乎预期 | 合乎预期 | 合乎预期 |
私有(暂时可访问的私有库) | 23 或更低 | 暂时可访问 | 合乎预期,但您会在目标设备上收到一个 logcat 警告和一条消息。 | 合乎预期,但您会收到一个 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 才能对其进行设置。对于面向 N SDK 的配置文件和设备所有者应用,您应在设备规范控制器 (DPC) 调用
DevicePolicyManager.setCertInstallerPackage()
之前安装授权证书安装程序。如果尚未安装此安装程序,则系统会引发IllegalArgumentException
。 - 针对设备管理员的重置密码限制现在也适用于配置文件所有者。设备管理员无法再使用
DevicePolicyManager.resetPassword()
来清除或更改已经设置的密码。设备管理员仍可以设置密码,但只能在设备没有密码、PIN 码或图案时这样做。 - 即使设置了限制,设备所有者和配置文件所有者仍可以管理帐户。而且,即使具有
DISALLOW_MODIFY_ACCOUNTS
用户限制,设备所有者和配置文件所有者仍可调用 Account Management API。 - 设备所有者可以更轻松地管理二级用户。当设备在设备所有者模式下运行时,系统将自动设置
DISALLOW_ADD_USER
限制。这样可以防止用户创建非托管二级用户。此外,CreateUser()
和createAndInitializeUser()
方法已弃用,取而代之的是DevicePolicyManager.createAndManageUser()
方法。 - 设备所有者可以访问设备标识符。设备所有者可以使用
DevicePolicyManagewr.getWifiMacAddress()
访问设备的 WLAN MAC 地址。如果设备上从未启用 WLAN,则此方法将返回一个null
值。 - 工作模式设置控制工作应用访问。当工作模式关闭时,系统启动器通过使工作应用显示为灰色来指示它们不可用。启用工作模式会再次恢复正常行为。
- 从 Settings UI 安装包含客户端证书链和对应的私钥的 PKCS #12 文件时,系统不再将该证书链中的 CA 证书安装到受信任的凭据存储空间。当应用稍后尝试检索客户端证书链时,这不会影响
KeyChain.getCertificateChain()
的结果。如果需要,使用 .crt 或 .cer 文件扩展名的 DER 编码格式通过 Settings UI 单独将 CA 证书安装到受信任的凭据存储空间。 - 从 Android 7.0 开始,可针对每个用户管理指纹登记和存储空间。如果配置文件所有者的设备规范客户端 (DPC) 面向 Android N 设备上的 Android N 之前的版本,则用户仍可以在该设备上设置指纹,但工作应用不能访问设备指纹。当 DPC 面向 Android N 和更高版本时,用户可以通过转到 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 中针对 Android for Work 所做变更的详细信息,请参阅 Android for Work 更新。
注解保留
Android 7.0 修复了一个注解可见性被忽略的错误。这种问题会导致应用可在运行时访问原本不允许访问的注解。这些注解包括:
VISIBILITY_BUILD
:仅应编译时可见。VISIBILITY_SYSTEM
:运行时应可见,但仅限底层系统。
如果您的应用依赖这种行为,请为运行时必须可用的注解添加保留政策。您可通过使用 @Retention(RetentionPolicy.RUNTIME)
来执行此操作。
其他重要说明
- 如果一个应用在 Android 7.0 上运行,但却是针对更低 API 级别开发的,那么在用户更改显示尺寸时,系统将终止此应用进程。应用必须能够妥善处理此情景。否则,当用户从最近使用记录中恢复运行应用时,应用将会出现崩溃现象。
您应测试应用以确保不会发生此行为。要进行此测试,您可以通过 DDMS 手动终止应用,以造成相同的崩溃现象。
在密度发生更改时,系统不会自动终止面向 N 及更高版本的应用;不过,这些应用仍可能对配置变更做出不良响应。
- Android 7.0 上的应用应能够妥善处理配置变更,并且在后续启动时不会出现崩溃现象。您可以通过更改字体大小 (Setting >Display > Font size) 并随后从最近使用记录中恢复运行应用,来验证应用行为。
- 由于之前的 Android 版本中的一项错误,系统未能将对主线程上的一个 TCP 套接字的写入操作举报为违反严格模式。Android 7.0 修复了此错误。呈现出这种行为的应用现在会引发
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”安全提供程序。