接下來我們繼續查看FE_FETCH,它對應的執行函數為ZEND_FE_FETCH_SPEC_VAR_HANDLER:
復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
// 注意指針是從EX_T(opline->op1.u.var).var.ptr獲取的
zval *array = EX_T(opline->op1.u.var).var.ptr;
……
switch (zend_iterator_unwrap(array, &iter TSRMLS_CC)) {
default:
case ZEND_ITER_INVALID:
……
case ZEND_ITER_PLAIN_OBJECT: {
……
}
case ZEND_ITER_PLAIN_ARRAY:
fe_ht = HASH_OF(array);
// 特別注意:
// FE_RESET指令中將數組內部元素的指針保存在EX_T(opline->op1.u.var).fe.fe_pos
// 此處獲取該指針
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
// 獲取元素的值
if (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_num);
}
if (use_key) {
key_type = zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, 1, NULL);
}
// 數組內部指針移動到下一個元素
zend_hash_move_forward(fe_ht);
// 移動之後的指針保存到EX_T(opline->op1.u.var).fe.fe_pos
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
break;
case ZEND_ITER_OBJECT:
……
}
……
}
根據FE_FETCH的實現,我們大致上明白了foreach($arr as $k => $v)所做的事情。它會根據zend_execute_data->Ts的指針去獲取數組元素,在獲取成功之後,將該指針移動到下一個位置再重新保存。
簡單來說,由於第一遍循環中FE_FETCH中已經將數組的內部指針移動到了第二個元素,所以在foreach內部調用key($arr)和current($arr)時,實際上獲取的便是1和'b'。
那為何會輸出3遍1=>b呢?
我們繼續看第9行和第13行的SEND_REF指令,它表示將$arr參數壓棧。緊接著一般會使用DO_FCALL指令去調用key和current函數。PHP並非被編譯成本地機器碼,因此php采用這樣的opcode指令去模擬實際CPU和內存的工作方式。
查閱PHP源碼中的SEND_REF:
復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// 從CV中獲取$arr指針的指針
varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
……
// 變量分離,此處重新copy了一份array專門用於key函數
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);
// 壓棧
zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}
上述代碼中的SEPARATE_ZVAL_TO_MAKE_IS_REF是一個宏:
復制代碼 代碼如下:
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv) \
if (!PZVAL_IS_REF(*ppzv)) { \
SEPARATE_ZVAL(ppzv); \
Z_SET_ISREF_PP((ppzv)); \
}
SEPARATE_ZVAL_TO_MAKE_IS_REF的主要作用為,如果變量不是一個引用,則在內存中copy出一份新的。本例中它將array('a','b','c')復制了一份。因此變量分離之後的內存為:
注意,變量分離完成之後,CV數組中的指針指向了新copy出來的數據,而通過zend_execute_data->Ts中的指針則依然可以獲取舊的數據。
接下來的循環就不一一贅述了,結合上圖來說:
•foreach結構使用的是下方藍色的array,會依次遍歷a,b,c
•key、current使用的是上方黃色的array,它的內部指針永遠指向b
至此我們明白了為何key和current一直返回array的第二個元素,由於沒有外部代碼作用於copy出來的array,它的內部指針便永遠不會移動。
問題3:
復制代碼 代碼如下:
$arr = array('a','b','c');
foreach($arr as $k => &$v) {
echo key($arr), '=>', current($arr);
}// 打印 1=>b 2=>c =>
本題與問題2僅有一點區別:本題中的foreach使用了引用。用VLD查看本題,發現與問題2代碼編譯出來的opcode一樣。因此我們采用問題2的跟蹤方法,逐步查看opcode對應的實現。
首先foreach會調用FE_RESET:
復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
// 從CV中獲取變量
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
// 針對遍歷array的情況
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
// 將保存array的zval設置為is_ref
Z_SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
……
}
……
}
問題2中已經分析了一部分FE_RESET的實現。這裡需要特別注意,本例foreach獲取值采用了引用,因此在執行的時候FE_RESET中會進入與上題不同的另一個分支。
最終,FE_RESET會將array的is_ref設置為true,此時內存中只有一份array的數據。
接下來分析SEND_REF:
復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// 從CV中獲取$arr指針的指針
varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
……
// 變量分離,由於此時CV中的變量本身就是一個引用,此處不會copy一份新的array
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);
// 壓棧
zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}
宏SEPARATE_ZVAL_TO_MAKE_IS_REF僅僅分離is_ref=false的變量。由於之前array已經被設置了is_ref=true,因此它不會被拷貝一份副本。換句話說,此時內存中依然只有一份array數據。
上圖解釋了前2次循環為何會輸出1=>b 2=>C。在第3次循環FE_FETCH的時候,將指針繼續向前移動。
復制代碼 代碼如下:
ZEND_API int zend_hash_move_forward_ex(HashTable *ht, HashPosition *pos)
{
HashPosition *current = pos ? pos : &ht->pInternalPointer;
IS_CONSISTENT(ht);
if (*current) {
*current = (*current)->pListNext;
return SUCCESS;
} else
return FAILURE;
}
由於此時內部指針已經指向了數組的最後一個元素,因此再向前移動會指向NULL。將內部指針指向NULL之後,我們再對數組調用key和current,則分別會返回NULL和false,表示調用失敗,此時是echo不出字符的。
問題4:
復制代碼 代碼如下:
$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $k => &$v){
$v *= 2;
}
var_dump($arr, $tmp); // 打印什麼?
該題與foreach關系不大,不過既然涉及到了foreach,就一起拿來討論吧:)
代碼裡首先創建了數組$arr,隨後將該數組賦給了$tmp,在接下來的foreach循環中,對$v進行修改會作用於數組$tmp上,但是卻並不作用到$arr。
為什麼呢?
這是由於在php中,賦值運算是將一個變量的值拷貝到另一個變量中,因此修改其中一個,並不會影響到另一個。
題外話:這並不適用於object類型,從PHP5起,對象的便總是默認通過引用進行賦值,舉例來說:
復制代碼 代碼如下:
class A{
public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 輸出100,$a1與$a2其實為同一個對象的引用
回到題目中的代碼,現在我們可以確定$tmp=$arr其實是值拷貝,整個$arr數組會被再復制一份給$tmp。理論上講,賦值語句執行完畢之後,內存中會有2份一樣的數組。
也許有同學會疑問,如果數組很大,豈不是這種操作會很慢?
幸好php有更聰明的處理辦法。實際上,當$tmp=$arr執行之後,內存中依然只有一份array。查看php源碼中的zend_assign_to_variable實現(摘自php5.3.26):
復制代碼 代碼如下:
static inline zval* zend_assign_to_variable(zval **variable_ptr_ptr, zval *value, int is_tmp_var TSRMLS_DC)
{
zval *variable_ptr = *variable_ptr_ptr;
zval garbage;
……
// 左值為object類型
if (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr, set)) {
……
}
// 左值為引用的情況
if (PZVAL_IS_REF(variable_ptr)) {
……
} else {
// 左值refcount__gc=1的情況
if (Z_DELREF_P(variable_ptr)==0) {
……
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr);
// 非臨時變量
if (!is_tmp_var) {
if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
ALLOC_ZVAL(variable_ptr);
*variable_ptr_ptr = variable_ptr;
*variable_ptr = *value;
Z_SET_REFCOUNT_P(variable_ptr, 1);
zval_copy_ctor(variable_ptr);
} else {
// $tmp=$arr會運行到這裡,
// value為指向$arr裡實際array數據的指針,variable_ptr_ptr為$tmp裡指向數據指針的指針
// 僅僅是復制指針,並沒有真正拷貝實際的數組
*variable_ptr_ptr = value;
// value的refcount__gc值+1,本例中refcount__gc為1,Z_ADDREF_P之後為2
Z_ADDREF_P(value);
}
} else {
……
}
}
Z_UNSET_ISREF_PP(variable_ptr_ptr);
}
return *variable_ptr_ptr;
}
可見$tmp = $arr的本質就是將array的指針進行復制,然後將array的refcount自動加1.用圖表達出此時的內存,依然只有一份array數組:
既然只有一份array,那foreach循環中修改$tmp的時候,為何$arr沒有跟著改變?
繼續看PHP源碼中的ZEND_FE_RESET_SPEC_CV_HANDLER函數,這是一個OPCODE HANDLER,它對應的OPCODE為FE_RESET。該函數負責在foreach開始之前,將數組的內部指針指向其第一個元素。
復制代碼 代碼如下:
static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zval *array_ptr, **array_ptr_ptr;
HashTable *fe_ht;
zend_object_iterator *iter = NULL;
zend_class_entry *ce = NULL;
zend_bool is_empty = 0;
// 對變量進行FE_RESET
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
// foreach一個object
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
// 本例會進入該分支
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
// 注意此處的SEPARATE_ZVAL_IF_NOT_REF
// 它會重新復制一個數組出來
// 真正分離$tmp和$arr,變成了內存中的2個數組
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
Z_SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
……
}
// 重置數組內部指針
……
}
從代碼中可以看出,真正執行變量分離並不是在賦值語句執行的時候,而是推遲到了使用變量的時候,這也是Copy On Write機制在PHP中的實現。
FE_RESET之後,內存的變化如下:
上圖解釋了為何foreach並不會對原來的$arr產生影響。至於ref_count以及is_ref的變化情況,感興趣的同學可以詳細閱讀ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND_SWITCH_FREE_SPEC_VAR_HANDLER的具體實現(均位於php-src/zend/zend_vm_execute.h中),本文不做詳細剖析:)