3.4.2 对象

对象是类的实例,PHP中要创建一个类的实例,必须使用 new 关键字。类应在被实例化之前定义(某些情况下则必须这样,比如3.4.1最后那几个例子)。

3.4.2.1 对象的数据结构

对象的数据结构非常简单:

  1. typedef struct _zend_object zend_object;
  2. struct _zend_object {
  3. zend_refcounted_h gc; //引用计数
  4. uint32_t handle;
  5. zend_class_entry *ce; //所属类
  6. const zend_object_handlers *handlers; //对象操作处理函数
  7. HashTable *properties;
  8. zval properties_table[1]; //普通属性值数组
  9. };

几个主要的成员:

(1)handle: 一次request期间对象的编号,每个对象都有一个唯一的编号,与创建先后顺序有关,主要在垃圾回收时用,下面会详细说明。

(2)ce: 所属类的zend_class_entry。

(3)handlers: 这个保存的对象相关操作的一些函数指针,比如成员属性的读写、成员方法的获取、对象的销毁/克隆等等,这些操作接口都有默认的函数。

  1. struct _zend_object_handlers {
  2. int offset;
  3. zend_object_free_obj_t free_obj; //释放对象
  4. zend_object_dtor_obj_t dtor_obj; //销毁对象
  5. zend_object_clone_obj_t clone_obj;//复制对象
  6. zend_object_read_property_t read_property; //读取成员属性
  7. zend_object_write_property_t write_property;//修改成员属性
  8. ...
  9. }
  10. //默认值处理handler
  11. ZEND_API zend_object_handlers std_object_handlers = {
  12. 0,
  13. zend_object_std_dtor, /* free_obj */
  14. zend_objects_destroy_object, /* dtor_obj */
  15. zend_objects_clone_obj, /* clone_obj */
  16. zend_std_read_property, /* read_property */
  17. zend_std_write_property, /* write_property */
  18. zend_std_read_dimension, /* read_dimension */
  19. zend_std_write_dimension, /* write_dimension */
  20. zend_std_get_property_ptr_ptr, /* get_property_ptr_ptr */
  21. NULL, /* get */
  22. NULL, /* set */
  23. zend_std_has_property, /* has_property */
  24. zend_std_unset_property, /* unset_property */
  25. zend_std_has_dimension, /* has_dimension */
  26. zend_std_unset_dimension, /* unset_dimension */
  27. zend_std_get_properties, /* get_properties */
  28. zend_std_get_method, /* get_method */
  29. NULL, /* call_method */
  30. zend_std_get_constructor, /* get_constructor */
  31. zend_std_object_get_class_name, /* get_class_name */
  32. zend_std_compare_objects, /* compare_objects */
  33. zend_std_cast_object_tostring, /* cast_object */
  34. NULL, /* count_elements */
  35. zend_std_get_debug_info, /* get_debug_info */
  36. zend_std_get_closure, /* get_closure */
  37. zend_std_get_gc, /* get_gc */
  38. NULL, /* do_operation */
  39. NULL, /* compare */
  40. }

Note: 这些handler用于操作对象(如:设置、读取属性),std_object_handlers是PHP定义的默认、标准的处理函数,在扩展中可以自定义handler,比如:重定义write_property,这样设置一个对象的属性时将调用扩展自己定义的处理函数,让扩展拥有了更高的控制权限。

需要注意的是:const zend_object_handlers handlers,这里的handlers指针加了const修饰符,const修饰的是handlers*指向的对象,而不是handlers指针本身,所以扩展中可以将一个对象的handlers修改为另一个zend_object_handlers指针,但无法修改zend_object_handlers中的值,比如:obj->handlers->write_property = xxx将报错,而:obj->handlers = xxx则是可以的。

(4)properties: 普通成员属性哈希表,对象创建之初这个值为NULL,主要是在动态定义属性时会用到,与properties_table有一定关系,下一节我们将单独说明,这里暂时忽略。

(5)properties_table: 成员属性数组,还记得我们在介绍类一节时提过非静态属性存储在对象结构中吗?就是这个properties_table!注意,它是一个数组,zend_object是个变长结构体,分配时会根据非静态属性的数量确定其大小。

3.4.2.2 对象的创建

PHP中通过new + 类名创建一个类的实例,我们从一个例子分析下对象创建的过程中都有哪些操作。

  1. class my_class
  2. {
  3. const TYPE = 90;
  4. public $name = "pangudashu";
  5. public $ids = array();
  6. }
  7. $obj = new my_class();

类的定义就不用再说了,我们只看$obj = new my_class();这一句,这条语句包括两部分:实例化类、赋值,下面看下实例化类的语法规则:

  1. new_expr:
  2. T_NEW class_name_reference ctor_arguments
  3. { $$ = zend_ast_create(ZEND_AST_NEW, $2, $3); }
  4. | T_NEW anonymous_class
  5. { $$ = $2; }
  6. ;

从语法规则可以很直观的看出此语法的两个主要部分:类名、参数列表,编译器在解析到实例化类时就创建一个ZEND_AST_NEW类型的节点,后面编译为opcodes的过程我们不再细究,这里直接看下最终生成的opcodes。

3.4.2 对象 - 图1

你会发现实例化类产生了两条opcode(实际可能还会更多):ZEND_NEW、ZEND_DO_FCALL,除了创建对象的操作还有一条函数调用的,没错,那条就是调用构造方法的操作。

根据opcode、操作数类型可知ZEND_NEW对应的处理handler为ZEND_NEW_SPEC_CONST_HANDLER():

  1. static int ZEND_NEW_SPEC_CONST_HANDLER(zend_execute_data *execute_data)
  2. {
  3. zval object_zval;
  4. zend_function *constructor;
  5. zend_class_entry *ce;
  6. ...
  7. //第1步:根据类名查找zend_class_entry
  8. ce = zend_fetch_class_by_name(Z_STR_P(EX_CONSTANT(opline->op1)), ...);
  9. ...
  10. //第2步:创建&初始化一个这个类的对象
  11. if (UNEXPECTED(object_init_ex(&object_zval, ce) != SUCCESS)) {
  12. HANDLE_EXCEPTION();
  13. }
  14. //第3步:获取构造方法
  15. //获取构造方法函数,实际就是直接取zend_class_entry.constructor
  16. //get_constructor => zend_std_get_constructor()
  17. constructor = Z_OBJ_HT(object_zval)->get_constructor(Z_OBJ(object_zval));
  18. if (constructor == NULL) {
  19. ...
  20. //此opcode之后还有传参、调用构造方法的操作
  21. //所以如果没有定义构造方法则直接跳过这些操作
  22. ZEND_VM_JMP(OP_JMP_ADDR(opline, opline->op2));
  23. }else{
  24. //定义了构造方法
  25. //初始化调用构造函数的zend_execute_data
  26. zend_execute_data *call = zend_vm_stack_push_call_frame(...);
  27. call->prev_execute_data = EX(call);
  28. EX(call) = call;
  29. ...
  30. }
  31. }

从上面的创建对象的过程看整个流程主要分为三步:首先是根据类名在EG(class_table)中查找对应zend_class_entry、然后是创建并初始化一个对象、最后是初始化调用构造函数的zend_execute_data。

我们再具体看下第2步创建、初始化对象的操作,object_init_ex(&object_zval, ce)最终调用的是_object_and_properties_init()

  1. //zend_API.c
  2. ZEND_API int _object_and_properties_init(zval *arg, zend_class_entry *class_type, ...)
  3. {
  4. //检查类是否可以实例化
  5. ...
  6. //用户自定义的类create_object都是NULL
  7. //只有PHP几个内部的类有这个值,比如exception、error等
  8. if (class_type->create_object == NULL) {
  9. //分配一个对象
  10. ZVAL_OBJ(arg, zend_objects_new(class_type));
  11. ...
  12. //初始化成员属性
  13. object_properties_init(Z_OBJ_P(arg), class_type);
  14. } else {
  15. //调用自定义的创建object的钩子函数
  16. ZVAL_OBJ(arg, class_type->create_object(class_type));
  17. }
  18. return SUCCESS;
  19. }

还记得上一节介绍zend_class_entry时有几个自定义的钩子函数吗?如果定义了create_object这个地方就会调用自定义的函数来创建zend_object,这种情况通常发生在内核或扩展中定义的内部类(当然用户自定义类也可以修改,但一般不会那样做);用户自定义类在这个地方又具体分了两步:分配对象结构、初始化成员属性,我们继续看下这里面的处理。

(1)分配对象结构:zend_object

  1. //zend_objects.c
  2. ZEND_API zend_object *zend_objects_new(zend_class_entry *ce)
  3. {
  4. //分配zend_object
  5. zend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));
  6. zend_object_std_init(object, ce);
  7. //设置对象的操作handler为std_object_handlers
  8. object->handlers = &std_object_handlers;
  9. return object;
  10. }

有个地方这里需要特别注意:分配对象结构的内存并不仅仅是zendobject的大小。我们在3.4.2.1介绍propertiestable时说过这是一个变长数组,它用来存放非静态属性的值,所以分配zendobject时需要加上非静态属性所占用的内存大小:zend_object_properties_size(),根据普通非静态属性个数确定,如果没有定义get()、set()等魔术方法则占用内存就是: _属性数*sizeof(zval) ,如果定义了这些魔术方法那么会多分配一个zval的空间,这个多出来zval的用途下面介绍成员属性的读写时再作说明。

另外这里还有一个关键操作:将object编号并插入EG(objects_store).object_buckets数组。zend_object有个成员:handle,这个值在一次request期间所有实例化对象的编号,每调用zend_objects_new()实例化一个对象就会将其插入到object_buckets数组中,其在数组中的下标就是handle。这个过程是在zend_objects_store_put()中完成的。

  1. //zend_objects_API.c
  2. ZEND_API void zend_objects_store_put(zend_object *object)
  3. {
  4. int handle;
  5. if (EG(objects_store).free_list_head != -1) {
  6. //这种情况主要是gc中会将中间一些object销毁,空出一些bucket位置
  7. //然后free_list_head就指向了第一个可用的bucket位置
  8. //后面可用的保存在第一个空闲bucket的handle中
  9. handle = EG(objects_store).free_list_head;
  10. EG(objects_store).free_list_head = GET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[handle]);
  11. } else {
  12. if (EG(objects_store).top == EG(objects_store).size) {
  13. //扩容
  14. }
  15. //递增加1
  16. handle = EG(objects_store).top++;
  17. }
  18. object->handle = handle;
  19. //存入object_buckets数组
  20. EG(objects_store).object_buckets[handle] = object;
  21. }
  22. typedef struct _zend_objects_store {
  23. zend_object **object_buckets; //对象数组
  24. uint32_t top; //当前全部object数
  25. uint32_t size; //object_buckets大小
  26. int free_list_head; //第一个可用object_buckets位置
  27. } zend_objects_store;

将所有的对象保存在EG(objects_store).object_buckets中的目的是用于垃圾回收(不确定是不是还有其它的作用),防止出现循环引用而导致内存泄漏的问题,这个机制后面章节会单独介绍,这里只要记得有这么个东西就行了。

(2)初始化成员属性

  1. ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type)
  2. {
  3. if (class_type->default_properties_count) {
  4. zval *src = class_type->default_properties_table;
  5. zval *dst = object->properties_table;
  6. zval *end = src + class_type->default_properties_count;
  7. //将非静态属性值从:
  8. //zend_class_entry.default_properties_table复制到zend_object.properties_table
  9. do {
  10. ZVAL_COPY(dst, src);
  11. src++;
  12. dst++;
  13. } while (src != end);
  14. object->properties = NULL;
  15. }
  16. }

这一步操作是将非静态属性的值从zend_class_entry.default_properties_table -> zend_object.properties_table,当然这里不是硬拷贝,而是浅复制(增加引用),两者当前指向的value还是同一份,除非对象试图改写指向的属性值,那时将触发写时复制机制重新拷贝一份。

上面那个例子,类有两个普通属性:$name、$ids,假如我们实例化了两个对象,那么zend_class_entry与zend_object中普通属性值的关系如下图所示。

3.4.2 对象 - 图2

以上就是实例化一个对象的过程,总结一下具体的步骤:

  • step1: 首先根据类名去EG(class_table)中找到具体的类,即zend_class_entry
  • step2: 分配zend_object结构,一起分配的还有普通非静态属性值的内存
  • step3: 初始化对象的非静态属性,将属性值从zend_class_entry浅复制到对象中
  • step4: 查找当前类是否定义了构造函数,如果没有定义则跳过执行构造函数的opcode,否则为调用构造函数的执行进行一些准备工作(分配zend_execute_data)
  • step5: 实例化完成,返回新实例化的对象(如果返回的对象没有变量使用则直接释放掉了)

3.4.2.3 成员属性的读写

普通成员属性的读写处理handler分别为zend_object.handlers中的:read_property、write_property,默认对应的函数为:zend_std_read_property()、zend_std_write_property(),访问获取修改一个普通成员属性时就是由这两个函数完成的。

(1)读取属性:

通过对象或方法内通过$this访问属性,比如:echo $obj->name;,具体的实现:

  1. zval *zend_std_read_property(zval *object, zval *member, int type, void **cache_slot, zval *rv)
  2. {
  3. zend_object *zobj;
  4. uint32_t property_offset;
  5. zobj = Z_OBJ_P(object);
  6. //根据属性名在zend_class.zend_property_info中查找zend_property_info,得到属性值在zend_object中的存储offset
  7. //注意:zend_get_property_offset()会对属性的可见性(public、private、protected)进行验证
  8. property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (type == BP_VAR_IS) || (zobj->ce->__get != NULL), cache_slot);
  9. if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
  10. if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
  11. //普通属性,直接根据offset取到属性值:((zval*)((char*)(zobj) + offset))
  12. retval = OBJ_PROP(zobj, property_offset);
  13. } else if (EXPECTED(zobj->properties != NULL)) {
  14. //动态属性的情况,没有在类中显式定义的属性,后面一节会单独介绍
  15. ....
  16. }
  17. } else if (UNEXPECTED(EG(exception))) {
  18. ...
  19. }
  20. //没有找到属性
  21. //调用魔术方法:__isset()
  22. if ((type == BP_VAR_IS) && zobj->ce->__isset) {
  23. ...
  24. }
  25. //调用魔术方法:__get()
  26. if (zobj->ce->__get) {
  27. zend_long *guard = zend_get_property_guard(zobj, Z_STR_P(member));
  28. ...
  29. if(!((*guard) & IN_ISSET)){
  30. *guard |= IN_ISSET;
  31. zend_std_call_issetter(&tmp_object, member, &tmp_result);
  32. *guard &= ~IN_ISSET;
  33. ...
  34. }
  35. }
  36. ...
  37. }

普通成员属性的查找比较容易理解,首先是从zend_class的属性信息哈希表中找到zend_property_info,并判断其可见性(public、private、protected),如果可以访问则直接根据属性的offset在zend_object.properties_table数组中取到属性值,如果没有在属性哈希表中找到且定义了get()魔术方法则会调用get()方法处理。

Note: 如果类存在get()方法,则在实例化对象分配属性内存(即:properties_table)时会多分配一个zval,类型为HashTable,每次调用get($var)时会把输入的$var名称存入这个哈希表,这样做的目的是防止循环调用,举个例子:

public function __get($var) { return $this->$var; }

这种情况是调用get()时又访问了一个不存在的属性,也就是会在get()方法中递归调用,如果不对请求的$var作判断则将一直递归下去,所以在调用get()前首先会判断当前$var是不是已经在get()中了,如果是则不会再调用get(),否则会把$var作为key插入那个HashTable,然后将哈希值设置为:*guard |= IN_ISSET,调用完get()再把哈希值设置为:*guard &= ~IN_ISSET。

这个HashTable不仅仅是给get()用的,其它魔术方法也会用到,所以其哈希值类型是zend_long,不同的魔术方法占不同的bit位;其次,并不是所有的对象都会额外分配这个HashTable,在对象创建时会根据 zend_class_entry.ce_flags 是否包含 ZEND_ACC_USE_GUARDS 确定是否分配,在类编译时如果发现定义了get()、set()、unset()、__isset()方法则会将ce_flags打上这个掩码。

(2)设置属性:

与读取属性不同,设置属性是对属性的修改操作,比如:$obj->name = "pangudashu";,看下具体的实现过程:

  1. ZEND_API void zend_std_write_property(zval *object, zval *member, zval *value, void **cache_slot)
  2. {
  3. zend_object *zobj;
  4. uint32_t property_offset;
  5. zobj = Z_OBJ_P(object);
  6. //与读取属性相同
  7. property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (zobj->ce->__set != NULL), cache_slot);
  8. if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
  9. if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) {
  10. //普通属性
  11. variable_ptr = OBJ_PROP(zobj, property_offset);
  12. if (Z_TYPE_P(variable_ptr) != IS_UNDEF) {
  13. goto found;
  14. }
  15. } else if (EXPECTED(zobj->properties != NULL)) {
  16. //动态属性哈希表已经初始化,直接插入zobj->properties哈希表,后面单独介绍
  17. ...
  18. if ((variable_ptr = zend_hash_find(zobj->properties, Z_STR_P(member))) != NULL) {
  19. found:
  20. //赋值操作,与普通变量的操作相同
  21. zend_assign_to_variable(variable_ptr, value, IS_CV);
  22. goto exit;
  23. }
  24. }
  25. } else if (UNEXPECTED(EG(exception))) {
  26. ...
  27. }
  28. //没有找到属性
  29. //如果定义了__set()则调用
  30. if (zobj->ce->__set) {
  31. //与__get()相同,也会判断set的变量名是否已经在__set()中
  32. ...
  33. ZVAL_COPY(&tmp_object, object);
  34. (*guard) |= IN_SET; //防止循环__set()
  35. if (zend_std_call_setter(&tmp_object, member, value) != SUCCESS) {
  36. }
  37. (*guard) &= ~IN_SET;
  38. }else if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) {
  39. ...
  40. }
  41. }

首先与读取属性的操作相同:先找到zend_property_info,判断其可见性,然后根据offset取到具体的属性值,最后对其进行赋值修改。

Note: 属性读写操作的函数中有一个cache_slot的参数,它的作用涉及PHP的一个缓存机制:运行时缓存,后面会单独介绍。

3.4.2.4 对象的复制

PHP中普通变量的复制可以通过直接赋值完成,比如:

  1. $a = array();
  2. $b = $a;

但是对象无法这么进行复制,仅仅通过赋值传递对象,它们指向的都是同一个对象,修改时也不会发生硬拷贝。比如上面这个例子,我们把$a赋值给$b,然后如果我们修改$b的内容,那么这时候会进行value分离,$a的内容是不变的,但是如果是把一个对象赋值给了另一个变量,这俩对象不管哪一个修改另外一个都随之改变。

  1. class my_class
  2. {
  3. public $arr = array();
  4. }
  5. $a = new my_class;
  6. $b = $a;
  7. $b->arr[] = 1;
  8. var_dump($a === $b);
  9. ====================
  10. 输出:bool(true)

还记得我们在《2.1.3.2 写时复制》一节讲过zval有个类型掩码: type_flag 吗?其中有个是否可复制的标识:IS_TYPE_COPYABLE ,copyable的意思是当value发生duplication时是否需要或能够copy,而object的类型是不能复制(不清楚的可以翻下前面的章节),所以我们不能简单的通过赋值语句进行对象的复制。

PHP提供了另外一个关键词来实现对象的复制:clone

  1. $copy_of_object = clone $object;

clone出的对象就与原来的对象完全隔离了,各自修改都不会相互影响,另外如果类中定义了__clone()魔术方法,那么在clone时将调用此函数。

clone的实现比较简单,通过zend_object.clone_obj(即:zend_objects_clone_obj())完成。

  1. //zend_objects.c
  2. ZEND_API zend_object *zend_objects_clone_obj(zval *zobject)
  3. {
  4. zend_object *old_object;
  5. zend_object *new_object;
  6. old_object = Z_OBJ_P(zobject);
  7. //重新分配一个zend_object
  8. new_object = zend_objects_new(old_object->ce);
  9. //浅复制properties_table、properties
  10. //如果定义了__clone()则调用此方法
  11. zend_objects_clone_members(new_object, old_object);
  12. return new_object;
  13. }

3.4.2.5 对象比较

当使用比较运算符(==)比较两个对象变量时,比较的原则是:如果两个对象的属性和属性值 都相等,而且两个对象是同一个类的实例,那么这两个对象变量相等;而如果使用全等运算符(===),这两个对象变量一定要指向某个类的同一个实例(即同一个对象)。

PHP中对象间的”==”比较通过函数zend_std_compare_objects()处理。

  1. static int zend_std_compare_objects(zval *o1, zval *o2)
  2. {
  3. ...
  4. if (zobj1->ce != zobj2->ce) {
  5. return 1; /* different classes */
  6. }
  7. if (!zobj1->properties && !zobj2->properties) {
  8. //逐个比较properties_table
  9. ...
  10. }else{
  11. //比较properties
  12. return zend_compare_symbol_tables(zobj1->properties, zobj2->properties);
  13. }
  14. }

“===”的比较通过函数zend_is_identical()处理,比较简单,这里不再展开。

3.4.2.6 对象的销毁

object与string、array等类型不同,它是个复合类型,所以它的销毁过程更加复杂,赋值、函数调用结束或主动unset等操作中如果发现object引用计数为0则将触发销毁动作。

  1. //情况1
  2. $obj1 = new my_function();
  3. $obj1 = 123; //此时将断开对zend_object的引用,如果refcount=0则销毁
  4. //情况2
  5. function xxxx(){
  6. $obj1 = new my_function();
  7. ...
  8. return null; //清理局部变量时如果发现$obj1引用为0则销毁
  9. }
  10. //情况3
  11. $obj1 = new my_function();
  12. //整个脚本结束,清理全局变量时
  13. //情况4
  14. $obj1 = new my_function();
  15. unset($obj1);

上面这几个都是比较常见的会进行变量销毁的情况,销毁一个对象由zend_objects_store_del()完成,销毁的过程主要是清理成员属性、从EG(objects_store).object_buckets中删除、释放zend_object内存等等。

  1. //zend_objects_API.c
  2. ZEND_API void zend_objects_store_del(zend_object *object)
  3. {
  4. //这个函数if嵌套写的很挫...
  5. ...
  6. if (GC_REFCOUNT(object) > 0) {
  7. GC_REFCOUNT(object)--;
  8. return;
  9. }
  10. ...
  11. //调用dtor_obj,默认zend_objects_destroy_object()
  12. //接着调用free_obj,默认zend_object_std_dtor()
  13. object->handlers->dtor_obj(object);
  14. object->handlers->free_obj(object);
  15. ...
  16. ptr = ((char*)object) - object->handlers->offset;
  17. efree(ptr);
  18. }

另外,在减少refcount时如果发现object的引用计数大于0那么并不是什么都不做了,还记得2.1.3.4介绍的垃圾回收吗?PHP变量类型有的会因为循环引用导致正常的gc无法生效,这种类型的变量就有可能成为垃圾,所以会对这些类型的zval.u1.type_flag打上IS_TYPE_COLLECTABLE标签,然后在减少引用时即使refcount大于0也会启动垃圾检查,目前只有object、array两种类型会使用这种机制。