Notification機(jī)制淺析(基于SDK23)

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為null

  • Notification#[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)行的流程圖

Notification contentView的創(chuàng)建
Notification contentView的創(chuàng)建

其中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ī)制簡單的分位兩大角色,NotificationManagerNotificationListener,前一個(gè)用來發(fā)布、更新、管理通知,后一個(gè)用于監(jiān)聽通知的變化,典型的觀察者模式

NotificationManager和NotificationListener的類圖
NotificationManager和NotificationListener的類圖

NotificationListener的注冊(cè)流程

NotificationListener的注冊(cè)流程
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的形式記錄在NotificationManagerServiceNotificationListener列表,并且會(huì)在NotificationListener的Binder對(duì)象銷毀的時(shí)候自動(dòng)從列表中移除

Notification的發(fā)布流程

Notificaion的發(fā)布流程
Notificaion的發(fā)布流程

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

參考

NotificationManagerService筆記

Android NotificationListenerService原理簡介

Heads-Up Notification

Android dev Notification

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容