在前面的视频、文章中我们介绍完了整个车载Android应用开发所需要的基础知识:
本期内容开始,我们将介绍原生Android Automotive中车载应用的实现方式和它的原理。首先要介绍的就是车载应用开发中非常重要的一个系统应用,Android系统的UI - SystemUI
。
由于原生Android系统的SystemUI
代码量很大、内容也非常庞杂,这里我会挑选出对车载SystemUI
开发具有参考意义的模块进行介绍,大约会有4-5期的内容,主要分为以下几个模块:
那么本期内容,我们先来分析 SystemUI「功能」与「源代码结构」。阅读本期内容你可以得到以下的收获:
SystemUI
SystemUI
中主要实现了哪些功能SystemUI
源码的结构SystemUI
是如何被系统启动的,以及它的初始化时序。在Android系统中SystemUI
是一个系统级的APP,它提供了系统的用户界面,由system_server
进程启动。
SystemUI
本身不属于system_server
进程,它是一个独立的进程。它的HMI包括了状态栏、导航栏、通知栏、锁屏、近期任务等等。
SystemServer是一个由Zogyte进程启动的程序,它负责启动和管理Android系统中的各种核心服务。 例如:ActivityManagerService和PackageManagerService,这些服务也都运行在system_server进程中,为Android系统提供各种功能和接口,供应用程序和其他服务调用。我们常说的Android Framework其实就是由这些Service组成的。
这部分将主要介绍那些对我们定制自己的SystemUI
时有参考价值的模块。
StatusBar,负责显示时间,电量,信号,通知等状态信息。
NavigationBar,显示返回,主页,最近任务等按钮。在车载Android中,我们多数时候会称为Dock栏(DockBar)。一般负责显示车控、主页、蓝牙电话等常用功能的快捷控制和入口。
NotificationPanel,显示、控制通知的界面。实际的车载项目中通知栏往往会和【消息中心】合并成一个独立的APP。
QuickSettings,这个面板可以让用户快速地调整一些常用的设置,例如亮度、飞行模式、蓝牙等。QS面板有多种状态,包括初级展开面板(Quick Quick Settings,QQS)和完整QS面板(Quick Settings,QS)。用户可以通过下拉通知栏来打开或关闭QS面板。
一些系统级的对话框、弹窗、动画、屏保等,这些内容相对比较简单,不再介绍了。而锁屏、媒体控制等一些功能,车载SystemUI开发时涉及的不多,也同样不再介绍。
SystemUI
的源码位置取决于你使用的Android版本和设备类型,本视频基于Android 13的源码进行分析。
Android 13的SystemUI
的源码位于**frameworks/base/packages/SystemUI**目录下。
SystemUI
的源码主要由Java和XML文件组成,其中Java文件实现了SystemUI
的各种功能和逻辑,XML文件定义了SystemUI
的界面和资源。SystemUI
的源码还包含了一些测试,工具,文档等辅助文件。SystemUI
的源码结构如下:
车载SystemUI
的源码位于 **/packages/apps/Car/SystemUI**目录下,CarSystemUI
是对SystemUI
的重用和扩展。CarSystemUI
的源码结构如下:
res: 包含了一些通用的资源文件,例如布局,图片,字符串等。
res-keyguard: 包含了一些用于锁屏界面的资源文件。
samples:包含CarSystemUI的换肤资源,主要是利用了Android的RRO机制。
src: 包含了CarSystemUI的主要源码文件,按照功能或模块进行分类,例如statusbar, navigationbar, notification, keyguard, recents等。这些文件中有一些是对SystemUI中同名文件的修改或扩展,有一些是新增的文件,用于实现车载设备特有的功能或逻辑。
tests: 包含了一些用于测试或验证CarSystemUI的源码文件
在Android源码的根目录下执行mm SystemUI
,这会编译SystemUI模块及其依赖项。如果你修改了其他模块,例如frameworks/base,也可以执行mm framework-minus-apex
来编译framework模块。
编译完成后,可以使用adb命令将新的SystemUI.apk推送到设备中,并重启SystemUI进程。具体的命令如下:
adb root
adb remount
adb push out/target/product/emulator_x86/system_ext/priv-app/CarSystemUI/CarSystemUI.apk /system_ext/priv-app/CarSystemUI/
adb shell ps -lef | grep systemui
adb shell kill
如果执行remount指令模拟器出现read only的提示,需要先关闭模拟器,使用下面的指令启动模拟器。
emulator -writable-system -netdelay none -netspeed full
adb root
adb remount
adb reboot // 重启模拟器
SystemUI
的启动时序是指SystemUI
作为一个系统应用在Android系统启动过程中的加载、初始化流程。
当Android系统启动完成后,system_server
进程会通过ActivityManagerService
启动一个名为com.android.systemui.SystemUIService的服务,这个服务是SystemUI
的入口类,它继承了Service类。
SystemServer的源码位置:/frameworks/base/services/java/com/android/server/SystemServer.java
mActivityManagerService.systemReady(() -> {
Slog.i(TAG, "Making services ready");
//...
t.traceBegin("StartSystemUI");
try {
startSystemUi(context, windowManagerF);
} catch (Throwable e) {
reportWtf("starting System UI", e);
}
t.traceEnd();
}, t);
从这里我们可以看出,SystemUI
本质就是一个Service,通过Pm获取到的Component是com.android.systemui/.SystemUIService。startSystemUi代码细节如下:
private static void startSystemUi(Context context, WindowManagerService windowManager) {
PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
Intent intent = new Intent();
intent.setComponent(pm.getSystemUiServiceComponent());
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
//Slog.d(TAG, "Starting service: " + intent);
context.startServiceAsUser(intent, UserHandle.SYSTEM);
windowManager.onSystemUiStarted();
}
以上就是SystemUI
的启动流程,接下来我们继续看SystemUI
是如何初始化的。
SystemUI
的初始化流程分为以下几步:
SystemUIApplication源码位置:/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
在SystemUI
启动后,首先会调用Application的onCreate方法,并在onCreate方法中对SystemUI
进行初始化。这里我把它分为四个部分的内容。
@Override
public void onCreate() {
super.onCreate();
Log.v(TAG, "SystemUIApplication created.");
// TimingsTraceLog 是一个用于跟踪代码执行时间的工具类,它可以在traceview中看到。
TimingsTraceLog log = new TimingsTraceLog("SystemUIBootTiming",Trace.TRACE_TAG_APP);
log.traceBegin("DependencyInjection");
// 此行用于设置Dagger的依赖注入,并应保持在onrecate方法的顶部。
mInitializer = mContextAvailableCallback.onContextAvailable(this);
mSysUIComponent = mInitializer.getSysUIComponent();
// BootCompleteCacheImpl 是一个用于缓存 BOOT_COMPLETED 广播的实现类。
mBootCompleteCache = mSysUIComponent.provideBootCacheImpl();
log.traceEnd();
// 设置主线程Looper的traceTag,这样就可以在traceview中看到主线程的消息处理情况了。
Looper.getMainLooper().setTraceTag(Trace.TRACE_TAG_APP);
// 设置所有服务继承的应用程序主题。请注意,仅在清单中设置应用程序主题仅适用于活动。请将其与在那里设置的主题同步。
setTheme(R.style.Theme_SystemUI);
...见第二部分
}
第一部分内容不多,主要是通过Dagger拿到SystemUI中的一些创建好的组件,同时设定一些调试工具。
首先判断当前进程是否属于系统用户。然后根据SF GPU上下文优先级设置设定SystemUI
的渲染器的上下文优先级,最后开启SystemServer
的binder调用trace跟踪。
@Override
public void onCreate() {
super.onCreate();
...见第一部分
// 判断当前进程是否是系统进程。如果是系统进程,那么就注册 BOOT_COMPLETED 广播接收器。
if (Process.myUserHandle().equals(UserHandle.SYSTEM)) {
// 创建 BOOT_COMPLETED 广播接收器的意图过滤器。
IntentFilter bootCompletedFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
bootCompletedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
// 如果SF GPU上下文优先级设置为实时,则SysUI应以高优先级运行。优先级默认为中等。
int sfPriority = SurfaceControl.getGPUContextPriority();
Log.i(TAG, "Found SurfaceFlinger's GPU Priority: " + sfPriority);
if (sfPriority == ThreadedRenderer.EGL_CONTEXT_PRIORITY_REALTIME_NV) {
Log.i(TAG, "Setting SysUI's GPU Context priority to: "+ ThreadedRenderer.EGL_CONTEXT_PRIORITY_HIGH_IMG);
// 设置SysUI的GPU上下文优先级为高。
ThreadedRenderer.setContextPriority(ThreadedRenderer.EGL_CONTEXT_PRIORITY_HIGH_IMG);
// ThreadedRenderer可以简单理解为一个渲染器,它可以在后台线程中渲染视图层次结构。优先级越高,渲染速度越快。
}
// 在system_server上为源自SysUI的调用启用trace跟踪
try {
ActivityManager.getService().enableBinderTracing();
} catch (RemoteException e) {
Log.e(TAG, "Unable to enable binder tracing", e);
}
...见第三部分
} else {
...见第四部分
}
}
ThreadedRenderer
可以简单理解为一个渲染器,它可以在后台线程中渲染视图层次结构。优先级越高,渲染速度越快。关于它具体作用可以参考: 理解Android硬件加速的小白文 - 掘金
Process.myUserHandle()
可以获取当前进程的用户类型。如果是从事移动端APP开发,很少会涉及Android系统的多用户机制。但是由于汽车是一种具有共享属性的工具,会存在多个家庭成员使用一辆车的情况,所以Android多用户在车载Android开发中较为常见。
当我们在系统设置中的「系统」「多用户」添加一个新用户并切换到这个新用户时,实际上会再启动一个SystemUI
进程,新的SystemUI
进程的用户ID会从U1X开始,原始的SystemUI的用户ID则始终是U0。
有关Android的多用户,可以参考官方资料:支持多用户 - Android,之后我也会单独写篇博客阐述Android系统的多用户机制。
注册监听开机广播,并在SystemUIService
启动后,再通知SystemUI
中的其它组件「系统启动完成」。
// 注册 BOOT_COMPLETED 广播接收器。
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mBootCompleteCache.isBootComplete()) return;
if (DEBUG) Log.v(TAG, "BOOT_COMPLETED received");
unregisterReceiver(this);
mBootCompleteCache.setBootComplete();
// 判断SystemUIService是否启动
if (mServicesStarted) {
final int N = mServices.length;
for (int i = 0; i < N; i++) {
// 通知SystemUI中各个组件,系统启动完成。
mServices[i].onBootCompleted();
}
}
}
}, bootCompletedFilter);
// Intent.ACTION_LOCALE_CHANGED 是系统语言发生变化时发送的广播。
IntentFilter localeChangedFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
if (!mBootCompleteCache.isBootComplete()) return;
// 更新SystemUi通知通道的名称
NotificationChannels.createAll(context);
}
}
}, localeChangedFilter);
如果当前用户非系统用户那么调用startSecondaryUserServicesIfNeeded方法。
} else {
// 我们不需要为正在执行某些任务的子进程初始化组件。例如:截图进程等
String processName = ActivityThread.currentProcessName();
ApplicationInfo info = getApplicationInfo();
if (processName != null && processName.startsWith(info.processName + ":")) {
return;
}
// 对于第二个用户,boot-completed永远不会被调用,因为它已经在启动时为主SystemUI进程广播了
// 对于需要每个用户初始化SystemUI组件的组件,我们现在为当前非系统用户启动这些组件。
startSecondaryUserServicesIfNeeded();
}
startSecondaryUserServicesIfNeeded方法也是通过startServicesIfNeeded方法来初始化SystemUI
中的功能组件。具体是如何初始化,我们之后再来分析。
void startSecondaryUserServicesIfNeeded() {
// 对startables进行排序,以便我们获得确定的顺序。
Map, Provider> sortedStartables = new TreeMap<>(Comparator.comparing(Class::getName));
sortedStartables.putAll(mSysUIComponent.getPerUserStartables());
startServicesIfNeeded(sortedStartables, "StartSecondaryServices", null);
}
到这里,我们简单总结一下SystemUIApplication
中其实最主要的工作,其实只有两个:
① 在系统用户空间中监听开机广播,并通知 SystemUI
的功能组件。
② 在非系统用户空间中,直接初始化 SystemUI
的功能组件。
当Application完成初始化之后,紧接着,SystemUIService就会被启动。
SystemUIService源码位置:/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java
SystemUIService
在onCreate()方法中会调用((SystemUIApplication) getApplication()).startServicesIfNeeded()方法
@Override
public void onCreate() {
super.onCreate();
// Start all of SystemUI
((SystemUIApplication) getApplication()).startServicesIfNeeded();
...
}
这里可能有个疑问:为什么不把startServicesIfNeeded的相关逻辑写在Service中,非要写到Application中?
是因为,当前用户不是系统用户时,startSecondaryUserServicesIfNeeded也需要去调用startServicesIfNeeded方法进行组件初始化,所以干脆把所有的初始化逻辑都写到Application中了。
public void startServicesIfNeeded() {
// vendorComponent 是一个字符串,它的值是:com.android.systemui.VendorServices
// com.android.systemui.VendorServices 是一个空类,它的作用是在SysUI启动时,启动一些第三方服务。
final String vendorComponent = mInitializer.getVendorComponent(getResources());
// 对startables进行排序,以便我们获得确定的顺序
// TODO: make #start idempotent and require users of CoreStartable to call it.
Map, Provider> sortedStartables = new TreeMap<>(
Comparator.comparing(Class::getName));
sortedStartables.putAll(mSysUIComponent.getStartables());
sortedStartables.putAll(mSysUIComponent.getPerUserStartables());
startServicesIfNeeded(sortedStartables, "StartServices", vendorComponent);
}
这个方法会根据配置文件config_systemUIServiceComponents或config_systemUIServiceComponentsPerUser中的定义使用反射来创建、启动一系列SystemUI
的服务,例如StatusBar
, NavigationBar
, NotificationPanel
, Keyguard
等。这些服务每一个都扩展自一个名为SystemUI的接口。
SystemUI
会为他们提供了一个Context,并为他们提供onConfigurationChanged和onBootCompleted的回调。这些服务是SystemUI
的主要组件,负责提供各种功能和界面。
增加了一个新的vendorComponent
,vendorComponent 是一个字符串,它的值是:com.android.systemui.VendorServices。VendorServices
继承自CoreStartable
但是内部没有任何实现,google的设计目的是,在SysUI启动时,可以用来启动一些第三方服务。
Android 13以前每个SystemUI
服务还会依赖于Dependency
类提供的自定义依赖注入,来获取一些跨越SystemUI
生命周期的对象。但是Android 13之后,SystemUI
功能组件的创建和依赖注入都是Dagger自动完成。
private void startServicesIfNeeded(Map, Provider> startables,String metricsPrefix,String vendorComponent) {
if (mServicesStarted) {
return;
}
mServices = new CoreStartable[startables.size() + (vendorComponent == null ? 0 : 1)];
if (!mBootCompleteCache.isBootComplete()) {
// 检查BOOT_COMPLETED是否已经发送。如果是这样,我们不需要等待它。
// see ActivityManagerService.finishBooting()
if ("1".equals(SystemProperties.get("sys.boot_completed"))) {
mBootCompleteCache.setBootComplete();
if (DEBUG) {
Log.v(TAG, "BOOT_COMPLETED was already sent");
}
}
}
mDumpManager = mSysUIComponent.createDumpManager();
Log.v(TAG, "Starting SystemUI services for user " + Process.myUserHandle().getIdentifier() + ".");
TimingsTraceLog log = new TimingsTraceLog("SystemUIBootTiming",Trace.TRACE_TAG_APP);
log.traceBegin(metricsPrefix);
int i = 0;
for (Map.Entry, Provider> entry : startables.entrySet()) {
String clsName = entry.getKey().getName();
int j = i; // Copied to make lambda happy.
// timeInitialization 记录初始化的时间
timeInitialization(clsName,
() -> mServices[j] = startStartable(clsName, entry.getValue()),
log,
metricsPrefix);
i++;
}
if (vendorComponent != null) {
timeInitialization(
vendorComponent,
() -> mServices[mServices.length - 1] =
startAdditionalStartable(vendorComponent),
log,
metricsPrefix);
}
for (i = 0; i < mServices.length; i++) {
if (mBootCompleteCache.isBootComplete()) {
mServices[i].onBootCompleted();
}
mDumpManager.registerDumpable(mServices[i].getClass().getName(), mServices[i]);
}
mSysUIComponent.getInitController().executePostInitTasks();
log.traceEnd();
mServicesStarted = true;
}
有关Android13 的SystemUI中Dagger是如何使用的。可以阅读官方文档:frameworks/base/packages/SystemUI/docs/dagger.md
我们再来小结一下SystemUIService
的初始化流程,可以归纳为以下四步:
①调用SystemUIApplication中的startServicesIfNeeded方法
②startServicesIfNeeded方法通过Dagger获取到创建好的SystemUI的功能组件,并依据包名、类名进行排序。
③依次调用SystemUI功能组件的start()方法,并记录耗时。
④当接收到BOOT_COMPLETED广播或检查SystemProperty中已经完成开机,则依次调用 SystemUI
功能组件的onBootCompleted()完成 SystemUI
的初始化。
本期内容我们简单介绍了Android系统中SystemUI
的功能、源码结构以及启动时序。
最近无论是视频还是博客更新的都很慢,原因其实我在B站发了动态说明,因为裁员,接下来相当一段时间不得不多花点时间在工作上了。
好,感谢你的阅读,希望对你有所帮助,我们下期内容再见。