GtkAboutDialog中的一个陷阱

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

       众所周知,在一个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。

Avatar_small
how to delete an ins 说:
2022年8月09日 08:02

Completely deleting your Instagram Account will get all your activity connection and activities through this account to be removed permanently, and also Comments, Likes, photos, Videos and your follower will be completely removed from the Instagram Account. how to delete an instagram account This process of deleting an Instagram Account is nowhere a recoverable process, thus if you want to get an account removed you may need to think twice and confirm, and make sure you have got the password for the Instagram Account that you want to delete from your MAC or Android, else you won’t be able to proceed further.


登录 *


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