3、GObject基础:GObject系统简介、类型系统、信号与槽、属性系统,为插件开发打基础
好,咱们进入正题。GObject 这套东西,说白了就是 GLib 给 C 语言穿上的一件「面向对象外衣」。你可能会问:C 语言本来没有 class、没有继承,GStreamer 为什么非要搞这么一套?
我刚开始接触 GStreamer 时也有这个疑惑。后来写了一个插件,发现要处理各种媒体格式、要动态创建元素、要响应状态变化……嗯,没有 GObject 这套基础设施,代码会乱成一锅粥。GStreamer 里几乎所有的东西——元素、垫(Pad)、总线(Bus)、缓冲区(Buffer)——都是 GObject 的子类。
3.1 GObject 系统简介
GObject 是 GLib 对象系统的核心。它提供了三个基本能力:
- 类型系统:运行时类型识别、继承、接口
- 信号与槽:对象间通信机制,类似 Qt 的信号槽
- 属性系统:对象的可配置参数,支持 get/set、通知
说白了,GObject 就是用 C 语言模拟了面向对象语言的三大特性:封装、继承、多态。你想想看,C 语言里没有 class 关键字,GObject 通过宏和约定来实现这些。
核心概念:GObject 的每个对象实例都包含一个 GObjectClass 结构体,这个结构体里存放的是「虚函数表」。子类可以覆盖父类的方法,实现多态。
3.2 类型系统:一切从 GType 开始
GType 是 GObject 类型系统的基石。每个 GObject 子类都有一个唯一的 GType ID,运行时通过这个 ID 来识别对象类型。
我记得第一次看 GStreamer 源码时,被各种 G_TYPE_* 宏搞得头晕。后来才明白,这些宏就是类型注册的入口。
看一个最简单的例子:
#include <glib-object.h>
// 定义一个结构体
typedef struct {
GObject parent;
gint my_data;
} MyObject;
// 定义对应的类结构体
typedef struct {
GObjectClass parent_class;
} MyObjectClass;
// 类型注册宏
#define MY_TYPE_OBJECT (my_object_get_type())
// 类型注册函数
GType my_object_get_type(void) {
static GType type = 0;
if (type == 0) {
static const GTypeInfo info = {
sizeof(MyObjectClass),
NULL, // base_init
NULL, // base_finalize
NULL, // class_init
NULL, // class_finalize
NULL, // instance_init
sizeof(MyObject),
0, // n_preallocs
NULL // instance_init
};
type = g_type_register_static(G_TYPE_OBJECT, "MyObject", &info, 0);
}
return type;
}
这段代码做了三件事:
- 定义实例结构体
MyObject和类结构体MyObjectClass - 实现
my_object_get_type()函数,这是类型注册的入口 - 调用
g_type_register_static()注册新类型
个人经验:我习惯把类型注册函数放在一个单独的头文件里,用宏 G_DECLARE_FINAL_TYPE 或 G_DECLARE_DERIVABLE_TYPE 来简化声明。GStreamer 插件开发中,99% 的情况用 G_DECLARE_FINAL_TYPE 就够了,因为插件元素通常不需要被继承。
3.3 信号与槽:对象间的「悄悄话」
信号与槽是 GObject 最强大的特性之一。它让对象之间可以解耦通信——发送者不需要知道接收者是谁,接收者也不需要轮询。
在 GStreamer 里,信号无处不在。比如 pad-added 信号,当动态 Pad 被创建时触发;state-changed 信号,当元素状态变化时触发。
定义一个信号需要三步:
// 1. 在 class_init 中注册信号
enum {
SIGNAL_MY_SIGNAL,
LAST_SIGNAL
};
static guint my_object_signals[LAST_SIGNAL] = { 0 };
static void
my_object_class_init(MyObjectClass *klass) {
my_object_signals[SIGNAL_MY_SIGNAL] =
g_signal_new("my-signal",
MY_TYPE_OBJECT,
G_SIGNAL_RUN_LAST,
0,
NULL, NULL,
g_cclosure_marshal_VOID__INT,
G_TYPE_NONE,
1,
G_TYPE_INT);
}
// 2. 发射信号
void
my_object_emit_signal(MyObject *self, gint value) {
g_signal_emit(self, my_object_signals[SIGNAL_MY_SIGNAL], 0, value);
}
// 3. 连接信号(在用户代码中)
g_signal_connect(my_object, "my-signal",
G_CALLBACK(on_my_signal_received), user_data);
这里有个坑,我曾经踩过:g_signal_new 的最后一个参数是参数类型列表,必须用 G_TYPE_* 宏指定。如果参数类型写错了,运行时不会报错,但回调函数收到的数据会是乱码。
避坑指南:我曾经在写一个音频处理插件时,把 G_TYPE_INT 写成了 G_TYPE_UINT,结果信号参数一直传不对。调试了整整一个下午才发现是类型不匹配。记住:信号参数类型必须和回调函数签名严格一致。
3.4 属性系统:对象的「可调旋钮」
属性系统让 GObject 对象有了可配置的参数。在 GStreamer 里,每个元素都有属性,比如 videotestsrc 的 pattern 属性、capsfilter 的 caps 属性。
属性系统的核心是 GParamSpec,它描述了属性的元数据:类型、取值范围、默认值、读写权限等。
实现属性系统需要四个步骤:
// 1. 在 class_init 中安装属性
enum {
PROP_0,
PROP_MY_DATA,
PROP_LAST
};
static GParamSpec *obj_properties[PROP_LAST] = { NULL };
static void
my_object_class_init(MyObjectClass *klass) {
obj_properties[PROP_MY_DATA] =
g_param_spec_int("my-data",
"My Data",
"The data value",
0, // 最小值
100, // 最大值
50, // 默认值
G_PARAM_READWRITE);
g_object_class_install_properties(
G_OBJECT_CLASS(klass), PROP_LAST, obj_properties);
}
// 2. 实现 get_property
static void
my_object_get_property(GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec) {
MyObject *self = MY_OBJECT(object);
switch (property_id) {
case PROP_MY_DATA:
g_value_set_int(value, self->my_data);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
break;
}
}
// 3. 实现 set_property
static void
my_object_set_property(GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec) {
MyObject *self = MY_OBJECT(object);
switch (property_id) {
case PROP_MY_DATA:
self->my_data = g_value_get_int(value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
break;
}
}
// 4. 在 class_init 中覆盖 get/set 方法
static void
my_object_class_init(MyObjectClass *klass) {
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
gobject_class->get_property = my_object_get_property;
gobject_class->set_property = my_object_set_property;
// ... 安装属性代码 ...
}
属性系统还有一个很实用的特性:属性通知。当你通过 g_object_set() 修改属性时,GObject 会自动发射 notify::property-name 信号。这样其他代码可以监听属性变化,而不需要轮询。
我的习惯:在 GStreamer 插件开发中,我会把所有可配置的参数都做成属性。这样用户可以通过 gst-launch-1.0 命令行直接设置,也可以通过 g_object_set() 在代码中设置。而且属性系统自动提供了参数验证和通知机制,比自己手写 getter/setter 省事多了。
3.5 三者的协作关系
类型系统、信号与槽、属性系统,这三者不是孤立的。它们共同构成了 GObject 的完整生态:
| 组件 | 作用 | 在 GStreamer 中的体现 |
|---|---|---|
| 类型系统 | 运行时类型识别、继承 | GstElement、GstPad 等类型的注册 |
| 信号与槽 | 对象间通信 | pad-added、state-changed 等事件 |
| 属性系统 | 对象参数配置 | 元素的 caps、bitrate 等参数 |
举个例子:当你在 GStreamer 中创建一个 videotestsrc 元素时,GObject 类型系统会分配内存并初始化实例;当你设置 pattern 属性时,属性系统会验证参数并更新内部状态;当属性变化时,信号系统会通知监听者。
嗯,这里要注意:GObject 的引用计数机制也很重要。每个 GObject 对象都有一个引用计数,当计数归零时自动销毁。GStreamer 里大量使用 gst_object_ref() 和 gst_object_unref() 来管理对象生命周期。如果你忘记 unref,就会造成内存泄漏——我早期写插件时就犯过这个错,一个晚上都在追查内存泄漏的源头。
好了,GObject 的基础就讲到这里。下一章我们会把这些知识用到实际的 GStreamer 插件开发中,到时候你会看到这些概念是如何落地的。