SplFixedArray不是“正常”的类

作者: oraoto | 来源:发表于2017-01-03 18:33 被阅读179次

    最近在SegmentFault答了一个关于SplFixedArray的问题,重新整理成本文。

    现象

    <?php
    
    $arrA = SplFixedArray ::fromArray(array(true));
    $arrB = SplFixedArray ::fromArray(array(false));
    
    //json_encode($arrB);
    
    $equal = ($arrA == $arrB);
    var_export($equal);
    

    注释掉json_encode($arrB)时,$equaltrue,去掉注释,$equalfalse

    这个现象在PHP 5.3.0 - PHP 7.1.0里都存在。

    PHP是如果比较对象相等的?

    按直觉,两个SplFixedArray对象里的数组内容是不同的,不应该出现$equaltrue的情况。我们看一下PHP源代码中比较对象相等的代码,我加了点注释:

    // Zend/zend_object_handlers.c
    // 注意:调用zend_std_compare_objects前已经判定了o1和o2地址不同
    
    static int zend_std_compare_objects(zval *o1, zval *o2) /* {{{ */
    {
        zend_object *zobj1, *zobj2;
    
        zobj1 = Z_OBJ_P(o1);
        zobj2 = Z_OBJ_P(o2);
    
        if (zobj1->ce != zobj2->ce) {  // 如果是不同类的对象,一定不相等
            return 1; /* different classes */
        }
    
        if (!zobj1->properties && !zobj2->properties) { // Step 1: 如果两个对象没有动态添加属性
            zval *p1, *p2, *end;
    
            if (!zobj1->ce->default_properties_count) { // Step 2: 如果类定义(Class Entry)里没有定义成员变量
                return 0;  // Step 3: 相等
            }
    
            // Step 4: 对比类定义的成员变量
            p1 = zobj1->properties_table;
            p2 = zobj2->properties_table;
            end = p1 + zobj1->ce->default_properties_count;
            Z_OBJ_PROTECT_RECURSION(o1);
            Z_OBJ_PROTECT_RECURSION(o2);
            do {
                ...
            } while (p1 != end);
            Z_OBJ_UNPROTECT_RECURSION(o1);
            Z_OBJ_UNPROTECT_RECURSION(o2);
            return 0;
        } else {
            // Step 4:重建properties
            if (!zobj1->properties) {
                rebuild_object_properties(zobj1);
            }
            if (!zobj2->properties) {
                rebuild_object_properties(zobj2);
            }
            // Step 5:对比properties
            return zend_compare_symbol_tables(zobj1->properties, zobj2->properties);
        }
    }
    

    ce表示Class Entry,保存类的定义,properties_table是对象的成员变量,properties是对象属性(包括了成员变量),两者是有区别的:

    class A {
        public $a;
        public $b = 2;
    }
    
    $a1 = new A();
    $a2 = new A();
    $a2->a = 1;
    $a2->c = 2;  // 添加了c
    

    执行完上面代码后,$a1$a2的properties_table都有两个元素(a, b),$a1properties是空的,而$a2的是有3元素的。
    即动态添加属性时,会把properties_table的成员变量到properties里,然后在添加到properties

    根据上面的代码,总结对象的比较规则:

    1. 如果两个对象是不同类型,不相等
    2. 如果两个对象都没有动态添加属性(properties为空),比较两者的成员变量(properties_table)
    3. 如果其中一个对象有动态添加属性(properties不为空),如果另一个没有的则添加(rebuild_object_properties会复制properties_table),然后比较两者的属性(properties

    回到第一部分SplFixedArray的测试代码,调试时发现,没有json_encode($arrB)时,$arrBproperties是空的,表示没有动态添加属性,而SplFixedArray类也没定义成员变量,
    所以走代码中的Step 1 -> Step 2 -> Step 3,直接返回0表示相等。
    而调用了json_encode($arrB)之后,$arrBproperties就不为空了,比较流程就变成:Step 1 -> Step 4 -> Step 5,这个时候就会比较对象的属性。

    到这里,我们可以确定:

    1. SplFixedArray对象本来是没有成员变量、没有动态添加的属性,==比较都返回true
    2. SplFixedArray对象在json_encode后有了动态添加的属性,==比较对象的属性

    json_encode为什么会动态添加属性?

    json_encode的代码,其中是这一句:myht = Z_OBJPROP_P(val)Z_OBJPROP_P的定义:

    #define Z_OBJPROP_P(zval_p) Z_OBJPROP(*(zval_p))
    
    #define Z_OBJDEBUG(zval,tmp) (Z_OBJ_HANDLER((zval),get_debug_info)?Z_OBJ_HANDLER((zval),get_debug_info)(&(zval),&tmp):(tmp=0,Z_OBJ_HANDLER((zval),get_properties)?Z_OBJPROP(zval):NULL))
    

    简单来说就是调用对象的object handler里的里的get_debug_info或者get_propertiesSplFixedArrayget_properties是这样的:

    static HashTable* spl_fixedarray_object_get_properties(zval *obj) /* {{{{ */
    {
        spl_fixedarray_object *intern  = Z_SPLFIXEDARRAY_P(obj);
        HashTable *ht = zend_std_get_properties(obj);
        zend_long  i = 0;
    
        if (intern->array) {
            ... 复制数组到ht
        }
    
        return ht;
    }
    

    其中调用了zend_std_get_properties

    ZEND_API HashTable *zend_std_get_properties(zval *object) /* {{{ */
    {
        zend_object *zobj;
        zobj = Z_OBJ_P(object);
        if (!zobj->properties) {
            rebuild_object_properties(zobj);
        }
        return zobj->properties;
    }
    

    其中又调用了rebuild_object_properties,创建了properties

    类似的,var_dump也会又类似的获取和创建对象属性的流程。

    结果

    1. SplFixedArray对象不能通过==进行比较
    2. 使用get_properties的函数(var_dumpjson_encode……)会导致SplFixedArray复制底层的C数组到PHP的数组,导致内存占用增大

    可能的修复方式

    1. SplFixedArrayobject handler要定义compare_objects,实现正确的比较
    2. SplFixedArrayget_properties不要调用zend_std_get_properties,而是直接返回一个HashTable,之后让gc清理掉,避免一直占用内存。

    但是,还没测试过,不知实际可不可行。

    相关文章

      网友评论

        本文标题:SplFixedArray不是“正常”的类

        本文链接:https://www.haomeiwen.com/subject/seenvttx.html