GtkAboutDialog中的一个陷阱

过眼云烟 posted @ 2011年4月22日 16:57 in GTK+学习笔记 with tags gtk+ GtkAboutDialog,内存泄漏 , 2700 阅读

       众所周知,在一个GtkAboutDialog控件中,是可以用gtk_about_dialog_set_logo来设置logo的,其函数原型如下:

void                gtk_about_dialog_set_logo           (GtkAboutDialog *about,
                                                         GdkPixbuf *logo);

       显而易见,logo在这里是以GdkPixbuf控件的形式插入到GtkAboutDialog控件about中的,然而这里却暗藏了一个陷阱,而这个陷阱,有可能导致严重的内存泄漏。

       我们不妨以一下一段代码为例(编译为test_about_dialog):

#include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>

void show_about_dialog(GtkWidget *button);

int main(int argc,char **argv)
{
	GtkWidget *window;
	GtkWidget *button;
	
	gtk_init(&argc,&argv);

	window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
	
	button=gtk_button_new_with_label("Click me!");
	gtk_container_add(GTK_CONTAINER(window),button);
	
	g_signal_connect(G_OBJECT(button),"clicked",G_CALLBACK(show_about_dialog),NULL);
	g_signal_connect(G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),NULL);

	gtk_widget_show_all(window);
	
	gtk_main();

	return 0;
}

void show_about_dialog(GtkWidget *button)
{
	GtkWidget *about_dialog;
	GdkPixbuf *logo;
	
	about_dialog=gtk_about_dialog_new();
	logo=gdk_pixbuf_new_from_file("./test.png",NULL);
	gtk_about_dialog_set_logo(GTK_ABOUT_DIALOG(about_dialog),logo);
	
	gtk_dialog_run(GTK_DIALOG(about_dialog));

	gtk_widget_destroy(about_dialog);

	return;

}


       程序运行以后,出来一个窗体,这时我们打开系统监视器(如果你熟悉命令行的话,用top命令也可以),这时可以看到test_about_dialog运行时占用了约1.4M的内存:

       这时我们点一下"Click me"按钮,test_about_dialog占用的内存量上升到约3.9M:

       此时,我们关闭AboutDialog,然后再次点击"click me"按钮以打开AboutDialog,此时,test_about_dialog占用的内存量已经上升到了5.9M:

       多次重复这一操作,test_about_dialog占用的内存会越来越多,并且,每次关闭AboutDialog以后,内存占用量并不降低,显然,AboutDialog中出现了内存泄漏。

       而事实上,关键就在于这句代码:

	gtk_widget_destroy(about_dialog);

       按照惯例,这句代码将释放about_dialog即其子控件所占用的所有资源,然而事实呢?

       我们可以看一下gtk_widget_destroy的代码:

void
gtk_widget_destroy (GtkWidget *widget)
{
  g_return_if_fail (GTK_IS_WIDGET (widget));

  gtk_object_destroy ((GtkObject*) widget);
}

        显然,它调用了gtk_object_destroy,而gtk_object_destroy的代码如下:

void
gtk_object_destroy (GtkObject *object)
{
  g_return_if_fail (object != NULL);
  g_return_if_fail (GTK_IS_OBJECT (object));
  
  if (!(GTK_OBJECT_FLAGS (object) & GTK_IN_DESTRUCTION))
    g_object_run_dispose (G_OBJECT (object));
}

         而g_object_run_dispose代码如下:

void
g_object_run_dispose (GObject *object)
{
  g_return_if_fail (G_IS_OBJECT (object));
  g_return_if_fail (object->ref_count > 0);

  g_object_ref (object);
  TRACE (GOBJECT_OBJECT_DISPOSE(object,G_TYPE_FROM_INSTANCE(object), 0));
  G_OBJECT_GET_CLASS (object)->dispose (object);
  TRACE (GOBJECT_OBJECT_DISPOSE_END(object,G_TYPE_FROM_INSTANCE(object), 0));
  g_object_unref (object);
}

         而GtkAboutDialog的dispose函数继承自GtkDialog,而GtkDialog的dispose函数又继承自GtkWindow,而GtkWindow的dispose函数如下所示:

static void
gtk_window_dispose (GObject *object)
{
  GtkWindow *window = GTK_WINDOW (object);

  gtk_window_set_focus (window, NULL);
  gtk_window_set_default (window, NULL);

  G_OBJECT_CLASS (gtk_window_parent_class)->dispose (object);
}

         事实上,dispose函数将会自底向上一层层地执行下去,那究竟是哪个语句最终导致资源被释放呢?

         其实就是g_object_run_dispose中的这个语句:

  g_object_unref (object);

         他将使得object的ref_count降到零,进而调用dispose函数以及destroy函数,而真正负责释放资源的,是destroy函数。由destroy函数的特点知,该函数将会自底向上一层层地执行。而实现释放其子控件占用的资源的,是GtkContainer的destroy函数:

static void
gtk_container_destroy (GtkObject *object)
{
  GtkContainer *container = GTK_CONTAINER (object);

  if (GTK_CONTAINER_RESIZE_PENDING (container))
    _gtk_container_dequeue_resize_handler (container);

  if (container->focus_child)
    {
      g_object_unref (container->focus_child);
      container->focus_child = NULL;
    }

  /* do this before walking child widgets, to avoid
   * removing children from focus chain one by one.
   */
  if (container->has_focus_chain)
    gtk_container_unset_focus_chain (container);

  gtk_container_foreach (container, (GtkCallback) gtk_widget_destroy, NULL);

  GTK_OBJECT_CLASS (parent_class)->destroy (object);
}

         事实上,这段中真正负责销毁控件的,便是这句:

  gtk_container_foreach (container, (GtkCallback) gtk_widget_destroy, NULL);

         而gtk_container_foreach代码如下:

void
gtk_container_foreach (GtkContainer *container,
		       GtkCallback   callback,
		       gpointer      callback_data)
{
  GtkContainerClass *class;
  
  g_return_if_fail (GTK_IS_CONTAINER (container));
  g_return_if_fail (callback != NULL);

  class = GTK_CONTAINER_GET_CLASS (container);

  if (class->forall)
    class->forall (container, FALSE, callback, callback_data);
}

     从这里我们可以看出,gtk_container_destroy将对所有子控件调用gtk_widget_destroy使得它们的ref_count都降1,如果它们的ref_count都等于1的话,显然这些子控件将被销毁——由此可见,用gtk_widget_destroy销毁一个控件及其子控件是有条件的——他们的ref_count都必须等于1,而这一般都是可以满足的——如果你不单独对某个控件使用g_object_ref的话。更重要的是,forall的代码也明确的指出,子控件指的是加入到box中的children链表中的控件(即用gtk_box_pack_start等函数加入到box中的控件)。注意:这里的forall实质上是一个纯虚函数指针,因为GtkContainer控件中的forall指针为NULL,而该指针到box控件中将会被重载:

 container_class->forall = gtk_box_forall;

       而gtk_box_forall的代码如下:

static void
gtk_box_forall (GtkContainer *container,
		gboolean      include_internals,
		GtkCallback   callback,
		gpointer      callback_data)
{
  GtkBox *box = GTK_BOX (container);
  GtkBoxChild *child;
  GList *children;

  children = box->children;
  while (children)
    {
      child = children->data;
      children = children->next;

      if (child->pack == GTK_PACK_START)
	(* callback) (child->widget, callback_data);
    }

  children = g_list_last (box->children);
  while (children)
    {
      child = children->data;
      children = children->prev;

      if (child->pack == GTK_PACK_END)
	(* callback) (child->widget, callback_data);
    }
}

       那为什么对GtkAboutDialog控件about调用gtk_widget_destroy并不能销毁控件logo呢?答案已经很显然了,logo并不是GtkAboutDialog的子控件!所以,你如果对GtkAboutDialog控件about调用gtk_widget_destroy的话,并不能销毁logo控件!换句话说,logo控件仍然会占用相应的内存资源,而这正是内存泄漏出现的关键!

       那为什么logo控件不是GtkAboutDialog的子控件呢?

       这是因为把logo控件“加入”到窗体,实质是把它转成GtkImage控件logo_image,并调用gtk_box_pack_start把logo_image加入到GtkAboutDialog控件的窗体中实现的。相关代码如下:

void
gtk_about_dialog_set_logo (GtkAboutDialog *about,
                           GdkPixbuf      *logo)
{
  GtkAboutDialogPrivate *priv;

  g_return_if_fail (GTK_IS_ABOUT_DIALOG (about));

  priv = (GtkAboutDialogPrivate *)about->private_data;

  g_object_freeze_notify (G_OBJECT (about));

  if (gtk_image_get_storage_type (GTK_IMAGE (priv->logo_image)) == GTK_IMAGE_ICON_NAME)
    g_object_notify (G_OBJECT (about), "logo-icon-name");

  if (logo != NULL)
    gtk_image_set_from_pixbuf (GTK_IMAGE (priv->logo_image), logo);
  else
    {
      GList *pixbufs = gtk_window_get_default_icon_list ();

      if (pixbufs != NULL)
        {
          GtkIconSet *icon_set = icon_set_new_from_pixbufs (pixbufs);

          gtk_image_set_from_icon_set (GTK_IMAGE (priv->logo_image),
                                       icon_set, GTK_ICON_SIZE_DIALOG);

          gtk_icon_set_unref (icon_set);
          g_list_free (pixbufs);
        }
    }

  g_object_notify (G_OBJECT (about), "logo");

  g_object_thaw_notify (G_OBJECT (about));
}

         把logo_image加入到GtkAboutDialog控件中的相关代码如下:

  priv->logo_image = gtk_image_new ();
  gtk_box_pack_start (GTK_BOX (vbox), priv->logo_image, FALSE, FALSE, 0);

         从中也可以看出,logo_name才是GtkAboutDialog的子控件。因此,对GtkAboutDialog控件调用gtk_widget_destroy,是无法销毁gtk_about_dialog_set_logo中logo指针所指向的GdkPixbuf控件的。而在我们写的示例程序中,每次执行show_about_dialog函数,都会执行:

	logo=gdk_pixbuf_new_from_file("./test.png",NULL);

         而在此之前,logo指针所指向的资源并没有被释放,于是内存泄漏就产生了。

         为了解决这一问题,我们需要对logo控件手动调用g_object_unref,故修改后的代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>

void show_about_dialog(GtkWidget *button);

int main(int argc,char **argv)
{
	GtkWidget *window;
	GtkWidget *button;
	
	gtk_init(&argc,&argv);

	window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
	
	button=gtk_button_new_with_label("Click me!");
	gtk_container_add(GTK_CONTAINER(window),button);
	
	g_signal_connect(G_OBJECT(button),"clicked",G_CALLBACK(show_about_dialog),NULL);
	g_signal_connect(G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),NULL);

	gtk_widget_show_all(window);
	
	gtk_main();

	return 0;
}

void show_about_dialog(GtkWidget *button)
{
	GtkWidget *about_dialog;
	GdkPixbuf *logo;
	
	about_dialog=gtk_about_dialog_new();
	logo=gdk_pixbuf_new_from_file("./test.png",NULL);
	gtk_about_dialog_set_logo(GTK_ABOUT_DIALOG(about_dialog),logo);
	
	gtk_dialog_run(GTK_DIALOG(about_dialog));

	gtk_widget_destroy(about_dialog);
	g_object_unref(G_OBJECT(logo));

	return;

}

 

Avatar_small
pingf 说:
2011年4月25日 15:05

win32下官方编译版本中那个g_obejct_unref总是报warning,无论用G_OBJECT宏或是强制转换.......

Avatar_small
Mike Ma 说:
2011年7月23日 22:01

哇,还有这种陷阱!建议你给GTK官方提交一个BUG吧

Avatar_small
tolbKni 说:
2011年9月02日 08:53

发现很久不用 C,有一点生疏,作者可以向 GNOME 提交一个 Bug。


登录 *


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