最近在SegmentFault答了一個關(guān)于SplFixedArray的問題,重新整理成本文。
現(xiàn)象
<?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。
這個現(xiàn)象在PHP 5.3.0 - PHP 7.1.0里都存在。
PHP是如果比較對象相等的?
按直覺,兩個SplFixedArray對象里的數(shù)組內(nèi)容是不同的,不應(yīng)該出現(xiàn)$equal為true的情況。我們看一下PHP源代碼中比較對象相等的代碼,我加了點注釋:
// Zend/zend_object_handlers.c
// 注意:調(diào)用zend_std_compare_objects前已經(jīng)判定了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: 如果兩個對象沒有動態(tài)添加屬性
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是對象屬性(包括了成員變量),兩者是有區(qū)別的:
class A {
public $a;
public $b = 2;
}
$a1 = new A();
$a2 = new A();
$a2->a = 1;
$a2->c = 2; // 添加了c
執(zhí)行完上面代碼后,$a1和$a2的properties_table都有兩個元素(a, b),$a1的properties是空的,而$a2的是有3元素的。
即動態(tài)添加屬性時,會把properties_table的成員變量到properties里,然后在添加到properties。
根據(jù)上面的代碼,總結(jié)對象的比較規(guī)則:
- 如果兩個對象是不同類型,不相等
- 如果兩個對象都沒有動態(tài)添加屬性(
properties為空),比較兩者的成員變量(properties_table) - 如果其中一個對象有動態(tài)添加屬性(
properties不為空),如果另一個沒有的則添加(rebuild_object_properties會復制properties_table),然后比較兩者的屬性(properties)
回到第一部分SplFixedArray的測試代碼,調(diào)試時發(fā)現(xiàn),沒有json_encode($arrB)時,$arrB的properties是空的,表示沒有動態(tài)添加屬性,而SplFixedArray類也沒定義成員變量,
所以走代碼中的Step 1 -> Step 2 -> Step 3,直接返回0表示相等。
而調(diào)用了json_encode($arrB)之后,$arrB的properties就不為空了,比較流程就變成:Step 1 -> Step 4 -> Step 5,這個時候就會比較對象的屬性。
到這里,我們可以確定:
-
SplFixedArray對象本來是沒有成員變量、沒有動態(tài)添加的屬性,==比較都返回true -
SplFixedArray對象在json_encode后有了動態(tài)添加的屬性,==比較對象的屬性
json_encode為什么會動態(tài)添加屬性?
看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))
簡單來說就是調(diào)用對象的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) {
... 復制數(shù)組到ht
}
return ht;
}
其中調(diào)用了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;
}
其中又調(diào)用了rebuild_object_properties,創(chuàng)建了properties!
類似的,var_dump也會又類似的獲取和創(chuàng)建對象屬性的流程。
結(jié)果
-
SplFixedArray對象不能通過==進行比較 - 使用
get_properties的函數(shù)(var_dump、json_encode……)會導致SplFixedArray復制底層的C數(shù)組到PHP的數(shù)組,導致內(nèi)存占用增大
可能的修復方式
-
SplFixedArray的object handler要定義compare_objects,實現(xiàn)正確的比較 -
SplFixedArray的get_properties不要調(diào)用zend_std_get_properties,而是直接返回一個HashTable,之后讓gc清理掉,避免一直占用內(nèi)存。
但是,還沒測試過,不知實際可不可行。