3、Camera HAL日志系统:ALOGV/ALOGD/ALOGI/ALOGW/ALOGE使用、日志标签定义、日志级别控制

做Camera HAL开发,说白了就是跟日志打交道。你想想看,相机跑起来那么多线程、那么多buffer在流转,出了问题你靠什么定位?靠猜吗?不,靠日志。

Android的日志系统,在HAL层用得最多的就是ALOG系列宏。我刚开始接触时也觉得不就是打印嘛,后来踩过坑才明白——日志用得好,调试效率翻倍;用得不好,logcat刷屏刷到你怀疑人生。

3.1 ALOG系列宏:五个级别,各司其职

Android HAL层提供了五个日志级别宏,从低到高分别是:

级别 含义 我常用的场景
ALOGV VERBOSE 最详细 帧率、buffer地址、时间戳等高频信息
ALOGD DEBUG 调试 函数调用流程、参数值变化
ALOGI INFO 信息 初始化完成、stream配置、状态切换
ALOGW WARN 警告 超时、重试、非致命异常
ALOGE ERROR 错误 硬件失败、参数非法、致命错误

我个人习惯是:上线版本只保留ALOGI及以上,ALOGV和ALOGD全部关掉。不然logcat里全是噪声,真正有用的信息反而被淹没了。

核心原则:日志级别不是越高越好,而是越精准越好。能用ALOGI说清楚的事,别用ALOGV刷屏。

3.2 日志标签定义:LOG_TAG的学问

每个使用ALOG宏的文件,都需要定义LOG_TAG。这个标签决定了你在logcat里用什么关键字过滤。

// 标准写法
#define LOG_TAG "CamHAL_ISP"

// 或者更细粒度
#define LOG_TAG "CamHAL_ISP_3A"

嗯,这里要注意——LOG_TAG必须在#include之前定义。我见过新手把顺序搞反,结果日志死活打不出来,排查了半天。

关于标签命名,我建议遵循这个规则:

  • 模块前缀:统一用CamHAL_开头,方便全局过滤
  • 子模块名:ISP、Sensor、Pipeline、Buffer等
  • 功能细分:比如CamHAL_ISP_StatsCamHAL_Sensor_EEPROM

我的小技巧:在logcat里用logcat -s CamHAL_ISP只过滤自己模块的日志,配合-v threadtime看线程和时间戳,定位问题快得多。

3.3 日志级别控制:动态开关的艺术

日志级别控制,说白了就是决定哪些日志能出来,哪些被过滤掉。Android提供了两种控制方式:

3.3.1 编译时控制

通过LOCAL_CFLAGSAndroid.mk/Android.bp中的宏定义:

// Android.bp 示例
cc_library_shared {
    name: "camera.xxx",
    cflags: [
        "-DLOG_NDEBUG=1",    // 关闭ALOGV和ALOGD
        "-DLOG_TAG=\"\\\"CamHAL\\\"\""
    ],
}

设置了LOG_NDEBUG=1后,ALOGV和ALOGD在编译阶段就被去掉了,运行时完全不产生开销。我建议发布版本一定要加这个。

3.3.2 运行时控制

通过property动态调整,这个在调试时特别有用:

// 设置某个标签的日志级别
adb shell setprop persist.vendor.camhal.log.level 2

// 代码中读取
int logLevel = property_get_int32("persist.vendor.camhal.log.level", 1);
if (logLevel >= 2) {
    ALOGD("debug info: %d", value);
}

我曾经在项目中遇到过一个问题:某个OEM客户反馈相机偶尔卡顿,但logcat里全是ALOGV的帧率打印。我让他们把日志级别调到WARN以上,卡顿时的关键错误信息立刻就暴露出来了——原来是某个sensor的帧同步信号不稳定。

避坑指南:我曾经在调试3A算法时,把ALOGV开了一整天,结果logcat文件涨到2GB,手机直接卡死重启。从那以后我学乖了——线上环境绝对不开ALOGV,调试时也只针对特定模块开。

3.4 实战:一个完整的日志使用示例

来看一个我在Camera HAL Pipeline中实际用过的日志模板:

#define LOG_TAG "CamHAL_Pipeline"

#include <log/log.h>

// 自定义日志宏,带函数名和行号
#define PIPELINE_LOGD(fmt, ...) \
    ALOGD("[%s:%d] " fmt, __FUNCTION__, __LINE__, ##__VA_ARGS__)

#define PIPELINE_LOGE(fmt, ...) \
    ALOGE("[%s:%d] " fmt, __FUNCTION__, __LINE__, ##__VA_ARGS__)

status_t processRequest(...) {
    // 关键流程用ALOGI
    ALOGI("processRequest: frame %d, stream %p", frameNumber, stream);

    // 详细参数用ALOGD
    PIPELINE_LOGD("input buffer: %p, size: %zu", buffer, size);

    // 异常情况用ALOGW
    if (timeout) {
        ALOGW("frame %d timeout after %d ms", frameNumber, timeoutMs);
    }

    // 致命错误用ALOGE
    if (hwFailed) {
        ALOGE("hardware error on frame %d, aborting", frameNumber);
        return NO_INIT;
    }

    return OK;
}

你看,每个日志级别都用在了最合适的地方。ALOGI告诉你「我在干什么」,ALOGD告诉你「具体参数是什么」,ALOGW提醒你「有点不对劲」,ALOGE直接喊「出大事了」。

3.5 日志系统的性能考量

最后说一个容易被忽略的点——日志是有性能开销的。特别是在Camera HAL这种高帧率场景下:

  • 字符串格式化ALOGD("value=%d", x) 即使日志被过滤,参数求值也会执行
  • IO操作:日志写入logd进程涉及跨进程通信,高频打印会拖慢帧率
  • 缓冲区溢出:logcat缓冲区有限,刷屏会导致早期日志被丢弃

我的建议是:热点路径(比如每帧都会调用的函数)只保留ALOGI及以上。如果非要打ALOGV,加个条件判断,比如每30帧才打印一次。

好了,日志系统这块就讲这么多。记住一句话:日志是你的眼睛,但别让眼睛花了。下一章我们聊聊更高级的调试手段——如何用文件日志来记录完整的运行轨迹。