头图

背景

在 HarmonyOS NEXT 中,日志打印一般有两种,HiLog 库和 console。

  • console.log 主要用于应用开发和调试阶段。它提供了一种快速简便的方式来输出信息,适用于简单的调试和测试。
  • Hilog 是HarmonyOS提供的日志系统,适用于更复杂的应用场景,尤其是需要对日志进行详细分类和管理的情况。hilog允许开发者自定义日志的业务领域、TAG和级别。

在我们的鸿蒙应用框架中,为了很好的管理日志系统,使用了HiLog 库来记录日志,便于后续的日志分析与管理工作。

Hilog 日志被截断问题处理

作为 HarmonyOS NEXT 的基础模块,HiLog 库对打印的日志长度有限制:日志打印最多打印4096字节,超出限制文本将被截断。

但对于我们的框架来说,4096字节的长度限制将不利于我们做后续的应用日志分析和维护。

因此,我们可以采用分段打印的方法来确保日志内容不会被截断。

import { hilog } from '@kit.PerformanceAnalysisKit';

class LogUtil {
  private static instance: LogUtil;
  private static DOMAIN: number = 0x0000;

  private constructor() {
      // 私有构造函数,防止外部实例化
  }

  public static getInstance(): LogUtil {
      if (!LogUtil.instance) {
          LogUtil.instance = new LogUtil();
      }
      return LogUtil.instance;
  }

  public print(logTag: string, content: string, printFunc: (logTag: string, content: string)=>void) {
      const maxSize = 1024;
      if (content.length <= maxSize) {
          // 长度小于等于限制直接打印
          printFunc(LogUtil.DOMAIN, logTag, content);
      } else {
          while (content.length > maxSize) {
              // 循环分段打印
              let logContent = content.substring(0, maxSize);
              content = content.replace(logContent, '');
              printFunc(LogUtil.DOMAIN, logTag, logContent);
          }
          // 打印剩余日志
          printFunc(LogUtil.DOMAIN, logTag, content);
      }
  }
  public debug(tag:string, content:string) {
    this.print(tag, content, hilog.debug)
  }

  public info(tag:string, content:string) {
    this.print( tag, content, hilog.info)
  }

  public warn(tag:string, content:string) {
    this.print( tag, content, hilog.warn)
  }

  public error(tag:string, content:string) {
    this.print(tag, content, hilog.error)
  }

  public fatal(tag:string, content:string) {
    this.print(tag, content, hilog.fatal)
  }
}

由于一个英文字符一个字节,一个汉字两个字节,一些生僻字和emoji一个占至少3个字节,所以这里使用了 1024 作为分段临界值

Hilog 日志打印崩溃

按照上述分段打印日志处理之后,可以用来打印长字符串了,但当打印超长字符串时,将会出现应用卡死导致应用崩溃问题。目前验证,当 while循环 1000 次左右会出现因为长时间占用应用主线程,导致应用卡死现象,从而崩溃。

造成卡死崩溃的原因也很简单

  1. 鸿蒙应用的watchdog线程会定期向主线程插入判活检测
  2. 如果在3秒内,这些判活检测没有被主线程执行,系统会首先上报 THREAD\_BLOCK\_3S 警告事件
  3. 如果超过6秒,这些判活检测仍然没有被执行,那么系统将上报 THREAD\_BLOCK\_6S 主线程卡死事件

上述分段处理中的while循环 1000 次左右,导致主线程长时间卡住,系统认定为应用无响应(AppFreeze)。这种情况下,系统为了保护设备和其他应用不被影响,可能会强制关闭该应用,从而导致崩溃。

Hilog 崩溃问题处理方法

若对日志系统没有管理和维护的需求,最简单的方法就是切换为 console,但如果对日志系统有管理和维护的需求(一般应用都会有个日志维护系统),那么此时只能解决崩溃问题

1. 优化长字符串,对超长字符串进行截断

此种方式和分段处理相似,若对超长字符串日志内容没有要求,可以通过简单的限制 while 循环的次数即可实现,如限制最多循环 500 次

public print(logTag: string, content: string, printFunc: (logTag: string, content: string)=>void) {
  const maxSize = 1024;
  const maxCount = 500;
  if (content.length <= maxSize) {
    // 长度小于等于限制直接打印
    printFunc(LogUtil.DOMAIN, logTag, content);
  } else {
    let count = 0
    while (content.length > maxSize && count < maxCount) {
      count++
      // 循环分段打印
      let logContent = content.substring(0, maxSize);
      content = content.replace(logContent, '');
      printFunc(LogUtil.DOMAIN, logTag, logContent);
    }
    // 打印剩余日志
    printFunc(LogUtil.DOMAIN, logTag, content);
  }
}

2. 崩溃后重启 --- 事后补救

应用可能存在很多种崩溃场景,崩溃后重启方法,不仅能解决 Hilog 导致的崩溃问题,还可以同时处理其他的卡死崩溃场景(虽然崩溃不应该存在,但无法保证不会存在)

故障处理有2种方式,一种是触发故障后自动重启,一种是监听故障状态后进行恢复处理

可以在应用模块初始化时配置应用恢复功能,一个在故障发生后,能将应用重启恢复到故障之前的状态的功能

import { appRecovery, AbilityStage } from '@kit.AbilityKit';

export default class MyAbilityStage extends AbilityStage {
  onCreate() {
    appRecovery.enableAppRecovery(
      appRecovery.RestartFlag.ALWAYS_RESTART,
      appRecovery.SaveOccasionFlag.SAVE_WHEN_ERROR,
      appRecovery.SaveModeFlag.SAVE_WITH_FILE
    );
  }
}

若只配置当应用出现 APP\_FREEZE 故障状态时重启,可配置对应的重启配置,JS\_CRASH 卡死故障事后补救方式也同理

import { appRecovery, AbilityStage } from '@kit.AbilityKit';

export default class MyAbilityStage extends AbilityStage {
  onCreate() {
    appRecovery.enableAppRecovery(
      appRecovery.RestartFlag.APP_FREEZE,
      appRecovery.SaveOccasionFlag.SAVE_WHEN_ERROR,
      appRecovery.SaveModeFlag.SAVE_WITH_FILE
    );
  }
}

上述功能还需要配置对应的 Ability,module.json5 文件中需要配置支持恢复

{
  "abilities": [
    {
      "name": "EntryAbility",
      "recoverable": true,
    }]
}

一般情况下,上述配置故障触发后自动重启就够用了,但如果想要在出现故障时并在应用崩溃前做额外的恢复处理,此时上述的自动重启可能不够用了,这个时候需要通过故障监听处理,但根据现有功能,目前注册后能捕获到应用产生的 js crash,应用崩溃时进程不会退出

故障名称故障监听状态保存自动重启日志查询
JS_CRASH支持支持支持支持
APP_FREEZE不支持支持支持支持
CPP_CRASH不支持不支持不支持支持

在 Ability 中采用主动保存状态,定义和注册故障监听事件,主动恢复或者选择被动恢复的方式使用 appRecovery 功能。

import { appRecovery, errorManager, UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

let registerId = -1;
let callback: errorManager.ErrorObserver = {
  onUnhandledException(errMsg) {
    console.log(errMsg);
    // 保存状态
    appRecovery.saveAppState();
    // 重启
    appRecovery.restartApp();
  }
}

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 故障监听
    registerId = errorManager.on('error', callback);

    windowStage.loadContent("pages/index", (err, data) => {
      if (err.code) {
        console.error('Failed to load the content. Cause:' + JSON.stringify(err));
        return;
      }
      console.info('Succeeded in loading the content. Data: ' + JSON.stringify(data));
    })
  }
}

3. 日志异步打印

如果一定要保证日志全信息的打印,并且不能因为日志打印而崩溃的话,那么就要处理日志打印的卡死现象了。

日志打印的卡死是因为js的长时间执行,影响了主线程的保活检测事件,导致应用卡死崩溃。那么处理这种由于主线程阻塞类型的卡死现象可通过以下两种方式处理

  • 异步处理
  • 代码优化

上述日志分段处理从代码上看是无法通过代码优化进行处理了,那么就只能通过异步打印日志处理了。

为了不影响主线程,可以通过worker 创建子进程来处理日志打印问题

日志 worker 线程如下:

// LogWorker.ets
import { worker, ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@kit.ArkTS';
import { hilog } from "@kit.PerformanceAnalysisKit";
let workerPort: ThreadWorkerGlobalScope = worker.workerPort;
interface logData {
  domain: number,
  type: string,
  content: string,
  logTag: string
}

const printMap: Record<string, Function> = {
  'debug': hilog.debug,
  'info': hilog.info,
  'error': hilog.error,
  'warn': hilog.warn,
  'fatal': hilog.fatal,
}
// 日志堆栈,先进先出原则,保证日志的时序
const logsList: Array<logData> = []
let isRunning: boolean = false

workerPort.onmessage = (e: MessageEvents): void => {
  const data: logData = e.data
  logsList.push(data)
  // 常驻任务
  if (!isRunning) {
    performTask()
  }
}

function performTask() {
  if (logsList.length > 0) {
    isRunning = true
    const data: logData | undefined = logsList.shift()
    if (data) {
      printFunc(data.type, data?.logTag, data.domain, data?.content)
    }
    performTask()
  } else {
    isRunning = false
  }
}

function printFunc(type: string, logTag:string, domain:number, content:string) {
  const logFunc: Function = printMap[type]
  if (!logFunc) {
    return
  }
  const maxSize = 1024
  if (content.length <= maxSize) {
    // 长度小于等于限制直接打印
    logFunc(domain, logTag, content);
  } else {
    let tempContent: string = content;
    let count = 0
    while (tempContent.length > maxSize) {
      count++
      //循环分段打印
      let logContent = tempContent.substring(0, maxSize)
      tempContent = tempContent.replace(logContent, "")
      logFunc(domain, logTag, logContent);
    }
    // 打印剩余日志
    logFunc(domain, logTag, content);
  }
}

在日志管理模块中引入 日志 worker 线程文件,并将日志模块中的print函数修改为发送消息到日志线程模块

// LogUtil.ets
import { worker } from '@kit.ArkTS';
class LogUtil {
  private static instance: LogUtil;
  private static DOMAIN: number = 0x0000;
  private workerInstance: worker.ThreadWorker = new worker.ThreadWorker('../LogWorker.ets.ets');
  ...
  public print(logTag: string, content: string, logType: string) {
      this.workerInstance.postMessage({
        type: logType,
        domain: LogUtil.DOMAIN,
        logTag: logTag,
        content: content
      })
  }
  ...
}

如此,即可避免超长日志打印时,应用卡死现象的发生了

最后

以上处理方式都比较通用,不只是针对日志打印卡死问题的处理。

比如崩溃后重启这样的事后补救措施,对于所有应用故障场景都适用。

再比如上述的异步处理方式,对于任何由于代码长时间执行导致的主线程阻塞的卡死问题都适用,如图像处理、视频编码、数据分析等 CPU 密集型任务或占用系统大量计算能力的任务。


九酒
1 声望0 粉丝