最近在需求列表里看见一个“适配安卓8.0”的需求。一脸懵逼,之前没有做过根据新系统适配的工作。想想还是很有必要的,虽然现在8.0的设备不多,但是总有一天会占主流,不如提前过一遍,要不以后都是坑。
Android 8.0发布之后,官方遍发布了一篇名为Android 8.0 Behavior Changes的文章,列举了Android 8.0中系统和API行为的变化。其中又分为只对targetApi=26的应用有影响的变化和对所有应用都有影响的变化(无论targetApi是多少)。详细的过了一遍,有的内容实在是不熟悉,应用中也没用到,所以改之前都不知道什么样的。。。写的不对的地方欢迎指正
对所有应用都有影响的变化
这些变化会影响所有运行在Android 8.0系统的应用,无论他们的target api是多少。
后台执行限制
作为Android 8.0提高电池续航的手段,当你的应用没有任何活跃的组件而进入缓存状态(cached state),系统将释放所有应用持有的wake lock。
进一步,为了提高设备性能,系统会限制不在前台运行的应用的某些特定行为:
- 后台应用访问后台服务自由度降低
- 应用不能用manifests注册隐式广播
这些限定默认只对targetApi=26的应用起作用。但是对于targetApi不是O的应用,用户可以在“设置”中打开这些限制。
对于一些特定的方法,Android 8.0还有以下变化:
- targetApi=26的应用,在没有创建后台服务的权限时调用startService()会抛出异常IllegalStateException
- 新方法Context.startForegroundService()会开启一个前台服务。后台应用也可以调用这个方法,但是在服务创建之后5秒钟之内必须调用startForeground()
更多Background Execution Limits
安卓后台位置限制
为了电池续航、用户体验和系统健康,后台程序收到位置更新的频率减少。这个改变影响所有接收位置更新的应用,包括Google Play services。
受影响API:
- Fused Location Provider (FLP)
- Geofencing
- GNSS Measurements
- Location Manager
- Wi-Fi Manager
更多Background Location Limits
App shortcuts
更多App Shortcuts
i18n
- 一些Locale.getDefault()替换为Locale.getDefault(Category.DISPLAY)
- 时区名解析更正确(使用SimpleDateFormat结果可能与之前版本不一样)
- 更多
警告窗
如果一个应用使用权限SYSTEM_ALERT_WINDOW
和以下任意一个窗口类型(window type)来显示窗口:
- TYPE_PHONE
- TYPE_PRIORITY_PHONE
- TYPE_SYSTEM_ALERT
- TYPE_SYSTEM_OVERLAY
- TYPE_SYSTEM_ERROR
那么这些窗口会显示在使用了TYPE_APPLICATION_OVERLAY
窗口类型的窗口之下。如果应用的targetApi=26,应用应该使用TYPE_APPLICATION_OVERLAY
来显示警告窗。
以上列举的5种窗口类型,在api 26中被废弃了,非系统应用使用窗口应该使用
SYSTEM_ALERT_WINDOW
TYPE_APPLICATION_OVERLAY
需要申请SYSTEM_ALERT_WINDOW
权限。会显示在所有活动窗口(activity windows)之上,但是会在重要的系统窗口(如状态栏、IME)之下。系统可以调整窗口的位置、大小和可见性,也可以随时调整进程的重要性防止低内存情况下窗口被杀。
输入和导航
随着Chrome OS应用的出现和一些在表格中的使用场景,键盘导航操作再次多了起来,在安卓8.0中,安卓升级了对键盘导航输入的支持。具体来说,安卓对元素的焦点行为做出了如下改变:
- 如果控件没有指定焦点状态颜色,系统将根据活动的主题自动设置一个默认颜色。如果不希望控件有焦点颜色,那么可以把
android:defaultFocusHighlightEnabled
设置为false,或者在代码中setDefaultFocusHighlightEnabled()
传入false。 - 启用Drawing > Show layout bounds来测试键盘输入多获取焦点的影响。在安卓8.0中打开这个选项后,拥有焦点的控件上回有个“X”。
toolbar自动作为一个键盘导航群(keyboard navigation clusters)
网页表格自动填充
由于安卓自动填充框架(Autofill Framework)内置了对自动填充的支持,所以对安装在安卓8.0系统的应用,对下列网页表格填充相关方法做了修改(大约相当于废弃了):
WebSettings
- getSaveFormData() 现在返回false。之前在WebView存储了表格数据时会返回true
- 调用setSaveFormData() 没有任何效果
WebViewDatabase
- 调用clearFormData()没有任何效果
- hasFormData()返回false。之前如果表格有数据则会返回true
辅助功能
更多Accessibility
网络和HTTP(S)链接
- 没有body的OPTIONS请求,会有一个
Content-Length: 0
的header,之前没有Content-Length
的header -
HttpURLConnection
规范了含有空路径的URL,它会在host或authority后面加上/。例如,把http://example.com
变成http://example.com/
- proxy selector有关......
- URI不能包含空标签
- 更多......
蓝牙
对ScanRecord.getBytes()
可以提取的数据长度做了改变:
-
getBytes()
不知道接收到了多少数据,所以应用不要依赖返回字节的最大或最小值。但是它会计算结果数组的长度。 - 兼容蓝牙5的设备可能返回大于之前最大长度数据(大于60字节)
- 如果远程设备对扫描没有响应,仍然可能返回小于60字节的数据
无缝连接
WiFi性能的一些优化
安全性
- 不再支持SSLv3
- 当对一个服务器建立HTTPS连接时,如果服务器没有正确实现TSL协议版本,
HttpsURLConnection
不再回退到之前版本再次尝试 - 安卓8.0对所有应用程序都使用了SECCOMP过滤器,限制了可以使用的syscall为bionic暴露的syscall。尽管由于后向兼容的原因,还有其他可以使用的syscall,但安卓强烈反对开发者使用这些syscall
- Webview对象运行在多进程模式。为了提供更好的安全性,网页内容会在一个应用进程外的独立进程处理
- APK不一定在以“-1”或者“-2”结尾的文件夹中。应用应该使用
sourceDir
获得路径,不应该直接使用文件夹的格式 - Native libraries
安卓8.0对未知来源的未知程序的变化:
-
INSTALL_NON_MARKET_APPS
总是为1(8.0之前版本里,1为允许安装,0为不允许安装)。 现在应该使用canRequestPackageInstalls()
来判断未知来源的程序能不能安装。 - 使用
setSecureSetting()
修改INSTALL_NON_MARKET_APPS
的值会抛出UnsupportedOperationException
异常。应该使用DISALLOW_INSTALL_UNKNOWN_SOURCES
来防止用户安装位置来源的应用。 - 运行安卓8.0系统的设备自动设置
DISALLOW_INSTALL_UNKNOWN_SOURCES
为true(之前默认值为false)。老版本升级到8.0时,DISALLOW_INSTALL_UNKNOWN_SOURCES
自动设置为true,除非在升级之前用户手动设置INSTALL_NON_MARKET_APPS
为1。
更多User opt-in for unknown apps and sources,Security for Android Developers。
隐私性
-
标识符的变化
- 对于升级到8.0之前安装的应用,
ANDROID_ID
会保持不变。如果卸载后重新安装的话,ANDROID_ID
将会改变。如果想保存卸载之前的ANDROID_ID
的值,可以使用Key/Value Backup将老值和新值关联起来。 - 对于安装在8.0系统的应用来说,
ANDROID_ID
根据应用签名和用户的不同而不同。ANDROID_ID
唯一决定于应用签名、用户和设备三者的组合。 - 只要应用签名不变,卸载再安装应用不会改变
ANDROID_ID
- 有Google Play服务的设备可以使用
Advertising ID
,其他设备应该继续使用ANDROID_ID
- 对于升级到8.0之前安装的应用,
- 访问
net.hostname
会返回null
未捕获异常日志
如果应用的Thread.UncaughtExceptionHandler
没有对接默认的 Thread.UncaughtExceptionHandler
,当有未捕获异常抛出时,系统不会杀死应用。安卓8.0开始,系统会打印堆栈跟踪;之前版本中系统则不会打印。
我们建议自定义的Thread.UncaughtExceptionHandler
总是要和缺省的Thread.UncaughtExceptionHandler
关联,这样做了的应用不会受到影响。
findViewById()签名改变
所有的findViewById()
返回值由View
变为
- 这个变化可能导致已有代码对返回值的歧异,例如以
findViewById()
的返回值作为someMethod(View)
和someMethod(TextView)
的调用参数时 - 在Java 8中,在返回值不受限的情况下需要明确的转换成
View
类型。例如assertNotNull(findViewById(...)).someViewMethod())
- 覆盖
findViewById()
方法的地方需要更新返回值类型(例如Activity.findViewById()
)
Contacts Provider的使用变化
之前版本中,获取了READ_CONTACTS
权限的应用可以读取联系人信息包括:邮箱、电话、联系次数、最后联系时间等。
安卓8.0之后,获取READ_CONTACTS
权限的应用仍然可以读取这些信息,但是返回的数据由原来的精确值改为近似值。
这个变化会影响以下参数:
- TIMES_CONTACTED
- TIMES_USED
- LAST_TIME_CONTACTED
- LAST_TIME_USED
Collection handling
没懂
安卓企业版
更多Android in the Enterprise
以安卓8.0为目标版本的变化
以下改变只对安卓8.0及以上版本有影响。targetSdkVersion=26或更高的应用需要作出相应调整。
警告窗
这里
内容改变通知
安卓8.0改变了ContentResolver.notifyChange(Uri, ContentObserver)
和registerContentObserver(Uri, boolean, ContentObserver)
的行为。
这些接口要求Uri中的authority背后真实定义了一个有效的ContentProvider。
控件的焦点
可点击的控件默认为可聚焦的,如果希望一个控件可点击但是不可聚焦,可以在资源文件中设置android:focusable=false
或者在setFocusable()
中传入false
。
安全性
- 如果网络安全配置关闭纯文本通信,WebView无法通过HTTP访问网站。WebView必须通过HTTPS。
- Allow unknown sources系统被移除了,Install unknown apps权限控制未知来源的位置应用。Unknown App Install Permissions
使用用户账户信息
在安卓8.0中,应用只能使用authenticator拥有的账户信息或者用户授权的账户信息。仅仅申请GET_ACCOUNTS
权限不足以获得账户信息的授权,为了获得使用权限,需要调用AccountManager.newChooseAccountIntent() ,或者其他authenticator相关的方法。得到了权限之后,应用可以调用AccountManager.getAccounts()
来获得账户信息。
安卓8.0废弃了LOGIN_ACCOUNTS_CHANGED_ACTION
,应该应该使用addOnAccountsUpdatedListener()
来获取运行时账户变化。
隐私性
- 系统属性
net.dns1
,net.dns2
,net.dns3
和net.dns4
不再可用。 - 需要获取像DNS服务器这样的网络信息,应用需要获得
ACCESS_NETWORK_STATE
权限,可以通过注册NetworkRequest
或者NetworkCallback
来获取网络信息。安卓5.0以上可用。 - Build.SERIAL废弃了。应用应该使用
Build.getSerial()
来获取硬件序列号。这个方法需要READ_PHONE_STATE
权限。 - LauncherApps API不再允许工作模式的应用获取获取主模式的信息。当用户在工作模式时,LauncherApps API会认为其他模式没有安装任何应用。像以前一样,访问不相关的模式下的信息会导致SecurityExceptions。
权限
安卓8.0之前,如果应用在运行时申请一个权限,并且用户授予了这个权限,那么系统会错误的将这个权限所属的权限组里的并且在manifest里注册过的权限都授予这个应用。
对于targetApi为8.0的应用,以上行为已经被修正了,应用将只被授予其申请的权限。但是,如果应用之后再申请同一权限组中的其他权限时,将自动被授予。
例如,应用在Manifest里同时注册了READ_EXTERNAL_STORAGE
和WRITE_EXTERNAL_STORAG
两个权限。在targetApi=25或之前的版本中,当应用请求READ_EXTERNAL_STORAGE
权限并且用户授权了之后,系统会自动授予WRITE_EXTERNAL_STORAG
权限,因为READ_EXTERNAL_STORAGE
和WRITE_EXTERNAL_STORAG
同属STORAGE
权限组其都在manifest里注册了。但在安卓8.0系统中,只有READ_EXTERNAL_STORAGE
会被授权,但是当应用再次申请WRITE_EXTERNAL_STORAG
权限时,系统会不提示用户直接授权。
多媒体
- 系统默认实现自动音频闪避功能。也就是说,当其他应用以
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
来请求焦点的时候,当前焦点应用可以降低音量,但是不会收到onAudioFocusChange()
回调,且不会失去音频焦点。有些应用可能不想要这种行为,如果这些应用想要失焦且暂停音频播放,在新API中提供了这种选项,开发者可以覆盖这种行为。 - 当用户接电话时,在电话持续的这段时间音频自动静音
- 所有音频相关的API应该使用
AudioAttributes
来控制音频重放,而不应该使用音频流类型参数。仅在音量控制时使用音频流类型。其他使用音频流类型的地方也会正常工作,但是系统会在日志中记录这种情况为一个错误。 - 当使用
AudioTrack
时,如果应用需要很大的音频缓冲,系统会尝试使用深度缓冲 -
在安卓8.0中多媒体按钮事件处理的差异:
- 在UI活动中的处理没有变化:前台活动仍然有处理多媒体按钮事件的优先级
- 如果前台活动不处理多媒体按钮事件,系统则把事件转发给本地最近播放过音频的应用。在决定哪个应用会获得多媒体按钮事件时,多媒体回话的活跃状态、标志和重放状态并不会影响最终决策。
- 如果应用的多媒体会话已经被释放了,如果应用有
MediaButtonReceiver
的话,系统会把多媒体按钮事件传给应用的MediaButtonReceiver
- 处理上述情况之外,系统则舍弃多媒体按钮事件
本地库
以安卓8.0为目标的应用中,如果本地库中含有同时可写和可执行的装入段,则本地库不会被加载。因为这个改变有些应该可能不能正常工作了。这个改变是一个加强安全的措施。更多Writable and Executable Segments。
链接器改变随着目标api的改变而改变。如果在某一api版本中链接器有变化,那么应用则不能正常工作。如果目标api设为链接器变化之前的api版本,日志中会出现一个警告。
集合处理
在安卓8.0中Collections.sort()
的实现依赖于List.sort()
的实现。在安卓7.x(api 24和25)中正相反,默认的List.sort()
实现依赖于Collections.sort()
的实现。
这个变化使Collections.sort()
可以受益于优化后的List.sort()
,但是有如下限制:
-
实现
List.sort()
时不要调用Collections.sort()
,否则会引起死循环,引起stakeoverflow。如果想要默认的List行为,尽量不要覆盖sort()
方法。如果父类覆盖了
sort()
且实现的不太好,那么可以依赖List.toArray()
、Arrays.sort()
和ListIterator.set()
自己实现sort()
方法,例如:@Override public void sort(Comparator super E> c) { Object[] elements = toArray(); Arrays.sort(elements, c); ListIterator
iterator = (ListIterator 大多数情况下,实现
List.sort()
时候也可以委托给其他API来实现,例如:@Override public void sort(Comparator super E> comparator) { if (Build.VERSION.SDK_INT <= 25) { Collections.sort(this); } else { super.sort(comparator); } }
如果使用后一种实现仅仅是因为想为所有API版本提供一个统一的
sort()
方法,那么应该取一个唯一的名字比如sortCompat()
,而不是覆盖sort()
。 - 在List实现中,有些地方调用了sort(),对于这些实现来说,
Collections.sort()
的变化会带来实现中结构上的修改。例如,在8.0之前的系统中,在遍历ArrayList的过程中调用sort()
,如果这个sort()
是通过List.sort()实现的话,则会引发ConcurrentModificationException
异常,但是通过Collections.sort()
则不会引发异常。这个变化使系统行为更一致:两种做法都会引发异常。
类加载
安卓8.0系统中在加载新类的时候,会做一些检查以确保类加载器不会破坏运行时原有的设定。不论新类是从Java引用的,还是从Dalvik字节码引用的,还是JNI,这些检查都会执行。系统不会拦截在Java中调用的loadClass()
方法及其返回值。这个新行为对于正常的类加载器没有影响。
系统会检查类加载器返回的类描述符是否匹配期望的类描述符,如果描述符不匹配,系统会抛出NoClassDefFoundError
错误,并且会在异常中记录详细信息。
系统还会检查描述符是否有效。这个检查会捕获由于JNI调用间接加载类的方法(如GetFieldID()
)中无效的描述符引起的错误。例如,签名中java/lang/String
的字段是无法找到,因为这不是一个有效的签名。正确的签名应该为Ljava/lang/String;
。
和JNI调用FindClass()
不同,这里java/lang/String
是有效的。
安卓8.0不知道多个类加载器使用同一个DexFile对象定义类。这样做会引起InternalError
错误,错误信息为“Attempt to register dex file
DexFile API已经被废弃了,强烈建议开发者使用包括PathClassLoader
和BaseDexClassLoader
在内的系统类加载器。
注意:可以创建多个类加载器引用文件系统中的同一个APK或者JAR文件容器。这么做正常来说不会消耗更多的内存:如果DEX文件是压缩文件,系统会调用
mmap
,而不会解压它。但是,如果系统必须解压DEX文件,那么这样做是很耗内存的。
在安卓中,类加载器都是有并行能力的。当多线程用同一个类加载器加载同一个类的时候,最先完成的线程获胜,其他线程使用其返回结果。这种行为和类加载器的返回值无关。无论类加载器返回的是与加载的类相同的类还是不同的类,或者是返回异常,对这种多线程情况的处理都是一样的。
注意:在安卓8.0以前,不遵循这些设定的话,会导致多次定义同一个类、由于类冲突导致的堆内存损坏或者其他不良后果。