日志系统与调试技巧
本页介绍 JL_OTA HarmonyOS 应用内置的日志系统设计与调试方法,涵盖 hilog 输出封装、Worker 线程异步落盘、日志文件滚动、故障日志(FaultLog)采集,以及配套的日志文件浏览页面。
Purpose and Scope
本页面向需要阅读、扩展或调试 JL_OTA 应用的开发者,完整说明 entry/src/main/ets/tool/log/ 目录下的日志子系统:
Log.ts:主线程日志门面,封装hilog输出并将日志投递给 WorkerLogWorker.ets:后台线程,负责消息分发、队列缓冲与文件写入LogWriter.ets:日志文件写入器(滚动、限长)model/LogMessage.ts:线程间消息模型与消息类型枚举LogFileListPage.ets:日志文件列表浏览页面FaultLogWriter.ets:故障日志(崩溃/卡死)落盘写入器
本页不覆盖 OTA 升级流程本身、蓝牙传输、设备通信协议等业务能力,这些内容请参见对应目录页。日志系统只是 OTA 调试过程中"记录证据"的基础设施。
Overview
OTA 升级是高风险操作:断连、校验失败、版本不匹配等问题往往发生在设备端,问题复现困难。为了在真机上定位问题,JL_OTA 应用实现了一套"双通道"日志机制:
- 实时通道(hilog):通过 HarmonyOS 的
@ohos.hilog将日志输出到系统日志缓冲区,配合hdc shell hilog命令实时查看,适合开发期联调。 - 持久通道(文件):所有日志同时格式化为纯文本,通过
worker线程投递到独立后台线程,以同步流的方式写入应用缓存目录下的appLog/文件夹,适合测试人员复现问题后直接导出日志文件。
设计意图在于:日志写入绝不能阻塞主线程(文件 I/O 是慢操作),同时 主线程日志 API 必须足够轻量(每个 API 只是格式化字符串 + postMessage)。系统还利用 @kit.PerformanceAnalysisKit 的 FaultLogger 查询 JS_CRASH 等故障记录,将崩溃现场(pid/uid/reason/fullLog)一并落盘,形成完整的故障证据链。
flowchart TD
subgraph sg_Main["主线程 (UI)"]
App["业务代码"] -->|"Log.d/i/w/e()"| LogFacade["Log 静态类<br/>domain=0xFFBB, prefix=zhjieli"]
LogFacade -->|"hilog.debug/info/warn/error"| HiLog["@ohos.hilog 系统日志"]
LogFacade -->|"outputLog() 格式化"| Post["postMessage(msg)"]
end
subgraph sg_Worker["Worker 线程 (LogWorker.ets)"]
OnMsg["workerPort.onmessage"] --> Switch{"MessageType 分发"}
Switch -->|"LOG"| Queue["Queue<string> 缓冲"]
Queue --> Handle["handleLog() 逐条弹出"]
Handle --> LW["LogWriter.append()"]
Switch -->|"START"| Init["初始化 Queue + LogWriter"]
Switch -->|"STOP"| Close["关闭写入器"]
Switch -->|"FAULT"| Fault["saveFaultLog()<br/>FaultLogger.query(JS_CRASH)"]
end
subgraph sg_Storage["存储层"]
LW --> File["cacheDir/appLog/jlota-YYYYMMDD-HHmmss.SSS.txt"]
Fault --> FaultWriter["FaultLogWriter"]
FaultWriter --> FFile["cacheDir/appLog/fault_*.txt"]
end
HiLog -->|"hdc shell hilog 实时查看"| Dev["开发者终端"]
File -->|"LogFileListPage 浏览/导出"| UI["日志列表页面"]
Post --> OnMsg
上图展示了日志系统的整体架构:主线程只负责格式化与投递,所有磁盘 I/O 均发生在独立 Worker 线程中,通过 MessageType 枚举实现消息语义分发,存储层统一落在 cacheDir/appLog/ 目录。
Architecture
分层职责
| 层 | 文件 | 职责 |
|---|---|---|
| 门面层 | Log.ts | 静态方法 d/i/w/e,hilog 输出 + 格式化 + 投递 Worker |
| 消息层 | model/LogMessage.ts | LogMessage<T> 载荷 + MessageType 消息语义 |
| 工作线程 | LogWorker.ets | 消息分发、Queue 缓冲、故障日志查询 |
| 写入层 | LogWriter.ets / FaultLogWriter.ets | 同步流写文件、8MB 滚动 |
| 展示层 | LogFileListPage.ets | 列出 appLog/ 目录下的日志文件供查看/删除 |
关键设计决策
- Worker 隔离 I/O:文件写入使用
fs.createStreamSync+writeSync+flushSync同步 API,若放在主线程会卡 UI;放到 Worker 线程后主线程日志调用变为非阻塞的postMessage。 - 消息驱动而非函数调用:主线程与 Worker 之间通过
postMessage传递LogMessage,Worker 端onmessage按type分发,天然支持 START/LOG/STOP/FAULT 四类语义。 - 日志与故障分离:普通日志走
LogWriter,崩溃现场走FaultLogWriter,两者都是BaseWriter的子类,复用同一套"打开/追加/关闭"生命周期。 - 防死循环约定:代码多处标注
Fixme: 此函数不能使用封装的Log,否则会死循环—— Worker 内部(如LogWriter.append出错时)若调用Log.e会再次向自身投递 LOG 消息,形成无限递归。因此 Worker 内部错误处理也直接调用Log.e(仅走 hilog 且tryToSave检测到 workerInstance 存在时仍会投递……实际代码中LogWriter的错误分支调用Log.e存在理论风险,但handleLog的同步 Promise 循环保证了单条消息处理完成前不会处理下一条,规避了递归风暴,详见"失败模式"章节)。
核心实现
Log 静态门面(Log.ts)
Log 是一个纯静态类,定义了三个关键常量:
domain: number = 0xFFBB:hilog 域 ID(应用自定义域,用于 hilog 过滤)prefix: string = "zhjieli":hilog 标签前缀,品牌标识format: string = '%{public}s':隐私格式化占位符,声明参数为公开内容(不脱敏)
四个对外方法 d/i/w/e 遵循同一模式:先调用对应级别的 hilog API 输出到系统日志,再调用私有 outputLog 格式化后投递 Worker。这样一条日志同时拥有实时通道与持久通道:
static d(tag: string, msg:string) {
hilog.debug(Log.domain, Log.prefix, Log.format, `${tag}: ${msg}`);
Log.outputLog(LogLevel.D, tag, msg)
}
static i(tag: string, msg: string) {
hilog.info(Log.domain, Log.prefix, Log.format, `${tag}: ${msg}`);
Log.outputLog(LogLevel.I, tag, msg)
}
static w(tag: string, msg: string) {
hilog.warn(Log.domain, Log.prefix, Log.format, `${tag}: ${msg}`);
Log.outputLog(LogLevel.W, tag, msg)
}
static e(tag: string, msg: string) {
hilog.error(Log.domain, Log.prefix, Log.format, `${tag}: ${msg}`);
Log.outputLog(LogLevel.E, tag, msg)
}
Source: Log.ts
日志行格式化(outputLog)
持久通道的每一行日志由 outputLog 拼接,格式为:
YYYYMMDD HH:mm:ss.SSS <级别> [module:]tag: message
其中 module 是受保护静态字段(默认空),alignNumber 负责月份/日期/时分秒补零(如 3 → 03)。注意时间格式与 hilog 原生格式不同,这是为了在文本文件里便于阅读和排序:
private static outputLog(level: LogLevel, tag: string, arg: string) {
let date = new Date()
let m = Log.module ? `${Log.module}:` : ''
const time = date.getFullYear() + alignNumber(date.getMonth() + 1) + alignNumber(date.getDate()) + ' '
+ alignNumber(date.getHours()) + ':' + alignNumber(date.getMinutes()) + ':' + alignNumber(date.getSeconds())
+ '.' + date.getMilliseconds();
const log = time + " " + level + " " + m + (tag ? tag : Log.prefix) + ": " + (arg ? arg : "null") + "\n";
Log.tryToSave(log)
}
Source: Log.ts
tryToSave 是主线程与 Worker 之间的唯一桥梁:仅在 workerInstance 已创建时投递 MessageType.LOG 消息,否则静默丢弃(避免日志系统自身未启动时抛异常):
private static tryToSave(log: string) {
if (workerInstance) {
let m = new LogMessage<string>(MessageType.LOG)
m.value = log;
workerInstance?.postMessage(m)
}
}
Source: Log.ts
生命周期管理
Log.startSaving(context) 负责创建 Worker:使用 worker.ThreadWorker('entry/ets/tool/log/LogWorker.ets') 指定 Worker 入口文件(相对 module 根),创建后立即发送 START 消息并注册 onexit 回调;stopSaving() 先发送 STOP 再 terminate() 回收线程。getFaultLog(context) 发送 FAULT 消息触发崩溃日志落盘。
消息模型(model/LogMessage.ts)
Worker 通信的载荷是 LogMessage<T> —— 一个极简的泛型信封,type 声明语义、value 携带数据:
export class LogMessage<T> {
public type: MessageType;
public value?: T;
constructor(type: MessageType) {
this.type = type;
}
}
export enum MessageType {
START = 1,
LOG = 2,
STOP = 3,
FAULT = 4, // Fault log
}
Source: LogMessage.ts
四种消息语义:
START:携带日志目录路径(context.cacheDir + '/appLog'),Worker 初始化Queue与LogWriterLOG:携带已格式化的日志行字符串,Worker 入队并触发写入STOP:关闭写入器、清空队列(线程随后由主线程terminate)FAULT:携带日志目录路径,Worker 查询系统故障日志并落盘
Worker 消息分发(LogWorker.ets)
Worker 入口通过 worker.workerPort.onmessage 注册处理函数,按 m.data.type 做 switch 分发。logHandler 是核心调度器:
function logHandler(m: MessageEvents) {
switch (m.data.type) {
case MessageType.LOG:// Fixme: 从这开始的函数都不要使用封装的Log,否则会死循环
queue?.add((m.data as LogMessage<string>).value)
handleLog()
break
case MessageType.START:
const logMessage = m.data as LogMessage<string>
if (logWriter && logWriter?.isOpened()) {
Log.e(TAG, 'Log writer has opened')
return
}
queue = new Queue()
let logDir = logMessage.value
logWriter = new LogWriter()
logWriter.open(logDir!)
break
case MessageType.STOP:
logWriter?.close()
logWriter = undefined
queue = undefined
break
case MessageType.FAULT:
saveFaultLog(m)
break;
}
}
Source: LogWorker.ets
值得注意的细节:
- 幂等保护:
START时若logWriter已打开则直接 return,防止重复初始化。 - 队列消费:
handleLog用while ((queue?.length ?? 0) > 0)在单个 Promise 内把队列清空,保证"一批消息一个写入批次",减少postMessage与文件 I/O 的切换开销:
async function handleLog(): Promise<boolean> {
let promise = new Promise<boolean>((resolve) => {
while ((queue?.length ?? 0) > 0) {
let log = queue?.pop() ?? '';
logWriter?.append(log)
}
resolve(true);
})
return promise
}
Source: LogWorker.ets
- 错误透传:
onmessageerror与onerror均被捕获并记录,避免 Worker 静默崩溃。
文件写入与滚动(LogWriter.ets)
LogWriter 继承 BaseWriter,实现 append/open/close/isOpened。最核心的约束是 单文件 8MB 上限(LOG_FILE_SIZE_LIMIT = 8 * 1024 * 1024),达到上限后关闭当前流、新建文件,实现日志滚动:
append(log: string) {
if (this.logStream) {
this.logStream.writeSync(log);
this.logStream.flushSync();
this.fileSize += log.length
if (this.fileSize >= LOG_FILE_SIZE_LIMIT) {
this.close() // close previous file
this.createFile() // create a new log file
}
}
}
private createFile(): boolean {
this.fileSize = 0 // reset
try {
if (!fs.accessSync(this.logDir)) {
fs.mkdirSync(this.logDir);
}
this.logPath = `${this.logDir}/${createFilename()}`
this.logStream = fs.createStreamSync(this.logPath, "w");
} catch (err) {
Log.e(TAG, "Create file error: " + err);
return false;
}
return true;
}
Source: LogWriter.ets
文件名由 createFilename() 按时间戳生成:jlota-YYYYMMDD-HHmmss.SSS.txt(毫秒级精度,理论上避免同秒重名冲突)。目录不存在时自动 mkdirSync,目录创建与流创建均包裹在 try/catch 中,失败时返回 false 并记录错误。
故障日志采集(saveFaultLog)
saveFaultLog 使用 FaultLogger.query(FaultLogger.FaultType.JS_CRASH) 查询系统记录的 JS 崩溃,逐条提取 pid / uid / type / timestamp / reason / module / summary / fullLog 字段,通过 FaultLogWriter 写入独立文件。getLogTypeDesc 将枚举映射为可读字符串(CPP_CRASH / JS_CRASH / APP_FREEZE / NO_SPECIFIC)。整个查询与写入包裹在 try/catch 中,任何一步失败都不会影响主线程。
日志文件浏览页面(LogFileListPage.ets)
LogFileListPage.ets 是日志系统的用户界面入口,负责展示 appLog/ 目录下生成的日志文件,支持查看与删除(仓库中同时存在 icon_log_delete.png 删除按钮资源)。它让测试人员无需连接 IDE 即可在应用内翻阅升级过程中的日志证据,是"调试技巧"中面向非开发者的最后一环。
核心流程
一条日志从业务代码到磁盘文件的完整旅程如下。以 OTA 过程中最常见的 Log.i(TAG, "upgrade start") 为例:
sequenceDiagram
participant Biz as 业务代码 (OTA流程)
participant Log as Log 静态类
participant HiLog as hilog 系统日志
participant Worker as LogWorker 线程
participant Queue as Queue<string>
participant Writer as LogWriter
participant FS as appLog/*.txt 文件
Note over Biz,Log: startSaving() 已先行执行
Biz->>Log: Log.i(TAG, "upgrade start")
Log->>HiLog: hilog.info(0xFFBB, "zhjieli", "%{public}s", "TAG: upgrade start")
Log->>Log: outputLog() 拼装 "20240920 10:00:00.123 i TAG: upgrade start"
Log->>Worker: postMessage({type: LOG, value: 行文本})
Worker->>Queue: queue.add(行文本)
Worker->>Queue: handleLog() while 循环弹出
Queue-->>Writer: logWriter.append(行文本)
Writer->>FS: writeSync + flushSync
Writer->>FS: fileSize >= 8MB ? 关旧流建新文件
FS-->>Biz: 下一行日志继续
启动时序
sequenceDiagram
participant Main as 主线程
participant W as Worker 线程
participant WL as LogWriter
participant FL as FaultLogWriter
Main->>W: new ThreadWorker('entry/ets/tool/log/LogWorker.ets')
Main->>W: postMessage({type: START, value: cacheDir/appLog})
W->>W: queue = new Queue()
W->>WL: new LogWriter().open(dir) 创建流
Note over Main,W: 升级过程中持续 LOG 消息
Main->>W: postMessage({type: FAULT, value: dir})
W->>FL: saveFaultLog() 查询 JS_CRASH 并写文件
Main->>W: postMessage({type: STOP})
W->>WL: close() 关闭流
Main->>W: terminate() 回收线程
关键点:START 必须先于任何 LOG 到达(tryToSave 在 worker 未创建时静默丢弃);STOP 后主线程立即 terminate,因此 Worker 无需自行 close()。
使用示例
基本用法:记录调试/信息/告警/错误
业务代码只需一行调用,四个级别覆盖全部场景。日志会自动同时出现在 hilog 与文件中:
Log.d(TAG, 'OTA upgrade start') // 调试级别
Log.i(TAG, 'Ble connected') // 信息级别
Log.w(TAG, 'Timeout, retry...') // 告警级别
Log.e(TAG, 'Check crc error: ' + err) // 错误级别
Source: Log.ts
启动/停止日志保存
通常在应用入口(如 aboutToAppear)调用 startSaving,退出时调用 stopSaving。startSaving 是幂等的:worker 已存在时直接返回,避免重复创建线程:
static startSaving(context: Context) {
if (workerInstance) {
return
}
workerInstance = new worker.ThreadWorker('entry/ets/tool/log/LogWorker.ets');
let m = new LogMessage<string>(MessageType.START)
m.value= context.cacheDir + LOG_DIR_NAME
workerInstance.postMessage(m)
workerInstance.onexit = (code: number) => {
Log.w(TAG, 'Log thread worker>> exit: ' + code)
}
}
static stopSaving() {
let m = new LogMessage<string>(MessageType.STOP)
workerInstance?.postMessage(m)
workerInstance?.terminate()
workerInstance = undefined
}
Source: Log.ts
采集崩溃故障日志
升级失败伴随 JS 崩溃时,调用 getFaultLog 将系统 FaultLogger 中的崩溃现场(含 fullLog)写入 appLog/ 目录:
static getFaultLog(context: Context) {
let m = new LogMessage<string>(MessageType.FAULT)
m.value = context.cacheDir + LOG_DIR_NAME
workerInstance?.postMessage(m)
}
Source: Log.ts
自定义消息扩展
需要向 Worker 增加新语义时(例如"导出日志"),先在 MessageType 中追加枚举值,再在 logHandler 的 switch 中增加分支:
export enum MessageType {
START = 1,
LOG = 2,
STOP = 3,
FAULT = 4, // Fault log
}
Source: LogMessage.ts
配置选项
日志系统的可调参数集中在两个文件中,均为编译期常量(无运行时配置项):
| 常量 | 位置 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
LOG_DIR_NAME | Log.ts | string | '/appLog' | 日志目录名,拼接在 context.cacheDir 之后 |
Log.domain | Log.ts | number | 0xFFBB | hilog 域 ID,用于 hdc shell hilog -D 过滤 |
Log.prefix | Log.ts | string | "zhjieli" | hilog 标签前缀 |
Log.format | Log.ts | string | '%{public}s' | hilog 隐私格式化占位符(public 不脱敏) |
Log.module | Log.ts | string | '' | 受保护静态字段,可被子类覆盖,拼入文件行格式 |
LOG_FILE_SIZE_LIMIT | LogWriter.ets | number | 8 * 1024 * 1024 | 单个日志文件大小上限(字节),触发滚动 |
| Worker 入口路径 | Log.ts | string | 'entry/ets/tool/log/LogWorker.ets' | ThreadWorker 构造参数,相对 module 根 |
Source: Log.ts / LogWriter.ets
API 参考
Log 静态类
| 方法签名 | 说明 |
|---|---|
static d(tag: string, msg: string): void | 调试日志:hilog.debug + 落盘(级别 d) |
static i(tag: string, msg: string): void | 信息日志:hilog.info + 落盘(级别 i) |
static w(tag: string, msg: string): void | 告警日志:hilog.warn + 落盘(级别 w) |
static e(tag: string, msg: string): void | 错误日志:hilog.error + 落盘(级别 e) |
static startSaving(context: Context): void | 创建 Worker 并发送 START;幂等,已存在则直接返回 |
static stopSaving(): void | 发送 STOP 后 terminate Worker,置空实例 |
static getFaultLog(context: Context): void | 发送 FAULT 消息,触发崩溃日志落盘 |
参数约定:
tag:模块标签,写入 hilog 与文件行中(如LogWriter、OTA)msg:日志正文;outputLog对空值显示为nullcontext:用于获取cacheDir拼出日志目录
返回: 全部为 void,日志调用永不向调用方抛异常(Worker 未启动时静默丢弃)。
LogWriter 类(继承 BaseWriter)
| 方法签名 | 说明 |
|---|---|
open(logDir: string): boolean | 校验目录参数、创建文件流;失败返回 false |
append(log: string): void | 同步写入 + flush;超过 8MB 自动关闭旧流、新建文件 |
close(): boolean | 关闭流,置空引用;异常时返回 false |
isOpened(): boolean | 流是否已创建 |
LogWorker 内部函数
| 函数 | 说明 |
|---|---|
logHandler(m: MessageEvents) | onmessage 入口,按 MessageType 分发 |
handleLog(): Promise<boolean> | 单 Promise 内清空队列并逐条写入 |
saveFaultLog(m: MessageEvents) | 查询 JS_CRASH 并写入 FaultLogWriter |
getLogTypeDesc(type: FaultLogger.FaultType): string | 故障类型枚举转可读字符串 |
失败模式、边界情况与并发
死循环风险(自引用)
源码中两处 Fixme 注释(LogWriter 头部与 logHandler 的 LOG 分支)明确警告:Worker 内部不得调用封装的 Log。原因是 Log.e → outputLog → tryToSave → 若 workerInstance 存在则向 Worker 投递新 LOG 消息 → 再次进入写入逻辑 → 若再出错再投递……形成无限递归。实际防护机制:
- 文件写入失败路径(
createFile的 catch)调用Log.e,此时 Worker 正在处理消息,新 LOG 会入队,但handleLog的同步while循环与单线程模型确保当前批次先执行完毕,不会在消息处理中嵌套重入——风险被降级为"失败日志可能反复入队",而非栈溢出。 - 扩展时遵循同一约定:Worker 线程内只允许
console.*或直接 hilog,禁止调用封装 Log。
Worker 未启动时的日志丢失
tryToSave 在 workerInstance 为 undefined 时静默丢弃日志。若业务代码在 startSaving 之前打日志,这些行只出现在 hilog、不会落盘。设计取舍:日志系统自身失败不应影响业务主流程,属于有意的 fail-open。
文件系统异常
mkdirSync / createStreamSync 失败(磁盘满、权限不足)被 try/catch 捕获并返回 false;append 因 writeSync 抛错未捕获时,该批次写入中断,但 Worker 不崩溃(后续消息继续尝试)。close 同样捕获异常返回 false。注意:目录不存在时不会自动递归创建父目录,mkdirSync(this.logDir) 假定 cacheDir 已存在(系统保证)。
并发与一致性
- 单消费者模型:Worker 是 ArkTS 单线程,
Queue无锁安全;主线程的postMessage是有序投递,日志行顺序与调用顺序一致。 - 批量写入:
handleLog在一个 Promise 内清空队列,多条日志共享一次调度窗口,减少上下文切换;代价是高峰期日志可能短暂积压。 - 滚动竞态:8MB 触发的关流/建流在同一同步代码块内完成,Worker 单线程下无并发窗口,不会出现"双流写同一文件"。
重复初始化保护
startSaving 检查 workerInstance 幂等返回;Worker 端 START 分支检查 logWriter.isOpened() 防止重复 open。stopSaving 后 workerInstance = undefined,可安全再次 startSaving。
性能与运维注意事项
- 同步写盘:
writeSync+flushSync是阻塞 I/O,这是刻意隔离在 Worker 线程的原因;主线程只承担字符串格式化与postMessage(约微秒级),UI 流畅度不受日志量影响。 - 文件大小:单文件 8MB 上限防止日志无限膨胀;滚动文件名含毫秒时间戳,同秒滚动也几乎不会重名。运维上建议定期清理
cacheDir/appLog/(页面提供了删除入口)。 - 日志目录位置:日志写在
cacheDir(应用缓存目录),系统可能在空间紧张时清理缓存目录,重要日志应尽早通过LogFileListPage导出备份。 - hilog 实时过滤:开发期可用
hdc shell hilog | grep zhjieli过滤本应用日志;domain=0xFFBB也可用于hilog -D 0xFFBB级别的域过滤。
调试技巧
- 级别分层:开发期用
d,联调用i,可恢复异常用w,不可恢复错误用e。hilog 本身有级别过滤,落盘文件也保留级别便于事后检索。 - tag 约定:每个模块以自己的类名作 tag(如
TAG = 'LogWriter'),配合文件行中的[module:]tag前缀,grep时可按模块快速切片。 - 崩溃现场闭环:
FaultLogWriter输出含reason、summary、fullLog的完整崩溃栈;配合普通日志的时间线,可还原"崩溃前发生了什么"。 - 文件查看入口:
LogFileListPage直接在应用内列出并删除appLog/下的日志文件,无需数据线即可取证,适合测试与售后场景。 - 隐私占位符:
format使用%{public}s声明公开,若日志含用户敏感信息,应改为%{private}s由 hilog 自动脱敏。
扩展点
- BaseWriter 抽象基类:
LogWriter与FaultLogWriter共用"open/append/close/isOpened"契约,新增输出目标(如上报服务器、加密存储)只需实现BaseWriter并挂到logHandler。 - MessageType 枚举:新增消息语义(如导出、清空、采样开关)按"枚举 + switch 分支"两步扩展,主线程通过
LogMessage<T>泛型携带任意载荷。 - Log.module 受保护字段:可派生
Log子类覆盖module,为不同业务域打上独立前缀,实现日志分区。
Related Links
- Log.ts 源码
- LogWorker.ets 源码
- LogWriter.ets 源码
- LogMessage.ts 源码
- FaultLogWriter.ets 源码
- LogFileListPage.ets 源码
- 升级流程相关页面:OTA 业务流程文档(见目录导航)