前言

前面文件互传的各个业务的设计实现分析的差不多了,这里回顾分析下文件传输过程中,文件数据帧的设计,消息帧的设计以及其他的一些open Harmony关于内存对齐的设计。看完这篇就会理解为什么面试官老是揪着int占用几字节问不放了,哈哈哈。

消息帧设计

就比如文件发送完以后,软总线会把发送文件列表发送给接收端核对是否全部发完

发送文件列表给接收端源码

int32_t FileListToBuffer(const char **destFile, uint32_t fileCnt, FileListBuffer *outbufferInfo)
{
    if (destFile == NULL || outbufferInfo == NULL || fileCnt == 0) {
        TRANS_LOGE(TRANS_FILE, "invalid param");
        return SOFTBUS_INVALID_PARAM;
    }
    int32_t errCode = SOFTBUS_OK;
    uint32_t totalLength = 0;
    uint32_t offset = 0;
    for (uint32_t i = 0; i < fileCnt; i++) {
        size_t fileNameLength = strlen(destFile[i]);
        if (fileNameLength == 0 || fileNameLength > MAX_FILE_PATH_NAME_LEN) {
            TRANS_LOGE(TRANS_FILE, "bad file name at index=%{public}" PRIu32, i);
            return SOFTBUS_INVALID_PARAM;
        } else {
            totalLength += fileNameLength;
        }
    }

    size_t bufferSize = totalLength + (sizeof(struct FileListItem) * fileCnt);
    uint8_t *buffer = (uint8_t *)SoftBusCalloc(bufferSize);
    if (buffer == NULL) {
        TRANS_LOGE(TRANS_FILE, "calloc filelist failed");
        return SOFTBUS_MALLOC_ERR;
    }

    for (uint32_t index = 0; index < fileCnt; index++) {
        uint32_t fileNameSize = strlen(destFile[index]);
        struct FileListItem *fileItem = (struct FileListItem *)(buffer + offset);
        fileItem->index = htonl(index);
        fileItem->fileNameLength = htonl(fileNameSize);
        offset += sizeof(struct FileListItem);

        // note: no \0 here
        if (memcpy_s(fileItem->fileName, bufferSize - offset, destFile[index], fileNameSize) != EOK) {
            TRANS_LOGE(TRANS_FILE, "copy file name failed!");
            errCode = SOFTBUS_MEM_ERR;
            break;
        }

        offset += fileNameSize;
    }

    if (errCode != SOFTBUS_OK) {
        SoftBusFree(buffer);
        return errCode;
    }

    outbufferInfo->buffer = buffer;
    outbufferInfo->bufferSize = offset;
    return SOFTBUS_OK;
}

逐行解析

int32_t FileListToBuffer(const char **destFile, uint32_t fileCnt, FileListBuffer *outbufferInfo)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 参数校验
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (destFile == NULL || outbufferInfo == NULL || fileCnt == 0) {
        return SOFTBUS_INVALID_PARAM;
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1轮循环:计算所有文件名的总长度 ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    uint32_t totalLength = 0;
    for (uint32_t i = 0; i < fileCnt; i++) {
        size_t fileNameLength = strlen(destFile[i]);
        if (fileNameLength == 0 || fileNameLength > MAX_FILE_PATH_NAME_LEN) {
            return SOFTBUS_INVALID_PARAM;   // 文件名不能为空或超长
        }
        totalLength += fileNameLength;      // 累加文件名长度
    }
    // 假设 destFile = ["photo1.jpg", "doc.pdf"]
    // totalLength = 11 + 7 = 18

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 分配缓冲区 ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    size_t bufferSize = totalLength + (sizeof(struct FileListItem) * fileCnt);
    //  = 18 + 8*2 = 34 字节
    //     ↑文件名    ↑每个Item头部8字节(index+fileNameLength)
    
    uint8_t *buffer = (uint8_t *)SoftBusCalloc(bufferSize);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2轮循环:填充二进制数据 ⭐⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    uint32_t offset = 0;
    for (uint32_t index = 0; index < fileCnt; index++) {
        uint32_t fileNameSize = strlen(destFile[index]);

        // 把 buffer+offset 位置强转为 FileListItem 指针
        struct FileListItem *fileItem = (struct FileListItem *)(buffer + offset);

        // 填充头部(转网络字节序)
        fileItem->index = htonl(index);
        fileItem->fileNameLength = htonl(fileNameSize);

        // offset 跳过 Item 头部(8字节)这里为什么传入结构体的大小就能跳过头部后面有讲到
        offset += sizeof(struct FileListItem);

        // 拷贝文件名到 fileName[0] 的位置(不带 \0)
        memcpy_s(fileItem->fileName, bufferSize - offset, destFile[index], fileNameSize);
        //             ↑ 目标地址            ↑ 剩余空间        ↑ 源数据       ↑ 长度

        // offset 跳过文件名
        offset += fileNameSize;
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 输出结果
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    outbufferInfo->buffer = buffer;
    outbufferInfo->bufferSize = offset;
    return SOFTBUS_OK;
}

例子

输入:destFile = ["photo1.jpg", "doc.pdf"],fileCnt = 2

第1轮:计算总长度
  strlen("photo1.jpg") = 11
  strlen("doc.pdf")    =  7
  totalLength = 18

分配:bufferSize = 18 + 8*2 = 34 字节

第2轮:填充数据

  index=0:
    offset=0
    fileItem 指向 buffer+0
    fileItem->index = htonl(0) = 0x00000000
    fileItem->fileNameLength = htonl(11) = 0x0000000B
    offset = 0 + 8 = 8
    memcpy(fileItem->fileName, "photo1.jpg", 11)
    offset = 8 + 11 = 19

  index=1:
    offset=19
    fileItem 指向 buffer+19
    fileItem->index = htonl(1) = 0x00000001
    fileItem->fileNameLength = htonl(7) = 0x00000007
    offset = 19 + 8 = 27
    memcpy(fileItem->fileName, "doc.pdf", 7)
    offset = 27 + 7 = 34

输出:
  buffer → 34字节的二进制数据
  bufferSize = 34

最终二进制布局

为什么需要两轮循环

第1轮:算总长度 → 才知道要分配多少内存
第2轮:填充数据 → 往分配好的内存里写

不能合并成一轮:
  第1个文件时还不知道总长度 → 无法确定分配多少
  如果先分配一个估算值 → 可能不够或浪费
  两轮循环是最稳妥的方式

柔性数组设计

发送的帧里面,如果帧需要包含的数据长度不确定怎么办,这个项目提供了柔性数组的设计解决。

方案1:固定大小数组(浪费空间)
struct FileListItem {
    uint32_t index;
    uint32_t fileNameLength;
    char fileName[256];   // 固定256字节
};
→ 文件名只有 "a.jpg"(5字节),但占了256字节 → 浪费251字节
→ 文件名超过256字节就放不下 → 限制

方案2:指针(两次内存分配)
struct FileListItem {
    uint32_t index;
    uint32_t fileNameLength;
    char *fileName;       // 指针
};
→ 需要单独 malloc 文件名 → 两次内存操作
→ 序列化时麻烦:要序列化结构体 + 单独序列化字符串

方案3:柔性数组(最佳)✅
struct FileListItem {
    uint32_t index;          // 4字节
    uint32_t fileNameLength; // 4字节
    char fileName[0];        // 0字节占位符
};
→ 不占空间
→ 实际数据紧跟在后面
→ 长度由 fileNameLength 决定

柔性数组是什么

struct FileListItem {
    uint32_t index;          // 4字节
    uint32_t fileNameLength; // 4字节
    char fileName[0];        // ← 这就是柔性数组
};

在这里,柔性数组只是一个标识符,是一个大小为0的数组,不占用结构体空间。

普通数组:
  char name[32];   // 占32字节,不管你用不用
  → 浪费空间(文件名可能只有5字节)
  → 限制长度(超过32就放不下)

柔性数组:
  char name[0];    // 占0字节,只是一个标记
  → 实际数据紧跟在结构体后面
  → 长度由 fileNameLength 告诉你
  → 不浪费空间,不限制长度

项目中的效果

// 分配内存:结构体(8) + 文件名(11) = 19字节
uint8_t *buffer = SoftBusCalloc(sizeof(FileListItem) + strlen("photo1.jpg"));

// 内存长这样:
// buffer 指向的地址:
┌──────────┬──────────┬─────────────────────┐
│ index=0  │ len=11   │ p h o t o 1 . j p g │
│ (4字节)   │ (4字节)  │ (11字节)            │
└──────────┴──────────┴─────────────────────┘
0                     8                    19

// 使用:
FileListItem *item = (FileListItem *)buffer;
item->index = 0;                    // 前4字节
item->fileNameLength = 11;          // 第5-8字节
memcpy(item->fileName, "photo1.jpg", 11);  // 从第9字节开始写
//     ↑ fileName 就是 buffer+8 的别名

为什么使用memcpy而不是strcpy

// note: no \0 here(代码注释明确说了)
memcpy(item->fileName, destFile[index], fileNameSize);
// ↑ 没有 \0 结尾!节省1字节

strcpy(item->fileName, destFile[index]);
// ↑ 会自动加 \0 → 多1字节 → 网络传输浪费带宽

1字节对齐

这里就涉及到项目对内存对齐的处理了。当一个变量不满默认的4字节或8字节的时候,编译器会自动进行补齐,但是这样有两个坏处,第一帧变长,第二两端内容对不上了,这是为了保障发送过去的数据和接收到的数据保持一致。

发送端(1字节对齐):
  [index(4)][len(4)][fileName...]
  第8字节开始就是文件名

接收端(默认4字节对齐):
  [index(4)][len(4)][填充(4)][fileName...]
  第12字节才开始文件名  ← 位置对不上!

→ 两端结构体布局不一致 → 解析错误 💥

1字节对齐后:
  两端都是 [index(4)][len(4)][fileName...]
  → 布局一致 → 正确解析 ✅

对齐规则详解

默认对齐是什么

默认对齐值 = 编译器决定,通常等于该平台上最大的基本类型对齐要求

常见平台:
  x86 / ARM (32位):默认4字节对齐
  x86_64 / ARM64:  默认8字节对齐

可以用 #pragma pack(N) 或 __attribute__((aligned(N))) 手动指定

判断对齐值的公式

成员的对齐值 = min(成员自身大小, 结构体当前对齐值)

例1:默认4字节对齐
struct Example {
    char a;      // 大小1,对齐值=min(1,4)=1 → 偏移0
                  // 下一个成员偏移=0+1=1
    int b;       // 大小4,对齐值=min(4,4)=4 → 偏移必须是4的倍数
                  // 当前偏移=1,不是4的倍数
                  // → 插入3字节填充到偏移4
};

内存布局:
偏移:  0     1    2     3     4 5 6 7
内容: [a] [填充][填充][填充] [ b b b b ]
sizeof = 8

内存对齐在编码中的具体操作

#pragma pack(push, 1)    // 保存当前设置,设为1字节对齐

struct FileListItem {
    uint32_t index;             // 偏移0,大小4
    uint32_t fileNameLength;    // 偏移4,大小4(紧接index)
    char fileName[0];           // 偏移8,大小0
};                             // sizeof = 8,无填充!

#pragma pack(pop)         // 恢复之前的设置

对比默认对齐(假设4字节):

struct FileListItem {
    uint32_t index;             // 偏移0
    uint32_t fileNameLength;    // 偏移4
    char fileName[0];           // 偏移8
};                             // sizeof = 8(这个例子没区别)

但如果有个 char 成员在中间就有区别了:

#pragma pack(push, 1)
struct BadExample {
    char type;            // 偏移0
    uint32_t index;       // 偏移1(紧接type,无填充!)
};                       // sizeof = 5

默认4字节对齐:
struct BadExample {
    char type;            // 偏移0
                         // 偏移1-3填充
    uint32_t index;       // 偏移4
};                       // sizeof = 8

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐