程序员今日看点首页投稿(暂停使用,暂停投稿)

SplFixedArray不是“正常”的类

2017-01-03  本文已影响179人  oraoto

最近在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清理掉,避免一直占用内存。

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

上一篇下一篇

猜你喜欢

热点阅读