1. 程式人生 > >GObject學習教程---第十章:GObject 的訊號機制——概覽

GObject學習教程---第十章:GObject 的訊號機制——概覽

本文是學習學習他人的部落格的心得(具體詳見“樓主見解”),如果源網站可訪問的話,建議直接訪問源網站:

樓主見解:

主要講解訊號機制的實現,分下邊幾步

第一:繼承GObject,在class的inite函式中,建立一個訊號file_changed.函式g_signal_new比較複雜參考 g_signal_new () 引數的解釋

 g_signal_new ("file_changed",
                      MY_TYPE_FILE,
                      G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                      0,
                      NULL,
                      NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE,
                      0);

第二:觸發訊號的函式,傳送訊號。

g_signal_emit_by_name(self, "file_changed");

第三:註冊訊號接收者,其中file就是上邊類的例項控制代碼。

static void
file_print (gpointer gobject, gpointer user_data)
{       
        g_printf ("invoking file_print!\n");
}
g_signal_connect (file, "file_changed", G_CALLBACK (file_print), NULL);

最後簡單介紹了解構函式的使用,具體類似於get_property/set_property,解構函式對應的函式指標是dispose\finalize,具體下一章介紹。

GObject 的訊號機制——概覽

手冊所述,GObject 訊號(Gignal)主要用於特定事件與響應者之間的連線,它與作業系統級中的訊號沒有什麼關係。例如,當我向一個檔案中寫入資料的時候,我期望能夠有一個或多個函式響應這個“向檔案寫入資料”的事件,這一期望便可基於 GObject 訊號予以實現。

為了更好的理解 GObject 訊號機制的內幕,我們需要從回撥函式開始。

基於回撥函式與可變引數的事件響應

首先,寫出事件的製造者,它是一個向檔案寫入資料的函式 file_write:

1

2

3

4

5

6

7

#include <stdio.h>

void

file_write (FILE *fp, const char *buffer)

{

fprintf (fp, "%s\n", buffer);

}

向檔案寫入資料完畢之後,我們希望有一個函式能夠將檔案全部的內容在終端打印出來,所以我們又增加了一個函式 file_print,並對 file_write 函式進行一點修改:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

void

file_print (FILE *fp)

{

char *line = NULL;

size_t len = 0;

ssize_t read;

while ((read = getline(&line, &len, fp)) != -1){

printf("%s", line);

}

free (line);

}

void

file_write (FILE *fp, const char *buffer)

{

fprintf (fp, "%s\n", buffer);

file_print (fp);

}

但是,作為設計者應當儘可能的考慮更多更復雜的變化。單純增加一個 file_print 函式,並在 file_write 函式中呼叫,固然可以實現“檔案變化時便通知 file_print 函式去執行列印任務”,但是這只是我們的一廂情願的想法,也許 file_write 函式的其他使用者希望在向檔案寫入資料後能夠將檔案內容以 XML、TeX 或者別的甚麼格式打印出來呢?

為了應對更多的使用者的需求,我們需要使用回撥函式來隔離變化,例如:

1

2

3

4

5

6

7

8

typedef void (*ChangedCallback) (FILE *fp);

void

file_write (FILE *fp, const char *buffer, ChangedCallback callback)

{

fprintf (fp, "%s\n", buffer);

callback (fp);

}

這樣,如果 file_write 的使用者僅需要在檔案內容發生變動後列印檔案的原始資料,那麼就可以將前文中的 file_print 函式作為引數傳遞於 file_write 函式。如果 file_write 的使用者希望在檔案內容發生變動後以 XML 格式列印檔案,那麼他可以寫一個 file_print_xml 函式並將其傳遞於 file_write 函式。

如果進一步考慮更多的變化,例如在 file_write 向檔案寫入資料後,我們希望能夠一舉“通知”檔案原始資料列印、XML 格式列印、TeX 格式列印等函式,這應當如何處理?如果使用 C 語言的可變引數功能,這個問題很好解決。例如,可以將 file_write 函式定義為:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

void

file_write (FILE *fp, const char *buffer, ...)

{

fprintf (fp, "%s\n", buffer);

va_list args;

ChangedCallback callback;

va_start (args, buffer);

while (1) {

callback = va_arg (args, ChangedCallback);

if (!callback)

break;

callback (fp);

}

va_end(args);

}

這樣,在使用 file_write 函式的時候,可傳遞多個函式供其呼叫,例如:

1

2

3

4

5

file_write (fp, "Hello world!",

file_print,

file_print_xml,

file_print_tex,

NULL);

基於回撥函式與可變引數實現特定“事件”的多個“響應”,這種方案是最有效的,但不是最好的。例如,受到函式棧空間的大小限制,可變引數用盡之時。此外,這種方式使用起來也不夠直觀。

基於 GObject 訊號的事件響應

對於上一節的示例所解決的問題,基於 GObjet 訊號的解決方案大致像下面這樣:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

void

file_write (File *self, const char *buffer)

{

/* 向檔案寫入資料 */

... ... ...

/* 發射“檔案改變了”這一訊號 */

g_signal_emit (self, CHANGED, 0);

}

int

main (void)

{

File *file = file_new ("test.txt");

g_signal_connect (file, "changed", file_print, NULL);

g_signal_connect (file, "changed", file_print_xml, NULL);

g_signal_connect (file, "changed", file_print_tex, NULL);

... ... ...

}

上述程式碼的含義如下:

  • 在 file_write 函式中,檔案資料寫入操作完畢後,就這一事件向外發射一個“CHANGED”訊號,告訴所有響應者,檔案內容改變了。至於哪些函式是這一訊號的響應者,file_write 函式不必知道。
  • file_write 函式的使用者,如果希望哪些函式用於響應 file_write 函式修改檔案內容這一事件,那麼就使用 g_signal_connect 函式(實際上它是一個巨集)將響應函式與訊號掛接到一起。這樣,一旦事件的對應訊號被 g_signal_emit 所發射,這些響應函式便會被自動呼叫。

為了實現上述的“訊號/響應”模擬,那麼 file_write 函式的引數便不可能再是 FILE 型別的檔案指標了,而是我們自定義的 File 型別的物件,其中封裝了“訊號/響應”功能。事實上,GObject 類的內部便封裝了這些功能,所有經由 GObject 子類化而產生的物件,便可擁有這些功能。

GObject 子類物件的訊號處理

首先,我們定義 GObject 子類 MyFile。這個過程,我們應當已經不再陌生,參考文件 [1]。

my-file.h 標頭檔案內容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

#ifndef MY_FILE_H

#define MY_FILE_H

#include <glib-object.h>

#define MY_TYPE_FILE (my_file_get_type ())

#define MY_FILE(object) G_TYPE_CHECK_INSTANCE_CAST ((object), MY_TYPE_FILE, MyFile)

#define MY_IS_FILE(object) G_TYPE_CHECK_INSTANCE_TYPE ((object), MY_TYPE_FILE))

#define MY_FILE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MY_TYPE_FILE, MyFileClass))

#define MY_IS_FILE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MY_TYPE_FILE))

#define MY_FILE_GET_CLASS(object) (\

G_TYPE_INSTANCE_GET_CLASS ((object), MY_TYPE_FILE, MyFileClass))

typedef struct _MyFile MyFile;

struct _MyFile {

GObject parent;

};

typedef struct _MyFileClass MyFileClass;

struct _MyFileClass {

GObjectClass parent_class;

};

GType my_file_get_type (void);

#endif

my-file.c 原始檔內容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

#include "my-file.h"

G_DEFINE_TYPE (MyFile, my_file, G_TYPE_OBJECT);

#define MY_FILE_GET_PRIVATE(object) (\

G_TYPE_INSTANCE_GET_PRIVATE ((object), MY_TYPE_FILE, MyFilePrivate))

typedef struct _MyFilePrivate MyFilePrivate;

struct _MyFilePrivate {

GString *name;

GIOChannel *file;

};

enum PropertyDList {

PROPERTY_FILE_0,

PROPERTY_FILE_NAME

};

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);

}

static void

my_file_finalize (GObject *gobject)

{      

MyFile *self        = MY_FILE (gobject);

MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self);

g_string_free (priv->name, TRUE);

G_OBJECT_CLASS (my_file_parent_class)->finalize (gobject);

}

static void

my_file_set_property (GObject *object, guint property_id,

const GValue *value, GParamSpec *pspec)

{

MyFile *self = MY_FILE (object);

MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self);

switch (property_id){

case PROPERTY_FILE_NAME:

if (priv->name)

g_string_free (priv->name, TRUE);

priv->name = g_string_new (g_value_get_string (value));

priv->file = g_io_channel_new_file (priv->name->str, "a+", NULL);

break;

default:

G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);

break;

}

}

static void

my_file_get_property (GObject *object, guint property_id,

GValue *value, GParamSpec *pspec)

{

MyFile *self = MY_FILE (object);

MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self);

switch (property_id){

case PROPERTY_FILE_NAME:

g_value_set_string (value, priv->name->str);

break;

default:

G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);

break;

}

}

static

void my_file_init (MyFile *self)

{

}

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->set_property = my_file_set_property;

base_class->get_property = my_file_get_property;

base_class->dispose      = my_file_dispose;

base_class->finalize     = my_file_finalize;

GParamSpec *pspec;

pspec = g_param_spec_string ("name",

"Name",

"File name",

NULL,

G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);

g_object_class_install_property (base_class, PROPERTY_FILE_NAME, pspec);

g_signal_new ("file_changed",

MY_TYPE_FILE,

G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,

0,

NULL,

NULL,

g_cclosure_marshal_VOID__VOID,

G_TYPE_NONE,

0);

}

void

my_file_write (MyFile *self, gchar *buffer)

{

MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self);

g_io_channel_write_chars (priv->file, buffer, -1, NULL, NULL);

g_io_channel_flush (priv->file, NULL);

g_signal_emit_by_name(self, "file_changed"); 

}

MyFile 類的使用者——main.c 檔案內容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

#include "my-file.h"

static void

file_print (gpointer gobject, gpointer user_data)

{      

g_printf ("invoking file_print!\n");

}

static void

file_print_xml (gpointer gobject, gpointer user_data)

{      

g_printf ("invoking file_print_xml!\n");

}

static void

file_print_tex (gpointer gobject, gpointer user_data)

{      

g_printf ("invoking file_print_tex!\n");

}

int

main (void)

{

g_type_init ();

MyFile *file = g_object_new (MY_TYPE_FILE, "name", "test.txt", NULL);

g_signal_connect (file, "file_changed", G_CALLBACK (file_print), NULL);

g_signal_connect (file, "file_changed", G_CALLBACK (file_print_xml), NULL);

g_signal_connect (file, "file_changed", G_CALLBACK (file_print_tex), NULL);

my_file_write (file, "hello world!\n");

g_object_unref (file);

return 0;

}

雖然 GObject 子類化以及物件私有屬性等知識均已有所介紹,但是上述的 MyFile 類的實現依然有許多細節需要加以解釋。

首先,是在 MyFile 類的類結構題初始化函式 my_file_class_init 中,除了設定類屬性之外,我們呼叫了 g_signal_new 函式用於建立 MyFile 型別與 "file_changed" 訊號的關聯。至於究竟是何種關聯,那不是我們所關心的!還有,g_signal_new 函式的引數有很多,很複雜,推薦閱讀文件 [2]。

其次,是 MyFile 物件的解構函式。在 my-file.c 原始檔中,函式 my_file_dispose 與 my_file_finalize 構成了 MyFile 物件的解構函式,前者用於解除 MyFile 物件對其它物件(是指那些具有引用計數且被 GObject 庫的型別系統所管理的物件)的引用,後者用於 MyFile 物件屬性的記憶體釋放。至於分何要分為兩個階段進行 GObject 子類物件析構以及相關細節知識,還是另外開一篇文章來討論吧,否則問題會被越搞越複雜。或者,也可閱讀文件 [3]。

小結

當我剛開始寫這篇文章的時候,我期望能夠理清 GObject 訊號與閉包的關係,但是現在不得不宣佈很失敗。還是冷靜幾天再捲土重來吧。

這篇文章,寫了一整天。現在我不得不告訴你,其實 GObject 真的很複雜。不過,從我向自己丟擲了第一個謊言之後,一直堅持到現在。儘管複雜,但是我們正在一點一點克服它。但是,最大的敵人不是 GObject,而是我自己。因為在這個過程中,我經常無法抗拒一種解剖 GObject 的慾望。它導致我經常陷入一個又一個的技術細節,而忘記了當初的目標。這種慾望之所以出現,是因為 GObject 是開源的,它賦予了我們每個人可以窺視它內部實現的權力。

我需要再次糾正一下認識。對於 GObject 牌的汽車,我現在只需要學習如何駕駛它,根本不需要去了解它的發動機是如何工作的。

參考文件

[1] 溫故而知新