日志系统与调试模式
Ark-Pets 的日志子系统由 core 模块的静态日志门面 Logger(封装 Apache log4j 1.x)与 desktop 模块的 JavaFX 日志查看器 LogDialog 组成,负责将 core(桌宠进程)与 desktop(启动器进程)的运行日志按 PID 分文件落盘、分流到 stdout/stderr,并在 GUI 中聚合展示(含 JVM 崩溃日志)以便用户导出排障。
Purpose and Scope
本页覆盖"日志系统与调试模式"这一完整能力端到端的实现:
Logger静态门面:初始化流程、appender 拓扑、级别常量、日志格式与文件命名规则;- 日志文件生命周期管理(
Cleaner按修改时间滚动清理); - 调试模式面向用户的表面:
LogDialog(日志查看对话框)如何发现、展示并导出 core/desktop/JVM 崩溃三类日志; - 相关失败模式、并发与运维注意事项。
以下内容有意留给兄弟页面,本页仅作指引:
- 应用整体配置体系(
ArkPetsConfig/Const的其它常量组)→ 参见配置相关页面; - 启动器 GUI 对话框框架(
DialogController/DialogComposer)→ 参见启动器界面架构页面; - 更广泛的进程管理与遥测能力 → 参见同目录下其它 telemetry 页面。
Overview
Ark-Pets 是多进程应用:desktop(JavaFX 启动器)会拉起 core(桌宠渲染进程)。两个进程都依赖同一个 Logger 类输出日志,因此日志系统必须解决三个问题:
- 进程隔离:两个进程不能写同一个文件互相覆盖。方案是日志文件名带进程 PID(
"%s.%d.log".formatted(logPrefix, pid)),每个进程一份独立文件,天然避免写冲突,也便于按进程归因问题。 - 诊断信息自描述:日志文件头部写入环境快照(OS、架构、Java 版本、工作目录、创建时间、PID),使一份日志文件无需额外提问即可定位环境问题。
- 面向用户的调试模式:普通用户不会读控制台。
LogDialog把磁盘上的日志(包括 JVM 崩溃产生的hs_err_pid*.log)以树表形式呈现,支持全选/选最近/导出,把"反馈问题"变成几步点击。
关键概念与术语:
| 术语 | 含义 |
|---|---|
Logger | core 模块 cn.harryh.arkpets.utils.Logger,静态门面,业务代码通过 Logger.info(tag, msg) 等静态方法打日志 |
tag | 每条日志的模块前缀,经 combine() 拼接为 "tag: message" |
rootLogger / currentLogger | log4j 1.x 根 Logger;本实现将二者指向同一对象,即全应用共用一个 Logger 实例 |
| Appender | log4j 输出目标:FileAppender(日志文件)、两个 ConsoleAppender(stdout / stderr) |
LevelRangeFilter | 按级别区间过滤:stdout 只收 ≤ WARN,stderr 只收 ≥ ERROR |
Cleaner | Logger 内部静态类,按修改时间删除最旧的日志,把同前缀日志数量压到上限内 |
LogDialog | desktop 模块 cn.harryh.arkpets.controllers.LogDialog,日志查看对话框控制器 |
hs_err_pid*.log | JVM 致命错误自动生成在工作目录(而非日志目录)的崩溃转储日志 |
Architecture
架构要点(均可在源码中验证):
- 单例根 Logger:
rootLogger与currentLogger都指向org.apache.log4j.Logger.getRootLogger()(Logger.java#L21-L22)。整个应用不使用命名 Logger 层级,"tag" 承担了模块归属职责——这是一种刻意简化:不需要为每个模块维护 logger 实例,同时保证文件 appender 只需挂一次。 - 三路输出:一次
Logger.info(...)调用最多产生三路输出——文件(带时间戳与 header)、stdout(DEBUG~WARN)、stderr(ERROR/FATAL)。级别分流由两个LevelRangeFilter在静态初始化块中配置(Logger.java#L34-L40)。 - 文件 vs 控制台格式差异:文件布局含
%d{ABSOLUTE}时间戳与环境 header,控制台布局刻意省去时间戳以保持终端输出精简(Logger.java#L53-L54)。 - UI 只读磁盘:
LogDialog不直接对接 log4j,而是扫描日志目录中的.log文件——解耦了"产生日志"与"查看日志",因此启动器也能看到 core 子进程(不同 PID)的日志与 JVM 崩溃日志。
核心实现详解
初始化流程:Logger.initialize(logPrefix, maxFileCount)
初始化是静态、幂等式的重建过程,任何进程(core 或 desktop)在启动时调用一次:
1public static void initialize(String logPrefix, int maxFileCount) {
2 // Reset log appender
3 rootLogger.removeAllAppenders();
4
5 // Generate log header
6 PatternLayout fileLayout = getPatternLayout(logPrefix);
7 PatternLayout consoleLayout = new PatternLayout("[%p] %m%n");
8
9 // Initialize log appender
10 Logger.maxFileCount = Math.max(1, maxFileCount);
11 String logFilePath = "%s.%d.log".formatted(logPrefix, pid);
12 try {
13 Cleaner.cleanByModifiedTime(logPrefix, Logger.maxFileCount - 1);
14 FileAppender fileAppender = new FileAppender(
15 fileLayout, logFilePath, false, false, 0
16 );
17 fileAppender.setEncoding(charsetDefault);
18 currentLogger.addAppender(fileAppender);
19 isFileLoggerAvailable = true;
20 } catch (IOException e) {
21 LogLog.error("Failed to initialize the file logger.");
22 }
23 ConsoleAppender sysOutAppender = new ConsoleAppender(consoleLayout);
24 ConsoleAppender sysErrAppender = new ConsoleAppender(consoleLayout);
25 sysOutAppender.addFilter(sysOutFilter);
26 sysErrAppender.addFilter(sysErrFilter);
27 sysOutAppender.setTarget(ConsoleAppender.SYSTEM_OUT);
28 sysErrAppender.setTarget(ConsoleAppender.SYSTEM_ERR);
29 rootLogger.addAppender(sysOutAppender);
30 rootLogger.addAppender(sysErrAppender);
31
32 // Reset log level
33 setLevel(level);
34 isInitialized = true;
35}Source: Logger.java
逐段解读(WHY):
rootLogger.removeAllAppenders()— 幤等重建:若重复调用initialize,先清掉旧 appender,避免同一消息被写两次。这是防御性设计,因为静态字段在 JVM 内只初始化一次,但方法可能被多次调用。Cleaner.cleanByModifiedTime(logPrefix, maxFileCount - 1)— 在打开新文件之前清理旧日志,参数是maxFileCount - 1,为新文件预留一个名额。若清理失败抛IOException,会被 catch 并降级为"仅控制台日志"模式。new FileAppender(layout, path, false, false, 0)— 关键参数依次是:append = false(同名文件覆盖写,因为文件名含 PID,同 PID 重启属异常场景,覆盖更安全)、bufferedIO = false(无缓冲直写,崩溃时日志不丢失——对桌宠这种随时可能被杀死/崩溃的进程至关重要)、bufferSize = 0。fileAppender.setEncoding(charsetDefault)— 强制使用Const.charsetDefault写文件,保证中文在任意系统区域设置下不乱码(避免 Windows GBK 与 Linux UTF-8 差异)。- 文件失败 ≠ 功能失败 —
IOException时仅记录LogLog.error,isFileLoggerAvailable保持false,appender 不挂载;此时所有日志方法都会静默丢弃消息(见下文"门卫检查")。console appender 挂在 try 块之后,无论文件初始化成败都会挂上。 setLevel(level)— 把静态字段level(默认Level.INFO)应用到 root/current logger。若之前调用过setLevel(DEBUG)再initialize,级别得以保留。
日志文件头:环境快照
1private static PatternLayout getPatternLayout(String logPrefix) {
2 final String header = "ArkPets Log - " + new File(logPrefix).getName() + " (PID" + pid + ")";
3 return new PatternLayout("%d{ABSOLUTE} [%p] %m%n") {
4 @Override
5 public String getHeader() {
6 return "# *** " + header + " ***" + Layout.LINE_SEP +
7 "# Created: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS")) + Layout.LINE_SEP +
8 "# OS: " + System.getProperty("os.name") + " (" + System.getProperty("os.arch") + ")" + Layout.LINE_SEP +
9 "# Java version: " + System.getProperty("java.version") + Layout.LINE_SEP +
10 "# Working directory: " + System.getProperty("user.dir") + Layout.LINE_SEP +
11 Layout.LINE_SEP;
12 }
13 };
14}Source: Logger.java
FileAppender 每次打开新文件会写入一次 getHeader()。这份以 # 开头的环境快照使一份日志文件自带排障上下文(进程角色/PID/创建时间/OS 架构/Java 版本/工作目录),是"日志即诊断报告"设计意图的体现。
记录方法与门卫检查
1/** Logs a message with the level {@code ERROR},
2 * together with the detailed information (such as stacktrace).
3 */
4public static void error(String tag, String message, Throwable error) {
5 if (isFileLoggerAvailable)
6 currentLogger.error(combine(tag, message), error);
7}
8
9protected static String combine(String tag, String message) {
10 return tag + ": " + message;
11}Source: Logger.java
debug/info/warn/error四个方法(Logger.java#L133-L157)结构完全一致:先查isFileLoggerAvailable再委托 log4j。- 设计取舍(重要):门卫条件名为"fileLogger",但实际控制的是全部输出(含 console appender)。也就是说文件初始化失败时连控制台日志也一并静默。这是一种保守取舍:半残的日志系统输出不完整日志反而可能误导排障,宁可完全静默由
LogLog提示一次错误。 - 唯一带
Throwable的重载是error——堆栈信息只在最高级别才落盘,避免 DEBUG/INFO 刷爆日志体积。 combine把tag与消息拼接为"tag: message",因此 log4j 层看到的始终是单字符串,%m输出形如LogDialog: Refreshing table。
日志级别体系
1public static final int ERROR = 40000;
2public static final int WARN = 30000;
3public static final int INFO = 20000;
4public static final int DEBUG = 10000;Source: Logger.java
int 常量与 log4j 1.x Level 数值一致,供外部(如启动器在"调试模式"下切到 DEBUG)以 setLevel(int)/setLevel(String) 动态调整。三个 setLevel 重载(Logger.java#L102-L122)同时更新静态字段 level 与 root/current logger,保证后续 initialize 不会把级别重置回 INFO。注意 log4j 1.x 的 Level.toLevel(String) 对无法识别的字符串会回退到 DEBUG 而非抛错,配置拼写错误不会导致崩溃,但会让日志变多。
日志滚动清理:Cleaner
1public static void cleanByModifiedTime(String logPrefixName, int maxFileCount) {
2 List<File> fileList = getAllLogs(logPrefixName);
3 maxFileCount = Math.max(1, maxFileCount);
4 if (fileList.size() >= maxFileCount) {
5 sortByModifiedTime(fileList);
6 deleteButKeep(fileList, maxFileCount);
7 }
8}Source: Logger.java
Cleaner 实现的是"同前缀文件数量上限"策略而非按大小滚动:
getAllLogs用dir.listFiles(pathname -> pathname.getName().indexOf(finalLogPrefixName) == 0)取以日志前缀开头的所有文件(注意是"前缀匹配"而非精确匹配,logs/core会匹配到core.1234.log;副作用是若前缀本身就是另一前缀的子串可能多删);sortByModifiedTime按File.lastModified()升序(最旧在前);deleteButKeep删除列表头部多余的旧文件,删除时的SecurityException被捕获并记LogLog.error,清理失败不阻断日志初始化;- 默认
maxFileCount = 256(Logger.java#L26),initialize时实际传入上限可覆盖。
该策略的意图:桌面宠物应用每次启动都产生新 PID 新文件,若不清理,长期使用会积累海量小文件;按"数量 + 修改时间"淘汰最旧日志即可控制磁盘占用,且实现只有 ~40 行,无需引入额外滚动框架。
Core Flow
日志消息的完整生命周期
时序要点:
- 一次调用最多三路输出并行发生(log4j 遍历 root logger 的全部 appender)。
- WARN 恰好落在两个过滤器的边界上:
sysOutFilter.setLevelMax(Level.toLevel(WARN))含 WARN,sysErrFilter.setLevelMin(Level.toLevel(ERROR))从 ERROR 起——因此 WARN 进 stdout,ERROR/FATAL 进 stderr,DEBUG/INFO 也进 stdout。 - 文件写入为无缓冲直写(
bufferedIO = false),进程被强杀时已写日志不丢失。
进程启动 → 用户导出的端到端流程
调试模式 UI:LogDialog
LogDialog 是 desktop 模块实现 DialogController<ArkHomeFX> 的对话框控制器,绑定 assets/UI/LogDialog.fxml。其日志发现逻辑:
1public void refreshTable() {
2 Logger.info("LogDialog", "Refreshing table");
3 logView.getSelectionModel().clearSelection();
4 loadLogFiles(
5 desktopLogList,
6 new File(logDir),
7 (dir, name) -> name.startsWith("desktop") && name.endsWith(".log")
8 );
9 loadLogFiles(
10 coreLogList,
11 new File(logDir),
12 (dir, name) -> name.startsWith("core") && name.endsWith(".log")
13 );
14 loadLogFiles(
15 jvmCrashLogList,
16 new File("."),
17 (dir, name) -> name.startsWith("hs_err_pid") && name.endsWith(".log")
18 );
19 GuiPrefabs.Tables.autoResizeColumns(logView, 20.0);
20 GuiPrefabs.Tables.autoExpandRows(logView);
21 logView.refresh();
22}Source: LogDialog.java
三个 ObservableList 对应树的三个分组根节点,loadLogFiles 的过滤条件揭示了两类日志来源的物理位置差异:
1private void loadLogFiles(ObservableList<LogItem> targetList, File directory, FilenameFilter filter) {
2 targetList.clear();
3 File[] logFiles = directory.listFiles(filter);
4 if (logFiles != null) {
5 List<LogItem> logItems = Arrays.stream(logFiles)
6 .map(LogItem::new)
7 .filter(LogItem::isAvailable)
8 .sorted(Comparator.comparingLong(logItem -> -logItem.modifiedTime.toEpochMilli()))
9 .toList();
10 targetList.addAll(logItems);
11 Logger.debug("LogDialog", "Found " + logItems.size() + " log files in \"" + directory + "\"");
12 } else {
13 Logger.warn("LogDialog", "Directory \"" + directory + "\" is not available");
14 }
15}Source: LogDialog.java
- 目录扫描失败(
listFiles返回 null)时降级为 WARN 日志,不抛异常、不白屏——对查看器这种辅助功能,可用性优先。 - 按修改时间倒序排列(
-logItem.modifiedTime.toEpochMilli()),最新的日志总是排在最前,符合"用户最关心最近一次问题"的直觉。 LogItem.isAvailable()过滤不可用条目,表格渲染时用HumanSize/HumanInstant包装值并标记不可用状态(LogDialog.java#L145-L154)。
信息面板监听器实现了"递归选择"交互:选中一个分组根节点时,通过 Platform.runLater 异步把其全部子节点也加入选集,并实时更新"已选择 N 个文件"计数(LogDialog.java#L195-L220)。Platform.runLater 的使用是必要的:在 ListChangeListener 回调里同步修改选集可能触发 ConcurrentModificationException,延迟到 JavaFX 事件队列下一拍处理则安全。同时文件顶部导入的 ZipTask(LogDialog.java#L8)表明导出走通用后台打包任务,quickSelectAll / quickSelectRecent 两个快速选择按钮则对应"全选/选最近"两种典型排障场景。
Usage Examples
基础用法:按级别打日志
Logger.info("LogDialog", "Refreshing table");
Logger.debug("LogDialog", "Clearing table");
Logger.warn("LogDialog", "Directory \"" + directory + "\" is not available");Source: LogDialog.java
调用方只需提供稳定的 tag 字符串与人类可读消息。生成的文件日志形如 14:23:05,781 [INFO] LogDialog: Refreshing table,控制台形如 [INFO] LogDialog: Refreshing table。
进阶用法:带异常堆栈的错误日志
} catch (SecurityException e) {
LogLog.error("Cannot delete file: " + fileList.get(i), e);
}Source: Logger.java
上述是 log4j 自身的 LogLog 内部诊断 API;业务代码对等写法是 Logger.error(tag, message, throwable)(Logger.java#L162-L165),会把堆栈完整写入日志文件。这是 error 独有的重载——错误排障必须依赖堆栈,而低级别日志带堆栈会显著膨胀文件。
进阶用法:初始化日志系统并限制文件数量
Logger.initialize("logs/desktop", 256);
Logger.setLevel(Logger.DEBUG); // int 重载,进入调试模式Source: Logger.java
initialize 会清理 logs/desktop* 中除最新 255 个以外的旧文件,然后打开 logs/desktop.{当前PID}.log。
Configuration Options
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
logPrefix(initialize 参数) | String | — | 日志路径与前缀,决定文件名 {logPrefix}.{PID}.log;core 进程为 core、desktop 进程为 desktop,LogDialog 据此前缀识别分组 |
maxFileCount(initialize 参数) | int | 256(静态字段) | 同前缀日志文件数量上限;清理时保留 maxFileCount - 1 个旧文件为新文件留位 |
Logger.level(静态字段) | Level | Level.INFO | 全局日志级别;initialize 结束时会重新应用 |
charsetDefault(Const) | String | — | 日志文件编码,写入 FileAppender.setEncoding,统一跨平台字符集 |
Const.LogConfig.logDir | String | logs/ 相关常量 | 日志目录;LogDialog 扫描 core/desktop 日志与"在系统中打开"按钮均引用该常量(LogDialog.java#L35) |
| stdout 过滤上限 | Level | WARN(Level.toLevel(WARN)) | 静态块设定,sysOutFilter 的 levelMax |
| stderr 过滤下限 | Level | ERROR(Level.toLevel(ERROR)) | 静态块设定,sysErrFilter 的 levelMin |
| FileAppender 追加模式 | boolean | false | 同名日志文件覆盖写(文件名含 PID,正常情况下不会撞名) |
| FileAppender 缓冲 | boolean | false(bufferSize = 0) | 无缓冲直写,崩溃不丢日志 |
API Reference
Logger.initialize(logPrefix: String, maxFileCount: int): void
初始化静态日志器(默认级别 INFO)。重建 appender、先清理旧日志再打开 {logPrefix}.{PID}.log、挂载 stdout/stderr 双控制台 appender、重设级别并置 isInitialized = true。
Parameters:
logPrefix(String):日志路径与前缀,如"logs/myLog"maxFileCount(int):同前缀日志数量上限,内部会Math.max(1, maxFileCount)下限保护
Throws: 不抛出——文件初始化的 IOException 被捕获,仅经 LogLog.error 记录并降级为无文件日志模式。
Source: Logger.java
Logger.setLevel(level): void(三个重载)
public static void setLevel(Level level)
public static void setLevel(int level)
public static void setLevel(String level)Source: Logger.java
同时更新静态字段 Logger.level 与 rootLogger/currentLogger 的级别。int 与 String 重载经 Level.toLevel 归一(未识别字符串回退 DEBUG)。
Logger.getLevel(): Level
返回 rootLogger.getLevel(),即当前生效级别。
Source: Logger.java
Logger.debug / info / warn / error(tag: String, message: String): void
四个同构方法:门卫检查 isFileLoggerAvailable 后委托 currentLogger 对应级别输出 combine(tag, message)。
Source: Logger.java
Logger.error(tag: String, message: String, error: Throwable): void
在 error 基础上携带异常对象,log4j 会把完整堆栈写入输出目标。这是唯一接受 Throwable 的 API。
Source: Logger.java
LogDialog 对话框生命周期方法
| 方法 | 签名 | 行为 |
|---|---|---|
initializeWith | (ArkHomeFX app): void | 初始化三个 ObservableList、建表、建信息面板、绑定"刷新/在系统中打开"按钮 |
notifyDialogOpened | (Object data): void | 每次对话框打开时触发 refreshTable() |
notifyDialogClosed | (): void | 关闭时 clearTable() 清空列表 |
refreshTable | (): void | 扫描三个日志来源并重建表格 |
clearTable | (): void | 清空 core/desktop 日志列表 |
getDialogPane / getReturnButton | — | DialogController 接口要求的 FXML 根节点与返回按钮 |
Source: LogDialog.java
Failure Modes, Edge Cases & Concurrency
| 场景 | 行为 | 源码依据 |
|---|---|---|
| 日志文件无法创建(目录只读/磁盘满) | IOException 被捕获,LogLog.error 提示,isFileLoggerAvailable 保持 false,此后所有级别日志静默丢弃 | Logger.java#L67-L69 |
重复调用 initialize | removeAllAppenders() 先行清空,不会重复输出 | Logger.java#L50 |
| 同前缀日志超上限 | 初始化时先清理到 maxFileCount - 1,再开新文件 | Logger.java#L60 |
| 删除旧日志被安全管理器拒绝 | SecurityException 捕获,LogLog.error 记录后继续初始化 | Logger.java#L189-L191 |
Level.toLevel 收到非法字符串 | 回退 DEBUG(log4j 1.x 行为),不抛错 | Logger.java#L112 |
| 日志目录不存在/不可读(查看器侧) | listFiles 返回 null → WARN 日志,表格该分组为空 | LogDialog.java#L187-L189 |
| JVM 致命崩溃 | 崩溃日志 hs_err_pid*.log 由 JVM 写在工作目录而非日志目录,LogDialog 单独扫描 . | LogDialog.java#L123-L127 |
| 多进程并发写日志 | 文件名含 PID,core 与 desktop 各写各的文件,无锁竞争 | Logger.java#L58 |
| UI 选集回调中修改选集 | 递归选子节点包在 Platform.runLater 中延迟到 FX 事件队列,避免并发修改 | LogDialog.java#L204-L207 |
值得注意的两个边界:
- 同 PID 重启覆盖:
append = false意味着若 PID 被操作系统复用且日志未被清理,旧日志会被直接覆盖。因为Cleaner在每次初始化时都按数量清理,实际暴露窗口很小,这是用简洁换轻微风险的取舍。 - 前缀匹配的宽匹配:
getAllLogs用indexOf(prefix) == 0判定,logs/core前缀同样匹配coreXXX.log之类意外文件;由于日志目录由应用自管,此风险被接受。
Performance & Operational Notes
- 无缓冲直写(
bufferedIO = false):每次日志调用都触发文件写入,换取崩溃时日志完整性。桌宠进程常被用户直接关闭甚至强杀,日志可靠性优先于吞吐。 - 静态门面零分配:业务调用路径上只有一次字符串拼接(
combine)与 log4j 事件对象,无锁、无 per-module logger 查找(直接用根 Logger),调用成本极低。 - 日志文件数量受控:
maxFileCount(默认 256)+ 每次初始化清理,防止桌面应用长期运行累积小文件。单文件体积无上限——若某进程长时间停留在 DEBUG 级别,文件可能显著增大,运维上应以"调到 DEBUG 时才需要关注体积"理解。 - 调试模式的操作路径:用户侧调试流程为"启动器 → 打开日志对话框(
notifyDialogOpened自动refreshTable)→ 全选/选最近 → 导出";logExplore按钮调用app.popBrowser(new File(logDir).toURI())直接在系统文件管理器中打开日志目录(LogDialog.java#L87)。
Extension Points
- 新增日志分组:
LogDialog的分组由"分组根LogItem+ObservableList+loadLogFiles(filter)"三元组构成(LogDialog.java#L156-L161)。要展示新的日志类别(例如某个子组件日志),按同样模式追加一个列表与前缀过滤即可,RecursiveTreeItem会自动纳入树。 - 替换日志后端:
Logger是唯一对业务代码暴露的 API,内部全部经由 log4j 1.x 根 Logger。若需迁移到其它日志框架,只需重写Logger内部实现,业务调用点(Logger.info(tag, msg)等)无需改动。 - 动态级别调整:三个
setLevel重载(含int/String)即为运行时调试开关的挂点,可在启动器设置界面接入以实现"调试模式"一键切换。 - 自定义文件头:重写
getPatternLayout中getHeader()即可扩充环境快照字段(如显卡、内存等)。
Tests
本仓库当前未发现针对 Logger 或 LogDialog 的单元测试文件(在源码探索范围内未检索到相关测试)。上述行为描述均直接源自实现代码本身;若后续引入测试,Cleaner.cleanByModifiedTime 的数量边界(恰好等于/超过上限)与 initialize 的幂等性是最值得覆盖的场景。
Related Links
- Logger.java — 日志门面与文件清理核心实现
- LogDialog.java — 日志查看对话框控制器
- LogDialog.fxml — 日志对话框界面布局
- 桌宠渲染进程与进程模型 → 参见启动器/进程管理相关页面
- GUI 对话框框架(
DialogController、DialogComposer)→ 参见启动器界面架构页面 - 应用配置体系(
Const、ArkPetsConfig)→ 参见配置相关页面