天天看點

【PHP7核心剖析】3.4 面向對象-對象的實作

更多《PHP7核心剖析》系列文章:https://github.com/pangudashu/php7-internal

3.4.2 對象

對象是類的執行個體,PHP中要建立一個類的執行個體,必須使用 new 關鍵字。類應在被執行個體化之前定義(某些情況下則必須這樣,比如3.4.1最後那幾個例子)。

3.4.2.1 對象的資料結構

對象的資料結構非常簡單:

typedef struct _zend_object     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)handle: 一次request期間對象的編号,每個對象都有一個唯一的編号,與建立先後順序有關,主要在垃圾回收時用,下面會詳細說明。

(2)ce: 所屬類的zend_class_entry。

(3)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 = {
    ,
    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 */
}
           

(4)properties: 普通成員屬性哈希表,對象建立之初這個值為NULL,主要是在動态定義屬性時會用到,與properties_table有一定關系,下一節我們将單獨說明,這裡暫時忽略。

(5)properties_table: 成員屬性數組,還記得我們在介紹類一節時提過非靜态屬性存儲在對象結構中嗎?就是這個properties_table!注意,它是一個數組,

zend_object

是個變長結構體,配置設定時會根據非靜态屬性的數量确定其大小。

3.4.2.2 對象的建立

PHP中通過

new + 類名

建立一個類的執行個體,我們從一個例子分析下對象建立的過程中都有哪些操作。

class my_class
{
    const TYPE = ;
    public $name = "pangudashu";
    public $ids = array();
}

$obj = new my_class();
           

類的定義就不用再說了,我們隻看

$obj = new my_class();

這一句,這條語句包括兩部分:執行個體化類、指派,下面看下執行個體化類的文法規則:

new_expr:
        T_NEW class_name_reference ctor_arguments
            { $$ = zend_ast_create(ZEND_AST_NEW, $, $); }
    |   T_NEW anonymous_class
            { $$ = $; }
;
           

從文法規則可以很直覺的看出此文法的兩個主要部分:類名、參數清單,編譯器在解析到執行個體化類時就建立一個

ZEND_AST_NEW

類型的節點,後面編譯為opcodes的過程我們不再細究,這裡直接看下最終生成的opcodes。

【PHP7核心剖析】3.4 面向對象-對象的實作

你會發現執行個體化類産生了兩條opcode(實際可能還會更多):ZEND_NEW、ZEND_DO_FCALL,除了建立對象的操作還有一條函數調用的,沒錯,那條就是調用

構造方法

的操作。

根據opcode、操作數類型可知

ZEND_NEW

對應的處理handler為

ZEND_NEW_SPEC_CONST_HANDLER()

:

static int ZEND_NEW_SPEC_CONST_HANDLER(zend_execute_data *execute_data)
{
    zval object_zval;
    zend_function *constructor;
    zend_class_entry *ce;
    ...
    //第1步:根據類名查找zend_class_entry
    ce = zend_fetch_class_by_name(Z_STR_P(EX_CONSTANT(opline->op1)), ...);
    ...
    //第2步:建立&初始化一個這個類的對象
    if (UNEXPECTED(object_init_ex(&object_zval, ce) != SUCCESS)) {
        HANDLE_EXCEPTION();
    }
    //第3步:擷取構造方法
    //擷取構造方法函數,實際就是直接取zend_class_entry.constructor
    //get_constructor => zend_std_get_constructor()
    constructor = Z_OBJ_HT(object_zval)->get_constructor(Z_OBJ(object_zval));

    if (constructor == NULL) {
        ...
        //此opcode之後還有傳參、調用構造方法的操作
        //是以如果沒有定義構造方法則直接跳過這些操作
        ZEND_VM_JMP(OP_JMP_ADDR(opline, opline->op2));
    }else{
        //定義了構造方法
        //初始化調用構造函數的zend_execute_data
        zend_execute_data *call = zend_vm_stack_push_call_frame(...);
        call->prev_execute_data = EX(call);
        EX(call) = call;
        ...
    }
}
           

從上面的建立對象的過程看整個流程主要分為三步:首先是根據類名在EG(class_table)中查找對應zend_class_entry、然後是建立并初始化一個對象、最後是初始化調用構造函數的zend_execute_data。

我們再具體看下第2步建立、初始化對象的操作,

object_init_ex(&object_zval, ce)

最終調用的是

_object_and_properties_init()

//zend_API.c
ZEND_API int _object_and_properties_init(zval *arg, zend_class_entry *class_type, ...)
{
    //檢查類是否可以執行個體化
    ...

    //使用者自定義的類create_object都是NULL
    //隻有PHP幾個内部的類有這個值,比如exception、error等   
    if (class_type->create_object == NULL) {
        //配置設定一個對象
        ZVAL_OBJ(arg, zend_objects_new(class_type));
        ...
        //初始化成員屬性
        object_properties_init(Z_OBJ_P(arg), class_type);
    } else {
        //調用自定義的建立object的鈎子函數
        ZVAL_OBJ(arg, class_type->create_object(class_type));
    }
    return SUCCESS;
}
           

還記得上一節介紹zend_class_entry時有幾個自定義的鈎子函數嗎?如果定義了

create_object

這個地方就會調用自定義的函數來建立zend_object,這種情況通常發生在核心或擴充中定義的内部類(當然使用者自定義類也可以修改,但一般不會那樣做);使用者自定義類在這個地方又具體分了兩步:配置設定對象結構、初始化成員屬性,我們繼續看下這裡面的處理。

(1)配置設定對象結構:zend_object

//zend_objects.c
ZEND_API zend_object *zend_objects_new(zend_class_entry *ce)
{
    //配置設定zend_object
    zend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));

    zend_object_std_init(object, ce);
    //設定對象的操作handler為std_object_handlers
    object->handlers = &std_object_handlers;
    return object;
}
           

有個地方這裡需要特别注意:配置設定對象結構的記憶體并不僅僅是zend_object的大小。我們在3.4.2.1介紹properties_table時說過這是一個變長數組,它用來存放非靜态屬性的值,是以配置設定zend_object時需要加上非靜态屬性所占用的記憶體大小:

zend_object_properties_size()

(實際就是zend_class_entry.default_properties_count)。

另外這裡還有一個關鍵操作:将object編号并插入EG(objects_store).object_buckets數組。zend_object有個成員:handle,這個值在一次request期間所有執行個體化對象的編号,每調用

zend_objects_new()

執行個體化一個對象就會将其插入到object_buckets數組中,其在數組中的下标就是handle。這個過程是在

zend_objects_store_put()

中完成的。

//zend_objects_API.c
ZEND_API void zend_objects_store_put(zend_object *object)
{
    int handle;

    if (EG(objects_store).free_list_head != -) {
        //這種情況主要是gc中會将中間一些object銷毀,空出一些bucket位置
        //然後free_list_head就指向了第一個可用的bucket位置
        //後面可用的儲存在第一個空閑bucket的handle中
        handle = EG(objects_store).free_list_head;
        EG(objects_store).free_list_head = GET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[handle]);
    } else {
        if (EG(objects_store).top == EG(objects_store).size) {
            //擴容
        }
        //遞增加1
        handle = EG(objects_store).top++;
    }
    object->handle = handle;
    //存入object_buckets數組
    EG(objects_store).object_buckets[handle] = object;
}

typedef struct _zend_objects_store {
    zend_object **object_buckets; //對象數組
    uint32_t top; //目前全部object數
    uint32_t size; //object_buckets大小
    int free_list_head; //第一個可用object_buckets位置
} zend_objects_store;
           

将所有的對象儲存在

EG(objects_store).object_buckets

中的目的是用于垃圾回收(不确定是不是還有其它的作用),防止出現循環引用而導緻記憶體洩漏的問題,這個機制後面章節會單獨介紹,這裡隻要記得有這麼個東西就行了。

(2)初始化成員屬性

ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type)
{
    if (class_type->default_properties_count) {
        zval *src = class_type->default_properties_table;
        zval *dst = object->properties_table;
        zval *end = src + class_type->default_properties_count;

        //将非靜态屬性值從:
        //zend_class_entry.default_properties_table複制到zend_object.properties_table
        do {
            ZVAL_COPY(dst, src);
            src++;
            dst++;
        } while (src != end);
        object->properties = NULL;
    }
}
           

這一步操作是将非靜态屬性的值從

zend_class_entry.default_properties_table -> zend_object.properties_table

,當然這裡不是硬拷貝,而是淺複制(增加引用),兩者目前指向的value還是同一份,除非對象試圖改寫指向的屬性值,那時将觸發寫時複制機制重新拷貝一份。

上面那個例子,類有兩個普通屬性: name、 ids,假如我們執行個體化了兩個對象,那麼zend_class_entry與zend_object中普通屬性值的關系如下圖所示。

【PHP7核心剖析】3.4 面向對象-對象的實作

以上就是執行個體化一個對象的過程,總結一下具體的步驟:

* step1: 首先根據類名去EG(class_table)中找到具體的類,即zend_class_entry

* step2: 配置設定zend_object結構,一起配置設定的還有普通非靜态屬性值的記憶體

* step3: 初始化對象的非靜态屬性,将屬性值從zend_class_entry淺複制到對象中

* step4: 查找目前類是否定義了構造函數,如果沒有定義則跳過執行構造函數的opcode,否則為調用構造函數的執行進行一些準備工作(配置設定zend_execute_data)

* step5: 執行個體化完成,傳回新執行個體化的對象(如果傳回的對象沒有變量使用則直接釋放掉了)

3.4.2.3 對象的複制

PHP中普通變量的複制可以通過直接指派完成,比如:

$a = array();
$b = $a;
           

但是對象無法這麼進行複制,僅僅通過指派傳遞對象,它們指向的都是同一個對象,修改時也不會發生硬拷貝。比如上面這個例子,我們把

$a

指派給

$b

,然後如果我們修改

$b

的内容,那麼這時候會進行value分離,

$a

的内容是不變的,但是如果是把一個對象指派給了另一個變量,這倆對象不管哪一個修改另外一個都随之改變。

class my_class 
{
    public $arr = array();
}

$a = new my_class;
$b = $a;

$b->arr[] = ;

var_dump($a === $b);
====================
輸出:bool(true)
           

還記得我們在《2.1.3.2 寫時複制》一節講過zval有個類型掩碼: type_flag 嗎?其中有個是否可複制的辨別:IS_TYPE_COPYABLE ,copyable的意思是當value發生duplication時是否需要或能夠copy,而object的類型是不能複制(不清楚的可以翻下前面的章節),是以我們不能簡單的通過指派語句進行對象的複制。

PHP提供了另外一個關鍵詞來實作對象的複制:clone。

$copy_of_object = clone $object;
           

clone

出的對象就與原來的對象完全隔離了,各自修改都不會互相影響,另外如果類中定義了

__clone()

魔術方法,那麼在

clone

時将調用此函數。

clone

的實作比較簡單,通過

zend_object.clone_obj

(即:

zend_objects_clone_obj()

)完成。

//zend_objects.c
ZEND_API zend_object *zend_objects_clone_obj(zval *zobject)
{
    zend_object *old_object;
    zend_object *new_object;

    old_object = Z_OBJ_P(zobject);
    //重新配置設定一個zend_object
    new_object = zend_objects_new(old_object->ce);

    //淺複制properties_table、properties
    //如果定義了__clone()則調用此方法
    zend_objects_clone_members(new_object, old_object);

    return new_object;
}
           

3.4.2.4 對象比較

當使用比較運算符(==)比較兩個對象變量時,比較的原則是:如果兩個對象的屬性和屬性值 都相等,而且兩個對象是同一個類的執行個體,那麼這兩個對象變量相等;而如果使用全等運算符(===),這兩個對象變量一定要指向某個類的同一個執行個體(即同一個對象)。

PHP中對象間的”==”比較通過函數

zend_std_compare_objects()

處理。

static int zend_std_compare_objects(zval *o1, zval *o2)
{
    ...

    if (zobj1->ce != zobj2->ce) {
        return ; /* different classes */
    }
    if (!zobj1->properties && !zobj2->properties) {
        //逐個比較properties_table
        ...
    }else{
        //比較properties
        return zend_compare_symbol_tables(zobj1->properties, zobj2->properties);
    }
}
           

“===”的比較通過函數

zend_is_identical()

處理,比較簡單,這裡不再展開。

3.4.2.5 對象的銷毀

object與string、array等類型不同,它是個符合類型,是以它的銷毀過程更加複雜,指派、函數調用結束或主動unset等操作中如果發現object引用計數為0則将觸發銷毀動作。

//情況1
$obj1 = new my_function();

$obj1 = ; //此時将斷開對zend_object的引用,如果refcount=0則銷毀

//情況2
function xxxx(){
    $obj1 = new my_function();
    ...
    return null; //清理局部變量時如果發現$obj1引用為0則銷毀
}

//情況3
$obj1 = new my_function();
//整個腳本結束,清理全局變量時

//情況4
$obj1 = new my_function();
unset($obj1);
           

上面這幾個都是比較常見的會進行變量銷毀的情況,銷毀一個對象由

zend_objects_store_del()

完成,銷毀的過程主要是清理成員屬性、從EG(objects_store).object_buckets中删除、釋放zend_object記憶體等等。

//zend_objects_API.c
ZEND_API void zend_objects_store_del(zend_object *object)
{
    //這個函數if嵌套寫的很挫...
    ...
    if (GC_REFCOUNT(object) > ) {
        GC_REFCOUNT(object)--;
        return;
    }
    ...

    //調用dtor_obj,預設zend_objects_destroy_object()
    //接着調用free_obj,預設zend_object_std_dtor()
    object->handlers->dtor_obj(object);
    object->handlers->free_obj(object);
    ...
    ptr = ((char*)object) - object->handlers->offset;
    efree(ptr);
}
           

另外,在減少refcount時如果發現object的引用計數大于0那麼并不是什麼都不做了,還記得2.1.3.4介紹的垃圾回收嗎?PHP變量類型有的會因為循環引用導緻正常的gc無法生效,這種類型的變量就有可能成為垃圾,是以會對這些類型的

zval.u1.type_flag

打上

IS_TYPE_COLLECTABLE

标簽,然後在減少引用時即使refcount大于0也會啟動垃圾檢查,目前隻有object、array兩種類型會使用這種機制。