Notification机制浅析(基于SDK23)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容