最近在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)
时,$equal
为true
,去掉注释,$equal
为false
。
这个现象在PHP 5.3.0 - PHP 7.1.0里都存在。
PHP是如果比较对象相等的?
按直觉,两个SplFixedArray
对象里的数组内容是不同的,不应该出现$equal
为true
的情况。我们看一下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
),$a1
的properties
是空的,而$a2
的是有3元素的。
即动态添加属性时,会把properties_table
的成员变量到properties
里,然后在添加到properties
。
根据上面的代码,总结对象的比较规则:
- 如果两个对象是不同类型,不相等
- 如果两个对象都没有动态添加属性(
properties
为空),比较两者的成员变量(properties_table
) - 如果其中一个对象有动态添加属性(
properties
不为空),如果另一个没有的则添加(rebuild_object_properties
会复制properties_table
),然后比较两者的属性(properties
)
回到第一部分SplFixedArray
的测试代码,调试时发现,没有json_encode($arrB)
时,$arrB
的properties
是空的,表示没有动态添加属性,而SplFixedArray
类也没定义成员变量,
所以走代码中的Step 1 -> Step 2 -> Step 3
,直接返回0表示相等。
而调用了json_encode($arrB)
之后,$arrB
的properties
就不为空了,比较流程就变成:Step 1 -> Step 4 -> Step 5
,这个时候就会比较对象的属性。
到这里,我们可以确定:
-
SplFixedArray
对象本来是没有成员变量、没有动态添加的属性,==
比较都返回true
-
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_properties
,SplFixedArray
的get_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
也会又类似的获取和创建对象属性的流程。
结果
-
SplFixedArray
对象不能通过==
进行比较 - 使用
get_properties
的函数(var_dump
、json_encode
……)会导致SplFixedArray
复制底层的C数组到PHP的数组,导致内存占用增大
可能的修复方式
-
SplFixedArray
的object handler
要定义compare_objects
,实现正确的比较 -
SplFixedArray
的get_properties
不要调用zend_std_get_properties
,而是直接返回一个HashTable
,之后让gc清理掉,避免一直占用内存。
但是,还没测试过,不知实际可不可行。
网友评论