Android 8.0(target=26)的适配中,有一个关于Notification的适配点:8.0开始使用Notification时候,需要指定一个渠道channel,用来将不同的通知类型分类管理,通常我们的代码会如下处理
//service.startForeground()使用Notification
val channelId = "default"
val notification: Notification
val builder = Notification.Builder(service, channelId)
.setContentTitle("")
.setContentText("")
notification = builder.build()
service.startForeground(1, notification)
实现后,在App管理页的Notification页面中,应该可以看到相应的channel
可是当我们实际运行后发现,并没有任何channel选项,然后我们打开log,发现在系统进程里有error类的log
system_process E/NotificationService: No Channel found for pkg=com.kotlinapplication, channelId=default, id=-37201, tag=null, opPkg=com.kotlinapplication, callingUid=10075, userId=0, incomingUserId=0, notificationUid=10075, notification=Notification(channel=default pri=0 contentView=null vibrate=null sound=null defaults=0x0 flags=0x40 color=0xff607d8b vis=PRIVATE)
很显然是没有找到channel
然而,当我们将target升级到27,即8.1后,再用8.1系统的手机运行,问题就更严重了
android.app.RemoteServiceException: Bad notification for startForeground: java.lang.RuntimeException: invalid channel for service notification: Notification(channel=default pri=0 contentView=null vibrate=null sound=null defaults=0x0 flags=0x40 color=0x00000000 vis=PRIVATE)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1768)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
直接产生崩溃了!!!原因还是没有找到channel,当然,当运行在8.0的系统上时,还是会报系统log而不会崩溃
既然target是26时候不会崩溃,而27的时候会崩溃,我们就会联想到是8.1相关的适配问题
既然8.0手机不会崩溃而8.1手机会崩溃,那基本就可以确认,这个问题是8.1系统的适配问题
虽然是channel没有创建的问题,但是在NotificationManager.notify()来展示通知时,不会出现崩溃(只会log);只有startForeground()会出现此类问题
但是官网对于这块的适配被很对人忽略了部分内容:那就是这个channel一定要先创建。并且好像并不会造成业务上的异常,所以是个隐藏的坑。下面我们跟着源码自己来分析一下吧
首先,service.startForeground()方法,最终会调用到ServiceRecord对象的postNotification()方法中:
public void postNotification() {
...
if (foregroundId != 0 && foregroundNoti != null) {
...
final Notification _foregroundNoti = foregroundNoti;
ams.mHandler.post(new Runnable() {
public void run() {
NotificationManagerInternal nm = LocalServices.getService(
NotificationManagerInternal.class);
if (nm == null) {
return;
}
Notification localForegroundNoti = _foregroundNoti;
try {
...
try {
Notification.Builder notiBuilder = new Notification.Builder(ctx,
localForegroundNoti.getChannelId());
...
localForegroundNoti = notiBuilder.build();
} catch (PackageManager.NameNotFoundException e) {
}
}
...
nm.enqueueNotification(localPackageName, localPackageName,
appUid, appPid, null, localForegroundId, localForegroundNoti,
userId);
foregroundNoti = localForegroundNoti;
} catch (RuntimeException e) {
...
ams.crashApplication(appUid, appPid, localPackageName, -1,
"Bad notification for startForeground: " + e);
}
}
});
}
}
然后,会调用到NotificationManagerService的enqueueNotification()方法:
public void enqueueNotification(String pkg, String opPkg, int callingUid, int callingPid,
String tag, int id, Notification notification, int userId) {
enqueueNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, notification,
userId);
}
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
final int callingPid, final String tag, final int id, final Notification notification,
int incomingUserId) {
...
//寻找channel是否已经创建
String channelId = notification.getChannelId();
if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
channelId = (new Notification.TvExtender(notification)).getChannelId();
}
final NotificationChannel channel = mRankingHelper.getNotificationChannel(pkg,
notificationUid, channelId, false /* includeDeleted */);
//还未创建则输出error类的系统Log
if (channel == null) {
final String noChannelStr = "No Channel found for "
+ "pkg=" + pkg
+ ", channelId=" + channelId
+ ", id=" + id
+ ", tag=" + tag
+ ", opPkg=" + opPkg
+ ", callingUid=" + callingUid
+ ", userId=" + userId
+ ", incomingUserId=" + incomingUserId
+ ", notificationUid=" + notificationUid
+ ", notification=" + notification;
Log.e(TAG, noChannelStr);
doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
"Failed to post notification on channel \"" + channelId + "\"\n" +
"See log for more details");
return;
}
...
}
以上就是8.0系统输出log的源码跟踪
首先,还是ServiceRecord对象的postNotification()方法:
public void postNotification() {
...
if (foregroundId != 0 && foregroundNoti != null) {
...
final Notification _foregroundNoti = foregroundNoti;
ams.mHandler.post(new Runnable() {
public void run() {
NotificationManagerInternal nm = LocalServices.getService(
NotificationManagerInternal.class);
if (nm == null) {
return;
}
Notification localForegroundNoti = _foregroundNoti;
try {
...
try {
Notification.Builder notiBuilder = new Notification.Builder(ctx,
localForegroundNoti.getChannelId());
...
localForegroundNoti = notiBuilder.build();
} catch (PackageManager.NameNotFoundException e) {
}
}
//判断channel是否已经创建
if (nm.getNotificationChannel(localPackageName, appUid,
localForegroundNoti.getChannelId()) == null) {
int targetSdkVersion = Build.VERSION_CODES.O_MR1;
try {
final ApplicationInfo applicationInfo =
ams.mContext.getPackageManager().getApplicationInfoAsUser(
appInfo.packageName, 0, userId);
targetSdkVersion = applicationInfo.targetSdkVersion;
} catch (PackageManager.NameNotFoundException e) {
}
//没有创建,且target为27及以上,则会抛出异常
if (targetSdkVersion >= Build.VERSION_CODES.O_MR1) {
throw new RuntimeException(
"invalid channel for service notification: "
+ foregroundNoti);
}
}
...
nm.enqueueNotification(localPackageName, localPackageName,
appUid, appPid, null, localForegroundId, localForegroundNoti,
userId);
foregroundNoti = localForegroundNoti;
} catch (RuntimeException e) {
...
//转发到app进程抛出异常
ams.crashApplication(appUid, appPid, localPackageName, -1,
"Bad notification for startForeground: " + e);
}
}
});
}
}
看到这里已经明白了吧,8.1系统在调用enqueueNotification()方法前,先判断了是否有channel已经创建,如果并没有创建的话,将会抛出异常,前提是你的app已经适配到8.1,即target>=27
关于这个问题,在https://developer.android.com/training/notify-user/channels里面其实有说明,解决办法就是提前创建channel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "default"
val channel = NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_DEFAULT)
val nm = service.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
nm?.let {
if (it.getNotificationChannel(channelId) == null) {//没有创建
it.createNotificationChannel(channel)//则先创建
}
}
val notification: Notification
val builder = Notification.Builder(service, channelId)
.setContentTitle("")
.setContentText("")
notification = builder.build()
service.startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
}
而对于项目来说,可能很多业务方或者sdk方都有这种问题,不太可控,所以为了保险起见,我们可以通过一些方式来完全规避这种问题:
通知所有sdk方、业务方,让他们进行适配
通过crash和log,在Application时候,由开发者自己提前将这些channel创建
可以通过代码插装方式,在所有类似调用前,进行channel判断和创建的处理