Notification機(jī)制淺析(基于SDK23)
最近在使用自定義內(nèi)容的Notification的時(shí)候遇到一個(gè)bug,RemoteServiceException: Bad notification posted from package xxx, Couldn't expand RemoteViews for....可把我玩壞了,而且不知是不是Instant Run的問題有些情況下可以正確運(yùn)行,有時(shí)又不可以了,而且都是卸載安裝,搞了大半天,google上也沒找到合適的解決方案,后來想起公司分享提到過這個(gè)坑,解決辦法就是每次更新Notification的時(shí)候都新建RemoteView,問題也的確是解決了。。。后來自己新建一個(gè)項(xiàng)目DEMO想驗(yàn)證下到底哪里有問題,進(jìn)行類似操作也沒出現(xiàn)問題。。實(shí)在是一臉懵逼,既然這樣,就Read the fucking source code吧,順便簡單了解下Notification的機(jī)制
先來說原因和結(jié)論
因?yàn)槲恼聲?huì)涉及Binder機(jī)制,并不是每個(gè)人都了解,如果你只需要解決Couldn't expand RemoteViews for...,可以直接看原因和解決方案
原因
Notification#contentView為nullNotification#[contentView|bigContentView|headsUpContentView].apply()異常,contentView、bigContentView、headsUpContentView都是RemoteView,所以是RemoteViews#apply方法運(yùn)行異常
解決方案
首先是Notification#contentView不能為null,了解下contentView是怎么賦值/創(chuàng)建的,下面是平時(shí)用于構(gòu)建通知的代碼:
RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.notify); //取決于是否需要自定義顯示內(nèi)容
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()這個(gè)方法運(yùn)行的流程圖

其中makeContentView方法,判斷mContentView是否不為null,是就直接返回,這個(gè)mContentView是通過NotificationCompat.Builder#setContent()賦值的
Notification.java
private RemoteViews makeContentView() {
if (mContentView != null) {
return mContentView;
} else {
return applyStandardTemplate(getBaseLayoutResource());
}
}
即使沒有調(diào)用`NotificationCompat.Builder#setContent()`方法,最后還是會(huì)創(chuàng)建一個(gè)新的指定樣式的`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一般不會(huì)為null,所以這沒什么需要注意的
再來看,RemoteViews#apply方法運(yùn)行異常,從上面可以知道,Notification的內(nèi)容區(qū)域是用RemoteViews來"填充"的,但RemoteViews并不是一個(gè)View,不繼承自View,只是保存了需要顯示的布局文件IDmLayoutId和如何解析并初始化成View對(duì)象等,具體看RemoteViews#apply()方法
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);//一般為this,除非你指定橫屏或豎屏?xí)r候的樣式
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;
}
接著調(diào)用RemoteViews#performApply(),遍歷Action列表并執(zhí)行其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);
}
}
}
這個(gè)mActions是什么時(shí)候構(gòu)造并初始化的?其實(shí)際類型是ReflectionAction,當(dāng)我們調(diào)用RemoteViews#setXXX來初始化其控件的內(nèi)容時(shí)就會(huì)新建一個(gè)ReflectionAction并添加到mActions
如:
contentView.setTextViewText(R.id.notify_title, getString(R.string.title));
添加一個(gè)ReflectionAction
``` `java
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
//幫我們預(yù)設(shè)為setText方法名
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
最后看`ReflectionAction`的實(shí)現(xiàn),就是通過反射來設(shè)值
```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);
}
}
所以為了正確運(yùn)行,必須保證你對(duì)自己的RemoteView某個(gè)viewId所代表的View的初始化用合適RemoteViews#setter的方法,另外RemoteViews支持的控件也是有限制的,可以看官方文檔,所以文章開始所說的沒次更新通知的時(shí)候新建RemoteView在我目前看來并沒什么依據(jù)
Binder類結(jié)構(gòu)和流程
Notification的機(jī)制簡單的分位兩大角色,NotificationManager和NotificationListener,前一個(gè)用來發(fā)布、更新、管理通知,后一個(gè)用于監(jiān)聽通知的變化,典型的觀察者模式

NotificationListener的注冊(cè)流程

NotificationListener是SDK18+以上才有的,因?yàn)镾DK18開始提供了NotificationListenerService可以用來讀取應(yīng)用通知,其實(shí)現(xiàn)就是通過NotificationListener來實(shí)現(xiàn)的,其內(nèi)部有一個(gè)INotificationListener.Stub類型的Binder本地對(duì)象,把其Binder代理對(duì)象注冊(cè)到NotificationManager,監(jiān)聽通知的變更,而系統(tǒng)通知欄改也為了用NotificationListenerService來實(shí)現(xiàn),通知欄啟動(dòng)的時(shí)候啟動(dòng)其內(nèi)部的NotificationListenerService并注冊(cè),注冊(cè)的Binder代理對(duì)象會(huì)以ManagerServiceInfo的形式記錄在NotificationManagerService的NotificationListener列表,并且會(huì)在NotificationListener的Binder對(duì)象銷毀的時(shí)候自動(dòng)從列表中移除
Notification的發(fā)布流程

用戶通過NotificationManager.notify()方法發(fā)送、更新一個(gè)通知,通過Binder進(jìn)行通信,最終由NotificationManagerService來統(tǒng)一處理,每個(gè)Notificaion都會(huì)在NotificationManagerService中已NotificationRecord的形式為記錄,NotificationManagerService并沒有UI相關(guān)的邏輯