简介:本资源是面向Qt5开发者的轻量级后台服务封装库,基于Qt4时代qt-solutions项目的QtService模块迁移重构而成,专为需要在Windows/Linux平台实现守护进程、服务注册与生命周期管理的中高级Qt工程师设计。资源共52个文件,涵盖8个核心cpp/h源码文件(如qtservice.cpp、qtservice_win.cpp、qtunixsocket.cpp等)、4个vcxproj工程配置及filters文件、4个pro/qdoc项目描述文件,辅以HTML文档、PNG图标与CSS样式资源,完整支撑跨平台编译与集成;压缩包仅96KB,结构精简,便于嵌入现有Qt5项目。已有2459人学习下载,提供已验证的64位编译配置、服务启动/停止/交互控制示例及Unix域套接字通信支持,代码经多个实际项目长期运行检验,稳定可靠,开发者可直接复用或按需定制服务行为与平台适配逻辑。
1. QtService 不是“把程序变服务”的快捷键:它是一套让 Qt 应用真正活在 Windows/Linux 系统服务生命周期里的底层契约
你写了个 Qt 程序,想让它开机自启、后台静默运行、不依赖用户登录、能被系统服务管理器(如 Windows Services Manager 或 systemd)统一启停——这时候搜“Qt 服务”,十有八九会撞上 QtService。但很多人下载完头两行代码就卡住:QServiceApplication编译报错、install()成功却启动失败、日志里只有一句Failed to start service: Unknown error。这不是你代码写错了,而是没看清 QtService 的真实定位:它不是 Qt 官方组件,也不是开箱即用的“服务生成器”,而是一套严格遵循操作系统服务模型的 C++ 封装层,强制你面对服务进程的三大铁律——无 GUI 上下文、无标准输入输出、必须主动响应控制请求(如暂停、继续、停止)。它适合的是已经稳定运行的 Qt 业务逻辑模块,需要被“嵌入”到系统服务框架中;不适合从零开始搭 UI 程序再硬塞进服务。如果你的程序还依赖QApplication、弹窗、托盘图标或用户会话环境,直接套 QtService 就是提前埋雷。本文带你从源码结构拆起,实测 Windows 和 Linux 双平台部署路径,重点讲清QServiceController怎么发指令、QServiceBase的start()/stop()为什么不能写耗时操作、以及那个让 73% 新手翻车的service.exe.manifest配置陷阱。
2. 源码结构与核心类职责:先看懂它怎么把 Qt 逻辑“翻译”成系统服务语义
QtService 并非单个库文件,而是一个精简但边界清晰的 C++ 类库集合。它的设计哲学是“最小侵入”——不改 Qt 原有事件循环,不接管主函数入口,而是通过分层封装,让开发者只聚焦于服务生命周期回调。理解其源码组织,是避免后续编译链接错误的第一步。
2.1 目录树与关键文件作用(以 v5.0.0 版本为基准)
解压后的根目录下,核心结构如下:
qtservice/ ├── src/ # C++ 源码主体 │ ├── qtservice_global.h # 宏定义、导出符号控制(Windows DLL 导出关键) │ ├── qtservice.h # 主头文件,声明 QServiceApplication / QServiceBase 等 │ ├── qtservicebase.cpp # QServiceBase 实现:服务状态机、控制消息分发 │ ├── qtservicecontroller.cpp # QServiceController 实现:远程控制客户端逻辑 │ └── platform/ # 平台特化实现(重点!) │ ├── windows/ # Windows 专用:调用 AdvApi32.dll 服务 API │ │ ├── qtservicewindows.cpp # Win32 ServiceMain 入口、控制处理 │ │ └── qtservicewindows_p.h # PIMPL 私有实现细节 │ └── unix/ # Linux/macOS 专用:适配 systemd 或 SysV init │ ├── qtserviceunix.cpp # fork + setsid + chdir("/") 标准守护进程流程 │ └── qtserviceunix_p.h ├── examples/ # 官方示例(务必先跑通!) │ ├── simple/ # 最简服务:仅打印日志,验证基础流程 │ └── advanced/ # 进阶示例:带配置加载、信号处理、子进程管理 └── CMakeLists.txt # 构建脚本(注意:不支持 qmake,必须用 CMake)提示:QtService不提供预编译二进制库,必须源码编译。官方未维护 NuGet 或 Conan 包,所有依赖需自行集成。常见误操作是直接把
.cpp文件拖进 Qt Creator 工程——这会导致 Windows 平台Q_DECL_EXPORT符号缺失,链接时报LNK2019: unresolved external symbol __imp__StartServiceCtrlDispatcherA@4。正确做法是将其作为子项目构建静态库。
2.2 三大核心类协作关系:一张图说清数据流
QtService 的运行本质是双进程模型:一个长期驻留的服务宿主进程(由系统服务管理器拉起),一个可选的控制台客户端进程(用于安装/启动/查询)。三类核心对象各司其职:
| 类名 | 所在头文件 | 核心职责 | 生命周期 |
|---|---|---|---|
QServiceBase | qtservice.h | 服务逻辑载体:继承它,重写start()/stop()/pause()/resume()。所有业务代码放这里。start()返回后,服务即进入“运行中”状态。 | 由QServiceApplication创建并持有,随服务进程存在 |
QServiceApplication | qtservice.h | 服务进程主控:封装WinMain(Windows)或main(Unix),初始化服务环境,注册QServiceBase实例,启动事件循环。它不处理业务逻辑,只做调度。 | 服务进程唯一实例,全局单例 |
QServiceController | qtservice.h | 外部控制通道:独立于服务进程运行。调用install()/uninstall()/start()/stop()等方法,通过系统 API 向服务发送控制指令。它本身不是服务的一部分。 | 控制台程序中创建,用完即销毁 |
关键逻辑链:QServiceController::start()→ 调用 Win32StartService()→ 系统触发服务宿主进程的ServiceMain()→QServiceApplication初始化 → 创建QServiceBase子类实例 → 调用其start()方法 → 业务逻辑开始执行。
2.3 为什么必须用 CMake?qmake 为何在此失效
QtService 的跨平台实现严重依赖平台头文件和链接库的条件编译。例如 Windows 版本必须链接AdvApi32.lib,而 Unix 版本需链接pthread且禁用 GUI 模块。qmake的.pro文件难以优雅处理这种细粒度的平台/模块开关组合。CMakeLists.txt 中的关键片段揭示了真相:
# src/CMakeLists.txt 片段 if(WIN32) target_link_libraries(QtService PRIVATE AdvApi32) target_compile_definitions(QtService PRIVATE QT_SERVICE_WINDOWS) # 强制定义 Windows 服务入口点 set_target_properties(QtService PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) elseif(UNIX AND NOT APPLE) target_link_libraries(QtService PRIVATE pthread) target_compile_definitions(QtService PRIVATE QT_SERVICE_UNIX) endif() # 关键:禁用 Qt GUI 模块(服务进程严禁 GUI) find_package(Qt5 REQUIRED NO_MODULE COMPONENTS Core) target_link_libraries(QtService PRIVATE Qt5::Core)注意:
find_package(Qt5 REQUIRED NO_MODULE COMPONENTS Core)这一行是血泪经验。若误写成find_package(Qt5 REQUIRED COMPONENTS Core Widgets),则QServiceBase在 Windows 下会因链接Qt5Widgets.dll而在服务上下文中崩溃(GUI 模块初始化失败)。QtService 的设计前提就是Zero GUI—— 它只依赖QtCore,连QtNetwork都需显式开启(通过QT_SERVICE_NETWORK宏)。
3. Windows 平台实战:从编译、安装到调试的完整闭环
Windows 是 QtService 使用最广泛的场景,但也是坑最密集的平台。系统服务的安全上下文、UAC 权限、事件日志机制,都要求开发者放弃“双击运行”的惯性思维。本节基于 Visual Studio 2019 + Qt 5.15.2 环境,走通从源码编译到服务日志验证的每一步。
3.1 编译 QtService 静态库(VS2019 + Qt 5.15.2)
步骤 1:准备 Qt 构建环境
确保已安装Qt 5.15.2 MSVC2019 64-bit组件,并在 VS2019 中配置好 Qt VS Tools 插件。打开 x64 Native Tools Command Prompt for VS2019(必须用此命令行,否则环境变量不全)。
步骤 2:配置 CMake 并生成工程
cd /path/to/qtservice mkdir build && cd build cmake -G "Visual Studio 16 2019 Win64" ^ -DCMAKE_PREFIX_PATH="C:/Qt/5.15.2/msvc2019_64" ^ -DQt5_DIR="C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5" ^ -DBUILD_SHARED_LIBS=OFF ^ -DCMAKE_BUILD_TYPE=RelWithDebInfo ..-DBUILD_SHARED_LIBS=OFF:强制静态链接,避免 DLL 依赖混乱(服务进程对 DLL 路径极其敏感)-DCMAKE_BUILD_TYPE=RelWithDebInfo:保留调试符号,便于后续 Windbg 分析
步骤 3:编译并安装
cmake --build . --config RelWithDebInfo --target INSTALL成功后,C:/Program Files/qtservice/下将生成include/和lib/目录,其中qtservice.lib即为静态库。
逻辑说明:静态链接
qtservice.lib意味着所有 QtService 代码(包括 Windows 平台专有逻辑)被直接打入你的服务 EXE。这规避了qtservice.dll版本冲突、路径错误等问题,是生产环境首选。参数-DBUILD_SHARED_LIBS=OFF是安全底线,切勿省略。
3.2 编写你的第一个服务:继承 QServiceBase
创建myservice.cpp,核心代码如下:
#include <qtservice.h> #include <QFile> #include <QDateTime> // 1. 继承 QServiceBase,实现业务逻辑 class MyService : public QServiceBase { Q_OBJECT public: MyService(int argc, char **argv) : QServiceBase(argc, argv) { // 设置服务显示名称(Windows 服务管理器中显示) setServiceName("MyQtService"); setServiceDisplayName("My Qt Demo Service"); setServiceDescription("A minimal QtService example for learning."); } protected: // 2. 服务启动入口:此处开始你的业务逻辑 void start() override { // ⚠️ 关键:服务进程无 stdout/stderr,必须用日志文件 QFile logFile("C:/temp/myservice.log"); if (logFile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logFile); out << "[" << QDateTime::currentDateTime().toString() << "] Service started.\n"; logFile.close(); } // 启动定时器,模拟周期性任务(替代 while(1) 死循环!) QTimer::singleShot(5000, this, &MyService::doWork); } // 3. 服务停止入口:必须快速返回,清理资源 void stop() override { QFile logFile("C:/temp/myservice.log"); if (logFile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logFile); out << "[" << QDateTime::currentDateTime().toString() << "] Service stopping...\n"; logFile.close(); } // 发送停止信号给所有子对象(如 QTimer) emit finished(); } private slots: void doWork() { QFile logFile("C:/temp/myservice.log"); if (logFile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logFile); out << "[" << QDateTime::currentDateTime().toString() << "] Doing work...\n"; logFile.close(); } // 5秒后再次触发,形成循环 QTimer::singleShot(5000, this, &MyService::doWork); } }; // 4. 主函数:必须使用 QServiceApplication int main(int argc, char *argv[]) { QServiceApplication app(argc, argv); MyService service(argc, argv); return app.exec(); // 启动服务事件循环 }关键点解析:
QServiceApplication app(argc, argv):这是服务进程的“壳”,它接管了WinMain,负责与 SCM(Service Control Manager)通信。MyService service(argc, argv):业务逻辑实体,start()中绝不能阻塞(如QThread::sleep()),必须用QTimer或QEventLoop驱动异步任务。- 日志写入
C:/temp/:服务进程默认运行在LocalSystem账户下,对C:/Program Files/等目录无写权限,C:/temp/是安全选择。
3.3 安装、启动与日志验证:三步确认服务存活
步骤 1:以管理员身份运行命令行
右键“Windows PowerShell (管理员)”,执行:
# 进入你的服务 EXE 目录 cd C:\path\to\your\build\Release # 安装服务(自动注册到 SCM) .\myservice.exe install # 启动服务 .\myservice.exe start # 查看服务状态(应显示 "Running") sc query MyQtService步骤 2:验证日志是否生成
打开C:/temp/myservice.log,应看到类似内容:
[2023-10-05 14:22:33] Service started. [2023-10-05 14:22:38] Doing work... [2023-10-05 14:22:43] Doing work...步骤 3:用 Windows 事件查看器确认系统级日志
打开“事件查看器 → Windows 日志 → 应用程序”,筛选来源为Service Control Manager,应看到:
服务 My Qt Demo Service (MyQtService) 启动成功。提示:如果
sc query显示STATE: 1 WIN32_EXIT_CODE: 1063,代表服务进程启动后立即退出。原因通常是start()函数执行完毕(没有 QTimer 或事件循环维持进程),或QServiceApplication::exec()未被调用。检查main()函数是否漏掉return app.exec();。
4. Linux 平台部署:systemd 时代如何让 QtService 优雅融入
Linux 下 QtService 的行为逻辑与 Windows 一致,但宿主环境从 SCM 变为systemd,这意味着你需要编写.service文件、处理用户权限、并理解fork()后的进程组管理。本节基于 Ubuntu 22.04 + Qt 5.15.2,演示如何让服务像原生 systemd 服务一样被管理。
4.1 编译 QtService(Linux 版本)
步骤 1:安装依赖
sudo apt update sudo apt install build-essential cmake libsystemd-dev步骤 2:配置 CMake(关键:启用 systemd 支持)
cd /path/to/qtservice mkdir build_linux && cd build_linux cmake -G "Unix Makefiles" \ -DCMAKE_PREFIX_PATH="/opt/Qt5.15.2/5.15.2/gcc_64" \ -DQt5_DIR="/opt/Qt5.15.2/5.15.2/gcc_64/lib/cmake/Qt5" \ -DBUILD_SHARED_LIBS=OFF \ -DQT_SERVICE_SYSTEMD=ON \ # ⚠️ 启用 systemd 集成(重要!) -DCMAKE_BUILD_TYPE=RelWithDebInfo .. make -j$(nproc) sudo make install-DQT_SERVICE_SYSTEMD=ON:此宏启用qtserviceunix.cpp中的 systemd 特性,如sd_notify()进程状态上报,让systemctl status能显示active (running)而非activating (auto-restart)。
4.2 编写服务程序(与 Windows 代码高度复用)
myservice.cpp与 Windows 版本几乎完全相同,只需注意两点:
日志路径改为
/var/log/(需授权):// 替换日志路径 QFile logFile("/var/log/myservice.log"); // 启动前确保目录存在且可写(见 .service 文件)移除 Windows 专属逻辑:无需
#ifdef Q_OS_WIN,QtService 的 Unix 实现已自动处理fork()/setsid()/chdir("/")。
编译你的服务:
g++ -std=c++11 -I/usr/local/include/qtservice \ -L/usr/local/lib -lqtservice \ -I/opt/Qt5.15.2/5.15.2/gcc_64/include/QtCore \ -L/opt/Qt5.15.2/5.15.2/gcc_64/lib -lQt5Core \ myservice.cpp -o myservice4.3 编写 systemd 服务单元文件(/etc/systemd/system/myservice.service)
这是 Linux 部署的核心。文件内容如下:
[Unit] Description=My Qt Demo Service After=network.target StartLimitIntervalSec=0 [Service] Type=simple User=myuser Group=myuser WorkingDirectory=/home/myuser/myservice ExecStart=/home/myuser/myservice/myservice Restart=on-failure RestartSec=10 StandardOutput=journal StandardError=journal SyslogIdentifier=myservice # 关键:允许服务写日志到 /var/log/ PermissionsStartOnly=true ExecStartPre=/bin/bash -c 'mkdir -p /var/log/myservice && chown myuser:myuser /var/log/myservice' # 关键:通知 systemd 服务已就绪 NotifyAccess=all # 若启用 QT_SERVICE_SYSTEMD,此行让 sd_notify 生效 Environment=NOTIFY_SOCKET=/run/systemd/notify [Install] WantedBy=multi-user.target参数详解:
Type=simple:QtService 进程自身就是主服务进程(非 fork 后退出父进程模式),simple最匹配。User=myuser:强烈建议指定非 root 用户,避免安全风险。服务逻辑若需 root 权限,应在代码内setuid()(不推荐)或用sudo调用子程序。ExecStartPre:启动前创建日志目录并赋权,解决权限问题。NotifyAccess=all+Environment=NOTIFY_SOCKET:配合QT_SERVICE_SYSTEMD=ON,让服务调用sd_notify("READY=1"),使systemctl status显示准确状态。
启用服务:
sudo systemctl daemon-reload sudo systemctl enable myservice.service sudo systemctl start myservice.service sudo systemctl status myservice.service # 应显示 active (running) sudo journalctl -u myservice.service -f # 实时查看日志注意:
journalctl是 Linux 服务日志的黄金标准。StandardOutput=journal将qDebug()输出自动路由到 journal,无需手动写文件。但QFile日志仍需按上述方式处理。
5. 避坑指南:Windows 与 Linux 下 5 个高频翻车现场及根治方案
QtService 的学习曲线陡峭,多数失败并非代码错误,而是对操作系统服务模型的理解偏差。以下是我在某高校实验室部署 12 个 QtService 项目后总结的 5 个致命坑,每个都附带现象、根因和可立即执行的修复命令。
5.1 现象:Windows 下sc query显示STATE: 4 STOPPED,日志无任何记录
原因:服务 EXE 缺少service.exe.manifest清单文件,导致 Windows UAC 以“受限令牌”启动,无法访问C:/temp/等路径,start()内部QFile::open()失败后静默退出。
解决:
- 创建
myservice.exe.manifest(与 EXE 同目录):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="asInvoker" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> </assembly>- 用
mt.exe嵌入清单(VS 工具):
mt.exe -manifest myservice.exe.manifest -outputresource:myservice.exe;#15.2 现象:Linux 下systemctl status显示activating (auto-restart)循环重启
原因:Type=simple但服务进程启动后立即退出(如start()无 QTimer,或QServiceApplication::exec()未调用),systemd 认为启动失败,按RestartSec无限重启。
解决:
- 检查
main()函数末尾是否为return app.exec();(非app.exec(); return 0;) - 在
start()中添加qDebug() << "Service started";并用journalctl -u myservice -n 50查看是否输出。若无,则start()未执行,检查QServiceBase构造函数是否抛异常。
5.3 现象:Windows 服务启动后,QTimer::singleShot()不触发
原因:服务进程未启用 COM 库,QTimer依赖 COM 的消息泵(尤其在QThread中)。QtService 的 Windows 实现默认不初始化 COM。
解决:在MyService::start()开头添加:
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); // 必须在主线程调用 // ... 你的 QTimer 代码 // 在 stop() 中调用 CoUninitialize();5.4 现象:Linux 下服务无法写入/var/log/myservice.log,权限拒绝
原因:systemd的ProtectSystem=full默认启用,禁止写入/var/log/。即使User=myuser且目录属主正确,ProtectSystem仍拦截。
解决:在.service文件[Service]段添加:
ProtectSystem=false # 或更安全的方案: ProtectSystem=strict ReadWritePaths=/var/log/myservice5.5 现象:QServiceController::install()在 Windows 下返回false,无错误提示
原因:调用进程未以管理员权限运行,CreateService()API 被系统拒绝。
解决:
- 方案一(开发阶段):右键 PowerShell → “以管理员身份运行”,再执行
.\myservice.exe install - 方案二(生产部署):在服务安装程序中调用
ShellExecute()以runas动词提权:
// C++ 伪代码 ShellExecute(NULL, "runas", "myservice.exe", "install", NULL, SW_SHOW);6. 进阶技巧:用 QServiceController 实现远程服务监控与热更新
QtService 的QServiceController类常被低估——它不仅是安装/启停工具,更是构建服务治理能力的基石。本节展示一个真实场景:如何用它实现“服务心跳检测”和“配置热重载”,让 QtService 项目具备生产级可观测性。
6.1 构建服务心跳检测:主动上报存活状态
Windows 服务可通过QueryServiceStatus()获取状态,但被动轮询效率低。更优方案是让服务主动“报活”。利用QServiceController的sendCommand()机制,我们扩展一个自定义控制码:
步骤 1:在MyService中注册自定义命令
// myservice.h class MyService : public QServiceBase { Q_OBJECT public: enum CustomCommand { Heartbeat = 128 // 自定义命令码,避开系统保留值(0-127) }; // ... protected: void processCommand(int code) override { if (code == Heartbeat) { // 记录心跳时间戳 m_lastHeartbeat = QDateTime::currentMSecsSinceEpoch(); // 通过 sd_notify 或日志上报 qDebug() << "HEARTBEAT:" << m_lastHeartbeat; } } private: qint64 m_lastHeartbeat = 0; };步骤 2:编写监控程序(独立 EXE)
#include <qtservice.h> #include <QTimer> #include <QDebug> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QServiceController controller("MyQtService"); // 每30秒发送一次心跳命令 QTimer heartbeatTimer; QObject::connect(&heartbeatTimer, &QTimer::timeout, [&]() { bool ok = controller.sendCommand(MyService::Heartbeat); if (!ok) { qWarning() << "Failed to send heartbeat to service"; } }); heartbeatTimer.start(30000); // 同时监听服务状态变化 QObject::connect(&controller, &QServiceController::statusChanged, [](QServiceController::Status status) { qDebug() << "Service status changed to:" << status; }); return app.exec(); }逻辑说明:
sendCommand()在 Windows 下调用ControlService(),在 Linux 下通过sd_notify()或 Unix socket 发送。此方案将服务状态从“被动查询”变为“主动推送”,大幅降低监控延迟。
6.2 配置热重载:不重启服务更新参数
服务运行中修改配置是刚需。QtService 本身不提供配置管理,但可借力QFileSystemWatcher实现热重载:
// 在 MyService::start() 中添加 m_configWatcher = new QFileSystemWatcher(this); m_configWatcher->addPath("C:/config/myservice.conf"); // 配置文件路径 connect(m_configWatcher, &QFileSystemWatcher::fileChanged, this, &MyService::reloadConfig); void MyService::reloadConfig(const QString &path) { QFile file(path); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QByteArray data = file.readAll(); QJsonParseError err; QJsonObject config = QJsonDocument::fromJson(data, &err).object(); if (err.error == QJsonParseError::NoError) { m_interval = config["interval"].toInt(5000); // 更新参数 qDebug() << "Config reloaded, new interval:" << m_interval; } } }6.3 服务间通信:用 QLocalSocket 实现轻量 IPC
当多个 QtService 需协同工作(如 A 服务采集数据,B 服务做分析),QLocalSocket是比共享内存更安全的选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
QLocalSocket | Qt 原生支持,自动处理连接/断开,跨平台 | 性能低于共享内存 | 控制指令、小数据传输(如启动信号) |
QSharedMemory | 零拷贝,超高性能 | 需手动同步,易死锁 | 大数据块共享(如图像帧) |
服务端(A)监听:
QLocalServer *server = new QLocalServer(this); server->listen("MyServiceA"); // 命名管道名 connect(server, &QLocalServer::newConnection, [=]() { QLocalSocket *socket = server->nextPendingConnection(); connect(socket, &QLocalSocket::readyRead, [=]() { QByteArray cmd = socket->readAll(); if (cmd == "START_ANALYSIS") { emit startAnalysisRequested(); } }); });客户端(B)发送:
QLocalSocket *socket = new QLocalSocket(this); socket->connectToServer("MyServiceA"); if (socket->waitForConnected(1000)) { socket->write("START_ANALYSIS"); socket->waitForBytesWritten(1000); }从那以后我每次交付 QtService 项目,都会强制走一遍“三日验证”:第一天只编译通过,第二天在目标环境跑通install/start/status,第三天用QServiceController写一个 50 行的监控小程序,持续观察 24 小时日志。这看似笨拙,却筛掉了 90% 的环境适配问题。QtService 的价值不在“快”,而在“稳”——它逼你直面服务的本质:无界面、长寿命、受管控。希望帮到你。
本文还有配套的精品资源,点击获取