【PHP源码学习】2019-04-03 PHP类与对象

baiyan

类的存储

  • 谈到PHP中的类,我们知道,类是对象的抽象,是所有通过它new出来对象的模板,它是编译阶段的产物。一个类被抽象出来,它本身有自己的属性、方法等等要素。如果让我们自己去用C语言实现一个类的存储结构,我们如何设计?
  • 类的几大要素:类常量、普通属性、静态属性、方法
  • 类作用域:所有对象之间共享,如类常量、静态属性、方法
  • 对象作用域:所有对象之间独享,如普通属性、动态属性
  • 下面我们逐个来看究竟它们是被如何存储的:

类常量的存储

  • 在PHP7中,使用一个叫做zend_class_entry的结构体来存储类的相关数据。
  • 类常量不能被修改,属于类作用域,以const关键字标识,所有对象共享一份类常量。
  • 首先我们举一个PHP类常量的例子:
class A{
    const PI = 3.14;
}
  • 这里的PI就是一个类常量。常量名为PI,常量值为3.14。我们可以用两种方式来访问它:
  • 类外:A::PI
  • 类内:self::PI
  • 那么我们看一下常量的存储结构:
struct _zend_class_entry {
    ...
    HashTable constants_table; //常量哈希表,key为常量名,value为常量值
    ...
};
  • 在PHP7中,类是以一个zend_class_entry结构体来存储的。其中这个constants_table字段,就是用来存储类常量的。我们知道,常量是属于类作用域的,而不是对象作用域,所以它的值被直接放在类结构体中。它是一个hashtable,其中key为常量名,value为常量值。当访问某个常量值的时候,我们可以直接根据常量的名字作为key,到hashtable中查找对应的常量值即可,这里还是很好理解的。

普通属性的存储

  • 普通属性属于对象作用域,每个对象的属性值可以不同,因为我们现在讲的是类,所以我们在类作用域下讲解一下和普通属性相关的数据在类结构中,究竟在哪里有所体现。
  • 举一个PHP普通属性的例子:
class A{
    public $name = 'jby';
}
  • 这里name就是属性名,它有一个初始化值为jby,也有两种访问方式:
  • 类内部:$this->name
  • 类外部:对象->name
  • 下面看一下在类结构zend_class_entry中,与普通属性存储相关的字段:
struct _zend_class_entry {
    ...
    int default_properties_count; //普通属性的数量总和
    ...
    zval *default_properties_table; //存放普通属性的初始化值的数组
    ...
    HashTable properties_info; //存储对象属性的信息哈希表,key为属性名,value为zend_property_info结构体
    ... 
}
  • int default_properties_count字段存储一个类中所有普通属性的数量之和
  • 我们知道,由于普通属性是对象作用域,所以每一个对象下的普通属性值是不同的,所以针对不同对象的属性值,需要放在具体不同对象的结构中去存储。但是,由于PHP允许普通属性具有初始化值(如上例的jby),而这个初始化值在所有对象实例中共享,故初始化值可以放在类作用域中进行存储。所以初始化的值(如上例的jby)可以直接存储在类结构体下的zval *default_properties_table这个zval数组中,这个zval就指向一个zend_string,其值为jby。
  • 然后我们看具体每个对象中属性的存储。由于普通属性有访问权限(public/protected/private)等额外信息需要存储,所以在类作用域内,存储普通属性的信息需要一个结构体,而且是一个普通属性就要对应一个结构体来存储它的信息。。
  • 在类结构zend_class_entry中,我们使用HashTable properties_info这个字段来存储普通属性的信息,而这个字段是一个hashtable,它的key为属性名,value为一个结构体,它就是用来存储每一个普通属性的信息的,叫做zend_property_info。每一个属性,就会对应一个zend_property_info结构:
typedef struct _zend_property_info {
    uint32_t offset; //表示普通属性的内存偏移值或静态属性的数组索引
    uint32_t flags;  //属性掩码,如public、private、protected及是否为静态属性
    zend_string *name; //属性名
    zend_string *doc_comment; //文档注释信息
    zend_class_entry *ce; //所属类
} zend_property_info;

//flags标识位
#define ZEND_ACC_PUBLIC     0x100
#define ZEND_ACC_PROTECTED  0x200
#define ZEND_ACC_PRIVATE    0x400
#define ZEND_ACC_STATIC      0x01
  • 我们看这个存储普通属性信息的结构体。下面的属性名等字段我们很容易理解,那么重点则是这个offset字段。由于类作用域是不能确定每个对象中普通属性的值的(不同对象属性值不同),所以普通属性的值会在对象存储结构zend_object中以数组的形式存储(其实是一个柔性数组,后面会讲到)。它的字面意义是偏移量,那么这个偏移量是相对于谁的偏移量呢?答案就是相对于上述的存储值的数组的偏移量,这个偏移量是以一个zval大小(16)递增的(下面讲到对象结构的时候会具体讲)

静态属性的存储

  • 静态属性也属于类作用域,以static关键字标识,所有对象共享类中的静态属性。所以在类结构zend_class_entry中,就可以直接将静态属性的值存到这个类结构中,静态属性的使用示例如下:
class A{
    static $instance = null;
}
  • 访问静态属性也有两种方式:
  • 类内部:self::$instance
  • 类外部:A::$instance
  • 静态属性在所有对象中共享,所以在类作用域中,可以直接存储它的值:
struct _zend_class_entry {
    ...
    int default_static_members_count;    //静态属性数量总和
    ...
    zval *default_static_members_table;  //存放静态属性初始化值的数组
    zval *static_members_table; //存放静态属性值的数组
    ...
    HashTable properties_info; //存储对象属性的信息哈希表,key为属性名,value为zend_property_info结构体
    ...
}
  • int default_static_members_count字段存储一个类中所有静态属性的数量之和
  • default_static_members_table用来存放静态属性的初始化值,这一点和普通属性初始化值的存放是相同思想,不再赘述
  • static_members_table用来直接存放静态属性的值
  • HashTable properties_info同样也是一个key为属性名,value为zend_porperty_info结构体的hashtable,里面同样存放着offset,而这个offset代表每一个静态属性在static_members_table和default_static_members_table这两个存放值的数组中的索引。这样,我们可以快速地根据当前的静态属性名,根据静态属性名这个key,在hashtable中查找到zend_property_info结构体中的offset字段,根据这个偏移量,进而去对应的数组单元中,也就是static_members_table或default_static_members_table数组中,找到当前静态属性名对应的值,这样就快速地完成了一次静态属性的访问。

方法的存储

  • 由于方法也属于类作用域,所有对象共享相同的方法体。所以在类结构中,就可直接以一个hashtable存储方法。key为方法名称,value为具体的zend_function:
struct _zend_class_entry {
    ...
    HashTable function_table;  //成员方法哈希表
    ...
}

其他

  • 一个类,可能它是一个子类,也可能是是一个抽象类或接口、甚至是trait,还有类本身的构造函数、析构函数等等。那么这些信息,我们要如何去表示呢?现在我们看一下这个完整的zend_class_entry类结构:
struct _zend_class_entry {
    char type;          //类的类型:内部类ZEND_INTERNAL_CLASS(1)、用户自定义类ZEND_USER_CLASS(2)
    zend_string *name;  //类名
    struct _zend_class_entry *parent; //父类指针
    int refcount; //引用计数
    uint32_t ce_flags;  //类掩码,如普通类、抽象类、接口等等

    int default_properties_count;        //普通属性的数量总和
    int default_static_members_count;    //静态属性数量总和
    zval *default_properties_table;      //存放普通属性初始化值的数组
    zval *default_static_members_table;  //存放静态属性初始化值的数组
    zval *static_members_table; //存放静态属性值的数组
    HashTable function_table;  //成员方法哈希表
    HashTable properties_info; //存储对象属性的信息哈希表,key为属性名,value为zend_property_info结构体
    HashTable constants_table; //常量哈希表,key为常量名,value为常量值

    //构造函数、析构函数以及魔术方法的指针
    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 *__debugInfo;
    union _zend_function *serialize_func;
    union _zend_function *unserialize_func;

    zend_class_iterator_funcs iterator_funcs;

    //自定义的钩子函数,通常是定义内部类时使用,可以灵活的进行一些个性化的操作
    //用户自定义类不会用到,暂时忽略即可
    zend_object* (*create_object)(zend_class_entry *class_type);
    zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);
    int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */
    union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);

    /* serializer callbacks */
    int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);
    int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);

    uint32_t num_interfaces; //实现的接口数量总和
    uint32_t num_traits; //使用的trait数量总和
    zend_class_entry **interfaces; //实现的接口,可以理解为它指向一个一维数组,一维数组里全部存放的都是类结构的指针,指向它所实现的接口类

    zend_class_entry **traits; //所使用的trait,理解方法同上
    zend_trait_alias **trait_aliases; //trait别名,解决多个trait中方法重名冲突的问题
    zend_trait_precedence **trait_precedences;

    union {
        struct {
            zend_string *filename;
            uint32_t line_start;
            uint32_t line_end;
            zend_string *doc_comment;
        } user;
        struct {
            const struct _zend_function_entry *builtin_functions;
            struct _zend_module_entry *module; //所属扩展
        } internal;
    } info;
}

对象的存储

  • 现在我们再谈对象。我们知道,对象是类的具体实现,是运行阶段的产物。其普通属性是每个对象独享的,所以,在分析对象中,我们要尤其注重每个对象独特的普通属性值是如何存储的。由于之前在讲类存储的时候已经有了铺垫,还记得之前说的zend_property_info中的offset偏移量吗,我们带着这个知识点,直接看对象的存储结构。在PHP7中,使用一个叫做zend_object的结构体来存储对象相关的数据:
struct _zend_object {
    zend_refcounted_h gc; //内部存有引用计数
    uint32_t          handle; 
    zend_class_entry *ce; //所属的类
    const zend_object_handlers *handlers; 
    HashTable        *properties; //存储动态属性值
    zval              properties_table[1]; //柔性数组,每一个单元都是zval类型,用来存储普通属性值,offset就是相对于当前字段首地址的偏移量
};

普通属性的存储

  • 我们知道,一个对象,就对应一个zend_object结构。那么最重要的字段就是zval properties_table[1]字段了。它是一个柔性数组,放到结构体的末尾,可以存储变长大小的数据,且与结构体内存空间紧紧相连(柔性数组请看这一系列的前几篇文章有详细讲解)。
  • 在创建一个新对象的时候,在类作用域中存储的普通属性的初始化值,都会拷贝到对象结构中的柔性数组中
  • 那么现在,之前讲过的类结构中property_info哈希表中的字段的value值zend_property_info中的offset偏移量字段就要派上用场了。想一下,如果让我们访问某个对象的普通属性的值,应该如何访问:
- 通过指针ce找到当前对象对应的类结构zend_class_entry
 - 取出当前类结构中的Hashtable property_info字段,这个字段是一个哈希表,存有属性的信息。
 - 将要查找的属性名作为key,到哈希表中找到对应的value,即zend_property_info结构体,并取出结构体中的offset字段
 - 到当前对象zend_object结构体中,通过内存地址计算(柔性数组的起始地址+offset)就可以得到所要访问的当前对象的某个普通属性的值了
  • 那么我们看一下其他几个字段的作用:
  • handle:一次request期间对象的编号,每个对象都有一个唯一的编号,与创建先后顺序有关,主要在垃圾回收时使用
  • handlers:保存的对象相关操作的一些函数指针,比如属性的读写、方法的获取、对象的销毁/克隆等等,这些操作接口都有默认的函数,这里存储了这些默认函数的指针:
struct _zend_object_handlers {
    int                                     offset;
    zend_object_free_obj_t                  free_obj; //释放对象
    zend_object_dtor_obj_t                  dtor_obj; //销毁对象
    zend_object_clone_obj_t                 clone_obj;//复制对象
    
    zend_object_read_property_t             read_property; //读取成员属性
    zend_object_write_property_t            write_property;//修改成员属性
    ...
}

//处理对象的handler
ZEND_API zend_object_handlers std_object_handlers = {
    0,
    zend_object_std_dtor,                   /* free_obj */
    zend_objects_destroy_object,            /* dtor_obj */
    zend_objects_clone_obj,                 /* clone_obj */
    zend_std_read_property,                 /* read_property */
    zend_std_write_property,                /* write_property */
    zend_std_read_dimension,                /* read_dimension */
    zend_std_write_dimension,               /* write_dimension */
    zend_std_get_property_ptr_ptr,          /* get_property_ptr_ptr */
    NULL,                                   /* get */
    NULL,                                   /* set */
    zend_std_has_property,                  /* has_property */
    zend_std_unset_property,                /* unset_property */
    zend_std_has_dimension,                 /* has_dimension */
    zend_std_unset_dimension,               /* unset_dimension */
    zend_std_get_properties,                /* get_properties */
    zend_std_get_method,                    /* get_method */
    NULL,                                   /* call_method */
    zend_std_get_constructor,               /* get_constructor */
    zend_std_object_get_class_name,         /* get_class_name */
    zend_std_compare_objects,               /* compare_objects */
    zend_std_cast_object_tostring,          /* cast_object */
    NULL,                                   /* count_elements */
    zend_std_get_debug_info,                /* get_debug_info */
    zend_std_get_closure,                   /* get_closure */
    zend_std_get_gc,                        /* get_gc */
    NULL,                                   /* do_operation */
    NULL,                                   /* compare */
}

动态属性的存储

  • properties: 普通成员属性哈希表,key为动态属性名,value为动态属性值。对象创建之初这个值为NULL,主要是在动态定义属性时会用到。
  • 那么什么是动态属性呢?就是之前在类定义阶段未定义的属性,在运行期间动态添加的属性,如:
class A{
    public $name = 'jby';
}
$a = new A();
$a->age = 18;
  • 这里的age就是动态属性,而name是普通属性。
  • 基于之前讲过的查找普通属性的流程,我们由特殊到一般地得出查找所有类型的对象属性的方式:
  • 在查找一个对象的属性的时候,会首先按照我们之前讲过的查找普通属性的方式,首先找到偏移量offset,即类结构的zend_class_entry下的properties_info字段中的offset,然后根据这个偏移量offset到对象结构zend_object下的properties_table柔性数组中找。
  • 如果按照查找普通属性的方式没有找到,那么我们再去zend_object下的properties字段继续查找动态属性即可,整理如下:
- 通过指针ce找到当前对象对应的类结构zend_class_entry
 - 取出当前类结构中的Hashtable property_info字段,这个字段是一个哈希表,存有属性的信息。
 - 将要查找的属性名作为key,到哈希表中找到对应的value,即zend_property_info结构体,并取出结构体中的offset字段
 - 到当前对象zend_object结构体中,通过内存地址计算(柔性数组的起始地址+offset)就可以得到所要访问的当前对象的某个普通属性的值
 - 如果以上都没有找到,说明它是一个动态属性,那么就去zend_object下的properties哈希表中查找,属性名作为key,到这个哈希表中查找对应的value即可

相关推荐