GObject 的信号机制——概览
GObject 信号机制——信号注册

GObject 子类对象的析构过程

Garfileo posted @ 2011年3月22日 11:30 in GObject 笔记 with tags glib GObject dispose finalize , 12872 阅读

在“GObject 的信号机制”文中,谈到 GObject 子类对象的析构过程分为两个阶段,第一阶段是 dispose,第二阶段是 finalize。之所以划分成两个阶段而不是一步到位的内存释放,一切皆因尴尬现实之所迫。

引用计数与引用循环

现在,我们都知道了 GObject 类及其子类的对象,其内存管理基于引用计数机制而实现。所谓基于引用计数的内存管理,可大致描述为:

  • 使用 g_object_new 函数进行对象实例化的时候,对象的引用计数为 1;
  • 每次使用 g_object_ref函数引用对象时,对象的引用计数便会增 1;
  • 每次使用 g_object_unref 函数为对象解除引用时,对象的引用计数便会减 1;
  • 在 g_object_unref 函数中,如果发现对象的引用计数为 0,那么则调用对象的析构函数释放对象所占用的资源。

GObjec 类及其子类对象不仅存在继承的关系,还存在互相包含的关系,例如有一个 GObject 子类对象 A 包含(引用)了另一个 GObject 子类对象 B(也就是说对象 B 是对象 A 的属性),而对象 B 有可能反过来又引用了对象 A,这样便构成了引用循环。对于这种情况,对象 A 的析构函数不可能一步到位彻底释放它所占用的资源,可以论证一下:

  • 假设对象 A 的析构函数为 a_deconstruct。
  • a_deconstruct 函数只能在对象 A 的引用计数为 0 的时候被 g_object_unref 函数调用。
  • 在 g_object_new 对象 A 的时候,由于 A 所包含的对象 B 有可能反过来引用 A,那么 A 的引用计数就有可能不是 1 而是 2。
  • 在 g_object_unref 对象 A 的时候,由于此时对象 A 的引用计数可能为 2,所以 g_object_unref 只是将 A 的引用计数减掉 1,而不会调用 a_deconstruct 函数。
  • 对于对象 A 的使用者,在不清楚对象 A 的内部实现时,他会天真的认为 g_object_new 与 g_object_unref 的配对就可以回收 A 的资源,但事实则不然。
  • 对象 A 的使用者如果多次调用 g_object_unref 最终可以引发 a_deconstruct 函数工作,从而实现对象 A 的资源释放,但是使用者并不清楚究竟该运行几次 g_object_unref 才能有这种效果,并且在对象 A 已经被析构的情况下,再次 g_object_unref 对象 A,那么就会出现段错误。

James Henstridge 的方案

为解决引用循环的问题,James Henstridge 给出了一个方案,那就是将 GObject 类及其子类对象的析构过程分为 dispose 阶段与 finalize 阶段。在 dispose 阶段,只解除对象 A 对其所有属性的引用,而在 finalize 阶段释放对象 A 所占用的资源。dispose 阶段可被重复执行多次,而 finalize 阶段仅被执行一次

但是,与其说 James Henstridge 给出了一个方案,不如说他给出了一种约定。因为这种方案在 C 语言中是不可能自动实现,它需要 GObject 库的使用者小心谨慎的保证在 dispose 与 finalize 阶段之间不会出现程序错误(通常是段错误)。

例如“GObject 的信号机制”文中的 MyFile 对象的 dispose 代码:

static void
my_file_dispose (GObject *gobject)
{
        MyFile *self        = MY_FILE (gobject);
        MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self);
        if (priv->file){
                g_io_channel_unref (priv->file);
                priv->file = NULL;
        }
        G_OBJECT_CLASS (my_file_parent_class)->dispose (gobject);
}

MyFile 对象的私有属性 file 是一个指向 GIOChannel 类型变量的指针,它与 GObject 库没有丝毫关系,其内存释放实际上可在 finalize 阶段执行,之所以将其放在 dispose 阶段执行,主要是想揭示 James Henstridge 所给出的约定。这种约定就是必须保证 my_file_dispose 可被无限次的执行而不出错,所以在 my_file_dispose 函数中添加了file 指针有效性判断与野指针消除的代码。

在 GObject 手册中,也有一个 dispose 的示例:

static void
maman_bar_dispose (GObject *gobject)
{
        MamanBar *self = MAMAN_BAR (gobject);
        if (self->priv->an_object)
        {
                g_object_unref (self->priv->an_object);
                self->priv->an_object = NULL;
        }
        
        G_OBJECT_CLASS (maman_bar_parent_class)->dispose (gobject);
}

从上述代码中可看出,MamanBar 对象的私有属性 an_object 是一个 GObject 子类对象,也需要指针有效性判断与野指针消除的代码,因为我们必须要保证对象的 dispose 函数可被多次执行。

那么,多次执行对象的 dispose 函数的任务是由 g_object_unref 来完成吗?

肯定不是。因为 g_object_unref 也不知道该执行多少次 dispose 函数才可以将引用循环打破。

实际上,James Henstridge 所给出的这个约定,并非是让 GObject 自身可以智能的破解引用循环,而是认为 GObject 之外的程序能够分析出引用循环的存在,并由它多次执行对象的 dispose 函数。

例如,基于 GObject 的类型管理与引用计数机制,可在 GObject 库之上建立一个内存回收功能的程序库,GObject 库自身的内存管理机制仅仅是方便上层的内存回收库的实现而已。至于 GObject 库的使用者,依然要像 C 语言程序员那样,兢兢业业的处理好每一块内存的分配与回收。

析构需要向上回溯

在 GObject 子类对象的 dispose 与 finalize 函数中,末尾都有一行比较类似的代码。例如 MyFile 对象的 dispose 函数,有:

G_OBJECT_CLASS (my_file_parent_class)->dispose (gobject);

MyFile 对象的 finalize 函数,有:

G_OBJECT_CLASS (my_file_parent_class)->finalize (gobject);

这两行代码主要是“请求” MyFile 父类对象进行析构。

因为 C 语言不是内建支持面向对象,所以继承需要从上至下的进行结构体包含,那么析构就除了要释放自身资源还需要引发父类对象的析构过程,这样才可以彻底消除整条继承链所占用的资源。

向上回溯析构,这还倒好理解,但是 G_OBJECT_CLASS (my_file_parent_class) 是什么玩意?在 MyFile 类的实现中,我们从未声明与定义过 my_file_parent_class 这个变量或者宏。

不,其实我们既声明了它,也定义了它,其中的玄机就在 my-file.c 源文件开始部分的 G_DEFINE_TYPE 宏之中。如果我们使用命令

gcc -E -P my-file.c > my-file-extend.c

将 my-file.c 中所有的宏进行展开,可以发现:

G_DEFINE_TYPE (MyFile, my_file, G_TYPE_OBJECT);

的展开代码为:

static void my_file_init (MyFile * self);
static void my_file_class_init (MyFileClass * klass);
static gpointer my_file_parent_class = ((void *) 0);

static void
my_file_class_intern_init (gpointer klass)
{
	my_file_parent_class = g_type_class_peek_parent (klass);
	my_file_class_init ((MyFileClass *) klass);
} 

GType
my_file_get_type (void)
{
	static volatile gsize g_define_type_id__volatile = 0;
	if (g_once_init_enter (&g_define_type_id__volatile)) {
		GType g_define_type_id =
		    g_type_register_static_simple (((GType) ((20) << (2))),
						   g_intern_static_string
						   ("MyFile"),
						   sizeof (MyFileClass),
						   (GClassInitFunc)
						   my_file_class_intern_init,
						   sizeof (MyFile),
						   (GInstanceInitFunc)
						   my_file_init,
						   (GTypeFlags) 0); { { {
		};
		}
		}
		g_once_init_leave (&g_define_type_id__volatile,
				   g_define_type_id);
	}
	return g_define_type_id__volatile;
};

可以看出 my_file_parent_class 是一个静态的全局指针,它在 my_file_class_intern_init 函数中指向 MyFile 类的父类结构体。另外,还可以看出 MyFile 类的类结构体初始化函数 my_file_class_init 是由 my_file_class_intern_init 函数调用的,而后者会被 g_object_new 函数调用。

在 my_file_class_init 函数调用之前,将 MyFile 类的父类结构体的地址保存为 my_file_parent_class 指针是有用的。因为我们在 MyFile 类的类结构体初始化函数 my_file_class_init 中覆盖了 MyFile 类所继承的父类结构体的 dispose 与 finaliz 方法:

static
void my_file_class_init (MyFileClass *klass)
{
        g_type_class_add_private (klass, sizeof (MyFilePrivate));
         
        GObjectClass *base_class = G_OBJECT_CLASS (klass);
        ... ... ...
        base_class->dispose      = my_file_dispose;
        base_class->finalize     = my_file_finalize;
        ... ... ...
}

而在 MyFile 对象的 dispose 与 finalize 函数中,我们需要将对象的析构向上回溯到其父类,这时如果直接从 MyFile 类的类结构体中提取父类结构体,那么就会出现 MyFile 对象的 dispose 与 finalize 函数的递归调用。由于预先保存了 MyFile 类的父类结构体地址,那么就可以保证回溯析构的顺利进行。

其实,我们做错了!?

但是 James Henstridge 的约定,不仅仅是要保证 dispose 函数可被多次执行,还要保证在 dispose 函数执行之后并且在 finalize 函数执行之前,程序不会出错。这意味着 dispose 函数不能影响对象的行为(方法)。

无论 MyFIle 类对象的 my_file_dispose 函数还是 GObject 手册中的 maman_bar_dispose 函数的实现,都不符合上述约定。

在 my_file_dispose 函数中,我们不仅释放了 GIOChannel 类型指针 file 所指向的内存区域,还消除了野指针。那么在执行 my_file_dispose 函数之后,显然所有要使用 file 指针的 MyFile 类对象的方法都会出现段错误。同理,maman_bar_dispose 函数也违背了 James Henstridge 的约定。

所以,在 MyFIle 类的设计中 my_file_dispose 函数应当是一个空函数,GObject 手册中的 maman_bar_dispose 函数也应当如此。

对于 GObject 子类对象的哪些属性应当在 dispose 函数中解除引用,哪些属性应当在 finalize 函数中进行资源释放,全面的判定准则就是:既要允许 dispose 函数可重复执行,还要不影响对象的方法。

因此,James Henstridge 的约定跟废话也差不了多少。因为在 dispose 函数中解除任何一个属性的引用都有可能破坏对象的方法!

转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com

Avatar_small
doyle 说:
2011年3月22日 16:33

最后一段真有趣,那么有后续的解决方法么?或者实际上大家在用的解决方法呢?

lemonhall 说:
2011年3月23日 12:56

.........................

那。。。VALA岂不是也是一样?

Avatar_small
Garfileo 说:
2011年3月23日 13:06

gobject 提供了 g_object_weak_ref/unref 来解决引用循环的问题,vala 利用了这个。

Avatar_small
pingf 说:
2011年3月23日 14:28

"至于 GObject 库的使用者,依然要像 C 语言程序员那样,兢兢业业的处理好每一块内存的分配与回收"
这句是亮点,当年就被GObject的介绍给骗了,后来发现不对劲,才知道被忽悠了......

lemonhall 说:
2011年3月23日 16:47

请详细介绍一下VALA的内存管理。。。

C我不是很熟悉,期待你下一篇的解药型文章~~

我是一个C#程序员~~~

lemonhall 说:
2011年3月23日 17:01

我仔细认真得看了:http://live.gnome.org/Vala/ReferenceHandling

很好的文章,但是仍然希望博主能更详细地讲解一下。

Avatar_small
pingf 说:
2011年3月23日 21:18

这篇文章出奇的好....

Avatar_small
pingf 说:
2011年3月23日 21:30

不过看完vala这篇文档,原先的那个感觉进一步得以印证
GObject的设计是为了方便更多高级语言的绑定,而不是为了方便直接用C来开发....

Avatar_small
Garfileo 说:
2011年3月23日 23:04

那个弱引用可以用于打破引用循环,道理很简单。因为弱引用不会增加对象的引用计数,这样引用循环就不存在了。

弱引用在 gobject 里最重要的用途是能够让引用者有机会知道,所引用的对象是否已经无效,这可以防止野指针的出现。详见 absurd 的一篇文档:http://blog.csdn.net/absurd/archive/2007/01/17/1486048.aspx

C 程序员是不畏惧内存管理的,所以有没有引用循环都无所谓。C 程序员害怕的只是段错误 :)

lemonhall 说:
2011年3月24日 07:49

同意LSSS的话,能不用C的地方我就尽全力不用C。

这几天已经积累了不少段错误的调试经验,作为一个C#程序员。

对内存管理也很讨厌~

当然,也许C程序员非常喜欢这种万事皆在我之手的感觉。

段错误是属于运行时错误,而我现在也发觉了野指针如此之容易出现。

===========
PS:这篇文章的确是质量很高

Avatar_small
pingf 说:
2011年3月25日 01:21

我的理解是段错误主要还是由于对内存控制能力不足造成的,
to ls
作为一个没事儿爱玩C的二手程序员,
能用C的地方就不用别的,能自己写代码就不用别人的库!

Avatar_small
doyle 说:
2011年3月28日 09:36

作为win上的c#程序员...
在工作中,
我现在很讨厌所有需要编译的语言...
所以,能用python的地方我都用python.

但是某些东西觉得有趣,所以会继续看下去.


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter