SystemUI 开发总结

作者: 大利猫 | 来源:发表于2019-07-03 14:50 被阅读6次

    SystemUI 开发总结

    | 目录-
    SystemUI 有哪内容

    初次开发 SystemUI 有哪些弯路?

    SystemUI 创建流程?

    应用通知视图是如何跨进程显示的?

    应用窗口的 flag 是如何影响状态栏的?

    后续:SystemUI 能否脱离对系统源码依赖?

    SystemUI 有哪内容

    从表面上看, 我们看到的状态栏、通知栏、下拉菜单、导航栏、锁屏、最近任务、低电提示等系统页面都是 SystemUI 的。SystemUI,在源码目录中位于: framework/base/packages 目录下, 可见 SystemUI 和 framework 是关联的, SystemUI 依赖了很多内部 API , 系统资源, SystemUI 编译是要依赖系统源码的。

    SystemUI 也是一个应用,不过这个应用特殊之处在于他没有启动图标、也没有入口 Activity 。他的入口程序是一个服务:SystemUIService。 这个服务会被系统服务拉起来, 这个服务起来, SystemUI 应用进程就创建起来了,具体启动过程后面会分析。除了 SystemUIService , SystemUI 还有很多服务, 例如: 负责锁屏的KeyguardService、负责最近任务的 RecentsSystemUserService、负责壁纸的 ImageWallpaper 、负责截屏的TakeScreenshotService 等。

    系统移植 、UI 改造

    如果要做系统移植, SystemUI 改造这块的资料还是挺少,大部分情况下都是啃源码,连蒙带猜的修改,然后再编译出来验证。通常我们会从布局着手看看哪个布局长得像就着手去改,不过这块完全是可以沉淀一下经验出来让后人去节省时间的。这里我也不再赘述了, 有人已经梳理过了, 我借花献佛吧:https://blog.csdn.net/azhengye/article/details/50419409

    架构关系

    在系统服务中,有一个服务是专门为 SystemUI 的状态栏服务的, 这个服务就是 StatusbarManagerService (简称:SMS),和这个服务关系比较密切的服务是 WindowManagerService(简称:WMS), SMS 主要管控的是状态栏、导航栏, 例如:我们可以设置全屏、沉浸式状态栏都是 SMS 在起作用。

    初次开发 SystemUI 有哪些弯路 (环境上的坑)

    失败方案1

    IDE独立编译 SystemUI , 把 SystemUI 所依赖的系统 jar 都拷贝带 IDE 下,使用 provided 方式依赖。 在 6.0 以下版本还勉强可行 , 8.0 以后就基本不可能了, 8.0 以后 SystemUI 合入了锁屏模块,依赖了太多的系统资源, 编译不过是一个问题, 就算编译过了, 所依赖的系统资源 ID 也会不一致。 经过 1~2 两天的尝试, 这个方案失败了。

    失败方案2

    使用 Google 源码编译, 然后在源码中修改 SystemUI , 将编译的 SystemUI 安装到 MTK 系统的版子上。 发现安装到 MTK 的板子以后跑不起来, 原因是某些服务启动不了, 同时也存在资源 ID 不一致的问题。 经过 2~3 天的这条这个方案失败了。

    最终方案


    最终不得不麻烦系统同学, 帮忙提供源码: 在 MTK 源码中编译。

    为了提高效率, 使用一台昨晚编译机, 另一台作为编辑机, 通过 ssh 搭建通道配合完成开发、编译、安装三个流程。

    SystemUI 是如何启动的?

    前面介绍过 SystemUIService 是 SystemUI 的入库程序。 SystemUIService 是在服务进程中启动的,我们来看下源码:

    SystemServer.java 中 SystemServer 是 zygote 进程起来的启动的第一个服务, 然后在这个服务的 run 方法方法中会一次启动 Android 系统服务。

    private void run() {
        
             // ... 省略一堆代码
             startBootstrapServices();
             startCoreServices();
             startOtherServices();
             // ... 省略一堆代码
        
        }
    
     其中 AMS 是在 startOtherServices() 这个方法中启动的:
    
    private void startOtherServices() {
        
             // ... 省略一堆代码
             mActivityManagerService = mSystemServiceManager.startService(
             ActivityManagerService.Lifecycle.class).getService();
             // ... 省略一堆代码
             mActivityManagerService.systemReady(() -> {
                 
                 // ... 省略一堆代码
                 try {
                    startSystemUi(context, windowManagerF);
                 } catch (Throwable e) {
                    reportWtf("starting System UI", e);
                 } 
                 // ... 省略一堆代码
             });
        
        }
    

    在 AMS 启动启动完成之后,会回调一个 systemReady() 传递进去的方法, 在其中调用 startSystemUi() 方法启动了 SystemUI :

    static final void startSystemUi(Context context, WindowManagerService windowManager) {
            Intent intent = new Intent();
            intent.setComponent(new ComponentName("com.android.systemui",
                        "com.android.systemui.SystemUIService"));
            intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
            //Slog.d(TAG, "Starting service: " + intent);
            context.startServiceAsUser(intent, UserHandle.SYSTEM);
            windowManager.onSystemUiStarted();
        }
    

    SystemUIService 逻辑也是相当简单, 启动之后主要调用 SystemUIApplication 的 startServicesIfNeeded()|

    @Override
    public void onCreate() {
        super.onCreate();
        ((SystemUIApplication) 
       getApplication()).startServicesIfNeeded();
    
        // For debugging RescueParty
        if (Build.IS_DEBUGGABLE && 
            SystemProperties.getBoolean("debug.crash_sysui", false)) {
            throw new RuntimeException();
         }
     }           
    

    在 SystemUIApplication 中启动了 SystemUI 的各个 UI 模块:

    public void startServicesIfNeeded() {
            startServicesIfNeeded(SERVICES);
        }
    

    例如 : SERVICES 包含了状态栏、电量、画中画、 锁屏等。

    通知视图是如何夸进程显示的?

    跨进程通讯的基础是 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();
    

    这样通道就建立起来了。

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

    <a name="13yage"></a>

    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 才能接收通知吗?

    答案是否定的, 只要有权限注册通知监听的应用都可以。 具体权限是: <uses-permission android:name="android.permission.STATUS_BAR_SERVICE"/>
    只要应用有这个权限就可以注册通知监听了, 这个权限只有系统应用才能申请, 也就是说,只要是系统应用都可以监听并显示通知的。 可以写一个简单的 demo 测试一下:
    一、 申请权限
    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE"/>
    二、 在布局中定义一个容器来装远程通知视图

    ...
         <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();
            }
        }
    

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

    应用窗口的 flag 是如何状态栏?


    在系统服务中,有一个服务是专门为 SystemUI 的状态栏服务的, 这个服务就是 StatusbarManagerService (简称:SMS),和这个服务关系比较密切的服务是 WindowManagerService(简称:WMS), SMS 主要管控的是状态栏、导航栏, 例如:我们可以设置全屏、沉浸式状态栏都是 SMS 在起作用。 我们看一下 window flag 是如何一步一步的影响系统状态栏的。

    通常我们这样添加窗口属性,例如设置 flag 让 SystemUI 状态栏支持绘制背景:

    Window window = activity.getWindow();
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    

    我们都知道 Android 系统为我们提供的 window 实现类是 PhoneWindow (不清楚的可以参考文章:https://www.jianshu.com/p/b4c23dee9206), flag 其实被仅仅是 WindowManager.LayoutParams 的一个标记而已。

    public void setFlags(int flags, int mask) {
            final WindowManager.LayoutParams attrs = getAttributes();
            attrs.flags = (attrs.flags&~mask) | (flags&mask);
            mForcedWindowFlags |= mask;
            dispatchWindowAttributesChanged(attrs);
        }
    

    所有窗口的 View 和 LayoutParams 最终会被添加到 WindowManagerService 中,WindowManagerService 会记录着窗口信息,包括 flag 属性 。 (View、Window 和 ViewRootImpl 的关系:参考:https://www.jianshu.com/p/47421ec56795
    每次窗口布局、焦点发生变化的时候,都会去重新计算当前窗口的属性, 包括 flag。

    public int addWindow(Session session, IWindow client, int seq,
                WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
                Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
                InputChannel outInputChannel) {
                //...省略一堆代码
                // 计算属性
                mPolicy.adjustWindowParamsLw(win.mAttrs);
                //...省略一堆代码
                updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS,
                                        false /*updateInputWindows*/);
            }
    

    mPolicy 是 PhoneWindowManager 的一个实例, adjustWindowParamsLw 主要是根据窗口的属性来决定接下来要展示什么样的
    SystemUI。例如:

    @Override
          public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
              // 省略一堆代码
              if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                      || forceWindowDrawsStatusBarBackground
                              && attrs.height == MATCH_PARENT && attrs.width == MATCH_PARENT) {
                  attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
              }
          }
    

    接下来, 会调用 PhoneWindowManager 的 focusChangedLw(), 在这里调用了更新 SystemUI 样式的方法 updateSystemUiVisibilityLw。

    @Override
          public int focusChangedLw(WindowState lastFocus, WindowState newFocus) {
              mFocusedWindow = newFocus;
              if ((updateSystemUiVisibilityLw()&SYSTEM_UI_CHANGING_LAYOUT) != 0) {
                  // If the navigation bar has been hidden or shown, we need to do another
                  // layout pass to update that window.
                  return FINISH_LAYOUT_REDO_LAYOUT;
              }
              return 0;
          }
    
    private int updateSystemUiVisibilityLw() {
              
              mHandler.post(new Runnable() {
                      @Override
                      public void run() {
                          StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
                          if (statusbar != null) {
                              statusbar.setSystemUiVisibility(visibility, fullscreenVisibility,
                                      dockedVisibility, 0xffffffff, fullscreenStackBounds,
                                      dockedStackBounds, win.toString());
                              statusbar.topAppWindowChanged(needsMenu);
                          }
                      }
                  });
              return diff;
          }
    

    statusbar 就是 StatusbarManagerService 的一个实例。 在 SystemUI 的启动过程中, SystemUI 会向 StatusbarManagerService
    服务注册一个回调, 专门用来接收 StatusbarManagerService 的调用, 这个回调器就是 CommandQueue。

    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
                  int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
              synchronized (mLock) {
                  // Don't coalesce these, since it might have one time flags set such as
                  // STATUS_BAR_UNHIDE which might get lost.
                  SomeArgs args = SomeArgs.obtain();
                  args.argi1 = vis;
                  args.argi2 = fullscreenStackVis;
                  args.argi3 = dockedStackVis;
                  args.argi4 = mask;
                  args.arg1 = fullscreenStackBounds;
                  args.arg2 = dockedStackBounds;
                  mHandler.obtainMessage(MSG_SET_SYSTEMUI_VISIBILITY, args).sendToTarget();
              }
          }
    

    CommandQueue 收到调用之后就会将消息发送到 SystemUI 的视图, 视图再根据收到的 vis 属性改变样式。

    相关文章

      网友评论

        本文标题:SystemUI 开发总结

        本文链接:https://www.haomeiwen.com/subject/hghgtctx.html