内容简介:跨进程通讯的基础是 IPC ,通知服务(NotificationManagerService, 简称 NMS)也离开 IPC ,核心架构还是 IPC 架构。这个通知监听需要向 NMS 注册:以上是 Android 为我们提供的通知接收管理服务类, SystemUI 有个NotificationListenerWithPlugins 类继承了 NotificationListenerService
应用知栏视图是如何夸进程显示到 SystemUI 的?
跨进程通讯的基础是 IPC ,通知服务(NotificationManagerService, 简称 NMS)也离开 IPC ,核心架构还是 IPC 架构。
消息通道
- 应用做作为通知的发送端, 需要调用 NMS ,发通知。例如:
String channelId = "channel_1"; String tag = "ailabs"; int id = 10086; int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = new NotificationChannel(channelId, "123", importance); // 通知栏要显示的视图布局 RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews); 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(); notification.contentView = remoteViews; manager.notify(tag, id , notification);
- 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 进程起来的时候调用 unregisterAsSystemService() 方法完成了注册:
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(); } }
运行起来后,注册成功, 然后任意应用发通知, 这里就能显示出来了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS小技巧·把子视图控制器的视图添加到父视图控制器
- CouchDB 视图简介及增量更新视图的方法
- c# – 将数据从部分视图传递到其父视图
- Django 基于函数的视图与基于类的视图
- 类视图
- laravel 基础教程 —— 视图
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。