又到了我一年一度写 Android 适配文章的时间, 本身这篇应该会早几个月发出来, 但是前两三个月主要忙于 Flutter 的项目, 所以这篇文章才姗姗来迟. 不过毕竟是 9.0 的适配, 还不算太晚哈!
1. 前言
国内从去年开始就有消息说, 应用上架或者更新要求 TargetSdkVersion 最低要为 26 以上, 也就是最低也要适配到 8.0. 今年来也都逐步地开始落实. 比如下图的 小米应用商店公告 https://dev.mi.com/console/doc/detail?pId=1695 :
当然 Google Play 的要求
更为严格:
还包括从 8 月份开始在 Google Play 上发布的应用必须支持 64 位架构. 可以看到适配工作真的不能像以前一样随心所欲了. 好在我之前也有写过相关的适配攻略, Android 适配系列:
Android 6.0 的动态权限管理
Android 7.0 脱坑指南
Android 8.0 适配指北
2. 准备工作
进入正题, 首先将我们项目中的 targetSdkVersion 改为 28. 接下来运行你的项目, 看有没中枪.
3. 网络
1.Http 请求失败
在 9.0 中默认情况下启用网络传输层安全协议 (TLS), 默认情况下已停用明文支持. 也就是不允许使用 http 请求, 要求使用 https.
比如我使用的是 okhttp, 会报错:
java.NET.UnknownServiceException: CLEARTEXT communication to xxxx not permitted by network security policy
解决方法是需要我们添加网络安全配置. 首先在 res 目录下新建 xml 文件夹, 添加 network_security_config.xml 文件:
- <?xml version="1.0" encoding="utf-8"?>
- <network-security-config>
- <base-config cleartextTrafficPermitted="true" />
- </network-security-config>
AndroidManifest.xml 中的 application 添加:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest ...>
- <application Android:networkSecurityConfig="@xml/network_security_config">
- ...
- </application>
- </manifest>
以上这是一种简单粗暴的配置方法, 要么支持 http, 要么不支持 http. 为了安全灵活, 我们可以指定支持的 http 域名:
- <?xml version="1.0" encoding="utf-8"?>
- <network-security-config>
- <!-- Android 9.0 上部分域名时使用 http -->
- <domain-config cleartextTrafficPermitted="true">
- <domain includeSubdomains="true">secure.example.com</domain>
- <domain includeSubdomains="true">cdn.example1.com</domain>
- </domain-config>
- </network-security-config>
当然不止这些配置, 还有抓包配置, 设置自定义 CA 以及各种场景下灵活的配置, 详细的方法可以查看 官方文档 .
2.Apache HTTP 客户端弃用
在 Android 6.0 时, 就已经取消了对 Apache HTTP 客户端的支持. 从 Android 9.0 开始, 默认情况下该库已从 bootclasspath 中移除. 但是耐不住有些 SDK 中还在使用, 比如我见到的 友盟 QQ 分享报错问题 .
所以要想继续使用 Apache HTTP , 需要在应用的 AndroidManifest.xml 文件中添加:
<uses-library Android:name="org.apache.http.legacy" Android:required="false"/>
4. 前台服务
可以试着搜索一下你的代码, 看是否有调用 startForegroundService 方法来启动一个前台服务.
startForegroundService 主要来源估计都是 8.0 适配时候加上的:
- Intent intentService = new Intent(this, MyService.class);
- if (Android.os.Build.VERSION.SDK_INT>= Android.os.Build.VERSION_CODES.O) {
- startForegroundService(intentService);
- } else {
- startService(intentService);
- }
9.0 要求创建一个前台服务需要请求 FOREGROUND_SERVICE 权限, 否则系统会引发 SecurityException .
- java.lang.RuntimeException: Unable to start service com.weilu.test.MyService@81795be with Intent { cmp=com.weilu.test/.MyService }:
- java.lang.SecurityException: Permission Denial: startForeground from pid=28631, uid=10626 requires Android.permission.FOREGROUND_SERVICE
- at Android.App.ActivityThread.handleServiceArgs(ActivityThread.java:3723)
- at Android.App.ActivityThread.access$1700(ActivityThread.java:201)
- at Android.App.ActivityThread$H.handleMessage(ActivityThread.java:1705)
- at Android.os.Handler.dispatchMessage(Handler.java:106)
- at Android.os.Looper.loop(Looper.java:207)
- at Android.App.ActivityThread.main(ActivityThread.java:6820)
- at java.lang.reflect.Method.invoke(Native Method)
- at com.Android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
- at com.Android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
解决方法就是 AndroidManifest.xml 中添加 FOREGROUND_SERVICE 权限:
<uses-permission Android:name="android.permission.FOREGROUND_SERVICE" />
5. 启动 Activity
在 9.0 中, 不能直接非 Activity 环境中 (比如 Service , Application ) 启动 Activity , 否则会崩溃报错:
- java.lang.RuntimeException: Unable to create service com.weilu.test.MyService: Android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
- at Android.App.ActivityThread.handleCreateService(ActivityThread.java:3578)
- at Android.App.ActivityThread.access$1400(ActivityThread.java:201)
- at Android.App.ActivityThread$H.handleMessage(ActivityThread.java:1690)
- at Android.os.Handler.dispatchMessage(Handler.java:106)
- at Android.os.Looper.loop(Looper.java:207)
- at Android.App.ActivityThread.main(ActivityThread.java:6820)
- at java.lang.reflect.Method.invoke(Native Method)
- at com.Android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
- at com.Android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
这类问题一般会在点击推送消息跳转页面这类场景, 解决方法就是 Intent 中添加标志 FLAG_ACTIVITY_NEW_TASK ,
- Intent intent = new Intent(this, TestActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
6. 异形屏适配
这类异形屏叫法很多, 刘海屏, 水滴屏, 挖孔屏, 美人尖...
其实如果你的页面不需要全屏显示, 那么不需要额外的适配工作.
如果页面是全屏显示(比如启动页). 为了防止你的内容被遮挡, 大部分场景下都是可以使用获取状态栏高度来处理遮挡的适配问题. 因为状态栏的高度都是大于等于刘海的高度.
当然, 如果你想利用起来刘海区域, 就需要获取刘海位置等信息进行适配. 在 Android 9.0 中官方提供了 DisplayCutout 类, 可以确定刘海区域的位置, 国内的部分厂商在 8.0 就有了自己的适配方案.
具体的我就不过多介绍了, 推荐大家看以下文章:
Android P 刘海屏适配全攻略
Android 刘海屏, 水滴屏全面屏适配方案 https://juejin.im/post/5cf635846fb9a07f0c466ea7
7. 权限
首先是权限组的变更:
上图可以看到, 在 9.0 中新增权限组 CALL_LOG 并将 READ_CALL_LOG , WRITE_CALL_LOG 和 PROCESS_OUTGOING_CALLS 权限从 PHONE 中移入该组.
1. 限制访问通话记录
如果应用需要访问通话记录或者需要处理去电, 则您必须向 CALL_LOG 权限组明确请求这些权限. 否则会发生 SecurityException .
2. 限制访问电话号码
要通过 PHONE_STATE Intent 操作读取电话号码, 同时需要 READ_CALL_LOG 权限和 READ_PHONE_STATE 权限.
要从
PhoneStateListener 的 onCallStateChanged()
中读取电话号码, 只需要 READ_CALL_LOG 权限. 不需要 READ_PHONE_STATE 权限.
8. 其他
在 Android 9 中, 调用 Build.SERIAL 会始终返回 UNKNOWN 以保护用户的隐私. 如果你的应用需要访问设备的硬件序列号, 那么需要先请求 READ_PHONE_STATE 权限, 然后调用 Build.getSerial() .
注意非 SDK 接口的限制. 主要是一些热修复, 插件化框架涉及比较多, 注意及时升级新版本.
总的来说, 9.0 的适配工作需要改动和注意的点相比较以前版本的适配来说并不多, 从本篇的篇幅就可以看出来, 详细的变化可以参看文末的链接. 后面如果遇到什么坑, 我也会及时补充进来. 感谢你的阅读!!
参考
Android 9.0 行为变更
targetSdkVersion 升级 28
来源: http://www.tuicool.com/articles/3UzUJfv