Android Notification显示过程详解

一、前言

最近在崩溃上报中发现了如下错误,notification报出来的错误,由于这只是在部分机型上面报出来,自己测试了几种机型都没能复现,所以只有分析一下Notification的显示过程来看一下能不能找到问题的原因。关于这个问题的分析我们留到最后再来看。

12-27 01:03:49.391 2072-2072/com.test.demo:mult E/AndroidRuntime: FATAL EXCEPTION: main
      Process: com.test.demo:mult, PID: 2072
      android.app.RemoteServiceException: Bad notification posted from package com.test.demo: Couldn't expand RemoteViews for: StatusBarNotification(pkg=com.test.demo user=UserHandle{0} id=189465103 tag=null score=0: Notification(pri=0 contentView=com.test.demo/0x7f030000 vibrate=default sound=default defaults=0xffffffff flags=0x10 kind=[null]))
          at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1363)
          at android.os.Handler.dispatchMessage(Handler.java:102)
          at android.os.Looper.loop(Looper.java:136)
          at android.app.ActivityThread.main(ActivityThread.java:5017)
          at java.lang.reflect.Method.invokeNative(Native Method)
          at java.lang.reflect.Method.invoke(Method.java:515)
          at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
          at dalvik.system.NativeStart.main(Native Method)

二、Notification的基本使用

1.创建一个Notification,并进行基本的配置。
(1)Android SDK11以后使用builder来创建

Notification.Builder notificationBuilder = new Notification.Builder(JPush.mApplicationContext)
                    .setContentTitle(notificationTitle)
                    .setContentText(alert)
                    .setTicker(alert)
                    .setSmallIcon(iconRes);
Notification notification = getNotification(notificationBuilder);

(2)Android SDK11前包括11直接创建Notification对象即可。

Notification notification = new Notification(iconRes, alert, System.currentTimeMillis());    
notification.setLatestEventInfo(mContext,notificationTitle, alert, null);     

(3)可以通过设置contentView来自定通知的样式。

2.显示NotificationManager显示Notification

NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(notifiId, notification);

三、Notification的显示过程

我们调用nm.notify就会将Notification显示出来,但是这中间是什么过程呢?下面我们就一步一步的看看这其中发生了什么事。
首先查看NotificationManager的notify,发现最终调用的是另一个重载的方法。

 public void notify(String tag, int id, Notification notification)
    {
        ...
        INotificationManager service = getService();
        Notification stripped = notification.clone();
        ...
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    stripped, idOut, UserHandle.myUserId());
        } catch (RemoteException e) {
        }
    }

上面关键代码就是service.enqueueNotificationWithTag,而这里的service实际上就是NotificationManagerService,查看源码发现,实际上最终调用的是enqueueNotificationInternal方法,其关键代码入下:

 public void enqueueNotificationInternal(final String pkg, String basePkg, final int callingUid,
            final int callingPid, final String tag, final int id, final Notification notification,
            int[] idOut, int incomingUserId)
    {
     //1.基本的校验(显示的消息的条数、notification、和contentView是否为空)   
     ...
        mHandler.post(new Runnable() {
            @Override
            public void run() {
              ...
             if (notification.icon != 0) {
                        if (old != null && old.statusBarKey != null) {
                            //2.更新一个旧的通知
                            r.statusBarKey = old.statusBarKey;
                            long identity = Binder.clearCallingIdentity();
                            try {
                                mStatusBar.updateNotification(r.statusBarKey, n);
                            }
                            finally {
                                Binder.restoreCallingIdentity(identity);
                            }
                        } else {
                            long identity = Binder.clearCallingIdentity();
                            try {
                                //3.增加一个通知到状态栏
                                r.statusBarKey = mStatusBar.addNotification(n);
                                if ((n.getNotification().flags & Notification.FLAG_SHOW_LIGHTS) != 0
                                        && canInterrupt) {
                                    mAttentionLight.pulse();
                                }
                            }
                            finally {
                                Binder.restoreCallingIdentity(identity);
                            }
                        }
                        // Send accessibility events only for the current user.
                        if (currentUser == userId) {
                            sendAccessibilityEvent(notification, pkg);
                        }

                        notifyPostedLocked(r);
                    } else {
                    }
              //4.其他配置(铃声、振动等)
              ... 
            }
        });
    }

继续查看新增的流程mStatusBar.addNotification(n),NotificationManagerService的addNotification最终调用PhoneStatusBar的addNotification(IBinder key, StatusBarNotification notification),如下:

 public void addNotification(IBinder key, StatusBarNotification notification) {
        //1.创建通知view 
        Entry shadeEntry = createNotificationViews(key, notification);
        ...
        //2.添加到通知栏
        addNotificationViews(shadeEntry);
        ...
    }

到这里整个从创建到显示的过程就完成了。 s

四、android.app.RemoteServiceException问题

根据前面的分析, 接下来查看创建View的代码,createNotificationViews是在父类BaseStatusBar里面定义的,如下:

 protected NotificationData.Entry createNotificationViews(IBinder key,
            StatusBarNotification notification) {
        ...
        if (!inflateViews(entry, mPile)) {
            handleNotificationError(key, notification, "Couldn't expand RemoteViews for: "
                    + notification);
            return null;
        }
        return entry;
    }

在代码中惊讶的发现和异常类似的字眼Couldn't expand RemoteViews for,那看来关键就在inflateViews中:

 public boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
        ...
        RemoteViews contentView = sbn.getNotification().contentView;
        if (contentView == null) {
            return false;
        }
        ...
        View contentViewLocal = null;
        View bigContentViewLocal = null;
        try {
            contentViewLocal = contentView.apply(mContext, adaptive, mOnClickHandler);
            if (bigContentView != null) {
                bigContentViewLocal = bigContentView.apply(mContext, adaptive, mOnClickHandler);
            }
        }
        catch (RuntimeException e) {
            final String ident = sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId());
            Log.e(TAG, "couldn't inflate view for notification " + ident, e);
            return false;
        }
        ... 
        return true;
    }

根据inflateViews的代码我们可以知道出现错误的原因有两种:
(1)contentView为null
(2)contentView.apply异常
因为contentView也就是RemoteViews如果我们有定制那么就是自定义的、如果没有自定义那么就是默认的,所以不可能为空,那关键就是RemoteViews的apply方法了,apply最终调用了performApply,如下:

 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);
            }
        }
    }

遍历所有的action,并调用其apply,那么这个action到底是哪里来的呢,实际这些action就是我们对布局的配置,如文字,图片什么的,以设置文字为例:

   public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }
    public void setCharSequence(int viewId, String methodName, CharSequence value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }

调用setTextViewText实际上是添加了一个RelectionAction,查看其apply方法:

 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);
            }
        }

可以看到是通过反射来调用相应的方法来进行设置的,但是反射可能会抛出异常的,导致崩溃:
(1)NoSuchMethodException 找不到方法,比如向一个ImageView,调用setText
(2)NullPointerException 调用的对象为空,由于RelectionAction中有对View的判断,所以此异常不会发生。
(3)IllegalAccessException 调用的方法是私有的,由于RemoteViews对外提供的方法都是控件的public的方法,所以不会发生
(4) InvocationTargetException 调用发生异常,这个不是很确定能不能发生。
通过测试发现(1)是可以重现的,也就是说布局上的错误,我们调用RemoteViews的setTextViewText在一个ImageView就会生这种情况,当然也会有其他情况。

五、总结

根据上面的分析可以知道发生这个问题,很有可能是布局的问题,基本都是反射的问题。当然RemoteViews使用的控件是有限制的,并不是所有的控件都能使用,否则肯定会崩溃,关于哪些控件是可用的可以查看官方文档。但是我这里的问题是某些机型会崩溃,而且我使用了自定义布局,所以我怀疑是可能某些机型对自定义通知有限制,导致在RelectionAction的apply时抛出了异常,所以最后解决办法是在nm.notify显示通知时catch掉,然后不使用自定义的通知,而使用系统默认的通知。

更正:之前没有注意,android.app.RemoteServiceException实际上是无法catch住的,因为发生改异常时,查看源码是直接底层崩溃,java层跟本捕获不到异常,后面我会继续研究这一块看看有没有别的处理办法

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

推荐阅读更多精彩内容