从gst的have-type信号,看signal new/connect/emit.

os posted @ 2013年8月22日 17:30 in GObject , 3145 阅读

gobjec相关学习文章的list.

 

上层: Caller (Signal connect + user callback )

下层: Callee (Signal New + Emission机制)

 

New signal:

 

gsttypefindelement.c:

  /**
   * GstTypeFindElement::have-type:
   * @typefind: the typefind instance
   * @probability: the probability of the type found
   * @caps: the caps of the type found
   *
   * This signal gets emitted when the type and its probability has
   * been found.
   */
  gst_type_find_element_signals[HAVE_TYPE] = g_signal_new ("have-type",
      G_TYPE_FROM_CLASS (typefind_class), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (GstTypeFindElementClass, have_type), NULL, NULL,
      gst_marshal_VOID__UINT_BOXED, G_TYPE_NONE, 2,
      G_TYPE_UINT, GST_TYPE_CAPS | G_SIGNAL_TYPE_STATIC_SCOPE);

  typefind_class->have_type =
      GST_DEBUG_FUNCPTR (gst_type_find_element_have_type);

这里2个比较重要:

1. 指定类的成员函数:G_STRUCT_OFFSET (GstTypeFindElementClass, have_type),

    是通过偏移地址进行的,在后面的函数g_type_class_meta_marshal里面,会根据这个函数偏移地址offset, 提取出这个函数的指针

2. 指定c_marshal: gst_marshal_VOID__UINT_BOXED

 

跟着代码进去:

guint
g_signal_new (const gchar	 *signal_name,
	      GType		  itype,
	      GSignalFlags	  signal_flags,
	      guint               class_offset,
	      GSignalAccumulator  accumulator,
	      gpointer		  accu_data,
	      GSignalCMarshaller  c_marshaller,
	      GType		  return_type,
	      guint		  n_params,
	      ...)
{
  va_list args;
  guint signal_id;

  g_return_val_if_fail (signal_name != NULL, 0);
  
  va_start (args, n_params);

  signal_id = g_signal_new_valist (signal_name, itype, signal_flags,
                                   class_offset ? g_signal_type_cclosure_new (itype, class_offset) : NULL,
				   accumulator, accu_data, c_marshaller,
                                   return_type, n_params, args);

因为class_offset !=0, 所以会首先调用g_signal_type_cclosure_new (itype, class_offset),

GClosure*
g_signal_type_cclosure_new (GType    itype,
			    guint    struct_offset)
{
  GClosure *closure;
  
  g_return_val_if_fail (G_TYPE_IS_CLASSED (itype) || G_TYPE_IS_INTERFACE (itype), NULL);
  g_return_val_if_fail (struct_offset >= sizeof (GTypeClass), NULL);
  
  closure = g_closure_new_simple (sizeof (GClosure), (gpointer) itype);
  if (G_TYPE_IS_INTERFACE (itype))
    g_closure_set_meta_marshal (closure, GUINT_TO_POINTER (struct_offset), g_type_iface_meta_marshal);
  else
    g_closure_set_meta_marshal (closure, GUINT_TO_POINTER (struct_offset), g_type_class_meta_marshal);
  
  return closure;
}

g_closure_set_meta_marshal (closure, GUINT_TO_POINTER (struct_offset), g_type_class_meta_marshal);

void
g_closure_set_meta_marshal (GClosure       *closure,
			    gpointer        marshal_data,
			    GClosureMarshal meta_marshal)
{
  GClosureNotifyData *notifiers;

  g_return_if_fail (closure != NULL);
  g_return_if_fail (meta_marshal != NULL);
  g_return_if_fail (closure->is_invalid == FALSE);
  g_return_if_fail (closure->in_marshal == FALSE);
  g_return_if_fail (closure->meta_marshal == 0);

  notifiers = closure->notifiers;
  closure->notifiers = g_renew (GClosureNotifyData, NULL, CLOSURE_N_NOTIFIERS (closure) + 1);
  if (notifiers)
    {
      /* usually the meta marshal will be setup right after creation, so the
       * g_memmove() should be rare-case scenario
       */
      g_memmove (closure->notifiers + 1, notifiers, CLOSURE_N_NOTIFIERS (closure) * sizeof (notifiers[0]));
      g_free (notifiers);
    }
 

  //marshal_data ->  gst_type_find_element_have_type()  
  closure->notifiers[0].data = marshal_data;
 

  //mata_marshal --> g_type_class_meta_marshal
  closure->notifiers[0].notify = (GClosureNotify) meta_marshal;
  SET (closure, meta_marshal, 1);
}

注意,这个meta_marshal 函数暂时还不会被调用。

 

假设被调用,其实就是emit后,发生的动作:

先根据offset, 提取函数的指针:

static void
g_type_class_meta_marshal (GClosure       *closure,
			   GValue /*out*/ *return_value,
			   guint           n_param_values,
			   const GValue   *param_values,
			   gpointer        invocation_hint,
			   gpointer        marshal_data)
{
  GTypeClass *class;
  gpointer callback;
  /* GType itype = (GType) closure->data; */
  guint offset = GPOINTER_TO_UINT (marshal_data);
  
  class = G_TYPE_INSTANCE_GET_CLASS (g_value_peek_pointer (param_values + 0), itype, GTypeClass);
  callback = G_STRUCT_MEMBER (gpointer, class, offset);
  if (callback)
    closure->marshal (closure,
		      return_value,
		      n_param_values, param_values,
		      invocation_hint,
		      callback);
}

callback = G_STRUCT_MEMBER (gpointer, class, offset);
到这里,

callback = gst_type_find_element_have_type()= marshal_data ,这个函数指针会作为marshal_data,传入meta_marshal,

meta_marshal = g_type_class_meta_marshal()


static void
g_type_class_meta_marshal (GClosure       *closure,
			   GValue /*out*/ *return_value,
			   guint           n_param_values,
			   const GValue   *param_values,
			   gpointer        invocation_hint,
			   gpointer        marshal_data)
{
  GTypeClass *class;
  gpointer callback;
  /* GType itype = (GType) closure->data; */
  guint offset = GPOINTER_TO_UINT (marshal_data);
  
  class = G_TYPE_INSTANCE_GET_CLASS (g_value_peek_pointer (param_values + 0), itype, GTypeClass);
  callback = G_STRUCT_MEMBER (gpointer, class, offset);
  if (callback)
    closure->marshal (closure,
		      return_value,
		      n_param_values, param_values,
		      invocation_hint,
		      callback);
}

 

同时后面会继续进行初始化:

  signal_id = g_signal_newv (signal_name, itype, signal_flags,
			     class_closure, accumulator, accu_data, c_marshaller,
			     return_type, n_params, param_types);
  /* setup permanent portion of signal node */
  if (!node)
    {
      SignalKey key;
      
      signal_id = g_n_signal_nodes++;
      node = g_new (SignalNode, 1);
      node->signal_id = signal_id;
      g_signal_nodes = g_renew (SignalNode*, g_signal_nodes, g_n_signal_nodes);
      g_signal_nodes[signal_id] = node;
      node->itype = itype;
      node->name = name;
      key.itype = itype;
      key.quark = g_quark_from_string (node->name);
      key.signal_id = signal_id;
      g_signal_key_bsa = g_bsearch_array_insert (g_signal_key_bsa, &g_signal_key_bconfig, &key);
      g_strdelimit (name, "_", '-');
      node->name = g_intern_string (name);
      key.quark = g_quark_from_string (name);
      g_signal_key_bsa = g_bsearch_array_insert (g_signal_key_bsa, &g_signal_key_bconfig, &key);

      TRACE(GOBJECT_SIGNAL_NEW(signal_id, name, itype));
    }
  node->destroyed = FALSE;
  node->test_class_offset = 0;

  /* setup reinitializable portion */
  node->flags = signal_flags & G_SIGNAL_FLAGS_MASK;
  node->n_params = n_params;
  node->param_types = g_memdup (param_types, sizeof (GType) * n_params);
  node->return_type = return_type;
  node->class_closure_bsa = NULL;
  if (accumulator)
    {
      node->accumulator = g_new (SignalAccumulator, 1);
      node->accumulator->func = accumulator;
      node->accumulator->data = accu_data;
    }
  else
    node->accumulator = NULL;
  node->c_marshaller = c_marshaller;
  node->emission_hooks = NULL;
  if (class_closure)
    signal_add_class_closure (node, 0, class_closure);

先把C_marshaller保存下来:node->c_marshaller = c_marshaller;

这里node->c_marshaller: gst_marshal_VOID__UINT_BOXED

 

前面g_signal_type_cclosure_new (itype, class_offset)创建了class的closure, 并且指定了:

1. ) 指向class成员的gst_type_find_element_have_type(),这个估计是default handler.

2. )指定 class closure的meta marshal: g_type_class_meta_marshal()

下面这个函数,又给这个class closure指定了一个C Marshaller:

static void
signal_add_class_closure (SignalNode *node,
			  GType       itype,
			  GClosure   *closure)
{
  ClassClosure key;

  /* can't optimize NOP emissions with overridden class closures */
  node->test_class_offset = 0;

  if (!node->class_closure_bsa)
    node->class_closure_bsa = g_bsearch_array_create (&g_class_closure_bconfig);
  key.instance_type = itype;
  key.closure = g_closure_ref (closure);
  node->class_closure_bsa = g_bsearch_array_insert (node->class_closure_bsa,
						    &g_class_closure_bconfig,
						    &key);
  g_closure_sink (closure);
  if (node->c_marshaller && closure && G_CLOSURE_NEEDS_MARSHAL (closure))
    g_closure_set_marshal (closure, node->c_marshaller);
}

 

在后面调用时:

meta_marshal -->调用 C Marshaller --> 调用 callback类的成员函数(default handler)

此时:这个signal node节点手里握有3个API:

1. class的meta marshal: g_type_class_meta_marshal()

2. C marshaller: gst_marshal_VOID__UINT_BOXED

3. class的成员函数:gst_type_find_element_have_type()【default handler】

 

 

connect signal注册的user级别的callback, 会存放到handler list中;

Connect Signal:

很多地方可以connect那个signal:

比如:gst-typefind.c

  g_signal_connect (G_OBJECT (typefind), "have-type",
      G_CALLBACK (have_type_handler), &caps);
#define g_signal_connect(instance, detailed_signal, c_handler, data) \
    g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0)

这里注册C_handler, 可以说是上层用户自己的callback.

gulong
g_signal_connect_data (gpointer       instance,
		       const gchar   *detailed_signal,
		       GCallback      c_handler,
		       gpointer       data,
		       GClosureNotify destroy_data,
		       GConnectFlags  connect_flags)
{
  guint signal_id;
  gulong handler_seq_no = 0;
  GQuark detail = 0;
  GType itype;
  gboolean swapped, after;

  swapped = (connect_flags & G_CONNECT_SWAPPED) != FALSE;
  after = (connect_flags & G_CONNECT_AFTER) != FALSE;

  SIGNAL_LOCK ();
  itype = G_TYPE_FROM_INSTANCE (instance);
  signal_id = signal_parse_name (detailed_signal, itype, &detail, TRUE);
  if (signal_id)
    {
      SignalNode *node = LOOKUP_SIGNAL_NODE (signal_id);


	{
	  Handler *handler = handler_new (after);

	  handler_seq_no = handler->sequential_number;
	  handler->detail = detail;
	  handler->closure = g_closure_ref ((swapped ? g_cclosure_new_swap : g_cclosure_new) (c_handler, data, destroy_data));
	  g_closure_sink (handler->closure);
	  handler_insert (signal_id, instance, handler);
	  if (node->c_marshaller && G_CLOSURE_NEEDS_MARSHAL (handler->closure))
	    g_closure_set_marshal (handler->closure, node->c_marshaller);
	}
    }
  else
    g_warning ("%s: signal `%s' is invalid for instance `%p'", G_STRLOC, detailed_signal, instance);
  SIGNAL_UNLOCK ();

  return handler_seq_no;
}

1. 分配一个hanlder内存

2. 创建一个C closure, 并且绑定user 自己的callback

3. handler只有这个closure, 也就是拿住了用户的callback

4. 把hanlder, 插入到全局的handler list中去

5. 也把signal node的C marshal: gst_marshal_VOID__UINT_BOXED, 和handler的closure绑定。

此时:这个handler的closure掌握了:

1. User defined callback: have_type_handler()   【user defined handler】

2. C Marshal: gst_marshal_VOID__UINT_BOXED

 

从上面的2个步骤可以看出:

Signal的Class Closure 和Signal Handler的Closure的 C Marshaller

指向同一个:C Marshal: gst_marshal_VOID__UINT_BOXED

 

Signal Emit:

在某种条件下,会调用

  g_signal_emit (typefind, gst_type_find_element_signals[HAVE_TYPE], 0,
      probability, caps);

激发条件,暂时先不看。

 

看发射的过程,这里干了什么?如何和new/connect呼应起来?

void
g_signal_emit (gpointer instance,
	       guint    signal_id,
	       GQuark   detail,
	       ...)
{
  va_list var_args;

  va_start (var_args, detail);
  g_signal_emit_valist (instance, signal_id, detail, var_args);
  va_end (var_args);
}
void
g_signal_emit_valist (gpointer instance,
		      guint    signal_id,
		      GQuark   detail,
		      va_list  var_args)
{
  GValue *instance_and_params;
  GType signal_return_type;
  GValue *param_values;
  SignalNode *node;
  guint i, n_params;
  
  g_return_if_fail (G_TYPE_CHECK_INSTANCE (instance));
  g_return_if_fail (signal_id > 0);

  SIGNAL_LOCK ();
  node = LOOKUP_SIGNAL_NODE (signal_id);
  if (!node || !g_type_is_a (G_TYPE_FROM_INSTANCE (instance), node->itype))
    {
      g_warning ("%s: signal id `%u' is invalid for instance `%p'", G_STRLOC, signal_id, instance);
      SIGNAL_UNLOCK ();
      return;
    }
#ifndef G_DISABLE_CHECKS
  if (detail && !(node->flags & G_SIGNAL_DETAILED))
    {
      g_warning ("%s: signal id `%u' does not support detail (%u)", G_STRLOC, signal_id, detail);
      SIGNAL_UNLOCK ();
      return;
    }
#endif  /* !G_DISABLE_CHECKS */

  /* optimize NOP emissions */
  if (signal_check_skip_emission (node, instance, detail))
    {
      /* nothing to do to emit this signal */
      SIGNAL_UNLOCK ();
      /* g_printerr ("omitting emission of \"%s\"\n", node->name); */
      return;
    }

  n_params = node->n_params;
  signal_return_type = node->return_type;
  instance_and_params = g_slice_alloc0 (sizeof (GValue) * (n_params + 1));
  param_values = instance_and_params + 1;

  for (i = 0; i < node->n_params; i++)
    {
      gchar *error;
      GType ptype = node->param_types[i] & ~G_SIGNAL_TYPE_STATIC_SCOPE;
      gboolean static_scope = node->param_types[i] & G_SIGNAL_TYPE_STATIC_SCOPE;

      SIGNAL_UNLOCK ();
      G_VALUE_COLLECT_INIT (param_values + i, ptype,
			    var_args,
			    static_scope ? G_VALUE_NOCOPY_CONTENTS : 0,
			    &error);
      if (error)
	{
	  g_warning ("%s: %s", G_STRLOC, error);
	  g_free (error);

	  /* we purposely leak the value here, it might not be
	   * in a sane state if an error condition occoured
	   */
	  while (i--)
	    g_value_unset (param_values + i);

	  g_slice_free1 (sizeof (GValue) * (n_params + 1), instance_and_params);
	  return;
	}
      SIGNAL_LOCK ();
    }
  SIGNAL_UNLOCK ();
  instance_and_params->g_type = 0;
  g_value_init (instance_and_params, G_TYPE_FROM_INSTANCE (instance));
  g_value_set_instance (instance_and_params, instance);
  if (signal_return_type == G_TYPE_NONE)
    signal_emit_unlocked_R (node, detail, instance, NULL, instance_and_params);
  else
    {
      GValue return_value = { 0, };
      gchar *error = NULL;
      GType rtype = signal_return_type & ~G_SIGNAL_TYPE_STATIC_SCOPE;
      gboolean static_scope = signal_return_type & G_SIGNAL_TYPE_STATIC_SCOPE;
      
      g_value_init (&return_value, rtype);

      signal_emit_unlocked_R (node, detail, instance, &return_value, instance_and_params);

      G_VALUE_LCOPY (&return_value,
		     var_args,
		     static_scope ? G_VALUE_NOCOPY_CONTENTS : 0,
		     &error);
      if (!error)
	g_value_unset (&return_value);
      else
	{
	  g_warning ("%s: %s", G_STRLOC, error);
	  g_free (error);
	  
	  /* we purposely leak the value here, it might not be
	   * in a sane state if an error condition occured
	   */
	}
    }
  for (i = 0; i < n_params; i++)
    g_value_unset (param_values + i);
  g_value_unset (instance_and_params);
  g_slice_free1 (sizeof (GValue) * (n_params + 1), instance_and_params);
}

1. 把输入的C参数list放到一个GValue, Collect 中

2. 调用signal_emit_unblocked_R(), 并且GValue参数作为最后一个参数

3. 把返回的值再弄回C语言的形式。

 

下面重点看看:signal_emit_unblocked_R(),

在这个函数里面,会做如下的动作:

1. 找到class closure ( callback [default handler] + c_marshaller)

2. 找到别人注册到该信号的handlers (User callback + c_marshaller)

之后就是根据条件进行回调的处理:

有3个重要的条件:

1. if ((node->flags & G_SIGNAL_RUN_FIRST) && class_closure)  //这个处理class自己注册的handler, 一般是指定好的成员函数

    if ((node->flags & G_SIGNAL_RUN_LAST) && class_closure)

2. if (node->emission_hooks)

3. if (handler_list)   //这个处理用g_signal_connect_xxx()注册的用户自己的callback (user defined handlers)

 

先看第一个:

  if ((node->flags & G_SIGNAL_RUN_FIRST) && class_closure)
    {
      emission.state = EMISSION_RUN;

      emission.chain_type = G_TYPE_FROM_INSTANCE (instance);
      SIGNAL_UNLOCK ();
      g_closure_invoke (class_closure,
			return_accu,
			node->n_params + 1,
			instance_and_params,
			&emission.ihint);
      if (!accumulate (&emission.ihint, emission_return, &accu, accumulator) &&
	  emission.state == EMISSION_RUN)
	emission.state = EMISSION_STOP;
      SIGNAL_LOCK ();
      emission.chain_type = G_TYPE_NONE;
      return_value_altered = TRUE;
      
      if (emission.state == EMISSION_STOP)
	goto EMIT_CLEANUP;
      else if (emission.state == EMISSION_RESTART)
	goto EMIT_RESTART;
    }
void
g_closure_invoke (GClosure       *closure,
		  GValue /*out*/ *return_value,
		  guint           n_param_values,
		  const GValue   *param_values,
		  gpointer        invocation_hint)
{
  g_return_if_fail (closure != NULL);

  g_closure_ref (closure);      /* preserve floating flag */
  if (!closure->is_invalid)
    {
      GClosureMarshal marshal;
      gpointer marshal_data;
      gboolean in_marshal = closure->in_marshal;

      g_return_if_fail (closure->marshal || closure->meta_marshal);

      SET (closure, in_marshal, TRUE);
      if (closure->meta_marshal)
	{
	  marshal_data = closure->notifiers[0].data;
	  marshal = (GClosureMarshal) closure->notifiers[0].notify;
	}
      else
	{
	  marshal_data = NULL;
	  marshal = closure->marshal;
	}
      if (!in_marshal)
	closure_invoke_notifiers (closure, PRE_NOTIFY);
      marshal (closure,
	       return_value,
	       n_param_values, param_values,
	       invocation_hint,
	       marshal_data);
      if (!in_marshal)
	closure_invoke_notifiers (closure, POST_NOTIFY);
      SET (closure, in_marshal, in_marshal);
    }
  g_closure_unref (closure);
}

前面在New Signal的时候,就已经创建了class closure, 并且注册了:

marshal_data = gst_type_find_element_have_type()

mashal = g_type_class_meta_marshal()//这个mashal是我们指定的meta mashaller

所以会执行下面的:

      marshal (closure,
	       return_value,
	       n_param_values, param_values,
	       invocation_hint,
	       marshal_data);

即:

static void
g_type_class_meta_marshal (GClosure       *closure,
			   GValue /*out*/ *return_value,
			   guint           n_param_values,
			   const GValue   *param_values,
			   gpointer        invocation_hint,
			   gpointer        marshal_data)
{
  GTypeClass *class;
  gpointer callback;
  /* GType itype = (GType) closure->data; */
  guint offset = GPOINTER_TO_UINT (marshal_data);
  
  class = G_TYPE_INSTANCE_GET_CLASS (g_value_peek_pointer (param_values + 0), itype, GTypeClass);
  callback = G_STRUCT_MEMBER (gpointer, class, offset);
  if (callback)
    closure->marshal (closure,
		      return_value,
		      n_param_values, param_values,
		      invocation_hint,
		      callback);
}

这个函数非常有意思:

1. 根据传入的marshal_data ( gst_type_find_element_have_typed的地址),取出其offset

2. 根据offset, 取出对应class中的成员函数:callback = G_STRUCT_MEMBER (gpointer, class, offset);

    提取的结果:callback = gst_type_find_element_have_typed()

3. 在把这个callback作为最后一个参数传入到closure的C marshal: gst_marshal_VOID__UINT_BOXED

 真正执行的是下面的函数:

gst_marshal_VOID_UINT_BOXED(closure,
		      return_value,
		      n_param_values, param_values,
		      invocation_hint,
		      gst_type_find_element_have_typed);

 

在gstmarshal.c中:

/* VOID:UINT,BOXED (./gstmarshal.list:19) */
void
gst_marshal_VOID__UINT_BOXED (GClosure     *closure,
                              GValue       *return_value G_GNUC_UNUSED,
                              guint         n_param_values,
                              const GValue *param_values,
                              gpointer      invocation_hint G_GNUC_UNUSED,
                              gpointer      marshal_data)
{
  typedef void (*GMarshalFunc_VOID__UINT_BOXED) (gpointer     data1,
                                                 guint        arg_1,
                                                 gpointer     arg_2,
                                                 gpointer     data2);
  register GMarshalFunc_VOID__UINT_BOXED callback;
  register GCClosure *cc = (GCClosure*) closure;
  register gpointer data1, data2;

  g_return_if_fail (n_param_values == 3);

  if (G_CCLOSURE_SWAP_DATA (closure))
    {
      data1 = closure->data;
      data2 = g_value_peek_pointer (param_values + 0);
    }
  else
    {
      data1 = g_value_peek_pointer (param_values + 0);
      data2 = closure->data;
    }
  callback = (GMarshalFunc_VOID__UINT_BOXED) (marshal_data ? marshal_data : cc->callback);

  callback (data1,
            g_marshal_value_peek_uint (param_values + 1),
            g_marshal_value_peek_boxed (param_values + 2),
            data2);
}

    {
      data1 = g_value_peek_pointer (param_values + 0);  //拿到输入参数list
      data2 = closure->data; // = (gpointer) itype, 在GClosure*g_closure_new_simple (guint           sizeof_closure,      gpointer        data)指定的
    }
  callback = (GMarshalFunc_VOID__UINT_BOXED) (marshal_data ? marshal_data : cc->callback);

  marshal_data != NULL, 所以这里的callback使用的是marshal_data: gst_type_find_element_have_typed

 

  gst_type_find_element_have_typed (参数GValue,
            g_marshal_value_peek_uint (param_values + 1),
            g_marshal_value_peek_boxed (param_values + 2),
            itype);

 

在gsttypefindelement.c

static void
gst_type_find_element_have_type (GstTypeFindElement * typefind,
    guint probability, const GstCaps * caps)
{
  GstCaps *copy;

  g_assert (caps != NULL);

  GST_INFO_OBJECT (typefind, "found caps %" GST_PTR_FORMAT ", probability=%u",
      caps, probability);

  GST_OBJECT_LOCK (typefind);
  if (typefind->caps)
    gst_caps_unref (typefind->caps);
  typefind->caps = gst_caps_copy (caps);
  copy = gst_caps_ref (typefind->caps);
  GST_OBJECT_UNLOCK (typefind);

  gst_pad_set_caps (typefind->src, copy);
  gst_caps_unref (copy);
}

 

      G_VALUE_LCOPY (&return_value,             var_args,             static_scope ? G_VALUE_NOCOPY_CONTENTS : 0,             &error);

这个是把GValue的值再转回C语言的形式。

 

gobjec相关学习文章的list.

Avatar_small
seo service london 说:
2024年2月21日 22:31

I came to your website for the first time I like your website very much such nice information providing much such


登录 *


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