杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 项目简介与核心能力
    • 快速开始
    • 工程结构与依赖库
  • 核心功能

    • RCSP OTA 升级流程
    • BLE 升级通道
    • SPP 升级通道
    • 自动回连机制
  • 蓝牙通信架构

    • 蓝牙抽象层与基础组件
    • BLE 模块实现
    • SPP 模块实现
    • 蓝牙管理与 OTA 管理器
  • 示例应用

    • 应用入口与启动流程
    • 主界面与设备连接交互
    • 关于、日志与辅助页面
  • 调试与运维

    • 日志系统与调试技巧
    • 问题排查与技术支持
  • 开发者指南

    • SDK 版本历史
    • 集成与二次开发指南

关于、日志与辅助页面

本页介绍 JL_OTA 示例应用中的辅助页面体系:关于页面(AboutPage)、日志文件列表页面(LogFileListPage)、网页浏览页面(WebPage),以及支撑它们的日志基础设施(Log / LogWriter / FaultLogWriter)、偏好存储(PreferencesHelper)与页面路由协作机制。

Purpose and Scope

本页覆盖 demo 应用中与"信息展示、日志查看、辅助入口"相关的完整页面链路:

  • AboutPage(关于页):应用版本展示、用户协议/隐私政策入口、ICP 备案信息、隐藏的"日志保存"开关。
  • LogFileListPage(日志文件列表页):日志目录浏览、单文件删除、批量清空、系统分享、故障日志导出。
  • WebPage(网页浏览页):承载用户协议、隐私政策、ICP 查询等内嵌 Web 内容。
  • 日志基础设施:tool/log 目录下的 Log、LogWriter、FaultLogWriter、LogMessage 等组件。
  • 协作机制:PreferencesHelper 持久化日志开关、eventHub 事件广播、router 页面导航。

以下内容不在本页范围,由其他目录页覆盖:蓝牙扫描/连接/OTA 升级流程(见蓝牙与 OTA 相关页面)、主页面 MainPage 的设备列表与升级操作(见主流程页面)、启动页 LaunchPage 的初始化逻辑(见应用启动页面)。

Overview

JL_OTA 示例应用是一个基于 HarmonyOS ArkTS 的 OTA 升级 Demo。除核心的蓝牙扫描、连接、升级能力外,应用还包含一组面向用户与开发者的辅助页面,用于:

  1. 展示应用元信息:通过 bundleManager.getBundleInfoForSelf 获取应用自身的版本号(versionName / versionCode),并结合资源文件显示"关于应用"信息。
  2. 提供合规入口:用户协议(USER_AGREEMENT)与隐私政策(PRIVACY_POLICY)通过 WebPage 内嵌展示,ICP 备案信息可点击跳转工信部查询页。
  3. 支持调试与问题反馈:默认隐藏的日志保存开关——用户连续点击应用 Logo 5 次后开启日志落盘,之后可进入日志文件列表页查看、删除、分享或清空日志。
  4. 故障日志导出:通过 Log.getFaultLog 获取故障日志并触发系统分享面板。

整个辅助页面体系的设计意图是:把"诊断能力"藏在用户无感知的交互之后(连续点击 Logo),避免普通用户误操作;同时为开发/测试人员提供一条完整、可分享的日志回溯链路。

Architecture

flowchart TD
    subgraph sg_Pages["页面层 pages/"]
        AboutPage["AboutPage"]
        LogFileListPage["LogFileListPage"]
        WebPage["WebPage"]
        MainPage["MainPage"]
        LaunchPage["LaunchPage"]
    end

    subgraph sg_CommonView["通用视图 view/"]
        TopBarView["TopBarView"]
    end

    subgraph sg_ViewModel["视图模型 viewmodel/"]
        SettingsVM["SettingsVM"]
        ItemViewVM["ItemView"]
    end

    subgraph sg_Common["公共层 common/"]
        PreferencesHelper["PreferencesHelper"]
        Constant["Constant (AgreementUrl/CompanyInfo)"]
    end

    subgraph sg_Tool["工具层 tool/log/"]
        Log["Log"]
        LogWriter["LogWriter"]
        FaultLogWriter["FaultLogWriter"]
        LogMessage["LogMessage"]
    end

    subgraph sg_System["系统能力"]
        Router["@ohos.router"]
        Bundle["bundleManager"]
        Share["systemShare (ShareKit)"]
        FileIO["fileIo (CoreFileKit)"]
        EventHub["eventHub"]
        PrefStore["Preferences (持久化)"]
    end

    AboutPage -->|"router.pushUrl"| WebPage
    AboutPage -->|"router.pushUrl"| LogFileListPage
    AboutPage --> TopBarView
    AboutPage --> ItemViewVM
    AboutPage --> PreferencesHelper
    AboutPage --> Constant
    AboutPage -->|"Log.startSaving/stopSaving"| Log
    AboutPage -->|"getBundleInfoForSelf"| Bundle
    AboutPage -->|"eventHub.on/off"| EventHub

    LogFileListPage --> TopBarView
    LogFileListPage -->|"LOG_DIR_NAME"| Log
    LogFileListPage -->|"log目录操作"| FileIO
    LogFileListPage -->|"ShareController"| Share
    LogFileListPage --> PreferencesHelper
    LogFileListPage -->|"eventHub.emit"| EventHub

    Log --> LogWriter
    Log --> FaultLogWriter
    LogWriter --> LogMessage
    FaultLogWriter --> LogMessage
    PreferencesHelper -->|"getValue/putValue"| PrefStore

架构说明

  • 页面层:AboutPage 是辅助页面的枢纽,负责展示信息并导航到 WebPage 与 LogFileListPage;MainPage 与 LaunchPage 是主流程页面,与辅助页面通过 router 互相跳转。
  • 通用视图/视图模型:TopBarView 提供统一标题栏(返回键、右侧操作键),ItemView 提供"名称 + 值 + 图标"的列表行模型,SettingsVM 定义 EVENT_ON_LOG_CLOSED 事件常量用于页面间通信。
  • 公共层:PreferencesHelper 封装 HarmonyOS Preferences 的读写,PreferenceKey.LOG_SAVING_SWITCH 持久化日志保存开关;Constant 集中管理用户协议/隐私政策 URL 与公司备案信息。
  • 工具层:Log 是全局日志门面,LogWriter 负责普通日志落盘,FaultLogWriter 负责故障日志落盘,二者均继承 BaseWriter,LogMessage<T> 是写入队列的消息模型。
  • 系统能力:页面通过 router 导航、eventHub 广播事件、systemShare 分享日志、fileIo 操作日志文件、bundleManager 读取版本号。

该分层遵循 ArkTS 声明式开发的标准组织方式:UI 与逻辑分离、公共能力下沉到 common/tool、页面间通过 router + eventHub 解耦。日志目录固定为应用 cacheDir 下的 /appLog(LOG_DIR_NAME),因此日志生命周期随应用缓存管理,清除缓存即可释放日志空间。

关于页面(AboutPage)实现详解

AboutPage 是 @Entry @Component 声明的页面组件,通过 router 被 MainPage 或其他入口推入。其核心职责与实现要点如下。

页面状态与初始化

页面持有三个 @State 变量:version(版本号字符串)、openLogSaving(日志保存开关是否开启)、icpInfo(ICP 备案信息文本),以及两个私有成员 lastClickTimeMs 与 clicks(用于连续点击检测)。

aboutToAppear() 生命周期中依次完成三件事:

  1. 从资源中读取 ICP 备案信息字符串;
  2. 通过 PreferencesHelper.getInstance().getValue(PreferenceKey.LOG_SAVING_SWITCH) 读取持久化的日志开关状态,若为 boolean 则同步到 openLogSaving;
  3. 调用 bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION) 获取应用自身 bundle 信息,拼装版本号字符串 app_version: V${versionName}_${versionCode}。

同时注册 eventHub.on(SettingsVM.EVENT_ON_LOG_CLOSED, this.onCloseLog),用于监听"日志保存被关闭"事件;aboutToDisappear() 中调用 eventHub.off 反注册,避免泄漏。

列表项与页面跳转

页面使用 List + ForEach 渲染 items 数组(用户协议、隐私政策两个 ItemView<Resource, string> 项)。点击列表项时调用 browseWeb(title, url):

private browseWeb(title?: string, url?: string) {
  router.pushUrl({
    url: 'pages/WebPage',
    params: { "url": url, 'title': title }
  }, router.RouterMode.Standard, (err) => {
    if (err) {
      Log.e(TAG, `Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
  })
}

Source: AboutPage.ets

该方法的三个参数通过 router 的 params 透传给 WebPage,WebPage 据此渲染标题与 URL。RouterMode.Standard 允许同一页面多次入栈(例如先看协议再返回),而进入日志列表页则使用 RouterMode.Single(见下文),避免重复入栈。

隐藏的日志保存开关(连续点击 Logo)

这是本页最具设计意图的交互:连续 5 次(每次间隔 ≤ 1000ms)点击应用 Logo,才开启日志保存功能。实现如下:

private clickToOpenSavingLog() {
  const currTime = systemDateTime.getTime()
  if (currTime - this.lastClickTimeMs <= 1000) {
    this.clicks += 1
  } else {
    this.clicks = 1
  }
  this.lastClickTimeMs = currTime
  if (this.clicks >= 5) {
    PreferencesHelper.getInstance().putValue(PreferenceKey.LOG_SAVING_SWITCH, true).then(() => {
      this.openLogSaving = true
      Log.startSaving(getContext(this))
    })
  }
}

Source: AboutPage.ets

设计要点:

  • 防误触:日志落盘涉及隐私与存储开销,不应对普通用户默认开放;连点门槛(5 次、1 秒内)足以拦截误触,又足够简单,便于开发/测试人员开启。
  • 状态持久化:开关写入 Preferences(key 为 LOG_SAVING_SWITCH),App 重启后仍然生效,aboutToAppear 会重新读取。
  • 立即生效:putValue 成功后调用 Log.startSaving(getContext(this)) 启动落盘,并同步更新 UI 状态。
  • 联动显示:openLogSaving 为 true 时,"日志文件"入口行(visibility: Visibility.Visible)才显示,点击后 router.pushUrl({ url: "pages/LogFileListPage" }, router.RouterMode.Single) 进入日志列表页。

备案信息与版权行

页面底部通过 fancy('auto') 扩展样式显示 icpInfo + ": " + CompanyInfo.ICP_RECORD,点击后 browseWeb 跳转到 CompanyInfo.ICP_URL(工信部备案查询);下方固定显示 CompanyInfo.COPYRIGHT 版权文本。fancy 是 @Extend(Text) 声明的样式扩展函数,统一设置字号、颜色与对齐方式。

日志文件列表页面(LogFileListPage)实现详解

LogFileListPage 是日志管理页面,仅在日志保存功能开启后可达。它把日志目录(cacheDir + LOG_DIR_NAME)中的 .txt 文件列出,并提供 5 类操作:刷新、单文件删除(滑动)、清空目录、系统分享、获取故障日志。

日志目录与列表加载

页面初始化时计算日志目录 LOG_DIR = getContext(this).cacheDir + LOG_DIR_NAME,其中 LOG_DIR_NAME = '/appLog' 来自 Log.ts。aboutToAppear 调用 refreshFileLit() 异步加载文件列表:

private refreshFileLit() {
  tryToGetFileList(this.LOG_DIR).then((list) => {
    this.fileList = list
    Log.w(TAG, 'list=' + list.length)
  }).catch((e: BusinessError) => {
    Log.e(TAG, 'e=' + JSON.stringify(e))
  })
}

Source: LogFileListPage.ets

底层 tryToGetFileList 使用 fileIo.listFile(dirPath, { filter: { suffix: ['.txt'] } }) 过滤出日志文本文件,并逐文件用 fileIo.stat 组装 SharedInfo(实现 systemShare.SharedRecord,含 filename、path、utd 类型描述符)。

顶部操作栏

顶栏右侧依次提供 4 个操作:

操作图标行为
日志开关(Toggle)系统 Switch关闭即写回 LOG_SAVING_SWITCH=false,调用 Log.stopSaving(),eventHub.emit(SettingsVM.EVENT_ON_LOG_CLOSED) 通知其他页面,并 router.back() 返回
刷新ic_refresh重新执行 refreshFileLit()
分享ohos_ic_public_share将勾选的日志文件交给系统分享面板
故障日志ohos_ic_public_fail调用 Log.getFaultLog(getContext(this))

分享逻辑(systemShare)

tryToShare() 将勾选列表封装为 systemShare.SharedData,多条时循环 addRecord 追加,然后创建 ShareController 并监听 dismiss 事件复位勾选状态,最后 controller.show(context, { previewMode: SharePreviewMode.DETAIL, selectionMode: SelectionMode.BATCH }) 弹出分享面板:

tryToShare() {
  const list = this.selectedList
  if (list.length <= 0) {
    Log.e(TAG, 'no select')
    return
  }
  let data: systemShare.SharedData = new systemShare.SharedData(list[0]);
  Log.d(TAG, 'share list size=' + list.length)
  if (list.length > 1) {
    for (let index = 1; index < list.length; index++) {
      data.addRecord(list[index])
    }
  }
  let controller: systemShare.ShareController = new systemShare.ShareController(data);
  let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  controller.on('dismiss', () => {
    Log.e(TAG, 'Share panel closed')
    this.isSelected = true
    this.isSelected = false
  });
  controller.show(context, {
    previewMode: systemShare.SharePreviewMode.DETAIL,
    selectionMode: systemShare.SelectionMode.BATCH
  })
}

Source: LogFileListPage.ets

分享文件类型通过 SharedInfo.utd 声明为 uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,使日志以纯文本形式被系统识别。这一设计让"问题反馈"闭环:测试人员无需连接电脑,即可把设备日志直接分享给开发。

删除与清空

  • 单文件删除:列表项注册 swipeAction,左滑出现红色"删除"按钮,点击后 fileIo.unlink(path) 删除文件,并从 fileList 中同步移除,避免 UI 与磁盘不一致。
  • 清空目录:tryToClear() 先清空 fileList 数组,再 fileIo.rmdir(this.LOG_DIR) 删除整个日志目录,成功后执行 Log.stopSaving() + Log.startSaving(context) 重建目录并继续落盘。删除失败(目录非空等)通过 BusinessError 捕获并记录日志。

日志基础设施(tool/log)

辅助页面的日志能力由 entry/src/main/ets/tool/log/ 下的组件支撑,它们组成了一个典型的"门面 + 写入器"结构。

Log 门面类

Log.ts 定义默认导出类 Log,并导出常量 LOG_DIR_NAME = '/appLog':

export const LOG_DIR_NAME = '/appLog'
...
export default class Log {
  static domain: number = 0xFFBB

Source: Log.ts

Log 提供静态方法,页面层直接调用:

  • Log.d/i/w/e(TAG, message):按 debug/info/warn/error 级别输出日志(内部对接 hilog,domain 为 0xFFBB)。
  • Log.startSaving(context):启动日志落盘,创建 LogWriter 等写入器。
  • Log.stopSaving():停止日志落盘。
  • Log.getFaultLog(context):获取/导出故障日志。

日志目录约定为 context.cacheDir + '/appLog',即 LOG_DIR_NAME。把日志放在 cacheDir 下的好处是:日志被视为可清除的缓存数据,不占用用户可见的持久存储配额,也无需额外申请存储权限。

写入器与消息模型

  • LogWriter 继承 BaseWriter,负责把普通日志写入 logDir 下的文本文件(文件名形如日期/序号)。
  • FaultLogWriter 同样继承 BaseWriter,负责故障日志的写入与导出,供 Log.getFaultLog 使用。
  • LogMessage<T> 是队列消息模型,含 type: MessageType 等字段,用于写入器间传递日志内容。
classDiagram
    class BaseWriter {
        #logDir: string
        +write() void
        +close() void
    }
    class LogWriter {
        -logDir: string
    }
    class FaultLogWriter {
        -logDir: string
    }
    class LogMessage {
        +type: MessageType
    }
    class Log {
        +domain: number
        +startSaving(context) void
        +stopSaving() void
        +getFaultLog(context) void
        +d/i/w/e(TAG, msg) void
    }
    BaseWriter <|-- LogWriter
    BaseWriter <|-- FaultLogWriter
    LogWriter --> LogMessage
    FaultLogWriter --> LogMessage
    Log --> LogWriter
    Log --> FaultLogWriter

类关系依据:LogWriter.ets、FaultLogWriter.ets、LogMessage.ts

将普通日志与故障日志分离为两个写入器,使两者可以拥有不同的文件命名、保留策略与导出入口:普通日志面向日常调试(批量分享/删除),故障日志面向异常定位(一键导出)。BaseWriter 抽公共逻辑(目录管理、文件句柄、关闭等),各写入器只关注自己的格式与策略。

Core Flow

流程一:开启日志保存 → 浏览/分享日志

sequenceDiagram
    participant U as 用户
    participant AP as AboutPage
    participant PH as PreferencesHelper
    participant Log as Log (tool)
    participant LFP as LogFileListPage
    participant FS as fileIo / systemShare

    U->>AP: 连续点击 Logo 5 次(间隔≤1s)
    AP->>PH: putValue(LOG_SAVING_SWITCH, true)
    PH-->>AP: 持久化完成
    AP->>Log: startSaving(context)
    Log->>Log: 初始化 LogWriter 并落盘
    AP->>AP: openLogSaving = true,显示"日志文件"入口
    U->>AP: 点击"日志文件"
    AP->>LFP: router.pushUrl(pages/LogFileListPage, Single)
    LFP->>LFP: LOG_DIR = cacheDir + /appLog
    LFP->>FS: fileIo.listFile(dir, {suffix:['.txt']})
    FS-->>LFP: 文件列表 → SharedInfo[]
    U->>LFP: 勾选文件 → 点击分享
    LFP->>FS: ShareController.show(SharedData, BATCH)
    FS-->>U: 系统分享面板

流程二:关闭日志保存(事件联动)

flowchart TD
    Start(["LogFileListPage 中关闭 Toggle"]) --> A["PreferencesHelper.putValue(LOG_SAVING_SWITCH, false)"]
    A --> B["Log.stopSaving()"]
    B --> C["eventHub.emit(EVENT_ON_LOG_CLOSED)"]
    C --> D["AboutPage.onCloseLog: openLogSaving = false"]
    D --> E["router.back() 返回上一页"]
    E --> F["日志入口行隐藏"]

该流程展示了 eventHub 作为"事件总线"在页面间的解耦作用:LogFileListPage 无需持有 AboutPage 的引用,只需广播 SettingsVM.EVENT_ON_LOG_CLOSED,所有注册了该事件的页面(如 AboutPage)都会同步关闭日志入口,保证 UI 状态与持久化状态一致。

网页浏览页面(WebPage)

WebPage 是辅助页面的通用 Web 容器,由 AboutPage.browseWeb 通过 router.pushUrl 携带 params: { url, title } 进入。它接收参数后展示 TopBarView 标题栏(返回键 + 页面标题),并用 HarmonyOS Web 组件加载对应 URL。

用途场景:

  • 用户协议:AgreementUrl.USER_AGREEMENT
  • 隐私政策:AgreementUrl.PRIVACY_POLICY
  • ICP 备案查询:CompanyInfo.ICP_URL

URL 常量集中定义在 common/Constant.ets 中,页面代码不直接写死地址,便于按渠道/版本替换合规文档链接。

Usage Examples

示例 1:读取持久化开关并联动 UI(AboutPage)

aboutToAppear() {
  this.icpInfo = getContext(this).resourceManager.getStringSync($r('app.string.icp_filing_info'))
  PreferencesHelper.getInstance().getValue(PreferenceKey.LOG_SAVING_SWITCH).then((value) => {
    Log.w(TAG, `Open log saving=${value}`)
    if (typeof value === 'boolean') {
      this.openLogSaving = value
    }
  })

  bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION).then((bundleInfo) => {
    this.version = getContext(this).resourceManager.getStringSync($r('app.string.app_version'))
      + `: V${bundleInfo.versionName}_${bundleInfo.versionCode}`
  })

  getContext(this).eventHub.on(SettingsVM.EVENT_ON_LOG_CLOSED, this.onCloseLog)
}

Source: AboutPage.ets

示例 2:带筛选的文件列表加载(LogFileListPage)

async function tryToGetFileList(dirPath: string): Promise<SharedInfo[]> {
  const list = await fileIo.listFile(dirPath, {
    filter: {
      suffix: ['.txt']
    }
  });

  return await new Promise<SharedInfo[]>((resolve: Function) => {
    const fileList: Array<SharedInfo> = []
    for (let filename of list) {
      // 逐个 stat 组装 SharedInfo(filename/path/utd/uri)
    }
    resolve(fileList)
  });
}

Source: LogFileListPage.ets

示例 3:滑动删除单个日志文件

@Builder
SwipeItemAction(index: number) {
  Button($r('app.string.delete'), { type: ButtonType.Normal })
    .backgroundColor(Color.Red)
    .onClick(() => {
      let path: string = this.fileList[index].path
      Log.i(TAG, 'path=' + path)
      this.fileList.splice(index, 1);

      fileIo.unlink(path).then(() => {
        Log.i(TAG, "remove file succeed");
      }).catch((err: BusinessError) => {
        Log.e(TAG, "remove file failed with error message: " + err.message + ", error code: " + err.code);
      });
    })
}

Source: LogFileListPage.ets

Configuration Options

辅助页面体系的可配置项集中在 common/Constant.ets 与 common/PreferencesHelper.ets:

配置项类型默认值/位置说明
LOG_DIR_NAMEstring'/appLog'日志目录名,拼接在 cacheDir 之后(tool/log/Log.ts)
PreferenceKey.LOG_SAVING_SWITCHPreferenceKey持久化键日志保存开关,boolean 值
SettingsVM.EVENT_ON_LOG_CLOSEDstring事件名常量日志关闭广播事件
AgreementUrl.USER_AGREEMENTstring常量用户协议 URL
AgreementUrl.PRIVACY_POLICYstring常量隐私政策 URL
CompanyInfo.ICP_RECORDstring常量ICP 备案号文本
CompanyInfo.ICP_URLstring常量工信部备案查询 URL
CompanyInfo.COPYRIGHTstring常量版权声明文本
连点阈值number5 次 / 1000msAboutPage 隐藏开关的触发条件(代码内常量)
分享模式enumDETAIL / BATCHSharePreviewMode / SelectionMode

API Reference

Log(tool/log/Log.ts,静态门面)

方法签名说明
d/i/w/estatic d/i/w/e(tag: string, message: string): void按级别输出日志到 hilog
startSavingstatic startSaving(context: Context): void启动日志落盘(创建 LogWriter)
stopSavingstatic stopSaving(): void停止日志落盘
getFaultLogstatic getFaultLog(context: Context): void获取/导出故障日志
常量 LOG_DIR_NAMEstring = '/appLog'日志目录名

PreferencesHelper(common/PreferencesHelper.ets)

方法签名说明
getInstancestatic getInstance(): PreferencesHelper单例获取
getValuegetValue(key: PreferenceKey): Promise<Object>异步读取键值
putValueputValue(key: PreferenceKey, value: Object): Promise<void>异步写入键值

AboutPage(pages/AboutPage.ets)

方法签名说明
browseWebprivate browseWeb(title?: string, url?: string): void携带参数跳转 WebPage
clickToOpenSavingLogprivate clickToOpenSavingLog(): void连点检测,≥5 次开启日志保存

LogFileListPage(pages/LogFileListPage.ets)

方法签名说明
refreshFileLitprivate refreshFileLit(): void重新加载日志文件列表(注意:方法名中的拼写 Lit 为源码原样)
tryToSharetryToShare(): void分享勾选日志
tryToCleartryToClear(): void清空日志目录并重建
SwipeItemAction@Builder SwipeItemAction(index: number)列表项左滑删除按钮

路由与事件

  • router.pushUrl({ url, params }, mode, callback):页面入栈;RouterMode.Single 防重复入栈,RouterMode.Standard 允许多实例。
  • router.back():返回上一页。
  • eventHub.on/off/emit(eventName, ...args):页面级事件总线,用于 EVENT_ON_LOG_CLOSED 联动。

Failure Modes, Edge Cases & Concurrency

以下故障与边界行为均可在源码中直接观察到:

  1. 日志目录不存在:LogFileListPage 首次进入或清空后,fileIo.listFile 可能抛错。refreshFileLit 通过 .catch((e: BusinessError)) 捕获并仅记录日志,UI 停留在空列表而非崩溃;tryToClear 删除目录后通过 stopSaving + startSaving 重建目录。
  2. 单文件删除失败:fileIo.unlink 失败时(文件被占用、权限异常),UI 已先 splice 移除列表项,磁盘文件仍在;下次刷新会重新出现。源码未做回滚,属于已知取舍——删除失败仅记 error 日志。
  3. 未选择文件直接点分享:tryToShare 检查 list.length <= 0 并提前 return,避免构造空 SharedData。
  4. 分享面板关闭的并发勾选状态:dismiss 回调中连续两次赋值 isSelected = true / false,强制触发 UI 刷新复位全选框状态,规避框架状态未刷新问题。
  5. 连点窗口的并发:clickToOpenSavingLog 依赖 lastClickTimeMs 与 clicks,若两次点击间隔 > 1000ms 则重置计数,防止"长时间内凑满 5 次"误开;状态写入 Preferences 为异步,连续触发存在幂等风险,但 openLogSaving 为 true 后 Logo 的 onClick 不再调用该方法,天然防重复。
  6. 事件监听泄漏:aboutToAppear 注册 eventHub.on,aboutToDisappear 必须 off,否则页面销毁后仍会收到事件回调。
  7. 日志文件无限增长:源码未显示日志滚动/大小上限策略,长跑设备可能产生大量 .txt 文件;依赖用户手动清空或系统清理缓存。

Performance & Operational Considerations

  • 日志落盘是高频路径:Log 门面将日志经 LogWriter/FaultLogWriter 异步写入(LogMessage 队列模型),避免阻塞 UI 线程;但每行日志均落盘会放大 IO,调试完成后建议关闭 LOG_SAVING_SWITCH。
  • 文件列表加载:fileIo.listFile + 逐文件 stat 组装列表,当日志文件数量很大时耗时线性增长,页面提供手动刷新入口(ic_refresh)而非自动轮询。
  • 目录重建:tryToClear 中 rmdir → startSaving 重建,属于写密集型操作,期间日志可能短暂丢失(源码未做缓冲),可接受。
  • cacheDir 语义:日志放在 cacheDir,系统可在存储紧张时清理;正式发布版不应把用户数据放 cacheDir,但作为调试日志这是合理权衡。

Extension Points

  1. 新增辅助入口:在 AboutPage.items 数组追加 ItemView<Resource, string> 即可新增一个跳转 WebPage 的列表项;如需跳转自定义页面,仿照"日志文件"行的 router.pushUrl 即可。
  2. 自定义写入器:继承 BaseWriter 新增写入器(如按天滚动、加密日志),再接入 Log 门面即可扩展日志策略。
  3. 事件联动:复用 SettingsVM.EVENT_ON_LOG_CLOSED 事件名,在任意页面注册 eventHub.on,即可在日志保存关闭时同步业务状态。
  4. 分享目标扩展:SharedInfo.utd 目前为 PLAIN_TEXT,可改为其他 UniformDataType 以适配不同接收方。

Tests

仓库在 entry/src/ohosTest/ets/testability/pages/Index.ets 中存在 ohosTest 测试工程页面,表明应用具备测试ability基础;但本页聚焦的辅助页面(AboutPage/LogFileListPage/WebPage)未发现独立的单元测试文件,交互行为(连点开关、分享、删除)主要依赖手工验证。

Related Links

  • AboutPage.ets
  • LogFileListPage.ets
  • WebPage.ets
  • Log.ts
  • LogWriter.ets
  • FaultLogWriter.ets
  • LogMessage.ts
  • 主流程页面:MainPage(设备列表/升级)、LaunchPage(启动初始化)
  • 蓝牙与 OTA 能力:BluetoothManager / BluetoothOTAManager(见对应目录页)
Prev
主界面与设备连接交互