SystemUI-应用知栏视图是如何夸进程显示?

简介: ## 应用知栏视图是如何夸进程显示到 SystemUI 的? 跨进程通讯的基础是 IPC ,通知服务(NotificationManagerService, 简称 NMS)也不离开 IPC ,核心架构还是 IPC 架构。 ### 消息通道 1. 应用做作为通知的发送端, 需要调用 NMS ,发通知。例如: String channelId = "channe

应用知栏视图是如何夸进程显示到 SystemUI 的?

跨进程通讯的基础是 IPC ,通知服务(NotificationManagerService, 简称 NMS)也不离开 IPC ,核心架构还是 IPC 架构。

消息通道

  1. 应用做作为通知的发送端, 需要调用 NMS ,发通知。例如:
      String channelId = "channel_1";
      String tag = "ailabs";
      int id = 10086;
      int importance = NotificationManager.IMPORTANCE_LOW;
      NotificationChannel channel = new NotificationChannel(channelId, "123", importance);
      NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      manager.createNotificationChannel(channel);
      Notification notification = new Notification.Builder(MainActivity.this, channelId)
              .setCategory(Notification.CATEGORY_MESSAGE)
              .setSmallIcon(R.mipmap.ic_launcher)
              .setContentTitle("This is a content title")
              .setContentText("This is a content text")
              .setAutoCancel(true)
              .build();
       // 通知栏要显示的视图布局
      RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews);                 
      notification.contentView = remoteViews;
      manager.notify(tag, id , notification);
      
  1. SystemUI 作为通知的接收放需要注册监听器 INotificationListener 是监听通通知的一个 AIDL 接口,
    NotificationListenerService 是一个监听管理服务,他的内部类 NotificationListenerWrapper 实现了

INotificationListener 接口。 例如:

  /** @hide */
    protected class NotificationListenerWrapper extends INotificationListener.Stub {
        @Override
        public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update) {
                 // 接收通知
                  ....
                 省略了很多代码
        }

        @Override
        public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update, NotificationStats stats, int reason) {
                // 删除通知
                      ....
                 // 省略了很多代码
        }
        

这个通知监听需要向 NMS 注册:

   @SystemApi
      public void registerAsSystemService(Context context, ComponentName componentName,
              int currentUser) throws RemoteException {
          if (mWrapper == null) {
              mWrapper = new NotificationListenerWrapper();
          }
          mSystemContext = context;
          INotificationManager noMan = getNotificationInterface();
          mHandler = new MyHandler(context.getMainLooper());
          mCurrentUser = currentUser;
          noMan.registerListener(mWrapper, componentName, currentUser);
      }
  

以上是 Android 为我们提供的通知接收管理服务类, SystemUI 有个NotificationListenerWithPlugins 类继承了 NotificationListenerService
类。 并在 SystemUI 进程起来的时候调用 registerAsSystemService() 方法完成了注册:

NotificationListenerWithPlugins mNotificationListener = new NotificationListenerWithPlugins();
mNotificationListener.registerAsSystemService();

 

这样通道就建立起来了。

消息传递过程,大家可以按照这个思路器走读源码

RemoteViews

以上只是讲解了应用怎么把一个消息传递到 SystemUI , 理解 IPC 通讯的不难理解。 而神奇之处在于显示的视图布局明明是定义在一个应用中,为何能跨进程显示到 SystemUI 进程中呢?

发送通知, 传递的通知实体是 Notification 的实例, Notification 实现了 Parcelable 接口。 Notification 有个 RemoteViews 的成员变量

 
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews);
notification.contentView = remoteViews;

RemoteViews 也实现了 Parcelable 接口, 主要是封装了通知栏要展示的视图信息, 例如, 应用包名、布局ID。我们都知道实现了 Parcelable 这个接口就可以在 IPC 通道上夸进程传递。 RemoteView 支持的布局类型也是有限的,例如在 8.0 上仅支持如下类型:

  • android.widget.AdapterViewFlipper
  • android.widget.FrameLayout
  • android.widget.GridLayout
  • android.widget.GridView
  • android.widget.LinearLayout
  • android.widget.ListView
  • android.widget.RelativeLayout
  • android.widget.StackView
  • android.widget.ViewFlipper

RemoteView 携带了视图信息, 进程间传递的并不是真实的视图对象, 而主要是布局的 id ,那么显示在通知栏上的视图对象又是如何创建出来的呢?

### 通知视图创建

在通知的接收端创建的,上文说过 NotificationManagerService 内部类 NotificationListenerWrapper 监听通知消息, 在收到消息之后就在里面解析消息,并创建视图了。

protected class NotificationListenerWrapper extends INotificationListener.Stub {

      
      @Override
      public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
              NotificationRankingUpdate update) {
          StatusBarNotification sbn;
          try {
              sbn = sbnHolder.get();
          } catch (RemoteException e) {
              Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
              return;
          }

          try {
              // convert icon metadata to legacy format for older clients
              createLegacyIconExtras(sbn.getNotification());
              // 创建视图
              maybePopulateRemoteViews(sbn.getNotification());
              
              maybePopulatePeople(sbn.getNotification());
          } catch (IllegalArgumentException e) {
              // warn and drop corrupt notification
              Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
                      sbn.getPackageName());
              sbn = null;
          }

          // ... 省略代码

      }

      @Override
      public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
              NotificationRankingUpdate update, NotificationStats stats, int reason) {
          StatusBarNotification sbn;
          //... 省略代码

      }
  }
  

在 maybePopulateRemoteViews 这个方法中会去检查布局是否要加载, **其实我们比较好奇的是布局资源在应用进程中,
SystemUI 如何加载远程进程的布局资源?**

有两个关键的信息: 包名、布局ID。知道了包名 SystemUI 进程是有权限创建对应包名的上下文对象的,进而可以拿到对应应用的
资源管理器, 然后就可以加载布局资源创建对象了。 maybePopulateRemoteViews 方法跟踪下去, 会走到 RemoteViews 的

 private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
     // RemoteViews may be built by an application installed in another
     // user. So build a context that loads resources from that user but
     // still returns the current users userId so settings like data / time formats
     // are loaded without requiring cross user persmissions.
     final Context contextForResources = getContextForResources(context);
     Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);

     // If mApplyThemeResId is not given, Theme.DeviceDefault will be used.
     if (mApplyThemeResId != 0) {
         inflationContext = new ContextThemeWrapper(inflationContext, mApplyThemeResId);
     }
     LayoutInflater inflater = (LayoutInflater)
             context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

     // Clone inflater so we load resources from correct context and
     // we don't add a filter to the static version returned by getSystemService.
     inflater = inflater.cloneInContext(inflationContext);
     inflater.setFilter(this);
     View v = inflater.inflate(rv.getLayoutId(), parent, false);
     v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
     return v;
 }  
 
 

其中 getContextForResources 中的 context 对象就是通过应用包名创建的上下文对象,创建过程:

private static ApplicationInfo getApplicationInfo(String packageName, int userId) {
      if (packageName == null) {
          return null;
      }

      // Get the application for the passed in package and user.
      Application application = ActivityThread.currentApplication();
      if (application == null) {
          throw new IllegalStateException("Cannot create remote views out of an aplication.");
      }

      ApplicationInfo applicationInfo = application.getApplicationInfo();
      if (UserHandle.getUserId(applicationInfo.uid) != userId
              || !applicationInfo.packageName.equals(packageName)) {
          try {
              Context context = application.getBaseContext().createPackageContextAsUser(
                      packageName, 0, new UserHandle(userId));
              applicationInfo = context.getApplicationInfo();
          } catch (NameNotFoundException nnfe) {
              throw new IllegalArgumentException("No such package " + packageName);
          }
      }

      return applicationInfo;
}    


## 只有 SystemUI 才能接收通知吗?

答案是否定的, 只要有权限注册通知监听的应用都可以。 具体权限是:
只要应用有这个权限就可以注册通知监听了, 这个权限只有系统应用才能申请, 也就是说,只要是系统应用都可以监听并显示通知的。 可以写一个简单的 demo 测试一下:

一、 申请权限

二、 在布局中定义一个容器来装远程通知视图

  ...
 <FrameLayout
     android:layout_width="match_parent"
     android:layout_height="92px"
     android:id="@+id/notification">

 </FrameLayout>
 ...
 
 

三、注册监听并处理通知显示逻辑。

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ViewGroup notificationContainer = findViewById(R.id.notification);
    NotificationListenerService listenerService = new NotificationListenerService() {
        @SuppressLint("LongLogTag")
        @Override
        public void onNotificationPosted(StatusBarNotification sbn) {
            super.onNotificationPosted(sbn);
            Log.d("NotificationListenerService", "onNotificationPosted" + sbn);
            if (sbn.getNotification().contentView != null) {
                View view =  sbn.getNotification().contentView.apply(MainActivity.this, null);
                notificationContainer.addView(view);
                view.setVisibility(View.VISIBLE);
                Log.d("NotificationListenerService", "add contentView");
            }

            if (sbn.getNotification().bigContentView != null) {
                View view =  sbn.getNotification().bigContentView.apply(MainActivity.this, null);
                notificationContainer.addView(view);
                view.setVisibility(View.VISIBLE);
                Log.d("NotificationListenerService", "add bigContentView");
            }

            if (sbn.getNotification().headsUpContentView != null) {
                sbn.getNotification().headsUpContentView.apply(MainActivity.this, null);
                Log.d("NotificationListenerService", "add headsUpContentView");
            }

        }
        @SuppressLint("LongLogTag")
        @Override
        public void onNotificationRemoved(StatusBarNotification sbn) {
            super.onNotificationRemoved(sbn);
            Log.d("NotificationListenerService", "onNotificationRemoved" + sbn);
        }

        @SuppressLint("LongLogTag")
        @Override
        public void onListenerConnected() {
            super.onListenerConnected();
            Log.d("NotificationListenerService", "onNotificationRemoved");
        }

        @Override
        public void onListenerDisconnected() {
            super.onListenerDisconnected();
        }
    };

    // 调用注册方法 registerAsSystemService 不是公开的 API 反射

    try {
        Method method =
                NotificationListenerService.class.getMethod("registerAsSystemService", Context.class, ComponentName.class, int.class);

        method.setAccessible(true);
        method.invoke(listenerService, this,
                new ComponentName(getPackageName(), getClass().getCanonicalName()),
                -1);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}    

运行起来后,注册成功, 然后任意应用发通知, 这里就能显示出来了。

目录
相关文章
|
10天前
|
安全 开发者 Python
Python IPC大揭秘:解锁进程间通信新姿势,让你的应用无界连接
【9月更文挑战第11天】在编程世界中,进程间通信(IPC)如同一座无形的桥梁,连接不同进程的信息孤岛,使应用无界而广阔。Python凭借其丰富的IPC机制,让开发者轻松实现进程间的无缝交流。本文将揭开Python IPC的神秘面纱,介绍几种关键的IPC技术:管道提供简单的单向数据传输,适合父子进程间通信;队列则是线程和进程安全的数据共享结构,支持多进程访问;共享内存允许快速读写大量数据,需配合锁机制确保一致性;套接字则能实现跨网络的通信,构建分布式系统。掌握这些技术,你的应用将不再受限于单个进程,实现更强大的功能。
26 5
|
1月前
|
人工智能 PyTorch 算法框架/工具
Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
【8月更文挑战第6天】Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
Xinference实战指南:全面解析LLM大模型部署流程,携手Dify打造高效AI应用实践案例,加速AI项目落地进程
|
3天前
|
消息中间件 程序员 数据处理
探究操作系统中的进程间通信(IPC)机制及其在现代软件开发中的应用
本文深入探讨了操作系统中的核心概念——进程间通信(IPC),揭示了其在现代软件开发中的关键作用。通过对各种IPC机制如管道、消息队列、共享内存等的详细分析,本文旨在为读者提供一个清晰的理解框架,帮助他们掌握如何在实际应用中有效利用这些技术以实现进程间的协同工作。此外,文章还将探讨IPC在高并发环境下的性能优化策略,以及如何避免常见的IPC编程错误。通过结合理论与实践,本文不仅适合希望深入了解操作系统原理的技术人员阅读,也对那些致力于提升软件质量和开发效率的程序员具有重要参考价值。
12 0
|
1月前
|
Kubernetes Shell 测试技术
在Docker中,可以在一个容器中同时运行多个应用进程吗?
在Docker中,可以在一个容器中同时运行多个应用进程吗?
|
1月前
|
Android开发 开发者 Kotlin
Android 多进程情况下判断应用是否处于前台或者后台
本文介绍在多进程环境下判断Android应用前后台状态的方法。通过`ActivityManager`和服务信息`RunningAppProcessInfo`可有效检测应用状态,优化资源使用。提供Kotlin代码示例,帮助开发者轻松集成。
165 8
|
1月前
|
安全 数据处理 开发者
Python IPC大揭秘:解锁进程间通信新姿势,让你的应用无界连接
【8月更文挑战第1天】在编程领域,Python的进程间通信 (IPC) 架起了不同进程间信息交流的桥梁,使得应用能够跨越边界协同工作。Python提供了丰富的IPC工具,如管道(简单的单向数据通道,适用于父子进程通信)、队列(安全的共享数据结构,支持多进程间的先进先出数据处理)、共享内存(高效的数据共享机制,利用`multiprocessing.Value`和`multiprocessing.Array`实现)、以及套接字(不仅支持网络通信,在本地也能实现进程间通信)。掌握这些机制,开发者就能构建出能够自由穿梭于多个进程的应用,实现更加强大和复杂的功能。
26 1
|
1月前
|
JavaScript Windows
NodeJs——如何获取Windows电脑指定应用进程信息
NodeJs——如何获取Windows电脑指定应用进程信息
58 0
|
2月前
|
消息中间件 安全 数据处理
Python中的并发编程:理解多线程与多进程的区别与应用
在Python编程中,理解并发编程是提高程序性能和响应速度的关键。本文将深入探讨多线程和多进程的区别、适用场景及实际应用,帮助开发者更好地利用Python进行并发编程。
|
2月前
|
数据处理 调度 Python
Python并发编程实战指南:深入理解线程(threading)与进程(multiprocessing)的奥秘,打造高效并发应用!
【7月更文挑战第8天】Python并发编程探索:使用`threading`模块创建线程处理任务,虽受限于GIL,适合I/O密集型工作。而`multiprocessing`模块通过进程实现多核利用,适用于CPU密集型任务。通过实例展示了线程和进程的创建与同步,强调了根据任务类型选择合适并发模型的重要性。
44 5
|
1月前
|
Python
Python IPC深度探索:解锁跨进程通信的无限可能,以管道与队列为翼,让你的应用跨越边界,无缝协作,震撼登场
【8月更文挑战第3天】Python IPC大揭秘:解锁进程间通信新姿势,让你的应用无界连接
18 0

相关实验场景

更多