关于Android8.0版本适配涉及方面较多,包含权限分化、渠道通知、广播限制、后台Service限制、安装APK等
版本兼容
Google每年的新版本总会为开发者带来新的版本适配问题,而我一般都是在遇到兼容问题后,开始以点打面,统一记录
权限适配
Android 6.0 告诉我们权限分级,动态申请,同组权限申请其一即可全组使用;但是在Android 8.0 有了小小变化~
首先在8.0中Phone权限组新增了俩个权限
ANSWER_PHONE_CALLS
:允许您的应用通过编程方式接听呼入电话。要在您的应用中处理呼入电话,您可以使用acceptRingingCall()
函数。READ_PHONE_NUMBERS
:权限允许您的应用读取设备中存储的电话号码。
接回上文,在Android 8.0之前,同组权限只要申请一个,那么就会获得整组权限,如下图所示;但是… 在Android 8.0之后,那么申请的权限都是单一的,申请哪个权限就作用于哪个权限,同组的其他权限不受其影响!
那么在实际开发中,我们一般都会将需要申请的权限放在一起进行统一申请,现在我们应该经常可以看到用户首次登陆时,弹出一系列的授权请求。
通知适配
Google在Android 8.0前都是单一通知渠道,用户可能因部分原因而直接选择关闭通知(全部关闭),导致一些有效的通知也无法传达给用户; 不过Andoird 8.0 后则将通知渠道(分类)做了划分
,可选择性的关闭通知,避免了用户错过有效的通知
在通知渠道适配中,有几个重要参数需要注意一下
ChannelID
渠道Id,具备唯一性,不可重复ChannelName
渠道名,会展示在系统设置说明,建议别重复,容易误导用户importance
通知重要程度
关于通知重要程度 importance
分类较多,官方文档中关于重要程度等级如下所示
- 紧急:发出提示音,并以提醒式通知的形式显示。
- 高:发出提示音。
- 中:无提示音。
- 低:无提示音,且不会在状态栏中显示。
NotificationManager中关于importance的划分
IMPORTANCE_HIGH
可在任何地方显示,有声音IMPORTANCE_DEFAULT
可任何地方显示,有声音但不会在视觉上干扰IMPORTANCE_MIN
无声音,只出现在状态栏中,不能与startForeground
一起用
创建 - 通知渠道
private void createNotificationChannel() {
//8.0版本兼容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
//分组(可选 - 可分可不分)
//groupId要唯一
String groupId = "group_01";
NotificationChannelGroup group = new NotificationChannelGroup(groupId, "广告");
//创建渠道组group
notificationManager.createNotificationChannelGroup(group);
//channelId要唯一
String channelId = "channel_01";
//渠道组下的具体通知
NotificationChannel adChannel = new NotificationChannel(channelId,
"游戏信息", NotificationManager.IMPORTANCE_DEFAULT);
//补充channel的含义(可选)
adChannel.setDescription("游戏信息");
//将渠道添加进组(先创建组才能添加)
adChannel.setGroup(groupId);
//创建channel
notificationManager.createNotificationChannel(adChannel);
}
}
发送 - 渠道通知
/**
* 发送通知
*
* @param channelId 渠道具体的id,具备唯一性
*/
private void sendNotificationChannel(String channelId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
//创建通知时,标记你的渠道id
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("通知Title")
.setContentText("通知Content信息")
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
}
删除 - 通知渠道
/**
* 删除渠道
*/
private void deleteNotificationChannel(String channelId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.deleteNotificationChannel(channelId);
}
}
为了方便测试,也可以直接用下面的方法进行测试
private void createNotificationChannel() {
//8.0版本兼容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
//分组(可选)
//groupId要唯一
String groupId = "group_01";
NotificationChannelGroup group = new NotificationChannelGroup(groupId, "广告");
//创建渠道组group
notificationManager.createNotificationChannelGroup(group);
//channelId要唯一
String channelId = "channel_01";
//渠道组下的具体通知
NotificationChannel adChannel = new NotificationChannel(channelId,
"游戏信息", NotificationManager.IMPORTANCE_DEFAULT);
//补充channel的含义(可选)
adChannel.setDescription("游戏信息");
//将渠道添加进组(先创建组才能添加)
adChannel.setGroup(groupId);
//创建channel
notificationManager.createNotificationChannel(adChannel);
//创建通知时,标记渠道id
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("通知Title")
.setContentText("通知Content信息")
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
}
后台执行适配
8.0增加了许多特性,但是对我们开发过程中影响比较大的主要就是后台执行限制,后要执行限制分为后台服务限制和广播限制.
广播限制
之前看有人说:如果要接收系统广播,而对应的广播在Android8.0中无法被接收,那么只能暂时把App的targetSdkVersion改为25或以下。
但现在工信部要求targetSdkVersion最小为26(android8.0)
关于8.0提出的广播限制,本质上是提升系统性能;如果你了解7.0的话,就知道在7.0的时候已经禁止掉几个常用广播了,这种限制主要是避免不良app频繁监听系统广播造成CPU资源损耗。
在8.0的更新后,Google对广播的限制更加严格了,禁止了AndroidMainfest中注册部分隐式广播监听器(或者说是可以注册,但是这种方式注册后也无效,接收器是收不到部分广播的),更多的是建议开发者通过动态注册实现显式注册 ~
关于这方面的解决方式,正确且常规的一般是在组件内动态注册,同时要注意在onDestroy中注销,主要是防止内存泄漏
TestReceiver testReceiver;
//注册广播
public void initTestReceiver() {
testReceiver = new TestReceiver();
IntentFilter intentFilter = new IntentFilter();
//以下需要监听的广播可自行定义,这里只是示例
intentFilter.addAction("android.intent.action.NEW_OUTGOING_CALL");
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
context.registerReceiver(testReceiver, intentFilter);
}
//注销广播(通常在组件的onDestroy中注销)
public void destroyTestReceiver() {
if (testReceiver != null) {
context.unregisterReceiver(testReceiver);
}
}
也有的人喜欢弯道超车,我就要静态注册广播接收者,有没有什么办法收到信息?
嗯… 有的人还真是这么做过,主要是当发送广播的时候,指定广播接收者的包名,即发送显式广播
//参数为具体的包名+类名path
Intent intent = new Intent("com.xiaoqiang.try.something.receiver");
//第二入参为发送的消息
intent.putExtra("receive","test broadcast");
//这步也挺关键,有点PMS的意思
intent.setPackage(getPackageName());
//intent.setComponent(...)
sendBroadcast(intent);
隐式广播例外情况 - 豁免广播
受 Android 8.0(API 级别 26)后台执行限制的影响,以 API 级别 26 或更高级别为目标的应用无法再在其清单中注册用于隐式广播的广播接收器。不过,有几种广播目前不受这些限制的约束。无论应用以哪个 API 级别为目标,都可以继续为以下广播注册监听器。
注意:尽管这些隐式广播仍在后台运行,但您应避免为其注册监听器
官网文档提供的,我稍微整理了下
//豁免的原因这些广播仅在首次启动时发送一次,而且许多应用需要接收此广播以调度作业、闹钟等。
ACTION_LOCKED_BOOT_COMPLETED、ACTION_BOOT_COMPLETED
//这些广播受特许权限保护,因此大多数普通应用都无法接收它们。
ACTION_USER_INITIALIZE、"android.intent.action.USER_ADDED"、"android.intent.action.USER_REMOVED"
//当时间、时区或闹钟发生更改时,时钟应用可能需要接收这些广播以更新闹钟。
"android.intent.action.TIME_SET"、ACTION_TIMEZONE_CHANGED、ACTION_NEXT_ALARM_CLOCK_CHANGED
//仅在语言区域发生更改时发送,这种情况并不常见。当语言区域发生更改时,应用可能需要更新其数据。
ACTION_LOCALE_CHANGED
//如果某个应用需要了解这些与 USB 有关的事件,除了为广播进行注册,目前还没有很好的替代方法。
ACTION_USB_ACCESSORY_ATTACHED、ACTION_USB_ACCESSORY_DETACHED、ACTION_USB_DEVICE_ATTACHED、ACTION_USB_DEVICE_DETACHED
//如果应用接收到针对这些蓝牙事件的广播,则用户体验不太可能受到影响。
ACTION_CONNECTION_STATE_CHANGED、ACTION_CONNECTION_STATE_CHANGED、ACTION_ACL_CONNECTED、ACTION_ACL_DISCONNECTED
//OEM 电话应用可能需要接收这些广播。
ACTION_CARRIER_CONFIG_CHANGED、TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED、"TelephonyIntents.SECRET_CODE_ACTION"、ACTION_PHONE_STATE_CHANGED、ACTION_PHONE_ACCOUNT_REGISTERED、ACTION_PHONE_ACCOUNT_UNREGISTERED
//有些应用需要了解登录帐号的更改,以便为新帐号和已更改的帐号设置调度的操作。
LOGIN_ACCOUNTS_CHANGED_ACTION
//具有帐号可见性的应用会在帐号被移除后收到此广播。如果应用只需要对此帐号更改执行操作,则强烈建议应用使用此广播,而不是使用已弃用的 LOGIN_ACCOUNTS_CHANGED_ACTION。
ACTION_ACCOUNT_REMOVED
//仅在用户明确清除“设置”中的数据时发送,因此广播接收器不太可能对用户体验造成显著影响。
ACTION_PACKAGE_DATA_CLEARED
//某些应用可能需要在其他软件包被移除时更新其存储的数据;对于这些应用来说,除了为此广播进行注册,没有很好的替代方法。
ACTION_PACKAGE_FULLY_REMOVED
//注意:其他与软件包相关的广播(例如 ACTION_PACKAGE_REPLACED)未能免受新限制的约束。这些广播很常见,豁免的话可能会影响性能。
//应用需要接收此广播,以在用户拨打电话时采取相应操作。
ACTION_NEW_OUTGOING_CALL
//此直播的发送频率不高;某些应用需要接收它来了解设备的安全状态已发生更改。
ACTION_DEVICE_OWNER_CHANGED
//由日历提供程序发送,以向日历应用发布事件提醒。由于日历提供程序并不知道日历应用是什么,因此此广播必须是隐式的。
ACTION_EVENT_REMINDER
//这些广播会在用户与设备的物理互动(安装或移除存储卷)或启动初始化(可用卷装载时)过程中发送,并且通常受用户控制。
ACTION_MEDIA_MOUNTED、ACTION_MEDIA_CHECKING、ACTION_MEDIA_UNMOUNTED、ACTION_MEDIA_EJECT、ACTION_MEDIA_UNMOUNTABLE、ACTION_MEDIA_REMOVED、ACTION_MEDIA_BAD_REMOVAL
//短信接收者应用需要依赖这些广播
SMS_RECEIVED_ACTION、WAP_PUSH_RECEIVED_ACTION
从别处copy了一份 简短版
// 开机广播
Intent.ACTION_LOCKED_BOOT_COMPLETED
Intent.ACTION_BOOT_COMPLETED
// 用户增删
Intent.ACTION_USER_INITIALIZE
// 时区广播
Intent.ACTION_TIMEZONE_CHANGED
// 语言区域
Intent.ACTION_LOCALE_CHANGED
// USB
UsbManager.ACTION_USB_ACCESSORY_ATTACHED
UsbManager.ACTION_USB_ACCESSORY_DETACHED
UsbManager.ACTION_USB_DEVICE_ATTACHED
UsbManager.ACTION_USB_DEVICE_DETACHED
// 蓝牙
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED
BluetoothDevice.ACTION_ACL_CONNECTED
BluetoothDevice.ACTION_ACL_DISCONNECTED
// 电话
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED
TelephonyIntents.SECRET_CODE_ACTION
TelephonyManager.ACTION_PHONE_STATE_CHANGED
TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED
TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED
// 登录账号
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION
// 数据清除
Intent.ACTION_PACKAGE_DATA_CLEARED
后台服务限制
指的是应用处于空闲状态时,可以使用的后台服务存在限制。 这些限制不适用于前台服务,因为前台服务更容易引起用户注意。
常见应用前台场景,反之则为后台应用
- 具有可见Activity(不管该Activity已启动还是已暂停)
- 具有前台服务
- 另一个前台应用已关联到该应用
在Android8.0之前,创建前台服务的方式,通常是先创建一个后台服务,然后将后台服务推向前台;
在Android8.0之后,系统不允许后台应用创建后台服务.因此引入了一个全新的方法Context.startForegroundService()
,以在前台启动新服务
官网原文:Android 8.0 还对特定函数做出了以下变更
- 如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用
startService()
函数,则该函数将引发一个IllegalStateException
。 - 新的
Context.startForegroundService()
函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用Context.startForegroundService()
。不过,应用必须在创建服务后的五秒内调用该服务的startForeground()
函数。
简单归纳
- 不允许应用处于后台的时候创建应用服务,如需要开启,那么以前用
startService()
,现在用startForegroundService()
- 在系统创建服务后,应用有五秒的时间来调用该服务的
startForeground()
方法以显示新服务的用户可见通知,如超时未调用startForeground()
,则系统将停止服务,并报出 ANR
关于后台服务的适配方式,有篇blog写的不错,这里也借花献佛下
先提示下
- 有的blog说使用IntentService,在8.0以上的前、后台服务问题可以解决,但是又出现了新的异常(相对于前后台服务的异常,量级要小很多,但也确实出现了一些异常),且此异常暂时无法根除
- JOB_ID不可随意更改,慎重!
常规方式
这种方式也是基础方式, 由后台服务转为前台服务,根据版本判断,使用 startForegroundService()
,但是应用必须在创建服务后的五秒内调用该服务的 startForeground()
;若不调用,日志会提示没有调用 startForeground
,甚至会出现 ANR 应用崩溃;
这种方式的不足在于前台服务是以通知栏的方式提醒用户的,首先8.0已经加入了渠道通知,这点是否要适配?同时前台服务会告知用户该场景耗电… 最后很多人对于前台服务这种事务,都是随手就关闭的(我就是这样),所以常规方式适合理解这次改变后的变化,在正式开发中容易出问题
// 启动 Service
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(new Intent(MainActivity.this, TestService.class));
} else {
startService(new Intent(MainActivity.this, TestService.class));
}
public class TestService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
initNotification();
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
initNotification();
return super.onStartCommand(intent, flags, startId);
}
private void initNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager =
(NotificationManager) getSystemService(android.content.Context.NOTIFICATION_SERVICE);
NotificationChannel channel =
new NotificationChannel("push", "push_name", NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
Notification notification =
new NotificationCompat.Builder(this, "push").setContentTitle("ACE_DEMO").setContentText("前台服务").build();
startForeground(1, notification);
}
}
}
进阶方式
官方提供了 JobSchedul
解决方案,即使用Android 5.0之后引入的 JobService
替代 Service
;
JobService
中通过 onStartJob
处理业务逻辑,通过 onStopJob
结束作业;调用是借助 JobInfo.Builder
构造器来启动;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class TestJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
// do something
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
//8.0后使用JobService,8.0前依旧使用普通Service
public static void startTestService(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
JobInfo.Builder builder = new JobInfo.Builder(Constants.JOB_ALARM_SERVICE_ID, new ComponentName(context, TestJobService.class));
builder.setOverrideDeadline(5000);
scheduler.schedule(builder.build());
} else {
context.startService(new Intent(context, TestJobService.class));
}
}
三方库 - 方式
目前关于 JobService
和 Servic
的简便使用,已经有成熟的三方库 android-job,无需区分版本,最低支持到 API 14
,基本满足日常版本;
Job 中通过 onRunJob
处理业务逻辑,通过 JobRequest.Builder
构造器来调用;且 Job 提供了包括立即启动/延迟启动/循环启动等多种方式,详细方法请参照官网;
public class TestJobCreator implements JobCreator {
@Override
@Nullable
public Job create(@NonNull String tag) {
switch (tag) {
case TestSyncJob.TAG:
return new TestSyncJob();
default:
return null;
}
}
}
public class TestSyncJob extends Job {
public static final String TAG = "job_test_tag";
@Override
@NonNull
protected Result onRunJob(Params params) {
// run your job here
return Result.SUCCESS;
}
public static void scheduleJob() {
new JobRequest.Builder(TestSyncJob.TAG)
.setExecutionWindow(30_000L, 40_000L)
.build()
.schedule();
}
}
JobManager.create(this).addJobCreator(new TestJobCreator());
APK安装适配
如果你没有适配8.0的话,在应用内升级app时,很可能是会挂掉的;主要原因是8.0去除了“允许未知来源”选项,所以我们需要进行适配 ~
解决方式比较简单,在AndroidManifest文件中添加安装未知来源应用的权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
这样系统会自动询问用户完成授权。当然你也可以先使用 canRequestPackageInstalls()
查询是否有此权限,如果没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
这个action将用户引导至安装未知应用权限界面去授权(借鉴blog)。
private static final int REQUEST_CODE_UNKNOWN_APP = 100;
private void installAPK(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
} else {
//跳转至“安装未知应用”权限界面,引导用户开启权限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}else {
//安装应用
}
}
//接收“安装未知应用”权限的开启结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
installAPK();
}
}
Activity透明主题
一切来源于umeng的一个报错:Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
该错误来源于umeng的错误收集
-
umeng错误频率,发生142次,影响36位用户 (我自身的测试机无法复现问题)
-
错误详情
-
错误原因:只支持不透明的全屏activity自主设置界面方向,反之如果该全屏activity是透明状态的情况下,你设置了屏幕方向则会报错
-
AndroidManifest 场景
<activity
android:name=".activity.MyActivity"
android:configChanges="screenSize|keyboardHidden|orientation"
android:label="我的页面"
android:screenOrientation="portrait"
android:theme="@style/APPTheme"
/>
解决方式
方法 1
虽然这种方法可以解决问题,但是大多时候我们还是需要当前Activity保持垂直的状态
删除AndroidManifest.xml中相应Activity的 android:screenOrientation=""属性
方法 2
- 去掉AndroidManifes.xml里面的"android:screenOrientation="portrait"属性
- 在自己的BaseActivity的onCreate中加上setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
方法 3
在保持垂直属性的同时,设置可满足我们需求的theme
在theme的内部属性中查看是否有android:windowIsTranslucent属性 , 如拥有的话,可删除,或者设置为 false
正式结果 - FitTheme为我们下方新增style
<activity
android:name=".activity.MyActivity"
android:configChanges="screenSize|keyboardHidden|orientation"
android:label="我的页面"
android:screenOrientation="portrait"
android:theme="@style/FitTheme"
/>
在res → values 下新建 styles.xml (如已有可直接添加style,如不存在则需要values-26新建styles.xml ),添加以下配置
<resources>
<style name="FitTheme" parent="AppTheme">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<!--用背景图消除启动白屏-->
<item name="android:windowIsTranslucent">false</item>
</style>
</resources>
如上述设置无效,那么可以用到我项目中部分设置,因为我的FitTheme继承自MyTheme
<style name="MyTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="toolbarStyle">@style/ClubToolbar</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowDisablePreview">true</item>
</style>
<style name="ClubToolbar" parent="Widget.AppCompat.Toolbar">
<!-- 设置该属性解决空白部分-->
<item name="contentInsetStart">0dp</item>
</style>
悬浮窗适配
关于这部分的适配,我在开发中并未遇到,只记得有俩个属性windowIsFloating(是否浮现在activity之上),windowIsTranslucent(窗体半透明),此处当作记录把
使用 SYSTEM_ALERT_WINDOW
权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:
- TYPE_PHONE
- TYPE_PRIORITY_PHONE
- TYPE_SYSTEM_ALERT
- TYPE_SYSTEM_OVERLAY
- TYPE_SYSTEM_ERROR
相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY
的新窗口类型。
也就是说需要在之前的基础上判断一下:
加入权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}