Android 8.0/8.1 channel适配

Android 8.0/8.1 channel适配

  • 一.背景
  • 二.问题
  • 三.原因
    • 8.0系统源码
    • 8.1系统源码
  • 四.解决

一.背景

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
Android 8.0/8.1 channel适配_第1张图片

二.问题

可是当我们实际运行后发现,并没有任何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而不会崩溃

三.原因

  1. 既然target是26时候不会崩溃,而27的时候会崩溃,我们就会联想到是8.1相关的适配问题

  2. 既然8.0手机不会崩溃而8.1手机会崩溃,那基本就可以确认,这个问题是8.1系统的适配问题

  3. 虽然是channel没有创建的问题,但是在NotificationManager.notify()来展示通知时,不会出现崩溃(只会log);只有startForeground()会出现此类问题

但是官网对于这块的适配被很对人忽略了部分内容:那就是这个channel一定要先创建。并且好像并不会造成业务上的异常,所以是个隐藏的坑。下面我们跟着源码自己来分析一下吧

8.0系统源码

首先,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的源码跟踪

8.1系统源码

首先,还是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方都有这种问题,不太可控,所以为了保险起见,我们可以通过一些方式来完全规避这种问题:

  1. 通知所有sdk方、业务方,让他们进行适配

  2. 通过crash和log,在Application时候,由开发者自己提前将这些channel创建

  3. 可以通过代码插装方式,在所有类似调用前,进行channel判断和创建的处理

你可能感兴趣的:(疑难杂症,android相关,原理解析)