Android Camera HAL3-metadata

Metadata 是整个 Android camera 中类似于高楼大厦中的管道系统一样,贯穿了整个 Camera 的 Framework 和 vendor HAL,Metadata 里面几乎包含了所有的控制、参数、返回值等等,总线型的设计使得这个玩意儿承担的任务不可谓不重。本文从几个角度来学习一下 Metadata 的设计、使用、可优化的点,并不准备特别特别细节去介绍代码的每一行,主要是注重其想法层面的一些东西。

Metadata 的基本结构

先来看一张图,这个在 camera_metadata.c 文件里面有字符化的结构图,我把它扩充了下:
在这里插入图片描述
这张图就基本上完整概括了整个 camera_metadata 的内存分布,我们平常操作的接口就是那个 camera_metadata_t 的结构体,后面的所有内容都通过这个节点来进行索引操作。从结构图里面可以看出来或者说猜测的应该有下面几点(只是猜测,后面需要看代码去证实):

  1. 这一部分内存是连续的,这里指的是虚拟内存连续,这样才能够保证相对比较集散的存储,想想如果是零散的地址存储,那大规模拷贝将是一个灾难。
  2. 看着是有一定的初始条件限制,就是那些带有 capacity 的变量,这些理论上可以降低我们增删改查的复杂度,因为要动态扩容、搬移数据等等,完全是一个不受限或者说没那么受限的操作的话,可能会耗费比较多的精力去进行数据的整合。
  3. 预分配的空间应该足够把将要使用的 metadata 全部囊括进去,Add entry 或者 Remove entry 应该是不会改变 capacity 的大小,而只会改变 count 的大小。

可以看出来这是一个树形的结构,结构体 camera_metadata_t 是整个的顶层入口,往下一层是 camera_metadata_buffer_entry,用于存储每一个 tag 对应的信息,再往下就是每一个 tag 的具体 data 所在的位置了。

不同的 entry 可能会有不同的数据类型,具体地看下下面这个结构体:

/**
 * Type definitions for camera_metadata_entry
 * =============================================================================
 */
enum {
    // Unsigned 8-bit integer (uint8_t)
    TYPE_BYTE = 0,
    // Signed 32-bit integer (int32_t)
    TYPE_INT32 = 1,
    // 32-bit float (float)
    TYPE_FLOAT = 2,
    // Signed 64-bit integer (int64_t)
    TYPE_INT64 = 3,
    // 64-bit float (double)
    TYPE_DOUBLE = 4,
    // A 64-bit fraction (camera_metadata_rational_t)
    TYPE_RATIONAL = 5,
    // Number of type fields
    NUM_TYPES
};

typedef struct camera_metadata_buffer_entry {
    uint32_t tag;
    uint32_t count;
    union {
        uint32_t offset;
        uint8_t  value[4];
    } data;
    uint8_t  type;
    uint8_t  reserved[3];
} camera_metadata_buffer_entry_t;

FLOAT 类型经常会存储一些效果的参数,比如有一些矩阵类型的参数都是需要浮点类型的,就可以用这种枚举存储,有的可能是字符串或者是一些不定长度的结构体,那么就会用 BYTE 类型来进行存储,其余的不用多说基本上都可以知道什么时候该用什么类型存储。

这里面有一个小小的优化,就是小于 4 个 byte 的数据可以就地存储,就是直接存放在上面的那个 value[4] 里面可以用 memcpy 进行操作以避免类型或者字节序的问题。大于 4 的就需要用 offset 来指定偏移量了,这个偏移量想必是相对于 camera_metadata_t->data_start 这里来说的。

Camera HAL3 相关的庞大的参数集就可以使用这个数据类型进行存储、传递、使用了,一般情况下现在的相机一个完整的参数集少说也得申请个 100K+ 的空间进行存放了,其实已经是非常大的数据量了,所以后面也会带来一定的问题,那就是传递效率以及空间消耗的问题。

地址与对齐

在开始下一个步骤的介绍之前先看下地址与对齐的问题,对齐是为了方便数据访问、存储,不管是拷贝或者是地址运算,对齐的数据效率是比较高的,这部分在大学的「微机原理」课程里面有相关的介绍,告诉我们为什么对齐了它访问效率就比较高,现代计算机以及 SOC 的对齐目的与其类似,不再赘述。当然 metadata 结构体里面的对齐可能会有一点点不同的用途,比如下面描述的:// Make sure camera metadata can be stacked in continuous memory,等等作用不一而足。

下面是分配并初始化 camera_metadata_t 及其对应的数据结构的步骤和代码,删了一部分于理解无太大帮助的代码:

// 1. 我们外部先计算好最多有多少个 entry,也就基本等同于有多少个 tag(这个相对容易获得),还有 data_count,这个也要根据 type 和 entry_count 综合计算。
// 2. 由 calculate_camera_metadata_size 转换成标准 metadata 数据结构所需的空间。
// 3. 初始化 camera_metadata_t 这个结构体,使用 place_camera_metadata。

// 下面步骤其实就是计算:camera_metadata_t + entry + data 的过程,只不过中间穿插了些对齐的操作而已。
size_t calculate_camera_metadata_size(size_t entry_count,
                                      size_t data_count) {
    size_t memory_needed = sizeof(camera_metadata_t);
    // 先对齐一下 entry 的对齐字节数
    memory_needed = ALIGN_TO(memory_needed, ENTRY_ALIGNMENT);
    memory_needed += sizeof(camera_metadata_buffer_entry_t[entry_count]);
    // Start buffer list at aligned boundary
    memory_needed = ALIGN_TO(memory_needed, DATA_ALIGNMENT);
    memory_needed += sizeof(uint8_t[data_count]);
    // Make sure camera metadata can be stacked in continuous memory
    memory_needed = ALIGN_TO(memory_needed, METADATA_PACKET_ALIGNMENT);
    return memory_needed;
}

// metadata->data_start, metadata->entries_start 里面存放的都是全局地址,也就是整个用户空间级别的地址,而 entry 里面的 offset 则是相对于这里的全局地址而得来的偏移量。
camera_metadata_t *place_camera_metadata(void *dst,
                                         size_t dst_size,
                                         size_t entry_capacity,
                                         size_t data_capacity) {
    size_t memory_needed = calculate_camera_metadata_size(entry_capacity,
                                                          data_capacity);

    camera_metadata_t *metadata = (camera_metadata_t*)dst;
    metadata->entry_capacity = entry_capacity;
    metadata->entries_start =
            ALIGN_TO(sizeof(camera_metadata_t), ENTRY_ALIGNMENT);
    metadata->data_capacity = data_capacity;
    metadata->size = memory_needed;
    size_t data_unaligned = (uint8_t*)(get_entries(metadata) +
            metadata->entry_capacity) - (uint8_t*)metadata;
    metadata->data_start = ALIGN_TO(data_unaligned, DATA_ALIGNMENT);
    return metadata;
}

其实很大一部分 metadata 结构体繁琐的操作都体现在地址的分配、更新、设置上面,不过好在这部分在工作当中基本上不需要担心,因为这种比较底层的库代码都是比较完备的,出 bug 的可能性基本上没有,只要掌握它的设计原理,自己如果真的需要也完全可以实现出来,编码习惯比较好的话也可以写出基本没有 bug 的代码。

说起来我觉得工作当中最难的部分是理解业务逻辑,架构代码以及拆解架构,底层的子功能反而比较容易实现,即使看起来底层库使用率超级高超级高,觉得这玩意儿是不是技术含量超级高,我个人觉得这个不是成正比的,底层的逻辑一般比较单一,也不太会出现模块之间的耦合现象,但是中层的架构代码真的是难得一批,这个不是说你学学就会的,得在长期的实战项目中锻炼、感受、思考才能够做到的,因为这部分的逻辑实在是太过于复杂了。

还有一个

增删改查

数据结构的初始化

这里主要就是分配预定的空间,然后为结构体 camera_metadata_t 进行初始化,初始化的过程基本如下,上面已经描述过了:

  1. 我们外部先计算好最多有多少个 entry,也就基本等同于有多少个 tag(这个相对容易获得),还有 data_count,这个也要根据 type 和 entry_count 综合计算,这两个变量作为初始化函数参数传入。
  2. calculate_camera_metadata_size 转换成标准 metadata 数据结构所需的空间。
  3. 初始化 camera_metadata_t 这个结构体,使用 place_camera_metadata 函数。

添加一个 entry

先看下 add_camera_metadata_entry 函数的流程图:
在这里插入图片描述
原函数里面有两个变量需要稍微区分下:

  • data_bytes:表示经过对齐之后的该 entry 所占用的 data 字节数。
  • data_payload_bytes:为经过对齐的原始 data 字节数。

这个部分看起来蛮简单的哈,就是先判断下是否有空间足够放 entry 和 data,然后就是拷贝数据到指定的内存地址处,不过上图里面标红了一个函数 validate_camera_metadata_structure,这个函数里面做了好多检查,主要就是对齐,一定程度上可以保证数据的有效性。

更新一个 entry

首先我能想到几个问题:

  1. entry 的 data 占用字节数会不会有变化?答案是会的,尤其是大于 4 个字节往后的字节数占用。
  2. 变化之后会不会有数据空洞或者原来的 data 块放不下的情况?会,肯定会的,怎么办就需要看下代码流程了。

这个就不画图了,写出来步骤:

  1. 先计算原始的 data 大小与现在更新之后的 data 大小。
  2. 比较是否有差异,如果有差异的话,就需要移动内存,并且更新该 entry 块后面所有 entry 的 offset 与其对应的 data offset。
  3. 更新完毕之后拷贝新的数据进去,这部分和 add 一个 entry 差不多。

我看代码里面不管是数据变大了或者是变小了,都需要进行内存移动(memmove),其实我觉得如果是现在的数据比以前的数据少了,并且没有达到一定的阈值,那就不用去进行内存移动,毕竟内存移动的时间消耗还是不少的,数量变大那是没办法,必须得进行数据移动。但是,如果是为了时间效率上的优化,所有数据量变小的情况下我们都不去移动会怎么样呢?

考虑只减少 entry 对应的 data size,但是所有 entry 的 offset 不动,首先面临一个问题,那就是你如果将来要 remove 这个 entry,怎么保证不要出现数据空洞,这个就必须要另外添加一个变量用于存储 entry 对应的实际 data 内存块大小,现在已有的变量存储有效数据的大小,这样你 remove 的时候才能够保证数据的紧凑连续。那总的 data count 要不要更新呢,这个要不要也弄一个有效长度和实际占用长度,我觉得 Duck 不必,因为这个数据对于使用上来讲不应该会造成什么影响,当然新增一个也不是不可以。

删除一个 entry

这部分就完全可以参照上面那个 update 步骤进行,无非是把 update 的新值改为 0,其余的部分基本就是类似的,不详述。

至于还有其它的 append,merge 等等操作,原理上都和 update 有异曲同工之处,参考参考就可以实现出来了。

如何处理删除 entry 后零散的 data 空间

这个在 updata 这部分也说过了,目前代码里面都是用 memmove 加上更新所有 entry 的 offset 来完成,这部分看起来需要的操作还是不少的,耗费时间也会相对多那么一丢丢,但是这种如果是不管它,一定会造成数据零散,到最后出现整个的存储效率降低的情况。我想数据库软件肯定经常会遇到这种问题,但是没有去关注过如何处理,想必这里的处理方式是比较原始暴力的,可能是本身数据量也不是特别特别大吧。

如何优化设计

对于 Metadata 的操作和使用,我可以想到以下几个问题:

  1. 有没有办法降低 memmove 的操作频率,每一次 remove 的操作都会触发几乎整个表的更新,能不能降低这种概率。。
  2. 在使用上,通常数据输入输出都要独立,而 Metadata 全部更新的概率极低,每次也只会更新一小部分,那我们整个的内存里面就可能充斥着大量的无意义拷贝,不仅耗时,且会占用大量的内存空间。
  3. Merge、Append 都会对整个表进行操作,可不可以不要这样?

对于问题一来讲,想了想散列表的扩表操作,有一种办法是把整个表的移动操作分散到每一次操作里面,降低每一次操作的时间延迟,具体的原理可以看下网上相关的介绍。那这里肯定也会有一定的办法把删除之后更新数据空洞的操作分散到每一次的 Metadata 操作当中去,设定一个阈值来触发其进行处理,不过这样的话可能就会得增加另外一个表来维护删除信息了,算是空间换时间。

第二个问题是基本上所有的架构都会遇到的问题,也可以仿照某些数据库的处理方式,把 Metadata 进行分块,抽象出来一个索引层,有一些低改动频率的 Metadata 放在一起,其它的相同类别的 Metadata 放在一起,不同的 Metadata 实体之间可以共享这些分块,需要更新的时候就可以减少实际的内存拷贝,把一些没有改变的内存块索引地址直接拷贝过去就好。当然这里面会有很多数据一致性、竞争等等问题,需要详细的去设计方案编码实现。

第三个问题可以并到第二个问题里面进行处理,第二个问题解决了,第三个问题可以很轻松的一并解决掉了。

End

搞完收工,其实这篇文还是比较侧重于设计以及使用上的一些缺陷,其实我觉得最值钱的部分就是这里了,发现问题,提出解决方案,解决问题,程序员真正的价值是体现在这里了,而不是单纯的一台毫无感情的编码机器,那样是低价值的体现,要想提高自己的价值,还是要多往这方面去思考、实践,因为这些才是产品最急需的特质。


  • 12
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值