JSON 和 Protocol Buffers (protobuf) 是两种完全不同风格和目标的序列化方案。
核心结论
JSON 是给人看的,Protobuf 是给机器用的。
JSON 追求的是可读性和通用性,而 Protobuf 追求的是性能、效率和强规范。
全方位对比表
| 特性 | JSON (JavaScript Object Notation) | Protocol Buffers (protobuf) |
|---|---|---|
| 本质 | 纯文本、人类可读的数据格式 | 二进制、机器高效的编码协议 |
| 数据大小 | 大(存储键名、括号、引号等冗余字符) | 非常小(通常比 JSON 小 3-10 倍) |
| 序列化/反序列化速度 | 慢(需要解析语法、转换类型) | 非常快(直接按预定义格式编码/解码) |
| 可读性 | 高,无需工具即可阅读和编辑 | 低,二进制格式,人眼无法直接理解 |
| schema(模式) | 可选的、弱约束(如 JSON Schema) | 强制的、强类型(.proto 文件是合同) |
| 数据类型 | 基础类型(字符串、数字、布尔、null、数组、对象) | 丰富的标量类型(int32, double, bool, enum, bytes 等) |
| 向后/向前兼容 | 需要手动处理,容易出错 | 原生支持,通过字段编号的规则 |
| 语言支持 | 几乎所有编程语言 | 官方支持主流语言,其他语言有社区支持 |
| 网络开销 | 高 | 极低 |
直观示例:对比数据表示
假设我们需要表示一个用户信息:
1. JSON 表示
{
"userId": 12345,
"name": "张三",
"email": "zhangsan@example.com",
"age": 30,
"isVerified": true
}
大小:约 120 字节(可以看到,键名 "userId", "name" 等重复存储,还有引号、括号等结构字符)。
2. Protobuf 表示
首先,必须定义一个 .proto 模式文件:
// 1. 定义消息结构
message User {
int32 user_id = 1; // 字段编号,不是值!
string name = 2;
string email = 3;
optional int32 age = 4; // optional 表示可选字段
bool is_verified = 5;
}
然后,在代码中序列化的二进制数据大概是这样的(十六进制表示,仅为示意):
08 B9 60 12 06 E5 BC A0 E4 B8 89 1A 16 7A 68 61 6E 67 73 61 6E 40 65 78 61 6D 70 6C 65 2E 63 6F 6D 20 1E 28 01
大小:约 30 字节。它不存储字段名 "user_id",只存储字段编号 1 和对应的值 12345。
深度解析核心区别
1. 模式(Schema) vs 无模式(Schema-less)
这是最根本的设计哲学差异。
- JSON:是自描述的。数据本身和它的结构(键值对)在一起。你可以直接拿到一个 JSON 文件开始解析,无需提前知道它的具体结构。这很灵活,但也容易因结构不明确而产生错误。
- Protobuf:是契约优先的。你必须先严格地定义一个
.proto文件,明确每个字段的名称、类型和一个唯一的编号。代码是根据这个“合同”生成的。这保证了前后端数据模型的一致性。
2. 编码方式:文本 vs 二进制
- JSON:使用文本文档格式(通常是 UTF-8 编码)。优点是通用、可读;缺点是冗余信息多,解析需要分词、语法分析。
- Protobuf:使用复杂的二进制编码规则(如 Varint 编码、ZigZag 编码),用尽可能少的字节表示数据。例如,数字
12345会用 Varint 编码成 1-2 个字节,而不是 JSON 中的 5 个字符。优点是极致紧凑,解码速度极快(几乎就是内存拷贝)。
3. 兼容性设计
- JSON:添加新字段很容易,但删除或重命名字段可能破坏客户端。兼容性需要开发者自己通过代码逻辑保证。
- Protobuf:兼容性是一等公民。
- 规则:不能重用已使用的字段编号,不能更改字段的类型。新增的字段必须是
optional或repeated。 - 原理:当新版本的解析器遇到旧数据中不存在的字段(通过字段编号识别)时,会直接忽略;旧版本的解析器遇到新字段时也会忽略。这完美实现了向后/向前兼容。
- 规则:不能重用已使用的字段编号,不能更改字段的类型。新增的字段必须是
在证券/金融场景下的选型
适用 JSON 的场景:
- RESTful API:与外部系统、合作伙伴或 Web 前端交互。JSON 的可读性对调试、文档化非常友好。
- 例如:
GET /api/stock/AAPL/profile返回公司简介信息。
- 例如:
- 配置文件:App 的设置、功能开关等。
- 日志记录:便于开发人员直接阅读日志文件进行分析。
适用 Protobuf 的场景:
- 高性能微服务间通信:在证券系统内部,各个服务(如行情服务、交易服务、风控服务)之间需要高速、低延迟的通信,Protobuf 是首选。
- 实时行情数据推送:
- 痛点:行情数据量巨大,每秒可能有数万次更新。使用 JSON 会占用大量带宽和 CPU。
- 优势:Protobuf 极小的体积和极快的序列化速度,可以降低延迟、减少服务器负载。例如,一个订单簿的增量更新,用 Protobuf 可能只有几十字节,而 JSON 可能要几百字节。
- 移动端 App 与服务器的通信:
- 为了节省用户的流量并加快数据加载速度,所有重要的 API(尤其是高频调用的行情、资产更新接口)都应使用 Protobuf。
- 数据持久化:将结构化的数据(如 K 线数据、交易记录)以 Protobuf 格式存入磁盘或数据库,可以节省大量存储空间。
“JSON 和 Protobuf 最核心的区别在于设计目标不同。JSON 追求通用性和可读性,是一种文本格式;而 Protobuf 追求极致的性能和效率,是一种二进制协议。
具体体现在:
- 性能:Protobuf 的序列化速度更快,生成的数据体积更小,通常在3到10倍的差距,这对高并发和移动网络环境至关重要。
- 模式:Protobuf 要求强制的模式定义(.proto 文件),这保证了接口的稳定性和类型安全,并原生支持版本兼容。
- 可读性:JSON 的优势在于人类可读,易于调试。
在我们的证券App中,会对它们进行混合使用:
- 对外的、不要求极高性能的 管理类 API 使用 JSON。
- 对内的、要求低延迟高吞吐的 核心业务接口(如实时行情、交易指令)会使用 Protobuf,以最大化性能并节省用户流量。”
核心流程概述
- 准备工作:将
.proto文件集成到项目中,并生成对应的 Objective-C 模型类。 - 接收数据:从网络或蓝牙接收到原始的二进制数据(
NSData)。 - 解析数据:使用 Protobuf 生成的类方法
parseFromData:error:将NSData反序列化成对象。 - 使用数据:像使用普通 Objective-C 对象一样,通过属性访问解析后的数据。
详细步骤与示例代码
第1步:定义 Protobuf 格式(.proto 文件)
首先,你需要拥有设备厂商提供的 Protobuf 格式定义文件。这是所有工作的基础。
例如,一个简化的扫地机器人状态上报 robot_status.proto 文件可能如下所示:
// 指定使用 proto3 语法
syntax = "proto3";
// 包名,会影响生成的 Objective-C 类前缀
package eufy_robot;
// 定义消息,最终会生成 Objective-C 类 ORobotStatus
message RobotStatus {
string device_id = 1; // 设备ID
int32 battery_level = 2; // 电池电量 (百分比)
CleaningMode cleaning_mode = 3; // 清扫模式
repeated float position = 4; // 当前坐标 [x, y]
bool is_charging = 5; // 是否在充电
ErrorCode error_code = 6; // 错误码
// 枚举定义:清扫模式
enum CleaningMode {
MODE_SILENT = 0; // 安静
MODE_STANDARD = 1; // 标准
MODE_TURBO = 2; // 强劲
}
// 枚举定义:错误码
enum ErrorCode {
ERROR_NONE = 0; // 无错误
ERROR_WHEEL_STUCK = 1; // 轮子被卡住
ERROR_DUST_BIN_FULL = 2; // 尘盒已满
}
}
第2步:生成 Objective-C 模型类
- 安装 Protobuf 编译器:
- 使用 Homebrew:
brew install protobuf
- 使用 Homebrew:
- 使用
protoc命令生成代码:- 在终端中执行以下命令:
protoc --objc_out=./output robot_status.proto--objc_out指定输出 Objective-C 文件到./output目录。
- 将生成的文件加入项目:
- 命令会生成两个文件:
RobotStatus.pbobjc.h和RobotStatus.pbobjc.m。 - 将这两个文件拖入你的 Xcode 项目中。
- 重要:在项目的 Build Settings 中,找到 Other Linker Flags,添加
-lprotobuf。
- 命令会生成两个文件:
第3步:在代码中解析数据
现在,你可以在接收到设备上报的二进制数据后,进行解析了。
// 在相应的类中导入生成的头文件
#import "RobotStatus.pbobjc.h"
// 假设这是一个处理接收到的设备数据的方法
- (void)didReceiveRobotData:(NSData *)binaryData {
// 参数 binaryData 是从蓝牙 (如 CBCharacteristic 的 value) 或网络接收到的原始二进制数据
NSError *error = nil;
// 核心步骤:使用生成的类解析二进制数据
ORobotStatus *robotStatus = [ORobotStatus parseFromData:binaryData error:&error];
if (error) {
NSLog(@"❌ Protobuf 解析失败: %@", error.localizedDescription);
return;
}
if (!robotStatus) {
NSLog(@"❌ 解析结果为空");
return;
}
// 解析成功,现在可以像使用普通对象一样访问数据
NSLog(@"====== 扫地机器人状态更新 ======");
NSLog(@"设备ID: %@", robotStatus.deviceId);
NSLog(@"电池电量: %d%%", robotStatus.batteryLevel);
NSLog(@"是否在充电: %@", robotStatus.isCharging ? @"是" : @"否");
// 访问枚举值
switch (robotStatus.cleaningMode) {
case ORobotStatus_CleaningMode_ModeSilent:
NSLog(@"清扫模式: 安静模式");
break;
case ORobotStatus_CleaningMode_ModeStandard:
NSLog(@"清扫模式: 标准模式");
break;
case ORobotStatus_CleaningMode_ModeTurbo:
NSLog(@"清扫模式: 强劲模式");
break;
default:
break;
}
// 访问 repeated 字段 (数组)
if (robotStatus.positionArray.count >= 2) {
float x = [robotStatus.positionArray valueAtIndex:0];
float y = [robotStatus.positionArray valueAtIndex:1];
NSLog(@"当前位置: (%.2f, %.2f)", x, y);
}
// 处理错误码
if (robotStatus.errorCode != ORobotStatus_ErrorCode_ErrorNone) {
NSLog(@"⚠️ 设备发生错误,错误码: %d", robotStatus.errorCode);
[self handleRobotError:robotStatus.errorCode];
}
// 最后,将解析出的数据传递给UI或业务逻辑层
[self updateUIWithRobotStatus:robotStatus];
}
// 更新UI的方法
- (void)updateUIWithRobotStatus:(ORobotStatus *)status {
// 确保在主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.batteryLevelLabel.text = [NSString stringWithFormat:@"%d%%", status.batteryLevel];
self.cleaningModeLabel.text = [self cleaningModeToString:status.cleaningMode];
// ... 更新其他UI组件
});
}
关键注意事项
-
线程安全:
- 解析操作是 CPU 密集型任务,不要在主线程执行。确保
parseFromData:error:在后台线程调用。
- 解析操作是 CPU 密集型任务,不要在主线程执行。确保
-
错误处理:
- 务必检查
error和返回的robotStatus对象是否为nil。解析失败的原因可能是数据损坏或与.proto定义不匹配。
- 务必检查
-
枚举类型命名:
- 生成的 Objective-C 枚举会带有消息类型名前缀,例如
CleaningMode会变成ORobotStatus_CleaningMode。
- 生成的 Objective-C 枚举会带有消息类型名前缀,例如
-
重复字段(数组):
- 对于
repeated字段,生成的是GPB*Array类型(如GPBFloatArray),需要使用类似valueAtIndex:的方法来访问元素。
- 对于
-
版本兼容性:
- 如果设备的
.proto定义更新了,你需要重新生成RobotStatus.pbobjc.h/.m文件并替换项目中的旧文件。
- 如果设备的
“在 Objective-C 中解析 Protobuf 数据主要分为三步:
-
代码生成:首先使用
protoc工具,根据设备厂商提供的.proto文件生成对应的 Objective-C 模型类。这会得到.pbobjc.h/.m文件,需要将它们和-lprotobuf链接标志添加到项目中。 -
数据解析:当从蓝牙或网络接收到二进制数据(
NSData)后,在后台线程调用生成的parseFromData:error:类方法进行反序列化。这一步是核心,会将二进制流转换成我们可以操作的对象。 -
数据使用:解析成功后,就可以像使用普通 Objective-C 对象一样,通过点语法访问其属性。对于枚举、基本类型和重复字段,生成的头文件里有对应的访问方式。
在整个过程中,要特别注意错误处理、线程管理和版本兼容性问题。”