一、前言
最近在崩潰上報(bào)中發(fā)現(xiàn)了如下錯(cuò)誤,notification報(bào)出來(lái)的錯(cuò)誤,由于這只是在部分機(jī)型上面報(bào)出來(lái)讳苦,自己測(cè)試了幾種機(jī)型都沒能復(fù)現(xiàn),所以只有分析一下Notification的顯示過(guò)程來(lái)看一下能不能找到問(wèn)題的原因吩谦。關(guān)于這個(gè)問(wèn)題的分析我們留到最后再來(lái)看鸳谜。
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.創(chuàng)建一個(gè)Notification式廷,并進(jìn)行基本的配置卿堂。
(1)Android SDK11以后使用builder來(lái)創(chuàng)建
Notification.Builder notificationBuilder = new Notification.Builder(JPush.mApplicationContext)
.setContentTitle(notificationTitle)
.setContentText(alert)
.setTicker(alert)
.setSmallIcon(iconRes);
Notification notification = getNotification(notificationBuilder);
(2)Android SDK11前包括11直接創(chuàng)建Notification對(duì)象即可。
Notification notification = new Notification(iconRes, alert, System.currentTimeMillis());
notification.setLatestEventInfo(mContext,notificationTitle, alert, null);
(3)可以通過(guò)設(shè)置contentView來(lái)自定通知的樣式懒棉。
2.顯示NotificationManager顯示Notification
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(notifiId, notification);
三草描、Notification的顯示過(guò)程
我們調(diào)用nm.notify就會(huì)將Notification顯示出來(lái),但是這中間是什么過(guò)程呢策严?下面我們就一步一步的看看這其中發(fā)生了什么事穗慕。
首先查看NotificationManager的notify,發(fā)現(xiàn)最終調(diào)用的是另一個(gè)重載的方法妻导。
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) {
}
}
上面關(guān)鍵代碼就是service.enqueueNotificationWithTag逛绵,而這里的service實(shí)際上就是NotificationManagerService,查看源碼發(fā)現(xiàn)怀各,實(shí)際上最終調(diào)用的是enqueueNotificationInternal方法,其關(guān)鍵代碼入下:
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.基本的校驗(yàn)(顯示的消息的條數(shù)术浪、notification瓢对、和contentView是否為空)
...
mHandler.post(new Runnable() {
@Override
public void run() {
...
if (notification.icon != 0) {
if (old != null && old.statusBarKey != null) {
//2.更新一個(gè)舊的通知
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.增加一個(gè)通知到狀態(tài)欄
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.其他配置(鈴聲、振動(dòng)等)
...
}
});
}
繼續(xù)查看新增的流程mStatusBar.addNotification(n)胰苏,NotificationManagerService的addNotification最終調(diào)用PhoneStatusBar的addNotification(IBinder key, StatusBarNotification notification)硕蛹,如下:
public void addNotification(IBinder key, StatusBarNotification notification) {
//1.創(chuàng)建通知view
Entry shadeEntry = createNotificationViews(key, notification);
...
//2.添加到通知欄
addNotificationViews(shadeEntry);
...
}
到這里整個(gè)從創(chuàng)建到顯示的過(guò)程就完成了。 s
四硕并、android.app.RemoteServiceException問(wèn)題
根據(jù)前面的分析法焰, 接下來(lái)查看創(chuàng)建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;
}
在代碼中驚訝的發(fā)現(xiàn)和異常類似的字眼Couldn't expand RemoteViews for埃仪,那看來(lái)關(guān)鍵就在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;
}
根據(jù)inflateViews的代碼我們可以知道出現(xiàn)錯(cuò)誤的原因有兩種:
(1)contentView為null
(2)contentView.apply異常
因?yàn)閏ontentView也就是RemoteViews如果我們有定制那么就是自定義的、如果沒有自定義那么就是默認(rèn)的陕赃,所以不可能為空卵蛉,那關(guān)鍵就是RemoteViews的apply方法了,apply最終調(diào)用了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毙玻,并調(diào)用其apply,那么這個(gè)action到底是哪里來(lái)的呢,實(shí)際這些action就是我們對(duì)布局的配置廊散,如文字,圖片什么的梧疲,以設(shè)置文字為例:
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));
}
調(diào)用setTextViewText實(shí)際上是添加了一個(gè)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);
}
}
可以看到是通過(guò)反射來(lái)調(diào)用相應(yīng)的方法來(lái)進(jìn)行設(shè)置的,但是反射可能會(huì)拋出異常的幌氮,導(dǎo)致崩潰:
(1)NoSuchMethodException 找不到方法缭受,比如向一個(gè)ImageView,調(diào)用setText
(2)NullPointerException 調(diào)用的對(duì)象為空该互,由于RelectionAction中有對(duì)View的判斷米者,所以此異常不會(huì)發(fā)生。
(3)IllegalAccessException 調(diào)用的方法是私有的宇智,由于RemoteViews對(duì)外提供的方法都是控件的public的方法蔓搞,所以不會(huì)發(fā)生
(4) InvocationTargetException 調(diào)用發(fā)生異常,這個(gè)不是很確定能不能發(fā)生随橘。
通過(guò)測(cè)試發(fā)現(xiàn)(1)是可以重現(xiàn)的喂分,也就是說(shuō)布局上的錯(cuò)誤,我們調(diào)用RemoteViews的setTextViewText在一個(gè)ImageView就會(huì)生這種情況机蔗,當(dāng)然也會(huì)有其他情況蒲祈。
五甘萧、總結(jié)
根據(jù)上面的分析可以知道發(fā)生這個(gè)問(wèn)題,很有可能是布局的問(wèn)題梆掸,基本都是反射的問(wèn)題扬卷。當(dāng)然RemoteViews使用的控件是有限制的,并不是所有的控件都能使用酸钦,否則肯定會(huì)崩潰,關(guān)于哪些控件是可用的可以查看官方文檔怪得。但是我這里的問(wèn)題是某些機(jī)型會(huì)崩潰,而且我使用了自定義布局钝鸽,所以我懷疑是可能某些機(jī)型對(duì)自定義通知有限制汇恤,導(dǎo)致在RelectionAction的apply時(shí)拋出了異常,所以最后解決辦法是在nm.notify顯示通知時(shí)catch掉拔恰,然后不使用自定義的通知因谎,而使用系統(tǒng)默認(rèn)的通知。
更正:之前沒有注意颜懊,android.app.RemoteServiceException實(shí)際上是無(wú)法catch住的财岔,因?yàn)榘l(fā)生改異常時(shí),查看源碼是直接底層崩潰河爹,java層跟本捕獲不到異常匠璧,后面我會(huì)繼續(xù)研究這一塊看看有沒有別的處理辦法