HarmonyOS鸿蒙Next中如何收集应用的崩溃日志和异常信息?

HarmonyOS鸿蒙Next中如何收集应用的崩溃日志和异常信息?

  • 如何收集HarmonyOS应用的崩溃日志和异常信息?
  • 如何分析和定位线上问题?
3 回复

解决方案

1. 异常捕获基础

import errorManager from '@ohos.app.ability.errorManager'
import hilog from '@ohos.hilog'

// EntryAbility.ets
export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    // 注册全局异常监听
    this.registerErrorHandler()
  }

  private registerErrorHandler() {
    // 监听JS错误
    errorManager.on('error', (error) => {
      console.error('应用异常:', error.message)
      console.error('错误堆栈:', error.stack)

      // 收集错误信息
      this.collectErrorInfo({
        name: error.name,
        message: error.message,
        stack: error.stack || '',
        timestamp: Date.now()
      })
    })

    // 监听Promise rejection
    errorManager.on('unhandledRejection', (reason) => {
      console.error('未处理的Promise rejection:', reason)
      
      this.collectErrorInfo({
        name: 'UnhandledPromiseRejection',
        message: String(reason),
        stack: '',
        timestamp: Date.now()
      })
    })

    console.log('全局异常监听已注册')
  }

  private collectErrorInfo(error: ErrorInfo) {
    // 保存到本地或上传到服务器
    CrashReporter.report(error)
  }

  onDestroy() {
    // 取消监听
    errorManager.off('error')
    errorManager.off('unhandledRejection')
  }
}

interface ErrorInfo {
  name: string
  message: string
  stack: string
  timestamp: number
}

2. 日志管理

import hilog from '@ohos.hilog'

export enum LogLevel {
  DEBUG,
  INFO,
  WARN,
  ERROR
}

export class Logger {
  private static readonly DOMAIN = 0x0001  // 日志域
  private static readonly TAG = 'MyApp'    // 日志标签
  private static minLevel = LogLevel.DEBUG

  /**
   * 设置最小日志级别
   */
  static setMinLevel(level: LogLevel) {
    this.minLevel = level
  }

  /**
   * Debug日志
   */
  static debug(format: string, ...args: any[]) {
    if (this.minLevel <= LogLevel.DEBUG) {
      hilog.debug(this.DOMAIN, this.TAG, format, ...args)
    }
  }

  /**
   * Info日志
   */
  static info(format: string, ...args: any[]) {
    if (this.minLevel <= LogLevel.INFO) {
      hilog.info(this.DOMAIN, this.TAG, format, ...args)
    }
  }

  /**
   * Warning日志
   */
  static warn(format: string, ...args: any[]) {
    if (this.minLevel <= LogLevel.WARN) {
      hilog.warn(this.DOMAIN, this.TAG, format, ...args)
    }
  }

  /**
   * Error日志
   */
  static error(format: string, ...args: any[]) {
    if (this.minLevel <= LogLevel.ERROR) {
      hilog.error(this.DOMAIN, this.TAG, format, ...args)
    }
  }

  /**
   * 记录异常
   */
  static exception(error: Error, context?: string) {
    const message = context 
      ? `[${context}] ${error.message}\nStack: ${error.stack}`
      : `${error.message}\nStack: ${error.stack}`
    
    this.error(message)
  }
}

// 使用示例
@Entry
@Component
struct LoggerDemo {
  aboutToAppear() {
    Logger.info('页面加载')
    Logger.debug('调试信息: %{public}s', '详细数据')
  }

  build() {
    Column({ space: 16 }) {
      Button('记录Info日志')
        .width('100%')
        .onClick(() => {
          Logger.info('用户点击按钮')
        })

      Button('记录Warning')
        .width('100%')
        .onClick(() => {
          Logger.warn('这是一个警告')
        })

      Button('记录Error')
        .width('100%')
        .onClick(() => {
          Logger.error('发生错误')
        })

      Button('模拟异常')
        .width('100%')
        .onClick(() => {
          try {
            throw new Error('测试异常')
          } catch (error) {
            Logger.exception(error as Error, 'LoggerDemo')
          }
        })
    }
    .padding(16)
  }
}

3. 崩溃报告收集

import fs from '@ohos.file.fs'
import preferences from '@ohos.data.preferences'

interface CrashReport {
  id: string
  name: string
  message: string
  stack: string
  timestamp: number
  deviceInfo: DeviceInfo
  appInfo: AppInfo
}

interface DeviceInfo {
  brand: string
  model: string
  osVersion: string
}

interface AppInfo {
  version: string
  buildNumber: string
}

export class CrashReporter {
  private static readonly CRASH_DIR = 'crashes'
  private static readonly MAX_REPORTS = 10

  /**
   * 报告崩溃
   */
  static async report(error: ErrorInfo): Promise<void> {
    try {
      const crash: CrashReport = {
        id: this.generateId(),
        name: error.name,
        message: error.message,
        stack: error.stack,
        timestamp: error.timestamp,
        deviceInfo: await this.getDeviceInfo(),
        appInfo: this.getAppInfo()
      }

      // 保存到本地
      await this.saveCrashReport(crash)

      // 尝试上传
      await this.uploadCrashReport(crash)
    } catch (e) {
      console.error('保存崩溃报告失败:', e)
    }
  }

  /**
   * 保存崩溃报告到本地
   */
  private static async saveCrashReport(crash: CrashReport): Promise<void> {
    try {
      const filesDir = getContext().filesDir
      const crashDir = `${filesDir}/${this.CRASH_DIR}`

      // 确保目录存在
      if (!fs.accessSync(crashDir)) {
        fs.mkdirSync(crashDir)
      }

      // 保存报告
      const filePath = `${crashDir}/${crash.id}.json`
      const content = JSON.stringify(crash, null, 2)
      
      const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
      fs.writeSync(file.fd, content)
      fs.closeSync(file.fd)

      console.log('崩溃报告已保存:', filePath)

      // 清理旧报告
      await this.cleanupOldReports(crashDir)
    } catch (error) {
      console.error('保存崩溃报告失败:', error)
    }
  }

  /**
   * 上传崩溃报告
   */
  private static async uploadCrashReport(crash: CrashReport): Promise<void> {
    try {
      // 实现上传逻辑
      console.log('上传崩溃报告:', crash.id)
      
      // 示例: 调用API上传
      // await http.post('https://api.example.com/crashes', crash)
    } catch (error) {
      console.error('上传崩溃报告失败:', error)
    }
  }

  /**
   * 获取设备信息
   */
  private static async getDeviceInfo(): Promise<DeviceInfo> {
    return {
      brand: 'HarmonyOS',
      model: 'Unknown',
      osVersion: 'Unknown'
    }
  }

  /**
   * 获取应用信息
   */
  private static getAppInfo(): AppInfo {
    return {
      version: '1.0.0',
      buildNumber: '1'
    }
  }

  /**
   * 生成唯一ID
   */
  private static generateId(): string {
    return `crash_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
  }

  /**
   * 清理旧报告
   */
  private static async cleanupOldReports(crashDir: string): Promise<void> {
    try {
      const files = fs.listFileSync(crashDir)
      
      if (files.length > this.MAX_REPORTS) {
        // 按时间排序,删除最旧的
        const sortedFiles = files
          .map(file => ({
            name: file,
            path: `${crashDir}/${file}`,
            time: fs.statSync(`${crashDir}/${file}`).mtime
          }))
          .sort((a, b) => a.time - b.time)

        const toDelete = sortedFiles.slice(0, files.length - this.MAX_REPORTS)
        toDelete.forEach(file => {
          fs.unlinkSync(file.path)
          console.log('删除旧崩溃报告:', file.name)
        })
      }
    } catch (error) {
      console.error('清理旧报告失败:', error)
    }
  }

  /**
   * 获取所有崩溃报告
   */
  static async getAllReports(): Promise<CrashReport[]> {
    try {
      const filesDir = getContext().filesDir
      const crashDir = `${filesDir}/${this.CRASH_DIR}`

      if (!fs.accessSync(crashDir)) {
        return []
      }

      const files = fs.listFileSync(crashDir)
      const reports: CrashReport[] = []

      for (const file of files) {
        if (file.endsWith('.json')) {
          const filePath = `${crashDir}/${file}`
          const content = this.readFile(filePath)
          if (content) {
            reports.push(JSON.parse(content))
          }
        }
      }

      return reports.sort((a, b) => b.timestamp - a.timestamp)
    } catch (error) {
      console.error('获取崩溃报告失败:', error)
      return []
    }
  }

  private static readFile(filePath: string): string | null {
    try {
      const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY)
      const stat = fs.statSync(filePath)
      const buffer = new ArrayBuffer(stat.size)
      fs.readSync(file.fd, buffer)
      fs.closeSync(file.fd)
      return String.fromCharCode(...new Uint8Array(buffer))
    } catch (error) {
      console.error('读取文件失败:', error)
      return null
    }
  }
}

4. 崩溃报告查看器

@Entry
@Component
struct CrashReportViewer {
  @State reports: CrashReport[] = []
  @State isLoading: boolean = false
  @State selectedReport?: CrashReport

  async aboutToAppear() {
    await this.loadReports()
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('崩溃报告')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Button('刷新')
          .fontSize(14)
          .onClick(() => {
            this.loadReports()
          })
      }
      .width('100%')
      .padding(16)

      if (this.isLoading) {
        LoadingProgress()
          .width(40)
          .height(40)
          .margin({ top: 32 })
      } else if (this.reports.length === 0) {
        Column() {
          Text('暂无崩溃报告')
            .fontSize(16)
            .fontColor('#999999')
        }
        .width('100%')
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      } else {
        List() {
          ForEach(this.reports, (report: CrashReport) => {
            ListItem() {
              Column({ space: 8 }) {
                Row() {
                  Text(report.name)
                    .fontSize(16)
                    .fontWeight(FontWeight.Medium)
                    .layoutWeight(1)

                  Text(this.formatTime(report.timestamp))
                    .fontSize(12)
                    .fontColor('#999999')
                }
                .width('100%')

                Text(report.message)
                  .fontSize(14)
                  .fontColor('#666666')
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })

                Row({ space: 8 }) {
                  Text(`v${report.appInfo.version}`)
                    .fontSize(12)
                    .fontColor('#999999')

                  Text(report.deviceInfo.model)
                    .fontSize(12)
                    .fontColor('#999999')
                }
              }
              .width('100%')
              .padding(16)
              .onClick(() => {
                this.selectedReport = report
              })
            }
          })
        }
        .divider({ strokeWidth: 1, color: '#f0f0f0' })
        .layoutWeight(1)
      }

      // 崩溃详情弹窗
      if (this.selectedReport) {
        this.CrashDetailDialog()
      }
    }
  }

  @Builder
  CrashDetailDialog() {
    Column({ space: 16 }) {
      Row() {
        Text('崩溃详情')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Image($r('sys.media.ohos_ic_public_cancel'))
          .width(24)
          .height(24)
          .onClick(() => {
            this.selectedReport = undefined
          })
      }
      .width('100%')

      Scroll() {
        Column({ space: 12 }) {
          this.DetailItem('错误类型', this.selectedReport!.name)
          this.DetailItem('错误信息', this.selectedReport!.message)
          this.DetailItem('发生时间', this.formatTime(this.selectedReport!.timestamp))
          this.DetailItem('应用版本', `v${this.selectedReport!.appInfo.version}`)
          this.DetailItem('设备型号', this.selectedReport!.deviceInfo.model)

          Column({ space: 4 }) {
            Text('堆栈信息')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)

            Text(this.selectedReport!.stack)
              .fontSize(12)
              .fontColor('#666666')
              .fontFamily('monospace')
              .padding(12)
              .backgroundColor('#f5f5f5')
              .borderRadius(4)
              .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)
        }
      }
      .layoutWeight(1)

      Button('关闭')
        .width('100%')
        .onClick(() => {
          this.selectedReport = undefined
        })
    }
    .width('90%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({ radius: 8, color: 'rgba(0,0,0,0.2)' })
  }

  @Builder
  DetailItem(label: string, value: string) {
    Column({ space: 4 }) {
      Text(label)
        .fontSize(12)
        .fontColor('#999999')
      Text(value)
        .fontSize(14)
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }

  private async loadReports() {
    this.isLoading = true
    try {
      this.reports = await CrashReporter.getAllReports()
    } finally {
      this.isLoading = false
    }
  }

  private formatTime(timestamp: number): string {
    const date = new Date(timestamp)
    return date.toLocaleString('zh-CN')
  }
}

关键要点

  1. 全局监听: 使用errorManager监听未捕获异常
  2. 日志分级: DEBUG、INFO、WARN、ERROR不同级别
  3. 本地保存: 崩溃报告先保存到本地
  4. 异步上传: 网络可用时上传到服务器
  5. 隐私保护: 不记录用户敏感信息

最佳实践

  1. 及时上报: 应用启动时检查并上传崩溃报告
  2. 限制数量: 本地只保留最近的报告
  3. 详细信息: 包含设备信息、应用版本等
  4. 堆栈符号化: 服务端符号化堆栈信息
  5. 分析工具: 使用专业的崩溃分析平台

更多关于HarmonyOS鸿蒙Next中如何收集应用的崩溃日志和异常信息?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,收集应用崩溃日志和异常信息主要通过以下方式:

  1. HiLog日志系统:使用HiLog API记录应用运行日志,支持分类、分级存储。

  2. 应用事件日志:通过@ohos.hiviewdfx.hiappevent模块记录应用关键事件和异常信息。

  3. 崩溃信息收集:系统会自动捕获应用崩溃,相关信息存储在/data/log/hilog/目录下。

  4. 开发工具获取:使用DevEco Studio的日志查看器或hdc命令(hdc shell hilog)实时抓取日志。

需在应用配置文件中声明ohos.permission.READ_LOGS权限。

在HarmonyOS Next中,收集应用崩溃日志和异常信息主要依赖于ArkTS/ArkUI框架提供的错误处理机制以及系统日志工具。以下是关键方法:

  1. 全局异常捕获

    • 使用errorManager API注册全局错误监听器,可捕获JS/ArkTS层的未处理异常。
    • 示例:
      import errorManager from '@ohos.app.ability.errorManager';
      errorManager.on('error', (err) => {
        // 将err对象信息(堆栈、原因等)上报至服务器
      });
      
  2. 进程崩溃监控

    • 通过appManager监听应用进程状态变化,若进程异常退出,可结合系统日志分析。
    • 需在app.ets中配置并订阅processExit事件。
  3. 系统日志抓取

    • 使用hilog模块输出应用日志,崩溃时关键信息可通过hilog.error()记录。
    • 通过hdc shell hilog命令导出设备日志,筛选应用PID/Tag进行过滤。
  4. 性能看板与DevEco工具

    • DevEco Studio内置性能分析器,可实时查看异常日志。
    • 发布后可通过AGC(AppGallery Connect)查看崩溃统计,但需集成AGC SDK并开启“崩溃服务”。

分析与定位线上问题

  • 收集的日志应包含设备信息、OS版本、异常堆栈、操作路径等上下文。
  • 使用符号表(Debug版本保留)解析混淆后的堆栈,定位到具体代码行。
  • 结合用户操作序列复现问题,利用DevEco调试器或本地日志回放验证修复。

注意:需在隐私政策中声明日志收集目的,并确保用户知情同意。

回到顶部