在日常开发中,我很久以前就遇到了http请求报错的场景,虽当时已做记录,但是确记录到了我的另一篇Blog - 常见异常汇总 ,现在正好有时间就做一波
Android9.0适配
笔记 ~
版本兼容
- Android 7.0 兼容适配
- Android 8.0 兼容适配
- Android 9.0 兼容适配
- Android 10.0 兼容适配
Android每次版本更新后,总会为开发者来带来一部分的兼容工作,此处主要讲的就是关于Android P 的兼容适配 ~
- Apache HTTP 客户端弃用
- Http请求限制
- 前台服务
- 启动Activity
- 获取设备序列号
- 权限变更
- 花样屏适配
Apache HTTP 客户端弃用
其实在Android6.0时,就已经取消了对Apache HTTP
的支持,只是从Android9.0开始,默认情况下该库已从 bootclasspath
中移除;不过部分三方SDK可能依旧有使用,所以记录一下适配方式
如需继续使用Apache HTTP
,需要在应用的 AndroidManifest.xml
文件中添加<uses-library android:name="org.apache.http.legacy" android:required="false"/>
,具体如下
<manifest ... >
<application>
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
...
</application>
</manifest>
Http请求限制
由于 Android P(版本28及以上) 限制了明文流量的网络请求,非加密的流网络请求都会被系统禁止掉 ~
如果当前应用接口是http 请求,而非 https请求,这样就会导致系统禁止当前应用进行该请求,从而报出错误:java.net.UnknownServiceException: CLEARTEXT communication to xxx.ip not permitted by network security policy
关于http请求限制的兼容方式主要还是在客户端方面,有以下几种,可根据推荐进行兼容适配
有的人可能想后端方面没有适配的方式吗?
如果非要这么说的话,那就是将需要https请求的接口都更换为http请求;不过这样明显不靠谱啊,https本身就是出于数据安全考虑的,我们不能本末倒置!
客户端
方式1:修改目标版本(不太推荐,治标不治本)
build.gradle(Model):修改targetSdkVersion 为28以下的版本(27即可) ~
android {
compileSdkVersion 29
defaultConfig {
...
minSdkVersion 16
targetSdkVersion 27
...
}
}
方式2:粗暴加入网络配置(有时无效且报错,不推荐)
AndroidManifest的application标签内加入以下这行代码
android:usesCleartextTraffic="true"
方式3:加入网络安全配置,兼容所有域名(最常使用的兼容方式,推荐)
- 在 res - xml 目录下新建 network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:android="http://schemas.android.com/apk/res/android">
<!--开发中可以考虑使用-->
<!--Android API 28 即以上版本,关闭HTTPS服务器监测-->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
- 将网络安全配置加入到AndroidManifest的application下
android:networkSecurityConfig="@xml/network_security_config"
AndroidManifest的applications标签示例
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
方式4:加入网络安全配置,兼容指定域名(最常使用的兼容方式,推荐)
兼容范围:方式3允许所有域名,方式4仅允许指定域名
实现过程:除network_security_config内配置不同,其余配置过程一致
network_security_config
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--实际部署后只放开自己的服务端地址-->
<!--只将本服务XXX.XXX.XXX.XXX器放开-->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">XXX.XXX.XXX.XXX</domain>
</domain-config>
</network-security-config>
前台服务
如果你看过我写的Android 8.0 兼容适配,那么就知道在8.0中讲明了后台服务限制,其对应场景启动服务的方式有所不同,如果你有在应用后台阶段启动前台服务的话,那么在9.0中还有一点要适配,否则会报出SecurityException
异常
如何检测是否启动前台服务?
查看一下是否有startForegroundService
或startForeground
方法,因为这是8.0启动前台服务的方式
Intent myService = new Intent(this, MyService.class);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(myService );
} else {
startService(myService);
}
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" />
启动Activity
在9.0后不允许非Activity场景启动Activity
,如果没有适配直接启动的话会崩溃报RuntimeException
异常
异常借鉴
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);
获取设备序列号
关于设备序列号的适配,主要有俩点要注意
- 需要
READ_PHONE_STATE
权限,记得动态申请 - Api变更,由
Build.SERIAL
改为Build.getSerial()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
testView.setText(Build.getSerial());
} else {
testView.setText(Build.SERIAL);
}
权限变更
9.0新增CALL_LOG
权限组,同时将PHONE
权限组中的 READ_CALL_LOG
、WRITE_CALL_LOG
和 PROCESS_OUTGOING_CALLS
移入该组(主要借鉴该处)。
1.限制访问通话记录
如果应用需要访问通话记录或者需要处理去电,则您必须向CALL_LOG
权限组明确请求这些权限。 否则会发生 SecurityException
2.限制访问电话号码
- 要通过
PHONE_STATE
Intent 操作读取电话号码,同时需要READ_CALL_LOG
权限和READ_PHONE_STATE
权限。 - 要从
PhoneStateListener
的onCallStateChanged()
中读取电话号码,只需要READ_CALL_LOG
权限。 不需要READ_PHONE_STATE
权限
花样屏适配
虽然我是做Android的,但是不可否认Android手机的屏幕形状确实比IOS要多太多了,例如常见的刘海屏、水滴屏、美人尖等等,多种屏幕形状也为开发带来了大量的适配工作~
- Android刘海屏、水滴屏全面屏适配方案
- Android P 刘海屏适配全攻略
记录一部分刘海屏的适配方案,如需适配具体屏幕还需自行查询
layoutInDisplayCutoutMode
-
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
:默认情况,允许应用程序的内容在竖屏模式下自动延伸到刘海区域,而在横屏模式下则不会延伸到刘海区域
-
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
:不管手机处于横屏还是竖屏模式,都允许应用程序的内容延伸到刘海区域(窗口声明使用挖孔区域) -
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
:不允许应用程序的内容延伸到刘海区域(窗口声明不使用挖孔区域)
DEFAUL 默认情况
下,横屏无法延申到刘海屏,可通过重写onWindowFocusChanged
方法让视图延申到刘海屏
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus && Build.VERSION.SDK_INT >= 19) {
val decorView = window.decorView
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
}
当我们允许视图内容延申到刘海屏后,如果在刘海屏内有子视图控件的话,需要重新设置布局信息
在适配刘海屏方面,用到了DisplayCutout
类主要获取凹口位置和安全区域的位置等,常见方法如下
-
getBoundingRects()
:返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形。 -
getSafeInsetLeft ()
:返回安全区域距离屏幕左边的距离,单位是px。 -
getSafeInsetRight ()
:返回安全区域距离屏幕右边的距离,单位是px。 -
getSafeInsetTop ()
:返回安全区域距离屏幕顶部的距离,单位是px。 -
getSafeInsetBottom()
:返回安全区域距离屏幕底部的距离,单位是px。
适配方法
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
root_layout.setOnApplyWindowInsetsListener { view, windowInsets ->
val displayCutout = windowInsets.displayCutout
if (displayCutout != null) {
val left = displayCutout.safeInsetLeft
val top = displayCutout.safeInsetTop
val right = displayCutout.safeInsetRight
val bottom = displayCutout.safeInsetBottom
val leftParams: FrameLayout.LayoutParams = btn_left.layoutParams as FrameLayout.LayoutParams
leftParams.setMargins(left, top, right, bottom)
}
windowInsets.consumeSystemWindowInsets()
}
}