Android 13 NotificationChannels与Notification的加载流程

简介: Android 13 NotificationChannels与Notification的加载流程
一、NotificationChannel 的创建

这部分我觉得三方应用使用的较多,分析的时候也是源码与三方应用

结合分析的。

在源码中,我看到了一个很怪的类:NotificationChannels.java。这个类继承了 CoreStartable。

  • 注:CoreStartable 就是 SystemUI,只是我这的源码的命名不一样,下面为了便于他人阅读,就以 SystemUI 来叫。

NotificationChannels.java 就百十行代码,很简单,一起看看这个类:

NotificationChannels

// NotificationChannels.java
public class NotificationChannels extends CoreStartable {
    // ...
    // 省略代码
    public static void createAll(Context context) {
        final NotificationManager nm = context.getSystemService(NotificationManager.class);
        // 创建通道
        final NotificationChannel batteryChannel = new NotificationChannel(BATTERY,
                context.getString(R.string.notification_channel_battery),
                NotificationManager.IMPORTANCE_MAX);
        final String soundPath = Settings.Global.getString(context.getContentResolver(),
                Settings.Global.LOW_BATTERY_SOUND);
        batteryChannel.setSound(Uri.parse("file://" + soundPath), new AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
                .build());
        batteryChannel.setBlockable(true);
        // 创建通道
        final NotificationChannel alerts = new NotificationChannel(
                ALERTS,
                context.getString(R.string.notification_channel_alerts),
                NotificationManager.IMPORTANCE_HIGH);
        // 创建通道
        final NotificationChannel general = new NotificationChannel(
                GENERAL,
                context.getString(R.string.notification_channel_general),
                NotificationManager.IMPORTANCE_MIN);
        // 创建通道
        final NotificationChannel storage = new NotificationChannel(
                STORAGE,
                context.getString(R.string.notification_channel_storage),
                isTv(context)
                        ? NotificationManager.IMPORTANCE_DEFAULT
                        : NotificationManager.IMPORTANCE_LOW);
        // 创建通道
        final NotificationChannel hint = new NotificationChannel(
                HINTS,
                context.getString(R.string.notification_channel_hints),
                NotificationManager.IMPORTANCE_DEFAULT);
        // No need to bypass DND.
        // 注册通道
        nm.createNotificationChannels(Arrays.asList(
                alerts,
                general,
                storage,
                createScreenshotChannel(
                        context.getString(R.string.notification_channel_screenshot),
                        nm.getNotificationChannel(SCREENSHOTS_LEGACY)),
                batteryChannel,
                hint
        ));
        // Delete older SS channel if present.
        // Screenshots promoted to heads-up in P, this cleans up the lower priority channel from O.
        // This line can be deleted in Q.
        nm.deleteNotificationChannel(SCREENSHOTS_LEGACY);
        if (isTv(context)) {
            // TV specific notification channel for TV PIP controls.
            // Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest
            // priority, so it can be shown in all times.
            // 注册通道
            nm.createNotificationChannel(new NotificationChannel(
                    TVPIP,
                    context.getString(R.string.notification_channel_tv_pip),
                    NotificationManager.IMPORTANCE_MAX));
        }
    }
    /**
     * Set up screenshot channel, respecting any previously committed user settings on legacy
     * channel.
     * @return
     */
    @VisibleForTesting static NotificationChannel createScreenshotChannel(
            String name, NotificationChannel legacySS) {
        NotificationChannel screenshotChannel = new NotificationChannel(SCREENSHOTS_HEADSUP,
                name, NotificationManager.IMPORTANCE_HIGH); // pop on screen
        screenshotChannel.setSound(null, // silent
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
        screenshotChannel.setBlockable(true);
        if (legacySS != null) {
            // Respect any user modified fields from the old channel.
            int userlock = legacySS.getUserLockedFields();
            if ((userlock & NotificationChannel.USER_LOCKED_IMPORTANCE) != 0) {
                screenshotChannel.setImportance(legacySS.getImportance());
            }
            if ((userlock & NotificationChannel.USER_LOCKED_SOUND) != 0)  {
                screenshotChannel.setSound(legacySS.getSound(), legacySS.getAudioAttributes());
            }
            if ((userlock & NotificationChannel.USER_LOCKED_VIBRATION) != 0)  {
               screenshotChannel.setVibrationPattern(legacySS.getVibrationPattern());
            }
            if ((userlock & NotificationChannel.USER_LOCKED_LIGHTS) != 0)  {
                screenshotChannel.setLightColor(legacySS.getLightColor());
            }
            // skip show_badge, irrelevant for system channel
        } 
        return screenshotChannel;
    }
    @Override
    public void start() {
        createAll(mContext);
    }
    private static boolean isTv(Context context) {
        PackageManager packageManager = context.getPackageManager();
        return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
    }
}

NotificationChannels 扩展自 SystemUI 并重写了 start() 方法,它执行了 createAll() 方法,创建了通知通道有 batteryChannel(电池)、alerts(提醒)、storage(存储空间)、screenshot(屏幕截图)、hint (提示)、general(常规消息)。


此外,如果是 TV 设备的话还会创建画中画通知通道。


  • 怪在哪呢:为什么在这个类去创建注册那些通知通道,而且并没有提示消息什么的,意义在哪?
  • 注:下面我把该类当作三方应用。


下面围绕 NotificationChannels 一步一步的分析,上面调用 new NotificationChannel() 创建通知通道,然后 调用  nm.createNotificationChannels()方法注册通道。

nm 其实是 NotificationManager 的对象,这样就转到了 NotificationManager 中。这里我作了一个流程图:


image.png

从 NotificationManager.createNotificationChannel() 到 NotificationManagerService.createNotificationChannelsImpl() 都是正常流程,也好理解。创建的关键代码在 mPreferencesHelper.createNotificationChannel() 中,具体如下:

// PreferencesHelper.java
    @Override
    public boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
            boolean fromTargetApp, boolean hasDndAccess) {
        Objects.requireNonNull(pkg);
        Objects.requireNonNull(channel);
        Objects.requireNonNull(channel.getId());
        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
        boolean needsPolicyFileChange = false, wasUndeleted = false, needsDndChange = false;
        synchronized (mPackagePreferences) {
            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
            if (r == null) {
                throw new IllegalArgumentException("Invalid package");
            }
            if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
                throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
            }
            if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
                throw new IllegalArgumentException("Reserved id");
            }
            // 前面是各种条件检查,下面这行是关键点,先检索这个 channel 是否已经存在,以 channel id 为标志位。
            NotificationChannel existing = r.channels.get(channel.getId());
            // 如果通道已经存在就更新通道
            //  更新通道保留大部分已存在的设置,只更新了 name,description 等几项
            if (existing != null && fromTargetApp) {
                 // 省略部分代码......
            } else {
                 // 省略部分代码......
                // channel 未创建过,把用户创建的 channel 加入到系统的 cache 里
                r.channels.put(channel.getId(), channel);
                if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
                    needsDndChange = true;
                }
                MetricsLogger.action(getChannelLog(channel, pkg).setType(
                        com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
                mNotificationChannelLogger.logNotificationChannelCreated(channel, uid, pkg);
            }
        }
        if (needsDndChange) {
            updateChannelsBypassingDnd();
        }
        return needsPolicyFileChange;
    }


至此,一个通知完整的创建完成。

其实通过mPreferencesHelper.createNotificationChannel() 方法还能看出 NotificationChannel 一旦创建,那么能更改的东西就很少了(只有名字,描述,blocksystem,以及优先级),而 blocksystem 属性只有在系统源码里面才能使用(hide);


NotificationChannel 不会重复创建。


Android官方是这么解释这个设计的:NotificationChannel 就像是开发者送给用户的一个精美礼物,一旦送出去,控制权就在用户那里了。即使用户把通知铃声设置成《江南style》,你可以知道,但不可以更改。


二、Notification 的显示过程


这里代码有点多,我制作了一个通知传递的方法调用流程图:


image.png


上述流程图中,我们可能更比较关注 NotificationManagerService 是怎么与 SystemUI 交互的。

其实SystemUI向 NotificationManagerService 注册一个"服务"(一个Binder)。这个"服务"就相当于客户端 SystemUI 在服务端 NotificationManagerService 注册的一个回调。当有通知来临的时候,就会通过这个"服务"通知SystemUI,这个注册是在StatusBar#setUpPresenter()中完成的:

// StatusBar.java
    private void setUpPresenter() {
        // 省略部分代码......  
        // 这位置调用了NotificationsControllerImpl#initialize()的方法
        mNotificationsController.initialize(
                mPresenter,
                mNotifListContainer,
                mStackScrollerController.getNotifStackController(),
                mNotificationActivityStarter,
                mCentralSurfacesComponent.getBindRowCallback());
    }

NotificationsControllerImpl#initialize()中进行注册:

// NotificationsControllerImpl.kt
    override fun initialize(
        presenter: NotificationPresenter,
        listContainer: NotificationListContainer,
        stackController: NotifStackController,
        notificationActivityStarter: NotificationActivityStarter,
        bindRowCallback: NotificationRowBinderImpl.BindRowCallback
    ) {
        // 注册回调
        notificationListener.registerAsSystemService()
    }

上述注册了之后,每当有通知来时就会回调到:NotificationListener#onNotificationPosted() 中,接着就会到 NotificationEntryManager中。

下面分析通知视图的加载,这里就直接从 NotificationEntryManager#onNotificationPosted() 开始。

// NotificationEntryManager.java
    private final NotificationHandler mNotifListener = new NotificationHandler() {
        @Override
        public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
            final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey());
            // 通过key值进行判断,通知是否已存在
            if (isUpdateToInflatedNotif) {
                updateNotification(sbn, rankingMap);
            } else {
                addNotification(sbn, rankingMap);
            }
        }
    }


通过上述源码可以知道,通知到后,首先会进行判断该通知是否存在,存在则刷新,不存在则添加;这里以添加为例去分析。


先看两张图,可以知道下面分析的方向:


SystemUI组件思维导图:



image.png


SystemUI 关键布局图


image.png



根布局:super_status_bar.xml,

顶上状态栏: status_bar.xml, 通过CollapsedStatusBarFragment.java加载;PhoneStatusBarView(FrameLayout,)是里面的父控件; 对应 R.id.status_bar_container 。

下拉状态栏:(包括通知为status_bar_expanded.xml),最外层布局NotificationPanelView;qs_frame.xml 为下拉后的状态栏部分(用QSFragment管理,布局控件为QSContainerImpl),其高度更新在QSContainerImpl.java中;

NotificationStackScrollLayout 用于下拉的通知的相关问题(占满全屏,包括导航栏,会处理点击状态栏空白区的逻辑)。

NotificationStackScrollLayout:是一个滑动布局,里面嵌套着 ExpandableNotificationRow ,即通知。

接着上面分析:上面我们只关注 addNotification(sbn, rankingMap) ,而它内部时调用 addNotificationInternal() 方法实现的。

NotificationEntryManager#addNotificationInternal()

// NotificationEntryManager.java
   private void addNotificationInternal(
            StatusBarNotification notification,
            RankingMap rankingMap) throws InflationException {
        // 省略部分代码 ...
        NotificationEntry entry = mPendingNotifications.get(key);
        // 省略部分代码 ...
        // 构造视图
        if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
            // NotificationRowBinderImpl 为 NotificationRowBinder 的实现类
            mNotificationRowBinderLazy.get().inflateViews(entry, null, mInflationCallback);
        }
        // 省略部分代码 ...
    }

首先为通知创建一个 NotificationEntry 通知实例,然后再通过 NotificationRowBinderImpl 中的 inflateViews() 加载通知视图,绑定通知信息,并在通知栏添加通知视图,以及在状态栏添加通知图标。

NotificationRowBinderImpl#inflateViews()

// NotificationRowBinderImpl.java
    @Override
    public void inflateViews(
            NotificationEntry entry,
            NotifInflater.Params params,
            NotificationRowContentBinder.InflationCallback callback)
            throws InflationException {
        if (params == null) {
            // weak assert that the params should always be passed in the new pipeline
            mNotifPipelineFlags.checkLegacyPipelineEnabled();
        }
        // 获取查看父布局
        ViewGroup parent = mListContainer.getViewParentForNotification(entry);
        // 通知是否存在
        if (entry.rowExists()) {
            mIconManager.updateIcons(entry);
            ExpandableNotificationRow row = entry.getRow();
            row.reset();
            updateRow(entry, row);
            inflateContentViews(entry, params, row, callback);
        } else {
            // 创建图标
            mIconManager.createIcons(entry);
            mRowInflaterTaskProvider.get().inflate(mContext, parent, entry,
                    row -> {
                        // 为视图设置控制器.
                        ExpandableNotificationRowComponent component =
                                mExpandableNotificationRowComponentBuilder
                                        .expandableNotificationRow(row)
                                        .notificationEntry(entry)
                                        .onExpandClickListener(mPresenter)
                                        .listContainer(mListContainer)
                                        .build();
                        ExpandableNotificationRowController rowController =
                                component.getExpandableNotificationRowController();
                        rowController.init(entry);
                        entry.setRowController(rowController);
                        bindRow(entry, row);
                        updateRow(entry, row);
                        inflateContentViews(entry, params, row, callback);
                    });
        }
    }


上面无论走哪个分支,最后进入到inflateContentViews(entry, row, callback);这是一个回调:

NotificationRowBinderImpl#inflateContentViews()

// NotificationRowBinderImpl.java
    // 加载该行的基本内容视图
    private void inflateContentViews(
            NotificationEntry entry,
            NotifInflater.Params inflaterParams,
            ExpandableNotificationRow row,
            @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
         // 省略部分代码......
        params.rebindAllContentViews();
        mRowContentBindStage.requestRebind(entry, en -> {
            row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
            row.setIsLowPriority(isLowPriority);
            if (inflationCallback != null) {
                inflationCallback.onAsyncInflationFinished(en);
            }
        });
    }


inflationCallback 是 NotificationRowContentBinder 的一个内部接口;在 NotificationEntryManager 中被实现,所以将回调到 NotificationEntryManager#onAsyncInflationFinished() 中。

// NotificationEntryManager.java
        @Override
        public void onAsyncInflationFinished(NotificationEntry entry) {
            Trace.beginSection("NotificationEntryManager.onAsyncInflationFinished");
            mPendingNotifications.remove(entry.getKey());
            // If there was an async task started after the removal, we don't want to add it back to
            // the list, otherwise we might get leaks.
            if (!entry.isRowRemoved()) {
                boolean isNew = getActiveNotificationUnfiltered(entry.getKey()) == null;
                    // 省略部分代码......
                if (isNew) {
                    // 省略部分代码......
                    // 添加一个notification会走到这里、
                    // 包括一开机就显示出来的那些notification
                    addActiveNotification(entry);
                    // 更新视图
                    updateNotifications("onAsyncInflationFinished");
                    // 省略部分代码......
                } else {
                    // 省略部分代码......
                }
            }
            Trace.endSection();
        }


这里直接看 updateNotifications("onAsyncInflationFinished") 方法;

NotificationEntryManager#updateNotification()

// NotificationEntryManager.java
    public void updateNotifications(String reason) {
        // 省略部分代码......
        if (mPresenter != null) {
            // 更新视图
            mPresenter.updateNotificationViews(reason);
        }
        // 省略部分代码......
    }


mPresenter 的实现类是 StatusBarNotificationPresenter,所以接着看其里面的 updateNotificationViews() 方法。

StatusBarNotificationPresenter#updateNotificationViews()

// StatusBarNotificationPresenter.java
    @Override
    public void updateNotificationViews(final String reason) {
        if (!mNotifPipelineFlags.checkLegacyPipelineEnabled()) {
            return;
        }
        // The function updateRowStates depends on both of these being non-null, so check them here.
        // We may be called before they are set from DeviceProvisionedController's callback.
        if (mScrimController == null) return;
        // 不要在折叠期间修改通知。.
        if (isCollapsing()) {
            mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason));
            return;
        }
        // 把通知视图添加到通知面版的通知栏中
        mViewHierarchyManager.updateNotificationViews();
        // 这里不仅仅更新了通知面版的通知视图,也更新了状态栏的通知图标
        mNotificationPanel.updateNotificationViews(reason);
    }


我们这里看通知面板更新,即 mNotificationPanel.updateNotificationViews(reason) 方法。mNotificationPanel 为 NotificationPanelViewController 的对象。

NotificationPanelViewController#updateNotificationViews(reason)

// NotificationPanelViewController.java
    // 更新通知视图的部分和状态栏图标。每当显示的基础通知数据发生更改时,
    // 这由 NotificationPresenter 触发。
    public void updateNotificationViews(String reason) {
        // 更新NotificationStackScrollLayout 这个视图类的各种信息
        // updateSectionBoundaries() 这个方法还没弄明白,但我估计是添加/删除视图后布局重新定位,以及一个
        mNotificationStackScrollLayoutController.updateSectionBoundaries(reason);
        // Footer 其实就是通知面板底部的两个按钮:“管理”、“全部清除”。
        mNotificationStackScrollLayoutController.updateFooter();
        // 更新状态栏的通知图标
        mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList());
    }


至此通知面板的视图完成添加、更新。

下面接着看下状态栏的通知图标更新:

NotificationIconAreaController#updateNotificationIcons()

// NotificationIconAreaController.java
    public void updateNotificationIcons(List<ListEntry> entries) {
        mNotificationEntries = entries;
        updateNotificationIcons();
    }
    private void updateNotificationIcons() {
        Trace.beginSection("NotificationIconAreaController.updateNotificationIcons");
         // 更新状态栏图标
        updateStatusBarIcons();
        updateShelfIcons();
        // 更新 Aod 通知图标
        updateAodNotificationIcons();
        // 应用通知图标色调
        applyNotificationIconsTint();
        Trace.endSection();
    }


下面都是调用 update XXX Icons() 这种类似的方法,接着调用 updateIconsForLayout() 方法,我们直接分析 NotificationIconAreaController#updateIconsForLayout()

// NotificationIconAreaController.java
   private void updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function,
            NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority,
            boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia,
            boolean hideCenteredIcon) {
        // toShow保存即将显示的图标
        ArrayList<StatusBarIconView> toShow = new ArrayList<>(
                mNotificationScrollLayout.getChildCount());
        // 过滤通知,并保存需要显示的通知图标
        for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
            // 获取一个通知视图
            View view = mNotificationScrollLayout.getChildAt(i);
            if (view instanceof ExpandableNotificationRow) {
                NotificationEntry ent = ((ExpandableNotificationRow) view).getEntry();
                if (shouldShowNotificationIcon(ent, showAmbient, showLowPriority, hideDismissed,
                        hideRepliedMessages, hideCurrentMedia, hideCenteredIcon)) {
                    // 获取图标
                    StatusBarIconView iconView = function.apply(ent);
                    if (iconView != null) {
                        toShow.add(iconView);
                    }
                }
            }
        }
        // ...
        // 把需要显示的图标添加到hostLayout中
        final FrameLayout.LayoutParams params = generateIconLayoutParams();
        for (int i = 0; i < toShow.size(); i++) {
            StatusBarIconView v = toShow.get(i);
            // 如果刚刚删除并再次添加,视图可能仍会暂时添加
            hostLayout.removeTransientView(v);
            if (v.getParent() == null) {
                if (hideDismissed) {
                    v.setOnDismissListener(mUpdateStatusBarIcons);
                }
                // 执行到最后是 NotificationIconContainer.addView 添加视图
                // NotificationIconContainer本身没有addView、removeView方法,
                // 最终走的是其多层下去的父类ViewGroup的方法
                hostLayout.addView(v, i, params);
            }
        }
        // ...
    }


到这里整个 Notification 流程分析完毕。

相关文章
|
16天前
|
Android开发
Android面试之Activity启动流程简述
Android面试之Activity启动流程简述
66 6
|
14天前
|
Android开发 UED
Android 中加载 Gif 动画
【10月更文挑战第20天】加载 Gif 动画是 Android 开发中的一项重要技能。通过使用第三方库或自定义实现,可以方便地在应用中展示生动的 Gif 动画。在实际应用中,需要根据具体情况进行合理选择和优化,以确保用户体验和性能的平衡。可以通过不断的实践和探索,进一步掌握在 Android 中加载 Gif 动画的技巧和方法,为开发高质量的 Android 应用提供支持。
|
15天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
17天前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
22 3
|
18天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
19 2
|
1月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
33 3
|
17天前
|
Android开发
Android面试之Activity启动流程简述
Android面试之Activity启动流程简述
14 0
|
2月前
|
Android开发 开发者
Android面试之Activity启动流程简述
每个Android开发者都熟悉的Activity,但你是否了解它的启动流程呢?本文将带你深入了解。启动流程涉及四个关键角色:Launcher进程、SystemServer的AMS、应用程序的ActivityThread及Zygote进程。核心在于AMS与ActivityThread间的通信。文章详细解析了从Launcher启动Activity的过程,包括通过AIDL获取AMS、Zygote进程启动以及ActivityThread与AMS的通信机制。接着介绍了如何创建Application及Activity的具体步骤。整体流程清晰明了,帮助你更深入理解Activity的工作原理。
46 0
|
存储 缓存 Java
android 加载图片oom若干方案小结
本文根据网上提供的一些技术方案加上自己实际开发中遇到的情况小结。 众所周知,每个Android应用程序在运行时都有一定的内存限制,限制大小一般为16MB或24MB(视手机而定)。一般我们可以通过获取当前线程的可运行内存来判断,比如系统分给当前运行内存只有16M,而你的图片就有16M,这肯定会oom的。 相关知识介绍 1.颜色模型 常见的颜色模型有RGB、YUV、
1503 0
|
Android开发 数据格式 XML
android -- 加载gif 防止oom
项目中涉及到gif图片的展示,原来使用的是gifview,但是当频繁的,加载过大图片的时候会出现OOM的问题,后来去github上面找相关的库: https://github.com/koral--/android-gif-drawable android-gif-drawable是比较好的,并...
1136 0