3. Metadata数据结构详解:camera_metadata_t结构体、entry与buffer布局、内存对齐与序列化

Camera HAL层与Framework之间传递的Metadata并非简单的键值对,而是一种经过精心设计的、紧凑的二进制数据结构。理解其底层布局是进行高效读写、调试和序列化的基础。本章将深入剖析camera_metadata_t结构体、camera_metadata_entry_t的访问方式、Buffer的内存布局与对齐规则,以及序列化/反序列化的核心机制。

3.1 camera_metadata_t 结构体总览

camera_metadata_t是Android Camera子系统中最核心的数据结构,它定义在system/media/camera/include/system/camera_metadata.h中。该结构体采用“头部+数据区”的连续内存布局设计,旨在最小化内存碎片并支持高效的跨进程传输(通过binder或共享内存)。

核心设计理念: 所有元数据(tag、count、value)被紧凑地打包在单一连续内存块中,通过偏移量进行访问,避免了指针重定位问题,使得数据可以轻松地在进程间传递。

其简化后的结构体定义如下:

typedef struct camera_metadata {
    /** 元数据版本号,用于兼容性检查 */
    uint32_t          version;
    /** 当前已使用的entry数量 */
    uint32_t          entry_count;
    /** 当前已使用的data槽位数量(每个槽位4字节) */
    uint32_t          data_count;
    /** 当前已分配的entry容量 */
    uint32_t          entry_capacity;
    /** 当前已分配的data槽位总容量 */
    uint32_t          data_capacity;
    /** 保留字段,用于未来扩展 */
    uint32_t          padding;
    /** 指向entry数组的偏移量(相对于结构体起始地址) */
    uint32_t          entry_off;
    /** 指向data数组的偏移量(相对于结构体起始地址) */
    uint32_t          data_off;
    /** 存储实际entry和data的连续缓冲区起始地址 */
    uint8_t           buffer[0];
} camera_metadata_t;

该结构体通过buffer[0](零长度数组)实现了灵活的内存布局。整个内存块分为三个区域:

区域 内容 说明
头部 camera_metadata_t 固定字段 包含版本、计数、容量和偏移量信息,用于快速导航。
Entry数组 camera_metadata_entry_t 数组 每个entry描述一个tag的元信息(类型、计数、数据索引)。
Data池 原始数据缓冲区 以4字节为单位对齐存储所有tag的实际值。
camera_metadata_t 连续内存布局 头部 (Header) version entry_count data_count entry_capacity data_capacity entry_off data_off buffer[0] entry_off Entry 数组 Entry[0]: tag, type, count, data_index Entry[1]: tag, type, count, data_index Entry[2]: ... entry_count 个有效 entry data_off Data 池 4字节槽位[0] 4字节槽位[1] 4字节槽位[2] ... data_count 个槽位

3.2 Entry与Data的详细布局

每个camera_metadata_entry_t并不直接存储数据值,而是存储指向Data池的索引。这种间接访问方式使得数据可以紧凑排列,并支持复杂类型(如数组、矩形)。

3.2.1 camera_metadata_entry_t 结构

typedef struct camera_metadata_entry {
    /** 标签ID,如 ANDROID_SENSOR_EXPOSURE_TIME */
    uint32_t  tag;
    /** 数据类型,如 TYPE_BYTE, TYPE_INT32, TYPE_FLOAT, TYPE_INT64, TYPE_DOUBLE, TYPE_RATIONAL */
    uint8_t   type;
    /** 数据元素个数(非字节数) */
    uint32_t  count;
    /** 指向实际数据的指针(由库函数计算得出,非序列化字段) */
    union {
        void      *data;
        uint8_t   *u8;
        int32_t   *i32;
        float     *f;
        int64_t   *i64;
        double    *d;
        camera_metadata_rational_t *r;
    };
    /** 数据在Data池中的起始索引(以4字节为单位) */
    size_t    data_index;
} camera_metadata_entry_t;
关键点: data_index是Data池中的槽位索引,而非字节偏移。例如,一个TYPE_INT64类型的数据占用2个槽位(8字节)。data指针是在调用find_camera_metadata_entry()时由库函数根据data_index和type动态计算出来的,方便开发者直接访问。

3.2.2 Data池的内存对齐规则

Data池以4字节(一个槽位)为基本单位进行管理,但内部数据必须满足其自然对齐要求。这是保证跨平台兼容性和访问效率的关键。

数据类型 类型标识 单个元素大小 对齐要求 占用槽位数
BYTE 0 1 字节 1 字节 1 (4字节对齐)
INT32 1 4 字节 4 字节 1
FLOAT 2 4 字节 4 字节 1
INT64 3 8 字节 8 字节 2
DOUBLE 4 8 字节 8 字节 2
RATIONAL 5 8 字节 (int32_t numerator + int32_t denominator) 4 字节 2
对齐陷阱: 当Data池中连续存储不同类型的数据时,必须进行填充(padding)以保证每个数据都从其自然对齐边界开始。例如,一个TYPE_INT32数据后紧跟一个TYPE_INT64数据,需要在INT32后填充4个字节,使INT64从8字节对齐的地址开始。开发者手动构造Metadata时,必须使用camera_metadata_* API,这些API会自动处理对齐,切勿直接操作内存。
Data池内存对齐示例(每个格子=4字节) 槽位0 INT32 (4B) 槽位1 PADDING 槽位2 & 3 INT64 (8B, 8字节对齐) 槽位4 & 5 RATIONAL (8B, 4字节对齐) 槽位6 FLOAT (4B) 对齐规则说明 • INT32/FLOAT 从4字节对齐地址开始,占用1个槽位。 • INT64/DOUBLE 必须从8字节对齐地址开始,占用2个连续槽位。 • 当INT32后紧跟INT64时,需要插入1个槽位的PADDING以满足INT64的对齐要求。

3.3 序列化与反序列化机制

序列化是将camera_metadata_t转换为可传输的字节流的过程,反序列化则是其逆过程。由于camera_metadata_t本身就是连续内存块,序列化变得非常直接。

3.3.1 序列化过程

  1. 计算总大小: 调用get_camera_metadata_size()获取整个结构体占用的字节数。
  2. 分配缓冲区: 分配一个大小为总字节数的uint8_t缓冲区。
  3. 内存拷贝: 使用memcpy将camera_metadata_t结构体及其后的Entry数组和Data池完整拷贝到新缓冲区。
  4. 传输: 将序列化后的字节流通过Binder或共享内存发送给Framework。
// 序列化示例
camera_metadata_t *metadata = ...; // 假设已填充好数据
size_t size = get_camera_metadata_size(metadata);
uint8_t *serialized_buffer = new uint8_t[size];
memcpy(serialized_buffer, metadata, size);
// 现在 serialized_buffer 可以用于传输
// 接收方可以通过 reinterpret_cast<camera_metadata_t*>(serialized_buffer) 直接访问

3.3.2 反序列化过程

  1. 接收字节流: 从传输层获取序列化后的字节流。
  2. 类型转换: 将字节流指针强制转换为camera_metadata_t*。
  3. 有效性检查: 验证version、entry_count、data_count等字段的合理性,防止恶意数据或损坏。
  4. 克隆(可选): 如果需要修改,必须调用clone_camera_metadata()创建一个可写的副本,因为原始缓冲区可能是只读的。
性能优势: 由于camera_metadata_t的连续内存布局,序列化/反序列化仅需一次memcpy,无需遍历每个entry进行逐个编码。这使得它在高帧率场景下(如预览、录像)具有极低的延迟。
Metadata 序列化与反序列化流程 HAL 端 camera_metadata_t memcpy 序列化字节流 uint8_t buffer[] Binder/SHM Framework 端 reinterpret_cast 反序列化 可写副本 clone_camera_metadata() 修改后回传 序列化字节流 uint8_t buffer[] memcpy HAL 端 camera_metadata_t

3.4 调试技巧:使用dumpsys与日志

在开发过程中,经常需要检查Metadata的内容是否正确。Android提供了多种调试手段:

  • dumpsys media.camera: 可以打印出当前Camera设备的所有静态、动态和请求Metadata。这是最常用的调试方法。
  • camera_metadata_dump(): 在HAL层代码中直接调用此函数,可以将camera_metadata_t的内容格式化输出到logcat,方便在运行时跟踪。
  • camera_metadata_usage(): 打印当前Metadata的内存使用情况,包括entry和data的容量与使用量,有助于发现内存泄漏或过度分配问题。
// 在HAL代码中调试Metadata
void dump_metadata(const camera_metadata_t *metadata) {
    ALOGD("=== Metadata Dump ===");
    camera_metadata_dump(metadata, 0); // 0表示不缩进
    ALOGD("=== Metadata Usage ===");
    camera_metadata_usage(metadata);
}
实战建议: 当遇到“Metadata buffer too small”或“Entry not found”等错误时,首先使用camera_metadata_usage()检查entry_count和data_count是否在预期范围内。其次,使用camera_metadata_dump()对比HAL端和Framework端的Metadata内容,定位不一致的tag。

理解camera_metadata_t的底层数据结构,是编写高效、稳定Camera HAL的基础。掌握其内存布局、对齐规则和序列化机制,能够帮助开发者避免常见的踩内存、对齐错误和性能瓶颈问题。