Android 9适配经验总结

四大组件适配

Android 应用的开发离不开 Android 四大组件的使用,Android 四大组件分别是:Activity,用于展示前台页面;Service,用于执行后台任务;BroadCast,用于组件之间的通信;ContentProvider,用于应用间数据的分享。

Activity启动方式适配

Activity 组件用于和用户进行交互,在 Android 9 版本中,系统对于应用进程的任务栈做了调整,Activity 组件是运行在任务栈(task stack)中,而其它三大组件 Broadcast、Service、ContentProvider不需要运行在任务栈中,由这些组件打开 Activity 时需要为这个 Activity 的运行指定一个新的任务栈,否则会导致应用崩溃,并抛出异常信息:

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

适配方法,在 Broadcast、Service、ContentProvider 这些组件中拉起 Activity,需要添加FLAG_ACTIVITY_NEW_TASK标志,这样便会在拉起 Activity 的同时,创建出一个新的任务栈:

Intent intent = new Intent(context, "xxxxx");//xxxxx 表示 Activity 的类名
intent.addFlag(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActiviy(intent);

Service启动方式适配

Service 是 Activity 外最常用的组件之一,它承接着用户处理后台任务的需求,是一种长生命周期的,没有可视化界面,运行于后台的一种服务程序。在 Android 5.0 之后 google 出于安全的角度禁止了隐式声明 Intent 来启动 Service,隐式声明即是不通过指定 Service 包名和类名,而是通过Intent 过滤机制,设置 intent 的 action、dataType 等方式根据 intent 匹配规则来查找并调用 Service:

Intent intent = new Intent(); 
intent.setAction("xxxx");//xxxx 为Service 在 IntentFilter 中配置的 action
context.startService(intent);

隐式启动 Service 会产生一个问题,就是传入的 action、dataType 可能会匹配到别的应用的后台服务上去,Service 是在后台运行的,不被用户所感知,因此通过隐式方式启动 Service 可能拉起的不是实际想要启动的服务,从而会产生错误。

为了防止隐式启动 Service 带来的问题,从 Android5.0 开始系统就禁止隐式启动 Service 的方式,上述代码在 Android9 系统上运行时会导致应用崩溃,并抛出异常:

Service Intent must be explicit…

适配方法

解决方式一: 将隐式启动转换为显式启动,通过指定包名、类名的方式拉起服务:

ComponentName cn = new ComponentName("包名","类名"); 
Intent intent = new Intent(cn);//显示启动 Service 
context.startService(intent);

解决方式二:调用 intent 的 setPackage 方法指定包名:

Intent intent = new Intent(); 
intent.setAction("xxxx");//xxxx 为Service 在 IntentFilter 中配置的 action
intent.setPackage("应用包名");
context.startService(intent);

前台服务需要添加权限

在 Android 9.0 中,为了防止前台服务被滥用,比如各种通知信息,系统规定应用在使用前台服务之前必须先申请 FOREGROUND_SERVICE 权限,否则就会抛出 SecurityException 异常。

java.lang.SecurityException: Permission Denial: startForeground from pid=xxxx, uid=xxxx requires android.permission.FOREGROUND_SERVICE

适配方法,在 AndroidManifest 中添加权限声明:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
通过这样方式便可以申请前台服务的运行权限,前台服务的创建才会正常执行。

限制静态广播的接收

应用监听广播可以通过动态注册和静态注册两种方式,动态注册就是在程序运行起来后,调用注册方法进行注册,静态注册则是把广播注册放在 Manifest 配置文件中。

在一些场景中,比如监听开机启动广播来拉起应用,是需要通过静态注册方式来实现。但对于需要应用正常启动后才能对广播进行正确处理的场景,则应用使用动态注册的方式,这时如果采用静态注册的方式,在应用没有启动时,收到广播可能不会得到正确的处理,同时都采用静态注册的方式也会影响广播传递的效率,因为很多未启动的应用也会被广播唤醒。

也是出于这样的考虑,Android 9.0 之后,隐式广播将会被全面限制,用户的自定义广播和大部分系统广播通过隐式注册的方式,即在 AndroidManifest 中注册的 Receiver 的方式将不能够生效。

适配方法,使用动态注册的方式注册广播监听,如下代码:

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("xxxx");//xxxx 标识广播 action
context.registerReceiver(broadcastReceiver, intentFilter);

采用代码中动态注册广播的方式不仅是 Android9 系统的要求,也是一种比较好的开发习惯,这有利于对广播的使用进行控制,在应用功能已经初始化完成的时候添加广播的监听,确保广播到来时的功能执行能正常进行。

限制ContentResolver数据更新操作

Android 提供 ContentProvider/ContentResolver 组件来让用户开放自己应用中的数据或者访问别的应用的数据,为了防止用户数据监听被滥用,从 Android8.0 系统起,通过 ContentResolver的registerContentObserver 方法监听应用数据变化的操作被加以限制,直接操作会报出如下错误,并导致应用崩溃:

Failed to find provider xxx for user xxx; expected to find a valid ContentProvider for this authority

适配方法,在 AndroidManifest.xml 文件中定义一个 Provider,使用监听数据更新的目标 ContentProvider的 authorities 作为这个 provider 的 authorities:

<provider
	android:name="com.xx.content.ContentProvider"
	android:authorities="com.xxx.androidclient" 
	android:enabled="true" 
	android:exported="false">
</provider>

权限与安全相关主要适配点

运行时动态权限申请

Google 在 Android 6.0 开始引入了权限申请机制,将所有权限分成了正常权限和危险权限。应用的相关功能每次在使用危险权限时需要动态的申请并得到用户的授权才能使用。

系统权限分为两类:正常权限和危险权限。

正常权限不会直接给用户隐私权带来风险。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。

危险权限会授予应用访问用户机密数据的权限。如果应用在其清单中列出了危险权限,则用户必须明确批准应用使用这些权限。

危险权限主要是和获取用户数据有关,应用会用到的危险权限包括:

android.permission.READ_EXTERNAL_STORAGE 读取外部存储数据
android.permission.WRITE_EXTERNAL_STORAGE 向外部存储写入数据

常用的正常权限包括:

BLUETOOTH 使用蓝牙权限
BROADCAST_STICKY 粘性广播
CHANGE_NETWORK_STATE 改变网络状态
CHANGE_WIFI_STATE 控制 WiFi 开关,改变 WiFi 状态
GET_PACKAGE_SIZE 获取应用安装包大小
INTERNET 网络权限
RECEIVE_BOOT_COMPLETED 监听启动广播
REQUEST_INSTALL_PACKAGES 安装应用程序
WRITE_SYNC_SETTINGS 修改系统设置

适配方法,应用运行过程中,动态申请需要的危险权限,如下代码:

requestPermissions(final @NonNull Activity activity,final @NonNullString[] permissions, final int requestCode)//permissions 即为需要申请权限列表

默认不支持 http 请求

出于网络安全的考虑,Android9 系统上,应用将被禁止使用 http 协议进行网络数据传输,而必须使用 https 安全网络协议,如果应用中使用了 http 协议传输数据,将会抛出下面的错误:

java.net.UnknownServiceException: CLEARTEXT communication to xxxx not permitted by network security policy

最好的适配方式是修改所有的网络接口,改为 https 协议;除了服务端接口改动,Google 也提供了两个方案来让客户端支持 http 协议。
方案 1:
在 AndroidManifest.xml 中的 application 节点添加以下配置, 允许所有明文请求。

<application
... android:usersCleartextTraffic=“true”
>

方案 2:
添加自定义的网络安全配置在 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 添加:

<manifest ... >
	<application
	android:networkSecurityConfig="@xml/network_security_config">
	... </application>
</manifest>

SharedPreferences 适配

SharedPreferences 是 Android 中的数据存储组件,原先针对数据访问没有严格的限制,
Android9.0 以后给 SharedPreferences 引入数据文件访问权限控制,去除了被外部应用读取数据的权限,设置 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE会触发安全异常。

适配方法
方案 1:
MODE_WORLD_READABLE模 式 换 成MODE_PRIVATE, 通 过 这 种 方 式 将 应 用 的SharedPreference 数据访问权限设置为私有,以防止别的应用对 SharedPreference 数据进行任意的读写操作,保证了应用自身的安全性,对于可以向别的应用暴露的数据,也可以通过ContentProvider组件实现数据共享。

方案 2:
对于需要全局读写的功能,可使用其他方式替换 SharedPreferences,例如DataStoreMMKV等。

猜你喜欢

转载自blog.csdn.net/johnWcheung/article/details/129297452