目录
一、 class 介绍
⒈ 开发者自定义的 class 与 PHP 内部定义的 class 的比较
⒉ class 绑定
二、PHP 5 中的 object
⒈ object 中的方法
⒉ object 中的属性
⒊ object 的引用传递
⒋ $this
⒌ 析构方法 destruct
三、PHP 7 中的 object
⒈ 在内存布局和管理上的优化
⒉  底层自定义 object 的变化(PHP 扩展中会用到自定义 object)
首页 后端开发 php教程 简单对比一下PHP 7 和 PHP 5 中的对象

简单对比一下PHP 7 和 PHP 5 中的对象

Sep 03, 2021 pm 07:05 PM
php 7 对象

本篇文章带大家了解一下PHP 7 和 PHP 5 中对象,并比较一下,看看它们之间的差异!

简单对比一下PHP 7 和 PHP 5 中的对象

一、 class 介绍

   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 的比较

  所谓开发者自定义的 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 绑定指的是 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 的绑定都只会发生在代码执行阶段

二、PHP 5 中的 object

⒈ object 中的方法

  方法与函数的底层数据结构均为 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 值。

⒉ object 中的属性

  当对一个 class 进行实例化时,object 中的属性只是对 class 中属性的引用。这样,object 的创建操作就会相对轻量化,并且会节省一部分内存空间。

简单对比一下PHP 7 和 PHP 5 中的对象

  如果要对 object 中的属性进行修改,zend 引擎会单独创建一个 zval 结构,只对当前 object 的当前属性产生影响。

简单对比一下PHP 7 和 PHP 5 中的对象

  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 = &#39;a&#39;;
    protected $b = &#39;b&#39;;
    public $c = &#39;c&#39;;
}

class B extends A {
    private $a = &#39;aa&#39;;
    protected $b = &#39;bb&#39;;
    public $c = &#39;cc&#39;;
}

class C extends B {
    private $a = &#39;aaa&#39;;
    protected $b = &#39;bbb&#39;;
    public $c = &#39;ccc&#39;;
}

var_dump(new C());
登录后复制

zend_object 中 guards 的作用
guards 的作用是对 object 的重载提供递归保护。

class Foo {
    public function __set($name, $value) {
        $this->$name = $value;
    }
}

$foo = new Foo;
$foo->bar = &#39;baz&#39;;
var_dump($foo->bar);
登录后复制

   以上代码中,当为 foo动态设置foo 动态设置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 是引用传递的假象,原因在于我们传递给函数的参数中所存储的只是 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
登录后复制

简单对比一下PHP 7 和 PHP 5 中的对象

同一个 object 在 zend_objects_store 中只存储一次。要向 zend_objects_store 中写入新的对象,只能通过 new 关键字、unserialize 函数、反射、clone 四种方式。

⒋ $this

  $this 在使用时会自动接管当前对象,PHP 禁止对 this进行赋值操作。任何对this 进行赋值操作。任何对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&#39;s calling the function, the same as one of
	* the function&#39;s parents?
	*/
	while (fbc_scope) {
		if (fbc_scope==scope) {
			return 1;
		}
		fbc_scope = fbc_scope->parent;
	}

	/* Is the function&#39;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 = &#39;foo&#39;;
		$obj->a  = &#39;bar&#39;; /* yes, this is possible */
	}
}

$a = new A;
$b = new A;
$a->foo($b);
登录后复制

PHP 中 object 的作用域是 object 对应的 class

⒌ 析构方法 destruct

  在 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&#39;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 7 中的 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 相同
};
登录后复制

⒉ 底层自定义 object 的变化(PHP 扩展中会用到自定义 object)

/*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 中的对象

推荐学习:《PHP视频教程

以上是简单对比一下PHP 7 和 PHP 5 中的对象的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

使用PHP的json_encode()函数将数组或对象转换为JSON字符串 使用PHP的json_encode()函数将数组或对象转换为JSON字符串 Nov 03, 2023 pm 03:30 PM

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式,已经成为Web应用程序之间数据交换的常用格式。PHP的json_encode()函数可以将数组或对象转换为JSON字符串。本文将介绍如何使用PHP的json_encode()函数,包括语法、参数、返回值以及具体的示例。语法json_encode()函数的语法如下:st

使用Python的__contains__()函数定义对象的包含操作 使用Python的__contains__()函数定义对象的包含操作 Aug 22, 2023 pm 04:23 PM

使用Python的__contains__()函数定义对象的包含操作Python是一种简洁而强大的编程语言,提供了许多强大的功能来处理各种类型的数据。其中之一是通过定义__contains__()函数来实现对象的包含操作。本文将介绍如何使用__contains__()函数来定义对象的包含操作,并且给出一些示例代码。__contains__()函数是Pytho

如何将 MySQL 查询结果数组转换为对象? 如何将 MySQL 查询结果数组转换为对象? Apr 29, 2024 pm 01:09 PM

将MySQL查询结果数组转换为对象的方法如下:创建一个空对象数组。循环结果数组并为每一行创建一个新的对象。使用foreach循环将每一行的键值对赋给新对象的相应属性。将新对象添加到对象数组中。关闭数据库连接。

源码探秘:Python 中对象是如何被调用的? 源码探秘:Python 中对象是如何被调用的? May 11, 2023 am 11:46 AM

楔子我们知道对象被创建,主要有两种方式,一种是通过Python/CAPI,另一种是通过调用类型对象。对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们即可以通过[]创建,也可以通过list(),前者是Python/CAPI,后者是调用类型对象。但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。而一个对象如果可以被调用,那么这个对象就是callable,否则就不是callable。而决定一个对象是不是callable,就取决于其对应的类型对象中是否定义了某个方法。如

PHP 函数如何返回对象? PHP 函数如何返回对象? Apr 10, 2024 pm 03:18 PM

PHP函数可以通过使用return语句后跟对象实例来返回对象,从而将数据封装到自定义结构中。语法:functionget_object():object{}。这允许创建具有自定义属性和方法的对象,并以对象的形式处理数据。

使用Python的__le__()函数定义两个对象的小于等于比较 使用Python的__le__()函数定义两个对象的小于等于比较 Aug 21, 2023 pm 09:29 PM

标题:使用Python的__le__()函数定义两个对象的小于等于比较在Python中,我们可以通过使用特殊方法来定义对象之间的比较操作。其中之一就是__le__()函数,它用于定义小于等于比较。__le__()函数是Python中的一个魔法方法,并且是一种用于实现“小于等于”操作的特殊函数。当我们使用小于等于运算符(&lt;=)比较两个对象时,Python

数组和对象在 PHP 中的区别是什么? 数组和对象在 PHP 中的区别是什么? Apr 29, 2024 pm 02:39 PM

PHP中,数组是有序序列,以索引访问元素;对象是具有属性和方法的实体,通过new关键字创建。数组访问通过索引,对象访问通过属性/方法。数组值传递,对象引用传递。

C++ 函数返回对象时有什么需要注意的? C++ 函数返回对象时有什么需要注意的? Apr 19, 2024 pm 12:15 PM

在C++中,函数返回对象需要注意三点:对象的生命周期由调用者负责管理,以防止内存泄漏。避免悬垂指针,通过动态分配内存或返回对象本身来确保对象在函数返回后仍然有效。编译器可能会优化返回对象的副本生成,以提高性能,但如果对象是值语义传递的,则无需副本生成。

See all articles