Notification机制浅析(基于SDK23)

96
HelloCsl
2017.01.18 18:23* 字数 1049

Notification机制浅析(基于SDK23)

最近在使用自定义内容的Notification的时候遇到一个bug,RemoteServiceException: Bad notification posted from package xxx, Couldn't expand RemoteViews for....可把我玩坏了,而且不知是不是Instant Run的问题有些情况下可以正确运行,有时又不可以了,而且都是卸载安装,搞了大半天,google上也没找到合适的解决方案,后来想起公司分享提到过这个坑,解决办法就是每次更新Notification的时候都新建RemoteView,问题也的确是解决了。。。后来自己新建一个项目DEMO想验证下到底哪里有问题,进行类似操作也没出现问题。。实在是一脸懵逼,既然这样,就Read the fucking source code吧,顺便简单了解下Notification的机制

先来说原因和结论

因为文章会涉及Binder机制,并不是每个人都了解,如果你只需要解决Couldn't expand RemoteViews for...,可以直接看原因和解决方案

原因

  • Notification#contentView为null

  • Notification#[contentView|bigContentView|headsUpContentView].apply()异常,contentView、bigContentView、headsUpContentView都是RemoteView,所以是RemoteViews#apply方法运行异常

解决方案

首先是Notification#contentView不能为null,了解下contentView是怎么赋值/创建的,下面是平时用于构建通知的代码:

RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.notify); //取决于是否需要自定义显示内容
contentView.setTextViewText(R.id.notify_title, getString(R.string.title));
//...
mNotification = new NotificationCompat.Builder(getApplicationContext())
        .setContent(contentView) //setTitle...等
        .setTicker("")
        .setSmallIcon(getNotificationIcon())
        .setWhen(System.currentTimeMillis())
        .setPriority(NotificationCompat.PRIORITY_MAX)
        .setOngoing(true)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setContentIntent(PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
        .setCategory(NotificationCompat.CATEGORY_PROMO)
        .build();
mNotificationManager.notify(MY_ID, mNotification);

以下是NotificationCompat.Builder#build()这个方法运行的流程图

Notification contentView的创建
Notification contentView的创建

其中makeContentView方法,判断mContentView是否不为null,是就直接返回,这个mContentView是通过NotificationCompat.Builder#setContent()赋值的

Notification.java

private RemoteViews makeContentView() {
    if (mContentView != null) {
        return mContentView;
    } else {
        return applyStandardTemplate(getBaseLayoutResource());
    }
}

即使没有调用`NotificationCompat.Builder#setContent()`方法,最后还是会创建一个新的指定样式的`RemoteViews`并返回

/**
 * @param hasProgress whether the progress bar should be shown and set
 */
private RemoteViews applyStandardTemplate(int resId, boolean hasProgress) {
    RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId);

    resetStandardTemplate(contentView);
    //....
    if (mContentTitle != null) {
        contentView.setTextViewText(R.id.title, processLegacyText(mContentTitle));
    }
    if (mContentText != null) {
        contentView.setTextViewText(R.id.text, processLegacyText(mContentText));
        showLine3 = true;
    }
    //....
    return contentView;
}

所以可以看到,contentView一般不会为null,所以这没什么需要注意的

再来看,RemoteViews#apply方法运行异常,从上面可以知道,Notification的内容区域是用RemoteViews来"填充"的,但RemoteViews并不是一个View,不继承自View,只是保存了需要显示的布局文件IDmLayoutId和如何解析并初始化成View对象等,具体看RemoteViews#apply()方法

public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
    RemoteViews rvToApply = getRemoteViewsToApply(context);//一般为this,除非你指定横屏或竖屏时候的样式
    View result;
    final Context contextForResources = getContextForResources(context);
    Context inflationContext = new ContextWrapper(context) {
      //...
    };
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater = inflater.cloneInContext(inflationContext);
    inflater.setFilter(this);
    result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
    rvToApply.performApply(result, parent, handler);

    return result;
}

接着调用RemoteViews#performApply(),遍历Action列表并执行其Action#apply方法

private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
    if (mActions != null) {
        handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
        final int count = mActions.size();
        for (int i = 0; i < count; i++) {
            Action a = mActions.get(i);
            a.apply(v, parent, handler);
        }
    }
}

这个mActions是什么时候构造并初始化的?其实际类型是ReflectionAction,当我们调用RemoteViews#setXXX来初始化其控件的内容时就会新建一个ReflectionAction并添加到mActions

如:

contentView.setTextViewText(R.id.notify_title, getString(R.string.title));

添加一个ReflectionAction

``` `java
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}

//帮我们预设为setText方法名
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}


最后看`ReflectionAction`的实现,就是通过反射来设值

```java
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
    final View view = root.findViewById(viewId);
    if (view == null) return;

    Class<?> param = getParameterType();
    if (param == null) {
        throw new ActionException("bad type: " + this.type);
    }

    try {
        getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
    } catch (ActionException e) {
        throw e;
    } catch (Exception ex) {
        throw new ActionException(ex);
    }
}

所以为了正确运行,必须保证你对自己的RemoteView某个viewId所代表的View的初始化用合适RemoteViews#setter的方法,另外RemoteViews支持的控件也是有限制的,可以看官方文档,所以文章开始所说的没次更新通知的时候新建RemoteView在我目前看来并没什么依据

Binder类结构和流程

Notification的机制简单的分位两大角色,NotificationManagerNotificationListener,前一个用来发布、更新、管理通知,后一个用于监听通知的变化,典型的观察者模式

NotificationManager和NotificationListener的类图
NotificationManager和NotificationListener的类图

NotificationListener的注册流程

NotificationListener的注册流程
NotificationListener的注册流程

NotificationListener是SDK18+以上才有的,因为SDK18开始提供了NotificationListenerService可以用来读取应用通知,其实现就是通过NotificationListener来实现的,其内部有一个INotificationListener.Stub类型的Binder本地对象,把其Binder代理对象注册到NotificationManager,监听通知的变更,而系统通知栏改也为了用NotificationListenerService来实现,通知栏启动的时候启动其内部的NotificationListenerService并注册,注册的Binder代理对象会以ManagerServiceInfo的形式记录在NotificationManagerServiceNotificationListener列表,并且会在NotificationListener的Binder对象销毁的时候自动从列表中移除

Notification的发布流程

Notificaion的发布流程
Notificaion的发布流程

用户通过NotificationManager.notify()方法发送、更新一个通知,通过Binder进行通信,最终由NotificationManagerService来统一处理,每个Notificaion都会在NotificationManagerService中已NotificationRecord的形式为记录,NotificationManagerService并没有UI相关的逻辑

参考

NotificationManagerService笔记

Android NotificationListenerService原理简介

Heads-Up Notification

Android dev Notification

Android开发
Web note ad 1