baiyan
全部视频:https://segmentfault.com/a/11...
类的存储
- 谈到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数组中,default_properties_table里的元素的zend_value中的str指针指向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即可
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。