Notification显示过程详解
来源:互联网 发布:sql某列后加合计数 编辑:程序博客网 时间:2024/06/07 05:49
一、前言
最近在崩溃上报中发现了如下错误,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掉,然后不使用自定义的通知,而使用系统默认的通知。
- Notification显示过程详解
- Notification的显示过程
- Notification的显示过程
- Toast显示过程详解
- 显示Notification
- Notification 详解
- notification 详解
- Notification详解
- Notification详解
- notification 详解
- Notification详解
- Notification详解
- Notification详解
- Notification详解
- Notification详解
- Notification详解
- Notification详解
- Notification---Android Notification通知详解
- django-part01
- Java获取当前路径
- 对django的QuerySets(查询集)的理解
- Android:学习AIDL,这一篇文章就够了(下)
- 解决dubbo问题:forbid consumer
- Notification显示过程详解
- JSP的7个动作指令之forward指令
- 春运抢票攻略
- Map.Entry使用详解
- 临时关闭Mac SIP系统完整性保护机制
- 利用新浪云SAE搭建可访问的免费个人网站
- apache 2.4 多站点配置
- 我的Oracle技术笔记
- noVnc