当前位置:   article > 正文

车载Android应用开发与分析 - SystemUI 「功能」与「源码结构」分析_android systemui开发

android systemui开发

在前面的视频、文章中我们介绍完了整个车载Android应用开发所需要的基础知识:

  1. 【视频文稿】车载Android应用开发与分析 - 走进车载操作系统 - 掘金
  2. 【视频文稿】车载Android应用开发与分析 - AOSP的下载与编译 - 掘金
  3. 【视频文稿】车载Android应用开发与分析 - 开发系统应用 - 掘金
  4. 【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(上) - 掘金
  5. 【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(下) - 掘金

本期内容开始,我们将介绍原生Android Automotive中车载应用的实现方式和它的原理。首先要介绍的就是车载应用开发中非常重要的一个系统应用,Android系统的UI - SystemUI

由于原生Android系统的SystemUI代码量很大、内容也非常庞杂,这里我会挑选出对车载SystemUI开发具有参考意义的模块进行介绍,大约会有4-5期的内容,主要分为以下几个模块:

  1. SystemUI「功能」和「源代码结构」
  2. SystemUI 「导航栏」与「状态栏」的实现原理,
  3. SystemUI「通知栏」与「快捷控制」的实现原理
  4. SystemUI「近期任务」的实现原理

那么本期内容,我们先来分析 SystemUI「功能」与「源代码结构」。阅读本期内容你可以得到以下的收获:

  1. 了解什么是SystemUI
  2. 了解SystemUI中主要实现了哪些功能
  3. 了解SystemUI源码的结构
  4. 了解SystemUI是如何被系统启动的,以及它的初始化时序。

SystemUI 简介

在Android系统中SystemUI是一个系统级的APP,它提供了系统的用户界面,由system_server进程启动。

SystemUI本身不属于system_server进程,它是一个独立的进程。它的HMI包括了状态栏、导航栏、通知栏、锁屏、近期任务等等。

SystemServer是一个由Zogyte进程启动的程序,它负责启动和管理Android系统中的各种核心服务。 例如:ActivityManagerService和PackageManagerService,这些服务也都运行在system_server进程中,为Android系统提供各种功能和接口,供应用程序和其他服务调用。我们常说的Android Framework其实就是由这些Service组成的。

SystemUI 功能介绍

这部分将主要介绍那些对我们定制自己的SystemUI时有参考价值的模块。

  • 状态栏

StatusBar,负责显示时间,电量,信号,通知等状态信息。

  • 导航栏

NavigationBar,显示返回,主页,最近任务等按钮。在车载Android中,我们多数时候会称为Dock栏(DockBar)。一般负责显示车控、主页、蓝牙电话等常用功能的快捷控制和入口。

  • 通知栏

NotificationPanel,显示、控制通知的界面。实际的车载项目中通知栏往往会和【消息中心】合并成一个独立的APP。

  • 快捷控制

QuickSettings,这个面板可以让用户快速地调整一些常用的设置,例如亮度、飞行模式、蓝牙等。QS面板有多种状态,包括初级展开面板(Quick Quick Settings,QQS)和完整QS面板(Quick Settings,QS)。用户可以通过下拉通知栏来打开或关闭QS面板。

  • 其他功能

一些系统级的对话框、弹窗、动画、屏保等,这些内容相对比较简单,不再介绍了。而锁屏、媒体控制等一些功能,车载SystemUI开发时涉及的不多,也同样不再介绍。

SystemUI 源码结构

SystemUI的源码位置取决于你使用的Android版本和设备类型,本视频基于Android 13的源码进行分析。

SystemUI 源码位置与结构

Android 13的SystemUI的源码位于**frameworks/base/packages/SystemUI**目录下。

SystemUI的源码主要由Java和XML文件组成,其中Java文件实现了SystemUI的各种功能和逻辑,XML文件定义了SystemUI的界面和资源。SystemUI的源码还包含了一些测试,工具,文档等辅助文件。SystemUI的源码结构如下:

  • animation: 包含了一些动画相关的类和资源。
  • checks: 包含了一些代码检查和格式化的工具。
  • compose: 包含了一些使用Jetpack Compose编写的界面组件。
  • customization: 包含了一些用于定制SystemUI的类和资源。
  • docs: 包含了一些文档和说明文件。
  • monet: 包含了一些用于实现Material主题的类和资源。
  • plugin: 包含了一些用于实现插件化功能的类和接口。
  • plugin_core: 包含了一些用于支持插件化功能的基础类和接口。
  • res: 包含了一些通用的资源文件,例如布局,图片,字符串等。
  • res-keyguard: 包含了一些用于锁屏界面的资源文件。
  • res-product: 包含了一些用于特定产品或设备的资源文件。
  • screenshot: 包含了一些用于截屏功能的类和资源。
  • scripts: 包含了一些用于编译或运行SystemUI的脚本文件。
  • shared: 包含了一些用于共享给其他应用或模块的类和接口。
  • src: 包含了SystemUI的主要源码文件,按照功能或模块进行分类,例如statusbar, navigationbar, notification, keyguard, recents等。
  • src-debug: 包含了一些用于调试或测试SystemUI的源码文件。
  • src-release: 包含了一些用于发布或优化SystemUI的源码文件。
  • tests: 包含了一些用于测试或验证SystemUI的源码文件。
  • tools: 包含了一些用于开发或分析SystemUI的工具文件。
  • unfold: 包含了一些用于支持折叠屏设备的类和资源。

CarSystemUI 源码结构

车载SystemUI的源码位于 **/packages/apps/Car/SystemUI**目录下,CarSystemUI是对SystemUI的重用和扩展。CarSystemUI的源码结构如下:

  • res: 包含了一些通用的资源文件,例如布局,图片,字符串等。

  • res-keyguard: 包含了一些用于锁屏界面的资源文件。

  • samples:包含CarSystemUI的换肤资源,主要是利用了Android的RRO机制。

  • src: 包含了CarSystemUI的主要源码文件,按照功能或模块进行分类,例如statusbar, navigationbar, notification, keyguard, recents等。这些文件中有一些是对SystemUI中同名文件的修改或扩展,有一些是新增的文件,用于实现车载设备特有的功能或逻辑。

    • car: 包含了一些用于支持车载设备特有的功能或逻辑的类和资源,例如CarSystemUIFactory, CarNavigationBarController, CarStatusBarController等。
    • wm: 包含了一些用于管理窗口模式和布局的类和资源,例如SplitScreenController, PipController, TaskStackListenerImpl等。
    • wmshell: 包含了一些用于提供窗口外壳功能的类和资源,例如WmShellImpl, WmShellModule, WmShellStartableModule等。
    • 其他子目录和文件:除了以上三个子目录外,其他子目录和文件基本上与SystemUI中的相同或类似,只是有一些针对车载设备的修改或扩展。例如,StatusBar类在车载设备上不显示电池图标,而是显示汽油图标。
  • tests: 包含了一些用于测试或验证CarSystemUI的源码文件

修改、编译 SystemUI

在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 <pid>
  • 1
  • 2
  • 3
  • 4
  • 5

如果执行remount指令模拟器出现read only的提示,需要先关闭模拟器,使用下面的指令启动模拟器。

emulator -writable-system -netdelay none -netspeed full
adb root
adb remount
adb reboot // 重启模拟器
  • 1
  • 2
  • 3
  • 4

SystemUI 的启动时序

SystemUI的启动时序是指SystemUI作为一个系统应用在Android系统启动过程中的加载、初始化流程。

SystemUI 启动流程

当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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

从这里我们可以看出,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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以上就是SystemUI的启动流程,接下来我们继续看SystemUI是如何初始化的。

SystemUI 初始化流程

SystemUI的初始化流程分为以下几步:

  1. Application初始化

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);
    ...见第二部分
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

第一部分内容不多,主要是通过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 {
        ...见第四部分
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 第四部分

如果当前用户非系统用户那么调用startSecondaryUserServicesIfNeeded方法。

    } else {
        // 我们不需要为正在执行某些任务的子进程初始化组件。例如:截图进程等
        String processName = ActivityThread.currentProcessName();
        ApplicationInfo info = getApplicationInfo();
        if (processName != null && processName.startsWith(info.processName + ":")) {
            return;
        }
        // 对于第二个用户,boot-completed永远不会被调用,因为它已经在启动时为主SystemUI进程广播了
        // 对于需要每个用户初始化SystemUI组件的组件,我们现在为当前非系统用户启动这些组件。
        startSecondaryUserServicesIfNeeded();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

startSecondaryUserServicesIfNeeded方法也是通过startServicesIfNeeded方法来初始化SystemUI中的功能组件。具体是如何初始化,我们之后再来分析。

void startSecondaryUserServicesIfNeeded() {
    // 对startables进行排序,以便我们获得确定的顺序。
    Map<Class<?>, Provider<CoreStartable>> sortedStartables = new TreeMap<>(Comparator.comparing(Class::getName));
    sortedStartables.putAll(mSysUIComponent.getPerUserStartables());
    startServicesIfNeeded(sortedStartables, "StartSecondaryServices", null);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

到这里,我们简单总结一下SystemUIApplication中其实最主要的工作,其实只有两个:

① 在系统用户空间中监听开机广播,并通知 SystemUI 的功能组件。

② 在非系统用户空间中,直接初始化 SystemUI 的功能组件。

  1. 启动 SystemUIService

当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();
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里可能有个疑问:为什么不把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<Class<?>, Provider<CoreStartable>> sortedStartables = new TreeMap<>(
            Comparator.comparing(Class::getName));
    sortedStartables.putAll(mSysUIComponent.getStartables());
    sortedStartables.putAll(mSysUIComponent.getPerUserStartables());
    startServicesIfNeeded(sortedStartables, "StartServices", vendorComponent);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • Android 13以前

这个方法会根据配置文件config_systemUIServiceComponentsconfig_systemUIServiceComponentsPerUser中的定义使用反射来创建、启动一系列SystemUI的服务,例如StatusBar, NavigationBar, NotificationPanel, Keyguard等。这些服务每一个都扩展自一个名为SystemUI的接口。

SystemUI会为他们提供了一个Context,并为他们提供onConfigurationChanged和onBootCompleted的回调。这些服务是SystemUI的主要组件,负责提供各种功能和界面。

  • Android 13以后

增加了一个新的vendorComponent,vendorComponent 是一个字符串,它的值是:com.android.systemui.VendorServices。VendorServices继承自CoreStartable但是内部没有任何实现,google的设计目的是,在SysUI启动时,可以用来启动一些第三方服务。

Android 13以前每个SystemUI服务还会依赖于Dependency类提供的自定义依赖注入,来获取一些跨越SystemUI生命周期的对象。但是Android 13之后,SystemUI功能组件的创建和依赖注入都是Dagger自动完成。

private void startServicesIfNeeded(Map<Class<?>, Provider<CoreStartable>> 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<Class<?>, Provider<CoreStartable>> 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

有关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站发了动态说明,因为裁员,接下来相当一段时间不得不多花点时间在工作上了。

好,感谢你的阅读,希望对你有所帮助,我们下期内容再见。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/450111
推荐阅读
相关标签
  

闽ICP备14008679号