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_Stats、CamHAL_Sensor_EEPROM
我的小技巧:在logcat里用logcat -s CamHAL_ISP只过滤自己模块的日志,配合-v threadtime看线程和时间戳,定位问题快得多。
3.3 日志级别控制:动态开关的艺术
日志级别控制,说白了就是决定哪些日志能出来,哪些被过滤掉。Android提供了两种控制方式:
3.3.1 编译时控制
通过LOCAL_CFLAGS或Android.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帧才打印一次。
好了,日志系统这块就讲这么多。记住一句话:日志是你的眼睛,但别让眼睛花了。下一章我们聊聊更高级的调试手段——如何用文件日志来记录完整的运行轨迹。