baiyan
全部视频:https://segmentfault.com/a/11…
类的存储
- 谈到 PHP 中的类,我们知道,类是对象的抽象,是所有通过它 new 出来对象的模板,它是 编译阶段 的产物。一个类被抽象出来,它本身有自己的属性、方法等等要素。如果让我们自己去用 C 语言实现一个类的存储结构,我们如何设计?
- 类的几大要素:类常量、普通属性、静态属性、方法
- 类作用域:所有对象之间共享,如类常量、静态属性、方法
- 对象作用域:所有对象之间独享,如普通属性、动态属性
- 下面我们逐个来看究竟它们是被如何存储的:
类常量的存储
- 类常量不能被修改,属于类作用域,以 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 偏移量吗,我们带着这个知识点,直接看对象的存储结构:
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 即可