在日常开发中,我很久以前就遇到了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:加入网络安全配置,兼容所有域名(最常使用的兼容方式,推荐)
  1. 在 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>
  1. 将网络安全配置加入到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 异常

如何检测是否启动前台服务?

查看一下是否有startForegroundServicestartForeground 方法,因为这是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_LOGWRITE_CALL_LOGPROCESS_OUTGOING_CALLS移入该组(主要借鉴该处)。

1.限制访问通话记录
如果应用需要访问通话记录或者需要处理去电,则您必须向CALL_LOG权限组明确请求这些权限。 否则会发生 SecurityException

2.限制访问电话号码

  • 要通过 PHONE_STATE Intent 操作读取电话号码,同时需要 READ_CALL_LOG 权限和 READ_PHONE_STATE 权限。
  • 要从 PhoneStateListeneronCallStateChanged() 中读取电话号码,只需要 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()
    }
}