Android O 行为变更指南
Android O 除了提供诸多新特性和功能外,还对系统和 API 行为做出了各种变更。本文重点介绍您应该了解并在开发应用时加以考虑的一些主要变更。
其中大部分变更会影响所有应用,而不论应用针对的是何种版本的 Android。不过,有几项变更仅影响针对 Android O 的应用。为清楚起见,本页面分为两个部分:针对所有 API 级别的应用和针对 Android O 的应用。
针对所有 API 级别的应用
这些行为变更适用于在 Android O 平台上运行的所有应用,无论这些应用是针对哪个 API 级别构建。所有开发者都应查看这些变更,并修改其应用以正确支持这些变更(如果适用)。
网络连接和 HTTP(S) 连接
Android O 对网络连接和 HTTP(S) 连接行为做出了以下变更:
无正文的 OPTIONS 请求具有Content-Length: 0标头。之前,这些请求没有Content-Length标头。
HttpURLConnection 在包含斜线的主机或颁发机构名称后面附加一条斜线,使包含空路径的网址规范化。例如,它将http://example.com转化为http://example.com/。
通过 ProxySelector.setDefault ( ) 设置的自定义代理选择器仅针对所请求的网址(架构、主机和端口)。因此,仅可根据这些值选择代理。传递至自定义代理选择器的网址不包含所请求的网址的路径、查询参数或片段。
URI 不能包含空白标签。
之前,平台支持一种权宜方法,即允许主机名称中包含空白标签,但这是对 URI 的非法使用。此权宜方法只是为了确保与旧版 libcore 兼容。开发者如果对 API 使用不当,将会看到一条 ADB 消息:“URI example..com 的主机名包含空白标签。此格式不正确,将不被未来的 Android 版本所接受。”Android O 废除了此权宜方法;系统对格式错误的 URI 会返回 null。
Android O 在实现 HttpsURLConnection 时不会执行不安全的 TLS/SSL 协议版本回退。
对隧道 HTTP(S) 连接处理进行了如下变更:
在通过连接建立隧道 HTTP(S) 连接时,系统会在 Host 行中正确放置端口号 (:443) 并将此信息发送至中间服务器。之前,端口号仅出现在 CONNECT 行中
系统不再将隧道连接请求中的 user-agent 和 proxy-authorization 标头发送至代理服务器。
在建立隧道时,系统不再将隧道 Http(s)URLConnection 中的 proxy-authorization 标头发送至代理。相反,由系统生成 proxy-authorization 标头,在代理响应初始请求发送 HTTP 407 后将其发送至此代理。
同样地,系统不再将 user-agent 标头由隧道连接请求复制到建立隧道的代理请求。相反,库为此请求生成 user-agent 标头。
如果之前执行的 connect ( ) 函数失败,send( java.net.DatagramPacket )函数将会引发 SocketException:
如果存在内部错误,DatagramSocket.connect ( ) 会引发 pendingSocketException。对于 Android O 之前的版本,即使 send ( ) 调用成功,后续的 recv ( ) 调用也会引发 SocketException。为确保一致性,现在这两个调用均会引发 SocketException。
在回退到 TCP Echo 协议之前,InetAddress.isReachable ( ) 会尝试执行 ICMP:
对于某些屏蔽端口 7 (TCP Echo) 的主机(例如 google.com),如果它们接受 ICMP Echo 协议,现在也许能够访问它们。
对于确实无法访问的主机,此项变更意味着调用需要两倍的时间才能返回结果。
集合的处理
现在,AbstractCollection.removeAll ( )和AbstractCollection.retainAll ( )始终引发NullPointerException;之前,当集合为空时不会引发NullPointerException。此项变更使行为符合文档要求。
记录未捕获的异常
如果某个应用安装的Thread.UncaughtExceptionHandler未移交给默认的Thread.UncaughtExceptionHandler,则当出现未捕获的异常时,系统不会终止应用。从 Android O 开始,在此情况下系统将记录异常堆栈跟踪情况;在之前的平台版本中,系统不会记录异常堆栈跟踪情况。
我们建议,自定义Thread.UncaughtExceptionHandler实现始终移交给默认处理程序处理;遵循此建议的应用不受 Android O 此项变更的影响。
输入和导航
随着 Android 应用出现在 Chrome 操作系统和平板电脑等其他大尺寸设备上,我们看到,用户在 Android 应用中又重新开始使用键盘导航。在 Android O 中,我们又再次使用键盘作为导航输入设备,从而为基于箭头键和 Tab 键的导航构建了一种更可靠并且可预测的模型。
尤其要指出的是,我们对元素焦点行为做出以下变更:
现在,如果您没有为View对象(前景或背景图片)定义任何焦点状态颜色,框架会为View设置默认的焦点突出显示颜色。此焦点突出显示标志是基于操作组件主题背景的涟漪图片。
如果您不希望View对象在接收焦点时使用此默认突出显示标志,请在包含View的布局 XML 文件中将android:defaultFocusHighlightEnabled属性设置为false,或者将false传递至应用界面逻辑中的setDefaultFocusHighlightEnabled ( )。
要测试键盘输入对界面元素焦点有何影响,您可以启用 Drawing > Show layout bounds开发者选项。在 Android O 中,此选项在当前具有焦点的元素上显示一个 “X” 图标。
另外,Android O 中的所有工具栏元素自动组成键盘导航键区,用户可以更加轻松地导航进入和离开每个作为一个整体的工具栏。
如需详细了解如何在您的应用中改善对键盘导航的支持,请阅读以下链接中的支持键盘导航指南。
(https://developer.android.google.cn/training/keyboard-input/navigation.html)
安全性
Android O 包含以下与安全性有关的变更:
此平台不再支持 SSLv3
Android O 将使用安全计算 (SECCOMP) 过滤器来过滤所有应用。允许的系统调用列表仅限于通过 bionic 公开的系统调用。此外,还提供了其他几个后向兼容的系统调用,但我们不建议使用这些系统调用。
在与未正确实现 TLS 协议版本协商的服务器建立 HTTPS 连接时,HttpsURLConnection不再尝试回退到之前的 TLS 协议版本并重试的权宜方法。
现在,您的应用的WebView对象将在多进程模式下运行。网页内容在独立的进程中处理,此进程与包含应用的进程相隔离,以提高安全性。
您无法再假定 APK 驻留在名称以 -1 或 -2 结尾的目录中。应用应使用 sourceDir 获取此目录,而不能直接使用目录格式。
有关提升应用安全性的其他准则,请参阅以下链接中的面向 Android 开发者的安全性。
(https://developer.android.google.cn/topic/security/index.html)
后台执行限制
Android O 为提高电池续航时间而引入的变更之一是,当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。
此外,为提高设备性能,系统会限制未在前台运行的应用的某些行为。具体而言:
现在,在后台运行的应用对后台服务的访问受到限制。
应用无法使用其清单注册大部分隐式广播(即,并非专门针对此应用的广播)。
Android O 还对特定函数做出了以下变更:
如果针对 Android O 的应用尝试在不允许其创建后台服务的情况下使用startService ( )函数,则该函数将引发一个IllegalStateException。
新的Context.startForegroundService ( )函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用Context.startForegroundService ( )。不过,应用必须在创建服务后的五秒内调用该服务的startForeground ( )函数。
如需了解详细信息,请参阅以下链接中的后台执行限制。
(https://developer.android.google.cn/preview/features/background.html)
隐私性
Android O 对平台做出了以下与隐私性有关的变更:
现在,平台改变了标识符的处理方式:
对于在 OTA 之前安装到某个版本 Android O(API 级别 26)的应用,除非在 OTA 后卸载并重新安装,否则ANDROID_ID的值将保持不变。要在 OTA 后在卸载期间保留值,开发者可以使用密钥/值备份关联旧值和新值。
对于安装在运行 Android O 的设备上的应用,ANDROID_ID的值现在将根据应用签署密钥和用户确定作用域。应用签署密钥、用户和设备的每个组合都具有唯一的ANDROID_ID值。因此,在相同设备上运行但具有不同签署密钥的应用将不会再看到相同的 Android ID(即使对于同一用户来说,也是如此)。
只要签署密钥相同(并且应用未在 OTA 之前安装到某个版本的 O),ANDROID_ID的值在软件包卸载或重新安装时就不会发生变化。
即使系统更新导致软件包签署密钥发生变化,ANDROID_ID的值也不会变化。
要借助一个简单的标准系统实现应用获利,请使用广告 ID。广告 ID 是 Google Play 服务针对广告服务提供的唯一 ID,此 ID 可由用户重置。
查询net.hostname系统属性返回的结果为空。
针对 Android O 的应用
这些行为变更专门应用于针对 O 平台或更高平台版本的应用。针对 Android O 或更高平台版本进行编译,或将targetSdkVersion设为 Android O 或更高版本的应用开发者必须修改其应用以正确支持这些行为(如果适用)。
内容变更通知
Android O 更改了ContentResolver.notifyChange ( )和registerContentObserver ( Uri, boolean, ContentObserver )在针对 Android O 的应用中的行为方式。
现在,这些 API 需要在所有 URI 中为颁发机构定义一个有效的 ContentProvider。使用相关权限定义一个有效的ContentProvider可帮助您的应用防范来自恶意应用的内容变更,并防止将可能的私密数据泄露给恶意应用。
视图焦点
可点击的View对象现在默认也可以成为焦点。如果您希望View对象可点击但不可成为焦点,请在包含View的布局 XML 文件中将android:focusable属性设置为false,或者将false传递至应用界面逻辑中的setFocusable ( )。
权限
在 Android O 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。
对于针对 Android O 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
例如:
假设某个应用在其清单中列出READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。应用请求READ_EXTERNAL_STORAGE,并且用户授予了该权限。
如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予WRITE_EXTERNAL_STORAGE,因为该权限也属于同一STORAGE权限组并且也在清单中注册过。
如果该应用针对的是 Android O,则系统此时仅会授予READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。
集合的处理
在 Android O 中,Collections.sort ( )是在List.sort ( )的基础上实现的。在 Android 7.x(API 级别 24 和 25)中,则恰恰相反。在过去,List.sort ( )的默认实现会调用Collections.sort ( )。
此项变更使Collections.sort ( )可以利用优化的List.sort ( )实现,但具有以下限制:
List.sort ( )的实现不能调用Collections.sort ( ),因为这会导致堆栈因无限递归而溢出。相反,如果您需要 List 实现的默认行为,应避免重写 sort()。
如果父类以不适当的方法实现sort ( ),通常最好使用在List.toArray ( )、Arrays.sort ( )和ListIterator.set ( )的基础上构建的实现重写List.sort ( )。
例如:
@Override
publicvoidsort(Comparatorc){
Object[]elements=toArray();
Arrays.sort(elements,c);
ListIteratoriterator=(ListIterator)listIterator();
for(Objectelement:elements){
iterator.next();
iterator.set((E)element);
}
}
在大多数情况下,您也可以使用根据 API 级别委托给其他默认实现的实现重写List.sort ( )
例如:
@Override
publicvoidsort(Comparatorcomparator){
if(Build.VERSION.SDK_INT<=25){
Collections.sort(this);
}else{
super.sort(comparator);
}
}
如果您选择后者只是因为您希望开发一种适用于所有 API 级别的sort ( )函数,可以考虑赋予其一个唯一的名称,例如sortCompat ( ),而不是重写sort ( )。
现在,Collections.sort ( )只是对调用sort ( )的 List 实现进行的一项结构性修改。例如,在 Android O 之前的平台版本中,如果通过调用List.sort ( )进行排序,则当迭代处理ArrayList以及在迭代过程中调用sort ( )时,会引发ConcurrentModificationException。而Collections.sort ( )则不会引发异常。
此项变更使平台行为更加一致:现在,两种方法都会引发ConcurrentModificationException。
媒体
框架会执行音频闪避。进行AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时,应用不会失去焦点。新的 API 适用于需要暂停而不是闪避的应用。请注意,此行为无法在 Android O Developer Preview 1 版本中实现。
当用户打电话时,活动的媒体流将在通话期间静音。
所有与音频相关的 API 都应使用AudioAttributes而不是音频流类型来说明音频播放用例。仅为音量控制继续使用音频流类型。流类型(例如,已弃用的AudioTrack constructor)的其他用途仍然有效,但是系统会将其记录为错误。
使用AudioTrack时,如果应用请求了足够大的音频缓冲区,则框架将尝试使用深度缓冲区输出(如果可用)。
在 Android O 中,媒体按钮事件的处理有所不同:
在界面操作组件中处理媒体按钮未发生变化:前台操作组件在处理媒体按钮时仍然优先。
如果前台操作组件不处理媒体按钮,系统会将媒体按钮路由到最近在本地播放音频的应用。在确定哪些应用接收媒体按钮事件时,不再考虑活动状态、标志和媒体会话的播放状态。即使在应用调用setActive( false )后,媒体会话仍然可以接收媒体按钮事件。
如果应用的媒体会话已经释放,系统会将媒体按钮事件发送到应用的MediaButtonReceiver(如果有)。
对于任何其他情况,系统都会舍弃媒体按钮事件。与其开始播放错误的应用,不如不播放任何东西。
下图汇总了新的媒体按钮路由逻辑:
类加载行为
Android O 检查确保类加载器在加载新类时不会违反运行时假设条件。不论类引用自 Java(来自forName ( ))、Dalvik 字节码还是 JNI,都会执行这些检查。平台不会拦截 Java 对loadClass ( )函数的直接调用,也不会检查此类调用的结果。此行为不应影响运行良好的类加载器的正常运行。
平台将检查类加载器返回的类描述符是否与预期的描述符一致。如果返回的描述符与预期不符,平台会引发NoClassDefFoundError错误,并在异常日志中存储一条注明不一致之处的详细错误消息。
平台还检查请求的类描述符是否有效。此检查捕获间接加载诸如GetFieldID ( )等类的 JNI 调用,向这些类传递无效的描述符。例如,找不到包含java/lang/String签名的字段,是因为此签名无效;它应为Ljava/lang/String;。
这与 JNI 对FindClass ( )的调用不同,其中java/lang/String是一个有效的完全限定名称。
Android O 不支持多个类加载器同时尝试使用相同的 DexFile 对象来定义类。尝试进行此操作,会导致 Android 运行时引发InternalError错误,同时显示消息 “Attempt to register dex filewith multiple class loaders” 。
DexFile API 现已弃用,强烈建议您改为使用此平台的类加载器之一,包括PathClassLoader或BaseDexClassLoader。
注:您可以创建多个引用文件系统中同一个 APK 或 JAR 文件容器的类加载器。这样做通常不会占用大量内存:如果存储而不压缩容器中的 DEX 文件,平台可以对此类文件执行 mmap 操作,而不直接提取它们。但是,如果平台必须从容器中提取 DEX 文件,以这种方式引用 DEX 文件可能占用大量内存。
在 Android 中,所有类加载器都被视为支持并行运行。当多个线程争用同一个类加载器加载相同的类时,第一个完成此操作的线程胜出,而操作结果将用于其他线程。无论类加载器是返回同一个类、返回不同的类还是引发异常,都将发生此行为。该平台静默忽略此类异常。
注意:在低于 Android O 的平台版本中,违反这些假设条件可能导致多次定义同一个类、由于类混淆造成堆损坏和其他不良影响。