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