Wordpress 在 PHP7.1 下 wp_default_styles()报 Warning 的探究
Oct 7, 2016遇到问题
如果你使用最新的发布的 PHP7.1 来跑 WordPress,会惊讶的发现页面上会报出几个 Warning 的错误
Warning: Parameter 1 to wp_default_styles() expected to be a reference, value given in /Users/Home/Sites/WordPress-4.6/wp-includes/plugin.php on line 600
Warning: Parameter 1 to wp_default_scripts() expected to be a reference, value given in /Users/Home/Sites/WordPress-4.6/wp-includes/plugin.php on line 600
也许你会觉得这可能是某个主题或者插件导致,或者是某个版本的 WordPress 有问题,然而不幸的是,这是一个 WordPress 与 PHP7.1 间的兼容问题,几乎所有版本的 WordPress 都会中招。
探查原因
在 WordPress 的官网 support 里面有人提过这个 BUG,但是并没有获得有效的回应,只是提醒大家 PHP7.1 目前还不是稳定版本 😂😂
最后,我倒是在 php 的官方邮件组中找到了相关信息:
邮件地址:http://php-news.ctrl-f5.net/message/php.internals/94856
主要解答:
Thanks for pointing this out.
This is caused by the change to array_slice() as part of https://github.com/php/php-src/commit/e730c8fc90299789a7f551cb7142e182952d92e0#diff-497f073aa1ab88afcb8b248fc25d2a12R3014 ..
As a consequence of this change, an array_slice() on an array with rc=1 references will now not return these references in the result. (This is the correct behavior – previously it instead dropped the references in the original array, which is not wrong either, but non-standard.)
It looks like Wordpress is passing these arrays to call_user_func_array() with a function that expects a reference argument:
call_user_func_array($the_['function'], array_slice($args, 0, (int)
$the_['accepted_args']));
And this results in:
nikic@saturn:~/php-src-fast$ sapi/cgi/php-cgi -c php.ini -T1
.../wordpress-4.1/index.php | grep Warning
Warning: Parameter 1 to wp_default_styles() expected to be a
reference, value given in
/home/nikic/wordpress-4.1/wp-includes/plugin.php on line
571
Warning: Parameter 1 to wp_default_scripts() expected to be a
reference, value given in
/home/nikic/wordpress-4.1/wp-includes/plugin.php on line
571
So essentially, we’re winning 5% because these two calls do not occur…
这个 commit 是鸟哥(laruence) 提的,目的是优化 array_merge()
, 顺便也就修复了 array_slice()
这个方法中一个不符合标准的行为。
当我们用 array_slice()
处理一个包含引用次数为 1 的引用元素的数组时,该元素在返回的结果数组中会以值(value)的形式存在,而不是引用(references)。这里有点不同于之前的版本,之前的版本返回的结果依旧是引用,这两种方式并没有谁对谁错,只能说之前的处理方式不够标准。
复现与证实
本来调查到这里本来应该是『水落石出』了,但是,我英语差啊,一开始完全理解不到 Nikita 说的改动到底是什么,为了弄清楚这里面的详细,真是花去了我不少时间。
从哪儿报的错
先看测试代码:
function test(&$t) {
$t = $t + 1;
return $t;
}
$a = 1;
test($a);
// => 2
test(&$a);
// PHP Fatal error: Call-time pass-by-reference has been removed in eval()'d
call_user_func('test', $a);
// PHP warning: Parameter 1 to test() expected to be a reference, value given
call_user_func('test', &$a);
// PHP Fatal error: Call-time pass-by-reference has been removed in eval()'d
call_user_func_array('test', [$a]);
// PHP warning: Parameter 1 to test() expected to be a reference, value given
call_user_func_array('test', [&$a]);
// => 3
首先我们定义了一个 test()
方法,它将以引用的方式操作传入的参数。
正常调用,因为是引用,执行完后,
$a
的值变成 2.报错了,PHP 手册中引用传递一节解释了这个报错的原因。英文版手册中更是有明确提到这项改进是 5.3 加入的,5.4 开始会抛出一个 Fatal 错误。
获得了一个目标报错,说明使用回调函数调用时,会检测参数是否为引用。
同第2项报错,结合起来也说明了
call_user_func()
是无法使用引用元素作为回调参数的。同第3项,因为 WordPress 也是用的
call_user_func_array()
因此这项还原了报错信息的来源。成功的调用,
$a
的值被改写为 3.
这就证明了,WordPress 的这个报错确实是因为 array_slice($args, 0, (int)
$the_['accepted_args']))
返回数组中的元素,不是引用,而是值。
更多关于回调函数与引用做参数的讨论:Why does the error “expected to be a reference, value given” appear?
验证 array_slice() 时遇到的坑
完成上面的验证之后,我打算进一步还原 WordPress 中的场景,向测试代码中加入了 array_slice()
:
function test(&$t) {
$t = $t + 1;
return $t;
}
$a = 1;
call_user_func_array('test', array_slice([&$a], 0, 1));
讲道理的话,这个会报错么?会? NO! 😰, $a
的值被成功改为 2,没有报错。
但是为什么呢?
查看引用计数 xdebug_debug_zval()
继续翻手册,在引用计数基本知识中发现了可以查看变量引用计数的方法,Xdebug 的 xdebug_debug_zval()
方法。
顺便也看了 php 的 GC 机制,从描述看就是典型的 引用计数 方式,似乎还没有引入分代回收机制。
继续测试:
$a = 1;
$b = [&$a];
xdebug_debug_zval('b');
//b:
//(refcount=1, is_ref=0)
//array (size=1)
// 0 => (refcount=2, is_ref=1)int 1
$_b = array_slice($b, 0, 1);
xdebug_debug_zval('_b');
//_b:
//(refcount=1, is_ref=0)
//array (size=1)
// 0 => (refcount=3, is_ref=1)int 1
class Test
{
public function __construct() {
$t = array(&$this);
xdebug_debug_zval('t');
$_t = array_slice($t, 0, 1);
xdebug_debug_zval('_t');
}
}
new Test()
//t:
//(refcount=1, is_ref=0)
//array (size=1)
// 0 => (refcount=1, is_ref=1)
// object(Test)[1]
//_t:
//(refcount=1, is_ref=0)
//array (size=1)
// 0 => (refcount=4, is_ref=0)
// object(Test)[1]
// refcount 指引用次数,is_ref 指是否是引用
通过常规赋值的方式,$a
,$b[0]
会指向同一个内容对象,也就是说在我们完成这个赋值的时候,$a
所指向的内存对象已经被引用了两次,所以 refcount=2。这样也就不满足 Nikita 所说的 rc=1 的条件了,于是 array_slice()
返回的数组 $_b
中依旧是引用。
想要构造 refcount=1,is_ref=1 这个『苛刻』的条件,目前我所知道也就只有在 class 的方法中用 $this
引用自身的时候会出现。当满足这个条件的 $t
经 array_slice()
处理后,返回的 $_t
中元素的引用次数虽然增加了,但是不再是引用,而是拷贝,其 is_ref=0。
后者的情况,也正是 WordPress 中所遇到的情况,可以说正是在如此多低概率的巧合之下,才『终于』有了 WordPress 的这个报错。 😂
修复措施
知道原因之后,如果要修复这个问题就变得有点过于简单了。既然触发的条件是 rc=1, 那只需要在 array_slice()
之前随意用个变量把 $args
的元素多引用一次就可以解决了。当然也可以把array_slice()
处理之后的数组强行变回引用的方式。
最好的方法,还是在业务逻辑上就避免这种情况的发生。除非是迫不得已,否则千万不要在代码中使用太怪异的写法。
到 WordPress 核心维护小组的仓库中看了下新版本,也就是即将准备发布的 4.7,发现整个 do_action_ref_array()
方法都被重写了,根本不再使用 array_slice()
,也就没有这个问题的困扰了。
详见:https://core.trac.wordpress.org/changeset/38571
总结和感悟
- 有时候,找到一个合理的解释很容易,但是要真正的理解、复现并消化一个问题时,遇到的问题很可能会比你一开始想要解决的问题复杂得多。而且它们看上去并不是那么迫切的需要,追寻的过程中很容易放弃。
- 能写代码证明的,不要靠猜
- PHP 官方手册的中文版和英文版不是完全同步的,比如引用做什么中的第二个 Note,英文版已经移除。还有一些具体的细节处,英文版都有明确指出改动的版本,中文版很多没有。
- 鸟哥(Laruence)很强大
- PHP 是世界上最好的语言