PHP源码学习20190403-PHP类与对象

33次阅读

共计 8778 个字符,预计需要花费 22 分钟才能阅读完成。

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 即可

正文完
 0