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')
}
}
关键要点
- 全局监听: 使用errorManager监听未捕获异常
- 日志分级: DEBUG、INFO、WARN、ERROR不同级别
- 本地保存: 崩溃报告先保存到本地
- 异步上传: 网络可用时上传到服务器
- 隐私保护: 不记录用户敏感信息
最佳实践
- 及时上报: 应用启动时检查并上传崩溃报告
- 限制数量: 本地只保留最近的报告
- 详细信息: 包含设备信息、应用版本等
- 堆栈符号化: 服务端符号化堆栈信息
- 分析工具: 使用专业的崩溃分析平台
更多关于HarmonyOS鸿蒙Next中如何收集应用的崩溃日志和异常信息?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
在HarmonyOS Next中,收集应用崩溃日志和异常信息主要通过以下方式:
-
HiLog日志系统:使用HiLog API记录应用运行日志,支持分类、分级存储。
-
应用事件日志:通过
@ohos.hiviewdfx.hiappevent模块记录应用关键事件和异常信息。 -
崩溃信息收集:系统会自动捕获应用崩溃,相关信息存储在
/data/log/hilog/目录下。 -
开发工具获取:使用DevEco Studio的日志查看器或hdc命令(
hdc shell hilog)实时抓取日志。
需在应用配置文件中声明ohos.permission.READ_LOGS权限。
在HarmonyOS Next中,收集应用崩溃日志和异常信息主要依赖于ArkTS/ArkUI框架提供的错误处理机制以及系统日志工具。以下是关键方法:
-
全局异常捕获:
- 使用
errorManagerAPI注册全局错误监听器,可捕获JS/ArkTS层的未处理异常。 - 示例:
import errorManager from '@ohos.app.ability.errorManager'; errorManager.on('error', (err) => { // 将err对象信息(堆栈、原因等)上报至服务器 });
- 使用
-
进程崩溃监控:
- 通过
appManager监听应用进程状态变化,若进程异常退出,可结合系统日志分析。 - 需在
app.ets中配置并订阅processExit事件。
- 通过
-
系统日志抓取:
- 使用
hilog模块输出应用日志,崩溃时关键信息可通过hilog.error()记录。 - 通过
hdc shell hilog命令导出设备日志,筛选应用PID/Tag进行过滤。
- 使用
-
性能看板与DevEco工具:
- DevEco Studio内置性能分析器,可实时查看异常日志。
- 发布后可通过AGC(AppGallery Connect)查看崩溃统计,但需集成AGC SDK并开启“崩溃服务”。
分析与定位线上问题:
- 收集的日志应包含设备信息、OS版本、异常堆栈、操作路径等上下文。
- 使用符号表(Debug版本保留)解析混淆后的堆栈,定位到具体代码行。
- 结合用户操作序列复现问题,利用DevEco调试器或本地日志回放验证修复。
注意:需在隐私政策中声明日志收集目的,并确保用户知情同意。

