在这里插入图片描述

一、我们要做什么

1.1 调试与监控需求

前端应用运行在用户设备上,一旦出现白屏、闪退或功能异常,开发者无法直接查看控制台。一套完整的调试与日志体系需要覆盖从开发期到线上全链路。

场景 需求 预期效果
开发调试 查看 console 输出、网络请求、DOM 结构 移动端实时查看日志
错误定位 捕获 JS 异常、Promise 未处理拒绝 聚合展示错误栈
线上监控 采集用户端报错、性能数据 告警 + 错误看板
用户行为 点击事件、页面浏览、操作漏斗 行为回溯分析
鸿蒙适配 在鸿蒙设备上完成调试、日志拉取 打通鸿蒙调试链路

1.2 核心挑战

跨端一致性:Uniapp 编译到 iOS、Android、鸿蒙三端,日志工具需兼容各运行时差异。vConsole 在 H5 和 App WebView 中正常工作,但在鸿蒙 ACE 容器中需额外配置。

鸿蒙桥接:鸿蒙不兼容 Chrome DevTools 原生调试协议。需通过 hdc(HarmonyOS Device Connector)命令或 DevEco Studio 进行远程日志采集,工程复杂度高于 Android ADB。

错误栈混淆:生产包经过 Uniapp 和 Vite 双重编译打平,再经鸿蒙打包工具处理,原始错误栈的行号列号无法直接定位源码,必须借助 Sourcemap 还原。

性能损耗:每条日志立即写入 localStorage 会造成频繁 IO 阻塞 UI 线程。日志采集与上报必须异步化,不能阻塞主 UI 线程,特别是在低端鸿蒙设备上。

数据安全:日志中可能包含用户 token、手机号等隐私信息,上报前需脱敏过滤。

1.3 整体架构

[应用层] → 日志封装层 → [打印] → vConsole / 系统 logcat/hilog
                       → [缓存] → 内存环状队列 → 定时刷入存储
                       → [上报] → Sentry / 自建后端

链路上分三层:采集层统一 LogEntry 结构,控制层按级别/标签过滤,输出层分路到控制台、存储和上报。

二、数据模型设计

2.1 日志条目类型定义

// types/log.ts —— 日志条目与鸿蒙扩展字段

type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';

interface LogEntry {
  id: string;
  timestamp: number;            // Unix ms
  level: LogLevel;
  tag: string;                  // 模块标签,如 'payment' | 'auth' | 'network'
  message: string;
  data?: Record<string, unknown>;
  stack?: string;               // 错误栈字符串
  source?: 'vue' | 'js' | 'native' | 'network';
  extra: {
    platform: 'ios' | 'android' | 'harmony';
    appVersion: string;
    deviceModel?: string;
    route?: string;              // 当前页面路径
    userId?: string;
    networkType?: string;        // wifi | 4g | 5g | none
    // 鸿蒙扩展字段
    harmonyVersion?: string;     // 如 "5.0.0"
    deviceType?: string;         // matePad / matePhone
    hdcChannel?: string;        // USB / Wi-Fi
  };
}

2.2 上报配置类型

// types/config.ts

interface ReportConfig {
  sentryDsn: string;
  sampleRate: number;            // 0~1 采样率
  maxBatchSize: number;          // 批量最大条数
  flushInterval: number;         // 自动上报间隔(ms)
  enableVConsole: boolean;
  harmonyHdcPort: number;        // 鸿蒙 hdc 转发端口
  filters: {
    urlDenyList: string[];       // 不上报的 URL
    levelThreshold: LogLevel;    // 低于此级别不缓存
    maxStackLength: number;      // 错误栈截断长度
  };
}

interface UploadPayload {
  appId: string;
  sessionId: string;
  logs: LogEntry[];
  timestamp: number;
}

2.3 日志 ID 与链路追踪

每条日志通过 ${Date.now()}-${random(6)} 生成唯一 ID。Session 粒度的追踪在应用启动时生成 sessionId,存入 Storage,保持到应用关闭。这样同一会话内所有日志可关联排序,方便回溯用户在崩溃前的完整操作路径。

三、核心设计决策

3.1 方案对比表

功能维度 方案 A 方案 B 方案 C(选中)
前端调试面板 Chrome DevTools(仅 H5) 自建控制台 vConsole + 鸿蒙 DevEco Web Inspector
日志持久化 每条写入 localStorage IndexedDB 大容量 内存队列 + 定时批量刷入
错误监控 自建日志接收+聚合 友盟 U-APM Sentry + 自定义增强上报
埋点采集 自研完整 SDK 第三方(友盟/GrowingIO) 轻量自建埋点 + 复用日志通道
鸿蒙调试 仅 hdc shell logcat 仅 DevEco Studio hdc fport + DevEco Studio 组合

3.2 选型理由

vConsole vs Chrome DevTools:Chrome DevTools 仅适用于 H5 和 App 的 WebView 调试场景。鸿蒙原生 WebView(ACE)不暴露标准 DevTools 端口。vConsole 可内嵌到任意 WebView 渲染页面中,零配置即可在 App 内查看 console、Network、Element 面板。配合鸿蒙 DevEco Studio 的 Web Inspector 功能,开发期用 DevEco,测试期用 vConsole,两头覆盖。

Sentry 作为错误监控:Sentry 提供了完整的错误分组、告警规则、版本对比和用户反馈闭环。自建方案需要维护日志接收、聚合、可视化、告警四条链路,中小团队不划算。在 Uniapp 中通过 @sentry/vue@sentry/integrations 可直接接入。鸿蒙端通过 @sentry/harmony 插件覆盖原生 Crash。

内存队列 + 定时刷入:每条日志立即写入 localStorage 会造成频繁 IO 阻塞 UI 线程,尤其是在低端鸿蒙设备上 IO 性能更差。采用环形缓冲区(Ring Buffer)方案:日志先 push 到内存数组(O(1) 操作),每隔 5 秒或达到 50 条时批量写入存储并触发上报。读操作(批量序列化)从每毫秒一次降为每 5 秒一次,性能提升数个量级。

3.3 鸿蒙特有设计考量

维度 设计
hdc 端口转发 通过 hdc fport tcp:9222 localabstract:webview_devtools_remote_<uid> 映射调试通道
Web Inspector 鸿蒙 3.1+ 支持 Web Inspector 协议,通过 DevEco Studio 或 Chrome inspect 查看
日志过滤 hdc shell hilog -T <tag> -D -r 可过滤指定 tag 的 hilog 输出
Native Crash 通过 Sentry Harmony SDK 捕获 C++/ArkTS 层崩溃,与 JS 错误统一展示

3.4 错误采样与降级策略

生产环境全量采集错误会导致后端压力过大。采用分层采样:error/fatal 级别全量上报,warn 级别按 20% 采样,info/debug 从不生产上报。网络请求失败时日志暂存内存,下一周期重试,避免因上报失败引发二次异常。

四、完整代码实现

4.1 日志封装核心类

// utils/logger.ts —— 日志封装核心,支持分级过滤与批量上报

type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';

interface LogEntry {
  id: string; timestamp: number; level: LogLevel;
  tag: string; message: string; data?: Record<string, unknown>;
  stack?: string; extra: { platform: string; appVersion: string; route?: string };
}
type ReportHandler = (entry: LogEntry) => void;

class Logger {
  private buffer: LogEntry[] = [];
  private levelOrder: LogLevel[] = ['debug', 'info', 'warn', 'error', 'fatal'];
  private reporters: ReportHandler[] = [];

  constructor(private config: { level: LogLevel; sampleRate: number }) {
    setInterval(() => this.flush(), 5000);
  }

  private shouldLog(level: LogLevel, tag: string): boolean {
    if (tag.startsWith('_skip_')) return false;
    return this.levelOrder.indexOf(level) >= this.levelOrder.indexOf(this.config.level);
  }

  log(level: LogLevel, tag: string, message: string, data?: unknown) {
    if (!this.shouldLog(level, tag)) return;
    if (Math.random() > this.config.sampleRate) return;
    const entry: LogEntry = {
      id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      timestamp: Date.now(),
      level, tag, message,
      data: data as Record<string, unknown>,
      stack: level === 'error' || level === 'fatal' ? new Error().stack : undefined,
      extra: { platform: this.getPlatform(), appVersion: __APP_VERSION__ },
    };
    this.buffer.push(entry);
    this.printConsole(entry);
    if (this.buffer.length >= 50) this.flush();
  }

  addReporter(r: ReportHandler) { this.reporters.push(r); }

  private flush() {
    if (this.buffer.length === 0) return;
    const batch = this.buffer.splice(0, this.buffer.length);
    uni.setStorage({ key: 'log_batch', data: JSON.stringify(batch.slice(-200)) });
    this.reporters.forEach(r => batch.forEach(e => r(e)));
  }

  private printConsole(e: LogEntry) {
    const p = `[${e.level.toUpperCase()}] [${e.tag}]`;
    if (e.level === 'error' || e.level === 'fatal')
      console.error(p, e.message, e.data, e.stack);
    else if (e.level === 'warn') console.warn(p, e.message, e.data);
    else console.log(p, e.message, e.data);
  }

  private getPlatform(): string {
    // #ifdef H5
    return 'h5';
    // #endif
    // #ifdef APP-PLUS
    const info = uni.getSystemInfoSync();
    return info.platform;
    // #endif
    return 'unknown';
  }
}

export const logger = new Logger({ level: 'debug', sampleRate: 1.0 });

4.2 全局错误捕获

// utils/error-capture.ts —— 捕获 Vue 错误、Promise 拒绝与全局 JS 异常

import { logger } from './logger';

export function setupErrorCapture() {
  // Vue 渲染错误
  const app = getApp();
  if (app && 'config' in app) {
    app.config.errorHandler = (err: unknown, _vm: unknown, info: string) => {
      logger.log('error', 'vue', `Vue error: ${info}`, {
        message: err instanceof Error ? err.message : String(err),
        stack: err instanceof Error ? err.stack : undefined,
      });
      // 阻止传播避免白屏
      return false;
    };
  }

  // Promise 拒绝
  uni.onUnhandledRejection?.(({ reason }: { reason: unknown }) => {
    logger.log('fatal', 'promise', 'Unhandled rejection', {
      reason: reason instanceof Error ? reason.message : String(reason),
      stack: reason instanceof Error ? reason.stack : undefined,
    });
  });

  // 全局 JS 异常(H5 环境)
  if (typeof window !== 'undefined') {
    window.onerror = (message: unknown, source, lineno, colno, error) => {
      logger.log('fatal', 'global', String(message), {
        source, lineno, colno, stack: error?.stack,
      });
      return true;
    };
  }
}

4.3 Sentry 上报集成

// utils/sentry.ts —— Sentry 初始化与日志转 Sentry 事件

import * as Sentry from '@sentry/vue';
import { BrowserTracing } from '@sentry/tracing';
import type { LogEntry } from './logger';

export function initSentry(app: VueApp) {
  Sentry.init({
    app,
    dsn: 'https://your-dsn@sentry.io/project',
    environment: process.env.NODE_ENV ?? 'production',
    release: __APP_VERSION__,
    integrations: [new BrowserTracing()],
    tracesSampleRate: 0.2,
    beforeSend(event) {
      // 脱敏:过滤 token
      const auth = event.request?.headers?.authorization;
      if (auth) event.request!.headers!.authorization = '***';
      const url = event.request?.url;
      if (url) event.request!.url = url.replace(/token=[^&]+/, 'token=***');
      return event;
    },
  });
}

export function makeSentryReporter(): (entry: LogEntry) => void {
  return (entry: LogEntry) => {
    if (entry.level !== 'error' && entry.level !== 'fatal') return;
    Sentry.withScope(scope => {
      scope.setTag('module', entry.tag);
      scope.setExtra('data', entry.data);
      scope.setExtra('route', entry.extra.route);
      scope.setExtra('platform', entry.extra.platform);
      Sentry.captureMessage(entry.message, entry.level === 'fatal' ? 'fatal' : 'error');
    });
  };
}

4.4 埋点采集

// utils/tracker.ts —— 轻量埋点,复用日志上报道

interface TrackEvent {
  event: string;
  properties?: Record<string, string | number | boolean>;
  timestamp: number;
}

class Tracker {
  private queue: TrackEvent[] = [];

  constructor(private endpoint: string, private flushInterval = 10000) {
    setInterval(() => this.flush(), this.flushInterval);
  }

  track(event: string, properties?: TrackEvent['properties']) {
    this.queue.push({ event, properties, timestamp: Date.now() });
    if (this.queue.length >= 20) this.flush();
  }

  private async flush() {
    if (this.queue.length === 0) return;
    const batch = this.queue.splice(0, this.queue.length);
    try {
      await uni.request({
        url: this.endpoint,
        method: 'POST',
        data: { events: batch, sessionId: uni.getStorageSync('session_id') },
      });
    } catch {
      // 上报失败不阻塞,下一周期重试
    }
  }
}

export const tracker = new Tracker('https://tracking.example.com/collect');

4.5 鸿蒙 hdc 调试命令

# 1. 确认 hdc 工具可用(鸿蒙 SDK / DevEco Studio 内置)
hdc --version

# 2. 列出已连接的鸿蒙设备
hdc list targets
# 输出示例: 4d8f1c2e  device  MatePadPro

# 3. 转发 Web Inspector 端口到本地
hdc fport tcp:9222 localabstract:webview_devtools_remote_12345
# 验证: hdc fport list

# 4. 实时过滤应用日志(-T 指定 tag, -D 降序, -r 实时)
hdc shell hilog -T com.atomgit.app -D -r

# 5. 拉取设备端崩溃日志文件
hdc shell "find /data/log -name '*crash*' -maxdepth 3" | head -10

# 6. 生成并拉取线程 dump
hdc shell "kill -3 $(pidof com.atomgit.app)"
hdc file recv /data/log/dump-20260415.hprof ./dumps/

五、深度技术原理

5.1 Sourcemap 映射原理

Uniapp 项目经过 Vite/Webpack 编译后,TypeScript 源码被转译为压缩混淆后的 JavaScript。每一处错误栈中的 file:line:column 指向的是混淆后的位置而非源码。

Sourcemap 文件的核心结构如下:

{
  "version": 3,
  "file": "app.js",
  "sources": ["pages/index/index.ts", "utils/logger.ts"],
  "mappings": "AAAA,SAASA,EAAUC,EAASC,CAATC...",
  "names": ["Logger", "log", "level"]
}

mappings 字段使用 VLQ(Variable-Length Quantity)编码,每个分段代表原始位置到生成位置的对应关系。解码过程如下:

  1. Sourcemap 解码器逐行解析 mappings 字符串
  2. 每组分号分割行,逗号分割该行的片段
  3. 每个 VLQ 片段解码为 [生成列, 源文件索引, 原始行号, 原始列号, 符号名索引]
  4. 将混淆栈的 line:col 与解码后的映射匹配,反向查找原始位置

鸿蒙特殊性:鸿蒙 WebView 的错误栈格式与 Chrome 有差异。部分鸿蒙版本使用 ArkCompiler 的中间表示栈帧,格式为 at (libace.so:0x1234) 而非标准 JS 栈。解析时需先判断栈格式:如果是纯 JS 栈(如 vendor.js:1:25000),用标准 Sourcemap 还原;如果是 ArkCompiler 栈,需使用鸿蒙 SDK 提供的 deobfuscate 工具将地址偏移转换为源码行号。

// 简化的 Sourcemap 还原示例(Node 端工具)
import { SourceMapConsumer } from 'source-map';

async function resolveStackLine(line: string, mapUrl: string) {
  const raw = await fetch(mapUrl).then(r => r.json());
  const consumer = await new SourceMapConsumer(raw);
  const match = line.match(/:(\d+):(\d+)\)?$/);
  if (!match) return null;
  const pos = consumer.originalPositionFor({
    line: Number(match[1]),
    column: Number(match[2]),
  });
  consumer.destroy();
  return pos;
}

生产环境中将 .map 文件上传至 Sentry,由 Sentry 自动完成还原。Uniapp 项目在 vite.config.ts 中配置 build.sourcemap: 'hidden' 即可生成不携带 sourceMappingURL 的 map 文件,避免生产包暴露源码路径。

5.2 鸿蒙 hdc 调试通道

hdc(HarmonyOS Device Connector)是鸿蒙生态的设备连接调试工具,与 Android 的 ADB 类似但架构不同。其调试链路分三层:

传输层(USB / TCP):hdc 通过 USB 或 Wi-Fi 与设备建立 TCP 长连接。USB 连接时通过 USB 的 bulk 传输通道,Wi-Fi 连接时基于 TCP 8710 端口。

服务层(hdc daemon):设备端常驻守护进程 hdcd 接收来自 PC 端 hdc client 的命令请求。支持的命令类型包括:fport(端口转发)、shell(执行设备端命令)、file(文件传输)、app(安装/卸载应用)。

协议层:底层基于 CBOR 序列化协议的二进制帧通信,不同于 ADB 的文本协议。每个命令包包含 type(请求类型)、op(操作码)和数据载荷。

Uniapp 调试中的具体应用:当鸿蒙设备上的 WebView 加载 Uniapp 页面时,ACE 容器会为每个 WebView 实例暴露一个 webview_devtools_remote 抽象套接字。hdc 的 fport 命令将这个抽象套接字转发到 PC 的本地 TCP 端口,Chrome 或 DevEco Studio 即可通过该端口连接 WebView 的 V8 调试上下文,实现 Console、Sources、Network 面板的实时调试。

5.3 错误栈还原完整流程

用户设备端                        服务端
──────────────────           ──────────────────
1. 发生错误
2. 构建 LogEntry.stack     4. 接收上报日志
3. 上报至 Sentry ──────→   5. 根据 release 查找 .map
                            6. SourceMapConsumer 解码
                            7. 输出原始位置与符号名
                            8. 关联 git blame 定位责任人

关键点:每条 LogEntry 的 extra.appVersion 必须与 Sentry Release 版本号一致,否则 Server 端无法匹配正确的 Sourcemap 文件。Uniapp 项目通过 __APP_VERSION__ 编译常量注入版本号,Sentry 初始化时 release 字段应与之一致。

5.4 性能分析

操作 耗时 说明
日志 push 到数组 ~0.001ms O(1) 原地操作
批量序列化 50 条 ~5ms 仅每 5 秒或满 50 条触发
localStorage 写入 ~3ms 仅写入,不读取
Sentry 发送 ~100-500ms 异步 HTTP,不阻塞 UI
vConsole 渲染 ~1-10ms 仅每条日志触发一次 DOM 追加

六、常见问题解答

Q1: vConsole 在鸿蒙设备上显示空白怎么办?

检查 WebView 是否允许 JavaScript 执行。鸿蒙 ACE 容器默认开启 JS 执行,但不支持 localStorage 持久化 vConsole 配置。在 onLaunch 中手动初始化并关闭默认插件以减少性能开销:

import VConsole from 'vconsole';
// 鸿蒙设备仅启用 Log 和 Network 面板
const vConsole = new VConsole({ defaultPlugins: ['log', 'network'] });

确认 manifest.json 的鸿蒙平台配置中 pages 包含正确入口。同时在 DevEco Studio 中开启 Web Inspector:配置 → 调试器 → 启用 WebView 调试。

Q2: Sourcemap 还原后行号仍然不对?

可能原因有三:其一,Sourcemap 生成时开启了 cheap-source-map 而非 source-map,导致列号精度丢失;其二,生产包经过二次压缩(如 TerserPlugin)或鸿蒙打包工具重新混淆;其三,Hermes 引擎的错误栈有额外偏移。解决:使用 hidden-source-map 而非 cheap-source-map,并确保上报的 release 版本号与 map 文件 release 一致。鸿蒙 Hermes 场景需额外配置 Sentry 的 hermes 集成。

Q3: 日志上报频率太高影响 App 性能?

采用三层限流:第一层按日志级别过滤(生产仅上报 warn 及以上);第二层按采样率过滤(warn 20%,error/fatal 100%);第三层按时间窗口限流(同一 tag 同一错误 5 分钟内最多上报 10 次)。埋点默认每 10 秒聚合一次,单次最多 50 条。网络错误时降级为内存暂存,避免上报引发二次异常。

Q4: 如何在 DevEco Studio 中查看 Uniapp 的 console.log 输出?

鸿蒙真机调试三步走:第一步,在 DevEco Studio 中打开 Log 面板(View → Tool Windows → Log);第二步,在过滤条件中输入 com.atomgit.app 或进程 PID;第三步,在 Uniapp 代码中添加 --harmony-debug 编译参数确保 hilog 输出开启。注意 hilog 有标签长度限制(最大 31 字符),建议统一使用 atomgit 作为 tag 前缀。

Q5: 鸿蒙真机连接 hdc 失败?

排查步骤:1. 开启设备「设置 → 系统 → 开发者选项 → USB 调试」;2. hdc kill-server && hdc start-server 重启服务;3. 确认 USB 线支持数据传输(非仅充电线);4. hdc list targets 确认设备状态为 device;5. Wi-Fi 连接场景执行 hdc tmode port 8710;6. 检查 PC 端鸿蒙 SDK 环境变量 HDC_HOME 是否指向正确目录。

Q6: 页面白屏且无错误日志上报?

白屏通常发生在生命周期早期(App 启动阶段),此时日志系统可能尚未初始化。解决方案:将 setupErrorCapture()initSentry() 提升至 main.ts 中最顶部执行,在 createApp 之前完成。同时增加引导日志:在日志模块自身初始化后立即输出 [INFO] [system] Logger initialized,作为一条基线日志,如果这条日志都未出现,说明初始化过程已崩溃。

七、运行效果

7.1 日志打印效果(hilog 输出)

04-15 10:23:45.678 1234-5678 D atomgit  [INFO]  [auth] User logged in
04-15 10:23:48.123 1234-5678 D atomgit  [WARN]  [network] Request timeout
04-15 10:23:50.456 1234-5678 D atomgit  [ERROR] [payment] Payment failed
  at getPaymentStatus (vendor.js:1:45231)
  at handlePayResult (vendor.js:1:45300)
04-15 10:23:52.012 1234-5678 D atomgit  [FATAL] [vue] Cannot read properties of null
  at Proxy.render (pages/pay/pay.vue:120:15)

7.2 vConsole 实时面板

在这里插入图片描述

7.3 上报链路流程图

在这里插入图片描述

7.4 hdc 调试命令输出

$ hdc list targets
4d8f1c2e3b5a7f90  device      MatePadPro

$ hdc fport list
tcp:9222  →  localabstract:webview_devtools_remote_12345

$ hdc shell hilog -T com.atomgit.app -D -r
04-15 10:23:45.678  1234-5678  D  atomgit  [INFO]  [auth] User logged in
04-15 10:23:48.123  1234-5678  D  atomgit  [WARN]  [network] Request timeout
04-15 10:23:50.456  1234-5678  D  atomgit  [ERROR] [payment] Payment failed

7.5 Sentry 看板效果

每上报一条 error/fatal 级别日志,Sentry Issue 页面自动聚合相同错误栈的实例,显示首次发现时间、最近触发时间、影响用户数、触发次数。点击详情可查看每条日志的 extra.routeextra.platformextra.data 自定义上下文,配合 release 版本过滤快速定位回归版本。

八、扩展方向

8.1 性能监控(APM)

集成 Web Vitals 指标(FCP、LCP、TTFB、CLS),在 LogEntry 中增加 performance 类型,自动采集页面渲染性能数据。使用 PerformanceObserver API 监听关键指标,上报到同一条日志链路,在 Sentry 中与错误数据关联展示。

8.2 离线日志回捞

网络异常场景下日志缓存至 IndexedDB,应用下次打开时尝试补发。对日志做 lz-string 压缩减少存储空间(压缩率约 40-60%)。实现「日志回捞」功能:运维触发远程指令后,App 将本地历史日志打包上传,方便排查偶发问题。

8.3 智能告警聚合

基于日志级别、标签和出现频率做告警降噪。如相同错误 5 分钟内连续出现 100 次则升级为 P0 告警,否则归入日常 Issue。避免重复告警轰炸,支持按版本、设备型号、地域过滤告警范围。

8.4 用户会话回放(Session Replay)

通过 RRWeb 录制用户操作的 DOM 快照与 Mutation 事件,与日志时间线绑定。当 Sentry Issue 被查看时,可直接回放该用户在错误发生前 30 秒的完整操作,帮助复现低复现率问题。

8.5 鸿蒙原生崩溃捕获

使用 @sentry/harmony SDK 捕获 ArkTS/NAPI 层的 C++ Crash(包括 libace.so 中的 SIGSEGV),与 JS 层错误在同一个 Sentry 项目中聚合展示。鸿蒙原生崩溃栈的解析需使用 hdc shell "cat /data/log/crash-*" 拉取完整 log,再通过 Soupgrap(鸿蒙符号还原工具)解码为可读的函数名。

Logo

一站式 AI 云服务平台

更多推荐