天天看點

七十四、PHP核心探索:魔術函數與延遲綁定 ☞ 在某些特定的場景才會被觸發

PHP中有一些特殊的函數和方法,這些函數和方法相比普通方法的特殊之處在于:使用者代碼通常不會主動調用, 而是在特定的時機會被PHP自動調用。在PHP中通常以"__"打頭的方法都作為魔術方法, 是以通常不要定義以"__"開頭的函數或方法。 例如:__autoload()函數, 通常我們不會手動調用這個函數, 而如果在代碼中通路某個未定義的方法, 如過已經定義了__autoload()函數,此時PHP将會嘗試調用__autoload()函數, 例如在類的定義中如果定義了__construct()方法, 在初始化類的執行個體時将會調用這個方法, 同理還有__destuct()方法, 詳細内容請參考PHP手冊。

魔術函數和魔術方法

前面提到魔術函數和魔術方法的特殊之處在于這些方法(在這裡把函數和方法統稱方法)的調用時機是在某些特定的場景才會被觸發, 這些方法可以了解為一些事件監聽方法, 在事件觸發時才會執行。

根據前面的介紹, 魔術方法就是在類的某些場景下觸發的一些監聽方法。這些方法需要在類定義中進行定義, 在存儲上魔術方法自然存儲于類中, 而類在PHP内部是一個_zend_class_entry結構體,與普通方法一樣, 隻不過這些類不是存儲在類的函數表, 而是直接存儲在類結構體中:

  • 在_zend_class_entry結構體中的存儲位置不同;
  • 由ZendVM自動分情境進行調用;
  • 不是必須的,按需定義,自動調用

從以上三個方面可以發現,關于魔術變量的關鍵了解,主要集中在兩個方面:一,定義在哪裡; 二,如何判斷其存在并進行調用。

首先,魔術變量的存儲在_zend_class_entry中的代碼如下:

struct _zend_class_entry {
    ...
    //構造方法 __construct
    union _zend_function *constructor;
    //析構方法 __destruct
    union _zend_function *destructor;
    //克隆方法 __clone
    union _zend_function *clone;
    union _zend_function *__get;
    union _zend_function *__set;
    union _zend_function *__unset;
    union _zend_function *__isset;
    union _zend_function *__call;
    union _zend_function *__callstatic;
    union _zend_function *__tostring;
    //序列化
    union _zend_function *serialize_func;
    //反序列化
    union _zend_function *unserialize_func;
    ...
}      

這段代碼明确的在對象内部定義了不同的指針來儲存各種魔術變量。 關于Zend VM對魔術方法的調用機制,由于每種方法的調用情境不同,筆者在這裡也分開進行分析。

__construct

__construct構造方法,在對象建立時被自動調用。 與其它很多語言(如JAVA)不同的是,在PHP中,構造方法并沒有使用”與類定義同名“的約定方式,而是單獨用魔術方法來實作。 **__construct**方法的調用入口是new關鍵字對應的ZEND_NEW_SPEC_HANDLER函數。 Zend VM在初始化對象的時候,使用了new關鍵字,對其OPCODE進行分析後,使用GDB可以得到下面的堆棧資訊:

#0  ZEND_NEW_SPEC_HANDLER (execute_data=0x100d00080) at zend_vm_execute.h:461
#1  0x000000010041c1f0 in execute (op_array=0x100a1fd60) at zend_vm_execute.h:107
#2  0x00000001003e9394 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /Volumes/DEV/C/php-5.3.4/Zend/zend.c:1194
#3  0x0000000100368031 in php_execute_script (primary_file=0x7fff5fbff890) at /Volumes/DEV/C/php-5.3.4/main/main.c:2265
#4  0x00000001004d4b5c in main (argc=2, argv=0x7fff5fbffa30) at /Volumes/DEV/C/php-5.3.4/sapi/cli/php_cli.c:1193      

上面的椎棧資訊清晰顯示了new關鍵的調用過程,可以發現new關鍵字對應了ZEND_NEW_SPEC_HANDLER的處理函數, 在ZEND_NEW_SPEC_HANDLER中,Zend VM使用下面的代碼來擷取對象是否定義了__construct方法:

...
constructor = Z_OBJ_HT_P(object_zval)->get_constructor(object_zval TSRMLS_CC);
if (constructor == NULL){
    ...
} else {
    ...
}
 
//get_constructor的實作
ZEND_API union _zend_function *zend_std_get_constructor(zval *object TSRMLS_DC) 
{
    zend_object *zobj = Z_OBJ_P(object);
    zend_function *constructor = zobj->ce->constructor;
 
    if(constructor){ ... } else { ...}
    ...
}      

從上面的代碼可以看出ZendVM通過讀取zend_object->ce->constructor的值來判斷對象是不是定義的構造函數。

Z_OBJ_P(zval); Z_OBJ_P宏将一個zval類型變量構造為zend_object類型。

在判斷了__construct魔術變量存在之後,ZEND_NEW_SPEC_HANDLER中對目前EX(called_scope)進行了重新指派, 使ZEND_VM_NEXT_OPCODE();将opline指針指向__construct方法的op_array,開始執行__construct魔術方法

[c]
    EX(object) = object_zval;
    EX(fbc) = constructor;
    EX(called_scope) = EX_T(opline->op1.u.var).class_entry;
    ZEND_VM_NEXT_OPCODE();      

__destruct

__destruct是析構方法,運作于對象被顯示銷毀或者腳本關閉時,一般被用于釋放占用的資源。 __destruct的調用涉及到垃圾回收機制,在第七章中會有更詳盡的介紹。 本文筆者隻針對__destruct調用機制進行分析,其調用堆棧資訊如下:

//省略部分記憶體位址資訊後的堆棧:
#0  zend_call_function () at /..//php-5.3.4/Zend/zend_execute_API.c:767
#1  zend_call_method () at /..//php-5.3.4/Zend/zend_interfaces.c:97
#2  zend_objects_destroy_object () at /..//php-5.3.4/Zend/zend_objects.c:112
#3  zend_objects_store_del_ref_by_handle_ex () at /..//php-5.3.4/Zend/zend_objects_API.c:206
#4  zend_objects_store_del_ref () at /..//php-5.3.4/Zend/zend_objects_API.c:172
#5  _zval_dtor_func () at /..//php-5.3.4/Zend/zend_variables.c:52
#6  _zval_dtor () at zend_variables.h:35
#7  _zval_ptr_dtor () at /..//php-5.3.4/Zend/zend_execute_API.c:443
#8  _zval_ptr_dtor_wrapper () at /..//php-5.3.4/Zend/zend_variables.c:189
#9  zend_hash_apply_deleter () at /..//php-5.3.4/Zend/zend_hash.c:614
#10 zend_hash_reverse_apply () at /..//php-5.3.4/Zend/zend_hash.c:763
#11 shutdown_destructors () at /..//php-5.3.4/Zend/zend_execute_API.c:226
#12 zend_call_destructors () at /..//php-5.3.4/Zend/zend.c:874
#13 php_request_shutdown () at /..//php-5.3.4/main/main.c:1587
#14 main () at /..//php-5.3.4/sapi/cli/php_cli.c:1374      

__destruct方法存在與否是在zend_objects_destroy_object函數中進行判斷的。 在腳本執行結果時,ZendVM在php_request_shutdown階段會将對象池中的對象一一銷毀, 這時如果某對象定義了__destruct魔術方法,此方法便會被執行。

在zend_objects_destroy_object中,與__construct一樣, ZendVM判斷zend_object->ce->destructor是否為空,如果不為空,則調用zend_call_method執行__destruct析構方法。 進入__destruct的方式與__construct不同的是,__destruct的執行方式是由ZendVM直接調用zend_call_function來執行。

__call與__callStatic

  • __call:在對對象不存在的方法進行調用時自動執行;
  • __callStatic:在對對象不存在的靜态方法進行調用時自動執行;

__call與__callStatic的調用機制幾乎完全相同,關于函數的執行已經在上一章中提到, 使用者對函數的調用是由zend_do_fcall_common_helper_SPEC()方法進行處理的。

經過[ZEND_DO_FCALL_BY_NAME_SPEC_HANDLER]-> [zend_do_fcall_common_helper_SPEC]-> [zend_std_call_user_call]-> [zend_call_method]->[zend_call_function] 調用,經過zend_do_fcall_common_helper_SPEC的分發,最終使用zend_call_function來執行__call。      
經過[ZEND_DO_FCALL_BY_NAME_SPEC_HANDLER]-> [zend_do_fcall_common_helper_SPEC]-> [zend_std_callstatic_user_call]-> [zend_call_method]->[zend_call_function] 調用,經過zend_do_fcall_common_helper_SPEC的分發,最終使用zend_call_function來執行__callStatic。      

其他魔術方法

PHP中還有很多種魔術方法,它們的處理方式基本與上面類似,運作時執行與否取決的判斷根據, 最終都是_zend_class_entry結構體中對應的指針是否為空。 這裡列出它們的底層實作函數:

魔術方法 對應處理函數 所在源檔案
__set zend_std_call_setter() Zend/zend_object_handlers.c
__get zend_std_call_getter() Zend/zend_object_handlers.c
__isset zend_std_call_issetter() Zend/zend_object_handlers.c
__unset zend_std_call_unsetter() Zend/zend_object_handlers.c
__sleep php_var_serialize_intern() ext/standard/var.c
__wakeup php_var_unserialize() ext/standard/var_unserializer.c
__toString zend_std_cast_object_tostring() Zend/zend_object_handlers.c
__invoke ZEND_DO_FCALL_BY_NAME_SPEC_HANDLER() Zend/zend_vm_execute.h
__set_state php_var_export_ex() ext/standard/var.c
__clone ZEND_CLONE_SPEC_CV_HANDLER() Zend/zend_vm_execute.h

延遲綁定

從PHP 5.3.0開始,PHP增加了一個叫做後期靜态綁定的功能,用于在繼承範圍内引用靜态調用的類。 該功能從語言内部角度考慮被命名為“後期靜态綁定”。 “後期綁定”的意思是說,static::不再被解析為定義目前方法所在的類,而是在實際運作時計算的。 也可以稱之為”靜态綁定“,因為它可以用于(但不限于)靜态方法的調用。

延遲綁定的實作關鍵在于static關鍵字,如果以static調用靜态方法,則在文法解析時:

function_call:
...//省略若幹其它情況的函數調用
|   class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' { $4.u.opline_num = zend_do_begin_class_member_function_call(&$1, &$3 TSRMLS_CC); }
        function_call_parameter_list
        ')' { zend_do_end_function_call($4.u.opline_num?NULL:&$3, &$$, &$6, $4.u.opline_num, $4.u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
...//省略若幹其它情況的函數調用
 
class_name:
    T_STATIC { $$.op_type = IS_CONST; ZVAL_STRINGL(&$$.u.constant, "static", sizeof("static")-1, 1);}      
EX_T(opline->result.u.var).class_entry = zend_fetch_class(Z_STRVAL_P(class_name), 
Z_STRLEN_P(class_name), opline->extended_value TSRMLS_CC);