Glib之GObject简介(翻译)
GObject
GObject库是Glib库的动态类型系统实现,它实现了:
- 基于引用计数的内存管理
- 实例的构造和析构
- 通用的set/get的属性获取方法
- 简单易用的信号机制
对象实例化
所述g_object_new
的功能家族可用于实例化从GObject的基类型继承的任何的GType。所有这些函数都确保类和实例结构已经被GLib的类型系统正确地初始化,然后在一个或另一个地方调用用于的构造函数类方法:
- 调用
g_type_create_instance
分配并清空内存 - 根据构造参数初始化对象实例
虽然人们可以期望所有的类和实例成员(除了指向父母的字段)被设置为零,但是有些人认为明确地设置它们是一个好习惯。一旦所有施工操作完成并且构造器属性设置完毕,就调用构造的类方法。从GObject继承的对象被允许覆盖这个构造的类方法。以下示例显示了ViewerFile如何覆盖父项目的构建过程:
#define VIEWER_TYPE_FILE viewer_file_get_type ()
G_DECLARE_FINAL_TYPE(ViewerFile, viewer_file, VIEWER, FILE, GObject)
struct _ViewerFile
{
GObject parent_instance ;
/ *实例成员* /
};
/ *将创建viewer_file_get_type并设置viewer_file_parent_class * /
G_DEFINE_TYPE(ViewerFile, viewer_file, G_TYPE_OBJECT)
static void
viewer_file_constructed (GObject * obj)
{
/ *根据构造函数属性更新对象状态* /
/ *始终链接到父构造函数以完成对象
初始化。* /
G_OBJECT_CLASS(viewer_file_parent_class)->constructed(obj);
}
static void
viewer_file_class_init (ViewerFileClass * klass)
{
GObjectClass * object_class = G_OBJECT_CLASS(klass);
object_class->constructed = viewer_file_constructed;
}
static void
viewer_file_init (ViewerFile * self)
{
/ *初始化对象* /
}
如果用户用下面的方式实例化一个对象ViewerFile:
ViewerFile *file = g_object_new (VIEWER_TYPE_FILE, NULL);
如果这是这样一个对象的第一个实例化,那么 viewer_file_class_init
函数将在任何viewer_file_base_class_init
函数之后被调用。这将确保这个新对象的类结构被正确初始化,在这里viewer_file_class_init
预计会覆盖GObject
的类方法并设置类自己的方法。在上面的例子中,构造函数方法是唯一被覆盖的方法:它被设置为 viewer_file_constructor
。
一旦g_object_new
获得了一个初始化类结构的引用,它就调用它的构造函数方法来创建新对象的一个实例,如果构造函数已被覆盖viewer_file_class_init
,重写的构造函数必须链接到父项的构造函数,为了找到父类和链父类的构造函数,我们可以使用宏viewer_file_parent_class
为我们设置的指针G_DEFINE_TYPE
。
最后由链中最后一个构造函数调用g_object_constructor
。这个函数通过g_type_create_instance
分配对象的实例缓冲区,这时候如果注册了instance_init
函数,将会被调用。在instance_init
返回后,对象完成初始化,并允许用户调用其方法。当 g_type_create_instance
返回时,g_object_constructor
将设置构造属性(执行g_object_new时传入的参数),并返回到用户的构造函数。
上面描述的过程可能看起来有点复杂,但是可以通过下面的表格容易地总结,其中列出了调用的函数g_object_new
及其调用顺序:
// 下面只是伪代码,很多函数可能是都是调用链,例如g_object_constructor()等,这边都只使用最后调用的g_object_XX函数来代替
g_object_new
{
// 注册类信息
g_type_class_ref()
{
type_class_init_Wm()
{
g_object_base_class_init()
// 此处就是viewer_file_class_init
g_object_do_class_init()
}
}
g_object_new_internal()
{
// 构造函数被重载的情况
g_object_new_with_custom_constructor()
{
g_object_constructor()
{
g_type_create_instance()
{
// 此处就是viewer_file_init
instance_init()
}
}
// 此处就是viewer_file_constructed
g_object_constructed()
}
}
}
调用时间 | 函数调用 | 函数的参数 | 备注 |
---|---|---|---|
首次调用g_object_new创建目标类型 | 目标类型的base_class_init函数 | 从基类开始调用base_class_init,然后递归调用子类,直到到达目标类型。 | 从未在实践中使用,你不太可能会需要它。 |
首次调用g_object_new创建目标类型 | 目标类型的class_init函数 | 目标类型的类结构 | 在这里您应该确保初始化或重写类方法(即为每个类的方法分配其函数指针),并创建与对象关联的信号和属性。 |
首次调用g_object_new创建目标类型 | 接口的base_init函数 | 接口的vtable | - |
首次调用g_object_new创建目标类型 | 接口的interface_init函数 | 接口的vtable | - |
每次调用g_object_new创建目标类型 | 目标类型的类constructor方法:GObjectClass->constructor | 对象的实例 | 如果您需要以自定义的方式处理构造属性或者实现一个单例类,请重写构造方法,并确保在执行自己的初始化之前链接到对象的父类。如果存在疑问,则不要重写构造方法。 |
每次调用g_object_new创建目标类型 | 目标类型的instance_init函数 | 从基类开始调用instance_init,然后递归调用子类,直到到达目标类型。 | 提供一个instance_init函数来初始化你的对象,在它的构造属性被设置之前。这是初始化GObject实例的首选方法。这个函数相当于C++的构造函数。 |
每次调用g_object_new创建目标类型 | 目标类型的类constructed方法:GObjectClass->constructed | 对象的实例 | 如果在所有构造属性设置完毕后需要执行对象初始化步骤。这是对象初始化过程的最后一步,只有当constructor方法返回一个新的对象实例(而不是现有的单例)时才被调用。 |
读者应该关心函数调用顺序的一点点转变:从技术上讲,类的构造函数方法是在 GType的instance_init 函数之前g_type_create_instance调用的(因为哪个调用instance_init是由g_object_constructor顶层类的构造方法调用的 ,哪些用户需要连接到),用户提供的构造函数中运行的代码将始终在 GType的instance_init函数之后运行,因为在执行任何有用的操作之前,用户提供的构造函数必须(您已经被警告)链接起来。
内存管理
引用计数
使用线程安全的g_object_ref()
/g_object_unref()
函数来增加和减少对象引用计数。调用g_object_new
后引用计数被初始化成1。当引用计数减为0时,g_object_unref
还将调用析构函数释放对象。
调用时间 | 涉及函数 | 函数参数 | 备注 |
---|---|---|---|
目标类型实例最后一次调用g_object_unref |
目标类型的dispose 函数 |
GObject实例 | 当废弃函数执行完成后,对象将不再拥有任何成员变量对象的引用(对象本身的内存未被释放),虽然还能够被使用(在析构函数被调用前),当然很可能会返回错误码,但是不会抛出内存异常。废弃函数可以被多次调用而不用担心会抛出异常。废弃函数在函数返回前要调用父类的废弃函数实现,保持调用链的完整。 |
目标类型的finalize 函数 |
GObject实例 | 析构函数将完成废弃函数的后续动作-释放对象内存。析构函数只能够被调用一次,并且同废弃函数一样,需要在函数返回前调用父类的析构函数实现。 | |
目标类型的最后一个实例最后一次调用g_object_unref |
接口的interface_finalize 函数 |
接口的虚表vtable | 不要在实践中使用,除非有特殊需要 |
接口的base_finalize 函数 |
接口的虚表vtable | 不要在实践中使用,除非有特殊需要 | |
接口的class_finalize 函数 |
目标类型的类结构 | 不要在实践中使用,除非有特殊需要 | |
接口的base_finalize 函数 |
从基础类型到目标类型的的继承树上的每个类结构,都调用一次base_finalize |
不要在实践中使用,除非有特殊需要 |
弱引用
弱引用通常用来监视对象的析构,通过g_object_weak_ref
添加一个在对象析构时被调用的监控回调函数,这样就可以在不调用g_object_ref
的情况下安全的保存一个对象的指针。
void g_object_weak_ref(GObject *object, // 需要建立弱引用的GObject对象
GWeakNotify notify, // 对象被释放前需要调用的回调函数
gpointer data); // 传递给回调函数的参数
void (*GWeakNotify)(gpointer data, // 弱连接建立时传入的数据,一般是希望保存对象指针的GObject对象
GObject *where_the_object_was); // 被析构的弱引用的GObject对象
消息系统
闭包
闭包是在GTK+和GNOME应用中用来表示回调函数的一种通用的抽象方法,闭包结构主要包括三个内容:
- 回调函数本身的函数指针,如下:
return_type function_callback (… , gpointer user_data);
- 传递给回调函数的用户数据指针
user_data
- 闭包的析构函数
一个闭包会提供如下简单的服务:
- 闭包的调用
g_closure_invoke
:对调用者隐藏回调函数调用细节 - 通知:闭包会通知监听者某些事件,例如闭包的调用、失效以及终止。通过
g_closure_add_finalize_notifier
函数注册监听终止通知、g_closure_add_invalidate_notifier
函数注册监听失效通知,以及g_closure_add_marshal_guards
函数注册监听调用通知。通过g_closure_remove_finalize_notifier
和g_closure_remove_invalidate_notifier
函数可移除监听。
C 语言闭包
如果想使用C或C++关联回调函数到某个事件上,可使用GCClosures提供的最简单的API函数g_signal_connect
。
g_cclosure_new
/g_cclosure_new_swap
将会创建一个调用用户自定义回调函数的闭包,并且使用用户提供的数据作为回调函数入参。闭包终止后使用destroy_data
函数进行析构。
Non-C 语言闭包
闭包隐藏了回调函数调用的细节。在C语言中,回调函数的调用就和函数调用很类似:就是为调用函数创建正确的堆栈,然后执行汇编指令。
C闭包方法将表示函数参数的GValues数组转换成C类型的函数参数列表,然后调用用户提供的C函数,获取函数的返回值并将其转换成GValue类型返回给方法调用者。下面就是一个简单的闭包例子:
g_cclosure_marshal_VOID__INT (GClosure *closure,
GValue *return_value,
guint n_param_values,
const GValue *param_values,
gpointer invocation_hint,
gpointer marshal_data)
{
typedef void (*GMarshalFunc_VOID__INT) (gpointer data1,
gint arg_1,
gpointer data2);
register GMarshalFunc_VOID__INT callback;
register GCClosure *cc = (GCClosure*) closure;
register gpointer data1, data2;
g_return_if_fail (n_param_values == 2);
data1 = g_value_peek_pointer (param_values + 0);
data2 = closure->data;
callback = (GMarshalFunc_VOID__INT) (marshal_data ? marshal_data : cc->callback);
callback (data1,
g_marshal_value_peek_int (param_values + 1),
data2);
}
信号
GObject的信号和UNIX系统的信号没有任何关系。
信号的注册
我们通常使用g_signal_newv
、 g_signal_new_valist
和g_signal_new
来注册信号:
guint g_signal_newv (const gchar *signal_name,
GType itype,
GSignalFlags signal_flags,
GClosure *class_closure,
GSignalAccumulator accumulator,
gpointer accu_data,
GSignalCMarshaller c_marshaller,
GType return_type,
guint n_params,
GType *param_types);
参数名 | 说明 |
---|---|
signal_name | 信号的唯一字符串标识 |
itype | 触发信号的实例类型(g_signal_connect的第一个参数的类型) |
signal_flags | 定义关联到信号上的闭包的调用顺序 |
class_closure | 信号的默认闭包,如果它不为空,当信号被触发时候它将被调用,调用的时间取决于signal_flags(g_signal_new中使用G_STRUCT_OFFSET宏获取类成员函数作为默认闭包) |
accumulator | 一个函数指针,每个闭包被调用后都会执行此函数,如果函数返回FALSE,信号发射将停止,否则继续。它通常可以用来统计与信号关联的闭包的调用返回值。 |
accumulator_data | accumulator函数的入参 |
c_marshaller | 关联到此信号的回调方法类型(方法的返回值和参数类型列表) |
return_type | 信号的返回类型 |
n_params | c_marshaller的参数个数 |
n_params | c_marshaller的参数类型列表 |
g_signal_new("session-display-removed", G_OBJECT_CLASS_TYPE(object_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_HOOKS, G_STRUCT_OFFSET(VirtViewerSessionClass, session_display_removed),
NULL,
NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE,
1,
VIRT_VIEWER_TYPE_DISPLAY);
signals[SPICE_MAIN_AGENT_GOT_REAL_RESOLUTION] =
g_signal_new("real-resolution",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_LAST ,
0, // Pass 0 to not associate a class method slot with this signal.(如果关联类方法,则设置为G_STRUCT_OFFSET(SpiceSessionClass, channel_new))
NULL, NULL,
g_cclosure_user_marshal_VOID__INT_INT_INT,
G_TYPE_NONE, //return type of handler
3, //the number of parameter types to follow
G_TYPE_INT, G_TYPE_INT, G_TYPE_INT // a list of types, one for each parameter
) ;
//============绑定类方法===============
struct _SpiceSessionClass
{
GObjectClass parent_class;
/* signals */
void (*channel_new)(SpiceSession *session, SpiceChannel *channel);
void (*channel_destroy)(SpiceSession *session, SpiceChannel *channel);
/*< private >*/
/*
* If adding fields to this struct, remove corresponding
* amount of padding to avoid changing overall struct size
*/
gchar _spice_reserved[SPICE_RESERVED_PADDING];
};
关联信号
如果你想关联一个闭包到信号上,你有三种选择:
- 在信号注册的时候注册一个类闭包,这是一个系统范围内的操作:类闭包在信号每次被触发时都被会调用,与触发信号的实例无关
- 使用
g_signal_override_class_closure
重写类闭包,可在信号的继承类型上调用此函数 - 使用
g_signal_connect
家族函数,这是一个实例范围的操作:只有当给定的实例触发信号时,才会调用闭包
使用g_signal_add_emission_hook
和g_signal_remove_emission_hook
可以创建全局的触发钩子,且与触发信号的实例无关
如果关联的函数的参数多于定义的信号函数,那么需要在关联的时候传入,例如:
### 信号声明
struct _SpiceSessionClass
{
GObjectClass parent_class;
/* signals */
void (*channel_new)(SpiceSession *session, SpiceChannel *channel);
void (*channel_destroy)(SpiceSession *session, SpiceChannel *channel);
/*< private >*/
/*
* If adding fields to this struct, remove corresponding
* amount of padding to avoid changing overall struct size
*/
gchar _spice_reserved[SPICE_RESERVED_PADDING];
};
### 处理函数声明
static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data);
### 关联信号
g_signal_connect(conn->session, "channel-new", G_CALLBACK(channel_new), conn);
信号的触发
使用g_signal_emit
家族函数来触发信号
void g_signal_emitv (const GValue *instance_and_params,
guint signal_id,
GQuark detail,
GValue *return_value);
参数名 | 说明 |
---|---|
instance_and_params | GValues数组保存的信号的参数列表,数组的首元素是触发信号的对象实例指针 |
signal_id | 被触发的信号标识 |
detail | 描述信号被触发的细节标识 |
return_value | 保存在没有accumulator情况下最后一个闭包调用放返回值 |
g_signal_emit_by_name(session, "session-display-added", display);
信号触发的五个阶段:
阶段 | 说明 |
---|---|
RUN_FIRST | 如果信号注册时使用了G_SIGNAL_RUN_FIRST标志,并且存在类闭包,那么类闭包将被调用 |
EMISSION_HOOK | 如果信号被关联了触发钩子,那么将按照关联顺序依次调用钩子函数 |
HANDLER_RUN_FIRST | 使用g_signal_connect 关联的闭包将按照关联时的顺序被依次调用 |
RUN_LAST | 如果信号注册时使用了G_SIGNAL_RUN_LAST标志,并且存在类闭包,那么类闭包将被调用 |
HANDLER_RUN_LAST | 使用g_signal_connect_after 关联的闭包如果没有在HANDLER_RUN_FIRST中被调用,那么将按照关联时的顺序被依次调用 |
RUN_CLEANUP | 如果信号注册时使用了G_SIGNAL_RUN_CLEANUP标志,并且存在类闭包,那么类闭包将被调用,信号发射到此为止 |
在信号发射的任意阶段(除RUN_CLEANUP外),任意闭包调用g_signal_stop_emission
方法,都将使发射直接进入RUN_CLEANUP阶段。
在信号发射的任意阶段,如果有闭包或钩子再次出发相同信号,都将使发射回到RUN_FIRST阶段。
accumulator函数在所有阶段(除了EMISSION_HOOK和RUN_CLEANUP)的闭包被调用后都会被执行,如果accumulator函数返回不为TRUE,都将使发射直接进入RUN_CLEANUP阶段。