您好,登錄后才能下訂單哦!
本篇內容介紹了“PHP 7和PHP 5中的對象之間的差異”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
?? PHP 中的 class、interface、trait 在底層均以 zend_class_entry 結構體實現
struct _zend_class_entry { char type; const char *name; zend_uint name_length; struct _zend_class_entry *parent; int refcount; zend_uint ce_flags; HashTable function_table; HashTable properties_info; zval **default_properties_table; zval **default_static_members_table; zval **static_members_table; HashTable constants_table; int default_properties_count; int default_static_members_count; union _zend_function *constructor; union _zend_function *destructor; 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_class_iterator_funcs iterator_funcs; /* handlers */ zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC); zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC); int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* a class implements this interface */ union _zend_function *(*get_static_method)(zend_class_entry *ce, char* method, int method_len TSRMLS_DC); /* serializer callbacks */ int (*serialize)(zval *object, unsigned char **buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC); int (*unserialize)(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC); zend_class_entry **interfaces; zend_uint num_interfaces; zend_class_entry **traits; zend_uint num_traits; zend_trait_alias **trait_aliases; zend_trait_precedence **trait_precedences; union { struct { const char *filename; zend_uint line_start; zend_uint line_end; const char *doc_comment; zend_uint doc_comment_len; } user; struct { const struct _zend_function_entry *builtin_functions; struct _zend_module_entry *module; } internal; } info; };
??zend_class_entry 結構體中包含大量的指針以及 hashtable,這就導致結構體本身會占用不小的內存空間。另外,結構體中的指針還需要單獨分配相應的內存空間,這又會消耗一部分內存空間。
??所謂開發者自定義的 class 即使用 PHP 語言定義的 class,而 PHP 內部定義的 class 是指 PHP 源代碼中定義的 class 或 PHP 擴展中定義的 class。二者最本質的區別在于生命周期不同:
以 php-fpm 為例,當請求到來時,PHP 會解析開發者定義的 class 并為其分配相應的內存空間。其后在處理請求的過程中,PHP 會對這些 class 進行相應的調用,最后在處理完請求之后銷毀這些 class,釋放之前為其分配的內存空間。
為了節約內存空間,不要在代碼中定義一些實際并不使用的 class。可以使用 autoload 來屏蔽這些實際并不使用的 class,因為 autoload 只有在一個 class 被用到時才加載和解析,但這樣就會把 class 的解析和加載過程由代碼的編譯階段延后到代碼的執行階段,影響性能
另外需要注意的是,即使開啟了 OPCache 擴展,開發者自定義的 class 還是會隨著請求的到來而解析和加載,隨著請求的完成而銷毀,OPCache 只是提高了這兩個階段的速度
PHP 內部定義的 class 則不同。仍然以 php-fpm 為例,當一個 php-fpm 進程啟動時,PHP 會為這些 class 一次性永久分配內存空間,直到此 php-fpm 進程消亡(為避免內存泄漏,php-fpm 會在處理完一定數量的請求之后銷毀然后重啟)
if (EG(full_tables_cleanup)) { zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function_full TSRMLS_CC); zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class_full TSRMLS_CC); } else { zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function TSRMLS_CC); zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class TSRMLS_CC); } static int clean_non_persistent_class(zend_class_entry **ce TSRMLS_DC) { return ((*ce)->type == ZEND_INTERNAL_CLASS) ? ZEND_HASH_APPLY_STOP : ZEND_HASH_APPLY_REMOVE; }
??由以上代碼可以看出,在請求結束時,PHP 內部定義的 class 并不會被銷毀。另外,由于 PHP 擴展中定義的 class 也屬于 PHP 內部定義的 class 的范疇,所以,從節省內存空間的角度出發,不要開啟一些自己并不使用的擴展。因為,如果擴展一旦開啟,擴展中定義的 class 就會在 php-fpm 進程啟動時被解析和加載。
很多時候,為了處理方便,我們會通過繼承 \Exception 來自定義 exception。但由于 zend_class_entry 結構體非常龐大,這就導致在提高便利的同時耗費了大量的內存
?? class 綁定指的是 class 數據的準備過程
??對于 PHP 內部定義的 class,綁定過程在 class 注冊時就已經完成。此過程發生在 PHP 腳本運行之前,并且在整個 php-fpm 進程的生命周期中只發生一次。
??對于既沒有繼承 parent class,也沒有實現 interface,也沒有使用 trait 的 class,綁定過程發生在 PHP 代碼的編輯階段,并且不會消耗太多資源。此種 class 的綁定通常只需要將 class 注冊到 class_table 中,并檢查 class 是否包含了抽象方法但沒有被申明為 abstract 類型。
void zend_do_early_binding(TSRMLS_D) /* {{{ */ { zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-1]; HashTable *table; while (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)->opcodes) { opline--; } switch (opline->opcode) { case ZEND_DECLARE_FUNCTION: if (do_bind_function(CG(active_op_array), opline, CG(function_table), 1) == FAILURE) { return; } table = CG(function_table); break; case ZEND_DECLARE_CLASS: if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1 TSRMLS_CC) == NULL) { return; } table = CG(class_table); break; case ZEND_DECLARE_INHERITED_CLASS: { /*... ...*/ } case ZEND_VERIFY_ABSTRACT_CLASS: case ZEND_ADD_INTERFACE: case ZEND_ADD_TRAIT: case ZEND_BIND_TRAITS: /* We currently don't early-bind classes that implement interfaces */ /* Classes with traits are handled exactly the same, no early-bind here */ return; default: zend_error(E_COMPILE_ERROR, "Invalid binding type"); return; } /*... ...*/ } void zend_verify_abstract_class(zend_class_entry *ce TSRMLS_DC) { zend_abstract_info ai; if ((ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) && !(ce->ce_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) { memset(&ai, 0, sizeof(ai)); zend_hash_apply_with_argument(&ce->function_table, (apply_func_arg_t) zend_verify_abstract_class_function, &ai TSRMLS_CC); if (ai.cnt) { zend_error(E_ERROR, "Class %s contains %d abstract method%s and must therefore be declared abstract or implement the remaining methods (" MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT ")", ce->name, ai.cnt, ai.cnt > 1 ? "s" : "", DISPLAY_ABSTRACT_FN(0), DISPLAY_ABSTRACT_FN(1), DISPLAY_ABSTRACT_FN(2) ); } } }
??對于實現了 interface 的 class 的綁定過程非常復雜,大致流程如下:
檢查 interface 是否已經實現
檢查實現該 interface 的確實是一個 class,而不是 interface 自身(class、interface、trait 的底層數據結構都是 zend_class_entry)
復制常量,并檢查可能存在的沖突
復制方法,并檢查可能存在的沖突,除此之外還需要檢查訪問控制
將 interface 加入到 zend_class_entry 的 **interfaces
中
需要注意的是,所謂的復制只是將常量、屬性、方法的引用計數加 1
ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface TSRMLS_DC) { /* ... ... */ } else { if (ce->num_interfaces >= current_iface_num) { /* resize the vector if needed */ if (ce->type == ZEND_INTERNAL_CLASS) { /*對于內部定義的 class,使用 realloc 分配內存,所分配的內存在進程的生命周期中永久有效*/ ce->interfaces = (zend_class_entry **) realloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num)); } else { /*對于開發者定義的 class,使用 erealloc 分配內存,所分配的內存只在請求的生命周期中有效*/ ce->interfaces = (zend_class_entry **) erealloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num)); } } ce->interfaces[ce->num_interfaces++] = iface; /* Add the interface to the class */ /* Copy every constants from the interface constants table to the current class constants table */ zend_hash_merge_ex(&ce->constants_table, &iface->constants_table, (copy_ctor_func_t) zval_add_ref, sizeof(zval *), (merge_checker_func_t) do_inherit_constant_check, iface); /* Copy every methods from the interface methods table to the current class methods table */ zend_hash_merge_ex(&ce->function_table, &iface->function_table, (copy_ctor_func_t) do_inherit_method, sizeof(zend_function), (merge_checker_func_t) do_inherit_method_check, ce); do_implement_interface(ce, iface TSRMLS_CC); zend_do_inherit_interfaces(ce, iface TSRMLS_CC); } }
??對于常量的復制,zval_add_ref 用于將常量的引用計數加1;而對于方法的復制,do_inherit_method 除了將相應方法的引用計數加 1 之外,還將方法中定義的靜態變量的引用計數加 1。
static void do_inherit_method(zend_function *function) { function_add_ref(function); } ZEND_API void function_add_ref(zend_function *function) { if (function->type == ZEND_USER_FUNCTION) { zend_op_array *op_array = &function->op_array; (*op_array->refcount)++; if (op_array->static_variables) { HashTable *static_variables = op_array->static_variables; zval *tmp_zval; ALLOC_HASHTABLE(op_array->static_variables); zend_hash_init(op_array->static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0); zend_hash_copy(op_array->static_variables, static_variables, (copy_ctor_func_t) zval_add_ref, (void *) &tmp_zval, sizeof(zval *)); } op_array->run_time_cache = NULL; } }
??對于實現了 interface 的 class 的綁定,由于要進行多次的循環遍歷以及檢查,通常非常消耗 CPU 資源,但卻節省了內存空間。
現階段,PHP 將 interface 的綁定推遲到了代碼執行階段進行,以為這每次請求都會進行這些操作
??對于 class 繼承的綁定,過程與 interface 的綁定類似,但更為復雜。另外有一個值得注意的地方,如果 class 在綁定時已經解析到了父類,則綁定發生在代碼編譯階段;否則發生在代碼執行階段。
// A 在 B 之前申明,B 的綁定發生在編譯階段 class A { } class B extends A { } // A 在 B 之后申明,綁定 B 時編譯器無法知道 A 情況,此時 B 的綁定只能延后到代碼執行時 class B extends A { } class A { } // 這種情況會報錯:Class B doesn't exist // 在代碼執行階段綁定 C,需要解析 B,但此時 B 有繼承了 A,而 A 此時還是未知狀態 class C extends B { } class B extends A { } class A { }
如果使用 autoload,并且采用一個 class 對應一個文件的模式,則所有 class 的綁定都只會發生在代碼執行階段
??方法與函數的底層數據結構均為 zend_function。PHP 編譯器在編譯時將方法編譯并添加到 zend_class_entry 的 function_table 屬性中。所以,在 PHP 代碼運行時,方法已經編譯完成,PHP 要做的只是通過指針找到方法并執行。
typedef union _zend_function { zend_uchar type; struct { zend_uchar type; const char *function_name; zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; } common; zend_op_array op_array; zend_internal_function internal_function; } zend_function;
??當 object 嘗試調用方法時,首先會在其對應的 class 的 function_table 中查找該方法,同時還會檢查方法的訪問控制。如果方法不存在或方法的訪問控制不符合要求,object 會嘗試調用莫屬方法 __call
。
static inline union _zend_function *zend_get_user_call_function(zend_class_entry *ce, const char *method_name, int method_len) { zend_internal_function *call_user_call = emalloc(sizeof(zend_internal_function)); call_user_call->type = ZEND_INTERNAL_FUNCTION; call_user_call->module = (ce->type == ZEND_INTERNAL_CLASS) ? ce->info.internal.module : NULL; call_user_call->handler = zend_std_call_user_call; call_user_call->arg_info = NULL; call_user_call->num_args = 0; call_user_call->scope = ce; call_user_call->fn_flags = ZEND_ACC_CALL_VIA_HANDLER; call_user_call->function_name = estrndup(method_name, method_len); return (union _zend_function *)call_user_call; } static union _zend_function *zend_std_get_method(zval **object_ptr, char *method_name, int method_len, const zend_literal *key TSRMLS_DC) { zend_function *fbc; zval *object = *object_ptr; zend_object *zobj = Z_OBJ_P(object); ulong hash_value; char *lc_method_name; ALLOCA_FLAG(use_heap) if (EXPECTED(key != NULL)) { lc_method_name = Z_STRVAL(key->constant); hash_value = key->hash_value; } else { lc_method_name = do_alloca(method_len+1, use_heap); /* Create a zend_copy_str_tolower(dest, src, src_length); */ zend_str_tolower_copy(lc_method_name, method_name, method_len); hash_value = zend_hash_func(lc_method_name, method_len+1); } if (UNEXPECTED(zend_hash_quick_find(&zobj->ce->function_table, lc_method_name, method_len+1, hash_value, (void **)&fbc) == FAILURE)) { if (UNEXPECTED(!key)) { free_alloca(lc_method_name, use_heap); } if (zobj->ce->__call) { return zend_get_user_call_function(zobj->ce, method_name, method_len); } else { return NULL; } } /* Check access level */ if (fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) { zend_function *updated_fbc; /* Ensure that if we're calling a private function, we're allowed to do so. * If we're not and __call() handler exists, invoke it, otherwise error out. */ updated_fbc = zend_check_private_int(fbc, Z_OBJ_HANDLER_P(object, get_class_entry)(object TSRMLS_CC), lc_method_name, method_len, hash_value TSRMLS_CC); if (EXPECTED(updated_fbc != NULL)) { fbc = updated_fbc; } else { if (zobj->ce->__call) { fbc = zend_get_user_call_function(zobj->ce, method_name, method_len); } else { zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : ""); } } } else { /* Ensure that we haven't overridden a private function and end up calling * the overriding public function... */ if (EG(scope) && is_derived_class(fbc->common.scope, EG(scope)) && fbc->op_array.fn_flags & ZEND_ACC_CHANGED) { zend_function *priv_fbc; if (zend_hash_quick_find(&EG(scope)->function_table, lc_method_name, method_len+1, hash_value, (void **) &priv_fbc)==SUCCESS && priv_fbc->common.fn_flags & ZEND_ACC_PRIVATE && priv_fbc->common.scope == EG(scope)) { fbc = priv_fbc; } } if ((fbc->common.fn_flags & ZEND_ACC_PROTECTED)) { /* Ensure that if we're calling a protected function, we're allowed to do so. * If we're not and __call() handler exists, invoke it, otherwise error out. */ if (UNEXPECTED(!zend_check_protected(zend_get_function_root_class(fbc), EG(scope)))) { if (zobj->ce->__call) { fbc = zend_get_user_call_function(zobj->ce, method_name, method_len); } else { zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : ""); } } } } if (UNEXPECTED(!key)) { free_alloca(lc_method_name, use_heap); } return fbc; }
??這里需要指出的是:
由于 PHP 對大小寫不敏感,所以所有的方法名稱都會被轉為小寫(zend_str_tolower_copy())
為了避免不必要的資源消耗,PHP 5.4 開始引入了 zend_literal 結構體,即參數 key
typedef struct _zend_literal { zval constant; zend_ulong hash_value; zend_uint cache_slot; } zend_literal;
??其中,constant 記錄了轉為小寫后的字符串,hash_value 則是預先計算好的 hash。這樣就避免了 object 每次調用方法都要將方法名稱轉為小寫并計算 hash 值。
class Foo { public function BAR() { } } $a = new Foo; $b = 'bar'; $a->bar(); /* good */ $a->$b(); /* bad */
??在上例中,在代碼編譯階段,方法 BAR 被轉換成 bar 并添加到 zend_class_entry 的 function_table 中。當發生方法調用時:
第一種情形,在代碼編譯階段,方法名稱 bar 確定為字符串常量,編譯器可以預先計算好其對應的 zend_literal 結構,即 key 參數。這樣,代碼在執行時相對會更快。
第二種情形,由于在編譯階段編譯器對 $b 一無所知,這就需要在代碼執行階段現將方法名稱轉為小寫,然后計算 hash 值。
??當對一個 class 進行實例化時,object 中的屬性只是對 class 中屬性的引用。這樣,object 的創建操作就會相對輕量化,并且會節省一部分內存空間。
??如果要對 object 中的屬性進行修改,zend 引擎會單獨創建一個 zval 結構,只對當前 object 的當前屬性產生影響。
??class 的實例化對應的會在底層創建一個 zend_obejct 數據結構,新創建的 object 會注冊到 zend_objects_store 中。zend_objects_store 是一個全局的 object 注冊表,同一個對象在該注冊表中只能注冊一次。
typedef struct _zend_object { zend_class_entry *ce; HashTable *properties; zval **properties_table; HashTable *guards; /* protects from __get/__set ... recursion */ } zend_object; typedef struct _zend_objects_store {/*本質上是一個動態 object_bucket 數組*/ zend_object_store_bucket *object_buckets; zend_uint top; /*下一個可用的 handle,handle 取值從 1 開始。對應的在 *object_buckets 中的 index 為 handle - 1*/ zend_uint size; /*當前分配的 *object_buckets 的最大長度*/ int free_list_head; /*當 *object_bucket 中的 bucket 被銷毀后,該 bucket 在 *object_buckets 中的 index 會被有序加入 free_list 鏈表。free_list_head 即為該鏈表中的第一個值*/ } zend_objects_store; typedef struct _zend_object_store_bucket { zend_bool destructor_called; zend_bool valid; /*值為 1 表示當前 bucket 被使用,此時 store_bucket 中的 store_object 被使用;值為 0 表示當前 bucket 并沒有存儲有效的 object,此時 store_bucket 中的 free_list 被使用*/ zend_uchar apply_count; union _store_bucket { struct _store_object { void *object; zend_objects_store_dtor_t dtor; zend_objects_free_object_storage_t free_storage; zend_objects_store_clone_t clone; const zend_object_handlers *handlers; zend_uint refcount; gc_root_buffer *buffered; } obj; struct { int next; /*第一個未被使用的 bucket 的 index 永遠存儲在 zend_object_store 的 free_list_head 中,所以 next 只需要記錄當前 bucket 之后第一個未被使用的 bucket 的 index*/ } free_list; } bucket; } zend_object_store_bucket; ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC) { zend_object_value retval; *object = emalloc(sizeof(zend_object)); (*object)->ce = class_type; (*object)->properties = NULL; (*object)->properties_table = NULL; (*object)->guards = NULL; retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_storage, NULL TSRMLS_CC); retval.handlers = &std_object_handlers; return retval; }
?? 將 object 注冊到 zend_objects_store 中以后,將會為 object 創建屬性(對相應 class 屬性的引用)
ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type) { int i; if (class_type->default_properties_count) { object->properties_table = emalloc(sizeof(zval*) * class_type->default_properties_count); for (i = 0; i < class_type->default_properties_count; i++) { object->properties_table[i] = class_type->default_properties_table[i]; if (class_type->default_properties_table[i]) { #if ZTS ALLOC_ZVAL( object->properties_table[i]); MAKE_COPY_ZVAL(&class_type->default_properties_table[i], object->properties_table[i]); #else Z_ADDREF_P(object->properties_table[i]); #endif } } object->properties = NULL; } }
??需要指出的是,在創建屬性時,如果是非線程安全模式的 PHP,僅僅是增加相應屬性的引用計數;但如果是線程安全模式的 PHP,則需要對屬性進行深度復制,將 class 的屬性全部復制到 object 中的 properties_table 中。
這也說明,線程安全的 PHP 比非線程安全的 PHP 運行慢,并且更耗費內存
每個屬性在底層都對應一個 zend_property_info 結構:
typedef struct _zend_property_info { zend_uint flags; const char *name; int name_length; ulong h; int offset; const char *doc_comment; int doc_comment_len; zend_class_entry *ce; } zend_property_info;
??class 中聲明的每個屬性,在 zend_class_entry 中的 properties_table 中都有一個zend_property_info 與之相對應。properties_table 可以幫助我們快速確定一個 object 所訪問的屬性是否存在:
如果屬性不存在,并且我們嘗試向 object 寫入該屬性:如果 class 定義了 __set
方法,則使用 __set
方法寫入該屬性;否則會向 object 添加一個動態屬性。但無論以何種方式寫入該屬性,寫入的屬性都將添加到 object 的 properties_table 中。
如果屬性存在,則需要檢查相應的訪問控制;對于 protected 和 private 類型,則需要檢查當前的作用域。
在創建完 object 之后,只要我們不向 object 中寫入新的屬性或更新 object 對應的 class 中的屬性的值,則 object 所占用的內存空間不會發生變化。
屬性的存儲/訪問方式:
zend_class_entry->properties_info 中存儲的是一個個的 zend_property_info。而屬性的值實際以 zval 指針數組的方式存儲在 zend_class_entry->default_properties_table 中。object 中動態添加的屬性只會以 property_name => property_value 的形式存儲在 zend_object->properties_table 中。而在創建 object 時,zend_class_entry->properties_table 中的值會被逐個傳遞給 zend_object->properties_table。
zend_literal->cache_slot 中存儲的 int 值為 run_time_cache 中的索引 index。run_time_cache 為數組結構,index 對應的 value 為訪問該屬性的 object 對應的 zend_class_entry;index + 1 對應的 value 為該屬性對應的 zend_property_info 。在訪問屬性時,如果 zend_literal->cache_slot 中的值不為空,則可以通過 zend_literal->cache_slot 快速檢索得到 zend_property_info 結構;如果為空,則在檢索到 zend_property_info 的信息之后會初始化 zend_literal->cache_slot。
屬性名稱的存儲方式
private 屬性:"\0class_name\0property_name"
protected 屬性:"\0*\0property_name"
public 屬性:"property_name"
?? 執行以下代碼,看看輸出結果
class A { private $a = 'a'; protected $b = 'b'; public $c = 'c'; } class B extends A { private $a = 'aa'; protected $b = 'bb'; public $c = 'cc'; } class C extends B { private $a = 'aaa'; protected $b = 'bbb'; public $c = 'ccc'; } var_dump(new C());
zend_object 中 guards 的作用
guards 的作用是對 object 的重載提供遞歸保護。
class Foo { public function __set($name, $value) { $this->$name = $value; } } $foo = new Foo; $foo->bar = 'baz'; var_dump($foo->bar);
?? 以上代碼中,當為 bar 屬性時會調用 __set
方法。但 $bar 屬性在 Foo 中并不存在,按照常理,此時又會遞歸調用 __set
方法。為了避免這種遞歸調用,PHP 會使用 zend_guard 來判斷當前是否已經處于重載方法的上下文中。
typedef struct _zend_guard { zend_bool in_get; zend_bool in_set; zend_bool in_unset; zend_bool in_isset; zend_bool dummy; /* sizeof(zend_guard) must not be equal to sizeof(void*) */ } zend_guard;
??首先需要申明:object 并不是引用傳遞。之所以會出現 object 是引用傳遞的假象,原因在于我們傳遞給函數的參數中所存儲的只是 object 在 zend_objects_store 中的 ID(handle)。通過這個 ID,我們可以在 zend_objects_store 中查找并加載真正的 object,然后訪問并修改 object 中的屬性。
PHP 中,函數內外是兩個不同的作用域,對于同一變量,在函數內部對其修改不會影響到函數外部。但通過 object 的 ID(handle)訪問并修改 object 的屬性并不受此限制。
$a = 1; function test($a) { $a = 3; echo $a; // 輸出 3 } test($a); echo $a; // 輸出 1
同一個 object 在 zend_objects_store 中只存儲一次。要向 zend_objects_store 中寫入新的對象,只能通過 new 關鍵字、unserialize 函數、反射、clone 四種方式。
??$this
在使用時會自動接管當前對象,PHP 禁止對 this 的賦值操作都會引起錯誤
static zend_bool opline_is_fetch_this(const zend_op *opline TSRMLS_DC) { if ((opline->opcode == ZEND_FETCH_W) && (opline->op1_type == IS_CONST) && (Z_TYPE(CONSTANT(opline->op1.constant)) == IS_STRING) && ((opline->extended_value & ZEND_FETCH_STATIC_MEMBER) != ZEND_FETCH_STATIC_MEMBER) && (Z_HASH_P(&CONSTANT(opline->op1.constant)) == THIS_HASHVAL) && (Z_STRLEN(CONSTANT(opline->op1.constant)) == (sizeof("this")-1)) && !memcmp(Z_STRVAL(CONSTANT(opline->op1.constant)), "this", sizeof("this"))) { return 1; } else { return 0; } } /* ... ... */ if (opline_is_fetch_this(last_op TSRMLS_CC)) { zend_error(E_COMPILE_ERROR, "Cannot re-assign $this"); } /* ... ... */
?? 在 PHP 中進行方法調用時,對應執行的 OPCode 為 INIT_METHOD_CALL。以 $a->foo()
為例,在 INIT_METHOD_CALL 中,Zend 引擎知道是由 $a
發起的方法調用,所以 Zend 引擎會把 $a
的值存入全局空間。在實際執行方法調用時,對應執行的 OPCode 為 DO_FCALL。在 DO_FCALL 中,Zend 引擎會將之前存入全局空間的 $a
賦值給 $this
的指針,即 EG(This):
if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) { should_change_scope = 1; EX(current_this) = EG(This); EX(current_scope) = EG(scope); EX(current_called_scope) = EG(called_scope); EG(This) = EX(object); /* fetch the object prepared in previous INIT_METHOD opcode and affect it to EG(This) */ EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope : NULL; EG(called_scope) = EX(call)->called_scope; }
?? 在實際執行方法體中的代碼時,如果出現使用 $this
進行方法調用或屬性賦值的情況,如 $this->a = 8
對應的將執行 OPCode ZEND_ASSIGN_OBJ,此時將從 EG(This) 取得 $this 的值
static zend_always_inline zval **_get_obj_zval_ptr_ptr_unused(TSRMLS_D) { if (EXPECTED(EG(This) != NULL)) { return &EG(This); } else { zend_error_noreturn(E_ERROR, "Using $this when not in object context"); return NULL; } }
??Zend 引擎在構建方法堆棧時,$this
會被存入符號表,就像其他的變量一樣。這樣,當使用 $this
進行方法調用或將 $this
作為方法的參數時,Zend 引擎將從符號表中獲取 $this
。
if (op_array->this_var != -1 && EG(This)) { Z_ADDREF_P(EG(This)); /* For $this pointer */ if (!EG(active_symbol_table)) { EX_CV(op_array->this_var) = (zval **) EX_CV_NUM(execute_data, op_array->last_var + op_array->this_var); *EX_CV(op_array->this_var) = EG(This); } else { if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void **) EX_CV_NUM(execute_data, op_array->this_var))==FAILURE) { Z_DELREF_P(EG(This)); } } }
?? 最后是關于作用域的問題,當進行方法調用時,Zend 引擎會將作用域設置為 EG(scope)。EG(scope) 是 zend_class_entry 類型,也就是說,在方法中任何關于 object 的操作的作用域都是 object 對應的 class。對屬性的訪問控制的檢查也是同樣:
ZEND_API int zend_check_protected(zend_class_entry *ce, zend_class_entry *scope) { zend_class_entry *fbc_scope = ce; /* Is the context that's calling the function, the same as one of * the function's parents? */ while (fbc_scope) { if (fbc_scope==scope) { return 1; } fbc_scope = fbc_scope->parent; } /* Is the function's scope the same as our current object context, * or any of the parents of our context? */ while (scope) { if (scope==ce) { return 1; } scope = scope->parent; } return 0; } static zend_always_inline int zend_verify_property_access(zend_property_info *property_info, zend_class_entry *ce TSRMLS_DC) { switch (property_info->flags & ZEND_ACC_PPP_MASK) { case ZEND_ACC_PUBLIC: return 1; case ZEND_ACC_PROTECTED: return zend_check_protected(property_info->ce, EG(scope)); case ZEND_ACC_PRIVATE: if ((ce==EG(scope) || property_info->ce == EG(scope)) && EG(scope)) { return 1; } else { return 0; } break; } return 0; }
??正是由于上述特性,所以以下代碼可以正常運行
class A { private $a; public function foo(A $obj) { $this->a = 'foo'; $obj->a = 'bar'; /* yes, this is possible */ } } $a = new A; $b = new A; $a->foo($b);
PHP 中 object 的作用域是 object 對應的 class
??在 PHP 中,不要依賴 destruct 方法銷毀 object。因為當 PHP 發生致命錯誤時,destruct 方法并不會被調用。
ZEND_API void zend_hash_reverse_apply(HashTable *ht, apply_func_t apply_func TSRMLS_DC) { Bucket *p, *q; IS_CONSISTENT(ht); HASH_PROTECT_RECURSION(ht); p = ht->pListTail; while (p != NULL) { int result = apply_func(p->pData TSRMLS_CC); q = p; p = p->pListLast; if (result & ZEND_HASH_APPLY_REMOVE) { zend_hash_apply_deleter(ht, q); } if (result & ZEND_HASH_APPLY_STOP) { break; } } HASH_UNPROTECT_RECURSION(ht); } static int zval_call_destructor(zval **zv TSRMLS_DC) { if (Z_TYPE_PP(zv) == IS_OBJECT && Z_REFCOUNT_PP(zv) == 1) { return ZEND_HASH_APPLY_REMOVE; } else { return ZEND_HASH_APPLY_KEEP; } } void shutdown_destructors(TSRMLS_D) { zend_try { int symbols; do { symbols = zend_hash_num_elements(&EG(symbol_table)); zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC); } while (symbols != zend_hash_num_elements(&EG(symbol_table))); zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC); } zend_catch { /* if we couldn't destruct cleanly, mark all objects as destructed anyway */ zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC); } zend_end_try(); }
??在調用 destruct 方法時,首先會從后往前遍歷整個符號表,調用所有引用計數為 1 的 object 的 destruct 方法;然后從前往后遍歷全局 object store,調用每個 object 的 destruct 方法。在此過程中如果有任何錯誤發生,就會停止調用 destruct 方法,然后將所有 object 的 destruct 方法都標記為已調用過的狀態。
class Foo { public function __destruct() { var_dump("destroyed Foo"); } } class Bar { public function __destruct() { var_dump("destroyed Bar"); } } // 示例 1 $a = new Foo; $b = new Bar; "destroyed Bar" "destroyed Foo" // 示例 2 $a = new Bar; $b = new Foo; "destroyed Foo" "destroyed Bar" // 示例 3 $a = new Bar; $b = new Foo; $c = $b; /* $b 引用計數加 1 */ "destroyed Bar" "destroyed Foo" // 示例 4 class Foo { public function __destruct() { var_dump("destroyed Foo"); die();} } /* notice the die() here */ class Bar { public function __destruct() { var_dump("destroyed Bar"); } } $a = new Foo; $a2 = $a; $b = new Bar; $b2 = $b; "destroyed Foo"
?? 另外,不要在 destruct 方法中添加任何重要的代碼
class Foo { public function __destruct() { new Foo; } /* PHP 最終將崩潰 */ }
PHP 中對象的銷毀分為兩個階段:首先調用 destruct 方法(zend_object_store_bucket->bucket->obj->zend_objects_store_dtor_t),然后再釋放內存(zend_object_store_bucket->bucket->obj->zend_objects_free_object_storage_t)。
之所以分為兩個階段執行是因為 destruct 中執行的是用戶級的代碼,即 PHP 代碼;而釋放內存的代碼在系統底層運行。釋放內存會破壞 PHP 的運行環境,為了使 destruct 中的 PHP 代碼能正常運行,所以分為兩個階段,這樣,保證在釋放內存階段 object 已經不被使用。
??與 PHP 5 相比,PHP 7 中的 object 在用戶層并沒有基本沒有什么變化;但在底層實現上,在內存和性能方面做了一些優化。
?? ① 首先,在 zval 中移除了之前的 zend_object_value 結構,直接嵌入了 zend_object。這樣,既節省了內存空間,同時提高了通過 zval 查找 zend_object 的效率
/*PHP 7 中的 zend_object*/ struct _zend_object { zend_refcounted gc; uint32_t handle; zend_class_entry *ce; const zend_object_handlers *handlers; HashTable *properties; zval properties_table[1]; }; /*PHP 5 中的 zend_object_value*/ typedef struct _zend_object_value { zend_object_handle handle; const zend_object_handlers *handlers; } zend_object_value;
?? 在 PHP 5 中通過 zval 訪問 object,先要通過 zva 中的 zend_object_value 找到 handle,然后通過handle 在 zend_object_store 中找到 zend_object_store_bucket,然后從 bucket 中解析出 object。在 PHP 7 中,zval 中直接存儲了 zend_object 的地址指針。
?? ② 其次,properties_table 利用了 struct hack 特性,這樣使得 zend_object 和 properties_table 存儲在一塊連續的內存空間。同時,properties_table 中直接存儲了屬性的 zval 結構。
?? ③ guards 不再出現在 zend_object 中。如果 class 中定義了魔術方法( __set
、__get
、__isset
、__unset
),則 guards 存儲在 properties_table 的第一個 slot 中;否則不存儲 guards。
?? ④ zend_object_store 及 zend_object_store_bucket 被移除,取而代之的是一個存儲各個 zend_object 指針的 C 數組,handle 為數組的索引。此外,之前 bucket 中存儲的 handlers 現在移入 zend_object 中;而之前 bucket 中的 dtor、free_storege、clone 現在則移入了 zend_object_handlers。
struct _zend_object_handlers { /* offset of real object header (usually zero) */ int offset; /* general object functions */ zend_object_free_obj_t free_obj; zend_object_dtor_obj_t dtor_obj; zend_object_clone_obj_t clone_obj; /* individual object functions */ // ... 其他與 PHP 5 相同 };
/*PHP 5 中的 custom_object*/ struct custom_object { zend_object std; my_custom_type *my_buffer; // ... }; /*PHP 7 中的 custom_object*/ struct custom_object { my_custom_type *my_buffer; // ... zend_object std; };
?? 由于 PHP 7 的 zend_object 中使用了 struct hack 特性來保證 zend_object 內存的連續,所以自定義 object 中的 zend_object 只能放在最后。而 zval 中存儲的只能是 zend_object,為了能通過 zend_object 順利解析出 custom_object ,在 zend_object 的 handlers 中記錄了 offset。
“PHP 7和PHP 5中的對象之間的差異”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。