在php中我們通過try catch 捕獲異常, 通過 throw來拋出異常, 優(yōu)秀文章推薦:異常處理
先來說說try catch編譯完之后是什么內(nèi)容背率,比如說下面的代碼
<?php
echo "1111111\n";
try {
$data_1 = "222222";
throw new Exception("ddddddddddd");
} catch (\Exception $e) {
echo "22222222\n";
} finally {
echo "xxxxxxxxx\n";
}
echo "222222\n";
編譯后的OPCODE 和對應(yīng)的handler如下话瞧, (不知道如何看opcode的參考這篇php源碼-如何查看opcode源碼
)
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 38 ZEND_ASSIGN , index:1366 ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER
opcode: 68 ZEND_NEW , index:1672 ZEND_NEW_SPEC_CONST_HANDLER
opcode: 116 ZEND_SEND_VAL_EX , index:2488 ZEND_SEND_VAL_EX_SPEC_CONST_QUICK_HANDLER
opcode: 60 ZEND_DO_FCALL , index:1634 ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER
opcode: 108 ZEND_THROW , index:2369 ZEND_THROW_SPEC_VAR_HANDLER
opcode: 42 ZEND_JMP , index:1407 ZEND_JMP_SPEC_HANDLER
opcode: 107 ZEND_CATCH , index:2346 ZEND_CATCH_SPEC_CONST_CV_HANDLER
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 162 ZEND_FAST_CALL , index:3141 ZEND_FAST_CALL_SPEC_HANDLER
opcode: 42 ZEND_JMP , index:1407 ZEND_JMP_SPEC_HANDLER
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 163 ZEND_FAST_RET , index:3142 ZEND_FAST_RET_SPEC_HANDLER
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 62 ZEND_RETURN , index:1641 ZEND_RETURN_SPEC_CONST_HANDLER
怎么理解這個編譯結(jié)果呢,首先我們知道每個函數(shù)或者php文件最終都會編譯成一個opcode_array, 在_zend_op_array 結(jié)構(gòu)體中有一個異常處理的數(shù)據(jù)結(jié)構(gòu) try_catch_array
struct _zend_op_array {
//...
zend_try_catch_element *try_catch_array;
};
typedef struct _zend_try_catch_element {
uint32_t try_op; //try開始的opcode位置
uint32_t catch_op; //第1個catch塊的opcode位置
uint32_t finally_op; //finally開始的opcode位置
uint32_t finally_end;//finally結(jié)束的opcode位置
} zend_try_catch_element;
這個 try_catch_array 其實就定義了各個代碼塊的opcode位置寝姿,用于c語言層面的程序跳轉(zhuǎn)交排,比如異常發(fā)生了就可以jmp到catch_op 或者 finally_op處執(zhí)行。如果異常沒在當(dāng)前的opcode_array中被catch住饵筑,就會一層一層往上拋埃篓,如果都沒有被catch住,那么在最外層的opcode_array中就會結(jié)束當(dāng)前request的執(zhí)行(注意:這個結(jié)束request并不代表進(jìn)程退出根资,比如fpm sapi中代表request結(jié)束架专,進(jìn)程不退出, 這個跟各個sapi的實現(xiàn)有關(guān))
------------------------------------------------------------
以下內(nèi)容來自:異常處理
異常處理的編譯
異常捕獲及處理的語法:
try{
try statement;
}catch(exception_class_1 $e){
catch statement 1;
}catch(exception_class_2 $e){
catch statement 2;
}finally{
finally statement;
}
try表示要捕獲try statement中可能拋出的異常玄帕;catch是捕獲到異常后的處理部脚,可以定義多個,當(dāng)try中拋出異常時會依次檢查各個catch的異常類是否與拋出的匹配裤纹,如果匹配則有命中的那個catch塊處理委刘;finally為最后執(zhí)行的代碼,不管是否有異常拋出都會執(zhí)行鹰椒。
語法規(guī)則:
statement:
...
| T_TRY '{' inner_statement_list '}' catch_list finally_statement
{ $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); }
...
;
catch_list:
/* empty */
{ $$ = zend_ast_create_list(0, ZEND_AST_CATCH_LIST); }
| catch_list T_CATCH '(' name T_VARIABLE ')' '{' inner_statement_list '}'
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_CATCH, $4, $5, $8)); }
;
finally_statement:
/* empty */ { $$ = NULL; }
| T_FINALLY '{' inner_statement_list '}' { $$ = $3; }
;
從語法規(guī)則可以看出锡移,try-catch-finally最終編譯為一個ZEND_AST_TRY
節(jié)點,包含三個子節(jié)點吹零,分別是:try statement罩抗、catch list、finally statement灿椅,try statement套蒂、finally statement就是普通的ZEND_AST_STMT_LIST
節(jié)點钞支,catch list包含多個ZEND_AST_CATCH
節(jié)點,每個節(jié)點有三個子節(jié)點:exception class操刀、exception object及catch statement烁挟,最終生成的AST:
具體的編譯過程如下:
- (1) 向所屬zend_op_array注冊一個zend_try_catch_element結(jié)構(gòu),所有try都會注冊一個這樣的結(jié)構(gòu)骨坑,與循環(huán)結(jié)構(gòu)注冊的zend_brk_cont_element類似撼嗓,當(dāng)前zend_op_array所有定義的異常保存在zend_op_array->try_catch_array數(shù)組中,這個結(jié)構(gòu)用來記錄try欢唾、catch以及finally開始的位置且警,具體結(jié)構(gòu):
typedef struct _zend_try_catch_element {
uint32_t try_op; //try開始的opcode位置
uint32_t catch_op; //第1個catch塊的opcode位置
uint32_t finally_op; //finally開始的opcode位置
uint32_t finally_end;//finally結(jié)束的opcode位置
} zend_try_catch_element;
(2) 編譯try statement,編譯完以后如果定義了catch塊則編譯一條
ZEND_JMP
礁遣,此opcode的作用時當(dāng)無異常拋出時跳過所有catch跳到finally或整個異常之外的斑芜,因為catch塊是在try statement之后編譯的,所以具體的跳轉(zhuǎn)值目前還無法確定祟霍;(3) 依次編譯各個catch塊杏头,如果沒有定義則跳過此步驟,每個catch編譯時首先編譯一條
ZEND_CATCH
沸呐,此opcode保存著此catch的exception class醇王、exception object以及下一個catch塊開始的位置,編譯第1個catch時將此opcode的位置記錄在zend_try_catch_element.catch_op上崭添,接著編譯catch statement寓娩,最后編譯一條ZEND_JMP
(最后一個catch不需要),此opcode的作用與步驟(2)的相同滥朱;(4) 將步驟(2)根暑、步驟(3)中
ZEND_JMP
跳轉(zhuǎn)值設(shè)置為finally第1條opcode或異常定義之外的代碼,如果沒有定義finally則結(jié)束編譯徙邻,否則編譯finally塊排嫌,首先編譯一條ZEND_FAST_CALL
及ZEND_JMP
,接著編譯finally statement缰犁,最后編譯一條ZEND_FAST_RET
淳地。
編譯完以后的結(jié)構(gòu):
異常的拋出通過throw一個異常對象來實現(xiàn),這個對象必須繼承>自Exception類帅容,拋出異常的語法:
throw exception_object;
throw的編譯比較簡單颇象,最終只編譯為一條opcode:ZEND_THROW
。
4.6.2 異常的拋出與捕獲
上一小節(jié)我們介紹了exception結(jié)構(gòu)在編譯階段的處理并徘,接下來我們再介紹下運行時exception的處理過程遣钳,這個過程相對比較復(fù)雜,整體的講其處理流程整體如下:
- (1) 檢查拋出的是否是object麦乞,否則將導(dǎo)致error錯誤蕴茴;
-
(2) 將EG(exception)設(shè)置為拋出的異常對象劝评,同時將當(dāng)前stack(即:zend_execute_data)接下來要執(zhí)行的opcode設(shè)置為
ZEND_HANDLE_EXCEPTION
; -
(3) 執(zhí)行
ZEND_HANDLE_EXCEPTION
倦淀,查找匹配的catch:-
(3.1) 首先遍歷當(dāng)前zend_op_array下定義的所有異常捕獲蒋畜,即
zend_op_array->try_catch_array
數(shù)組,然后根據(jù)throw的位置撞叽、try開始的位置姻成、catch開始的位置、finally開始的位置判斷判斷異常是否在try范圍內(nèi)愿棋,如果同時命中了多個try(即嵌套try的情況)則選擇最后那個(也就是最里層的)科展,遍歷完以后如果命中了則進(jìn)入步驟(3.2)處理,如果沒有命中當(dāng)前stack下任何try則進(jìn)入步驟(4)糠雨; -
(3.2) 到這一步表示拋出的異常在當(dāng)前zend_op_array下有try攔截(注意這里只是表示異常在try中拋出的辛润,但是拋出的異常并一定能被catch),然后根據(jù)當(dāng)前try塊的
zend_try_catch_element
結(jié)構(gòu)取出第一個catch的位置见秤,將opcode設(shè)置為zend_try_catch_element.catch_op,跳到第一個catch塊開始的位置執(zhí)行真椿,即:執(zhí)行ZEND_CATCH
鹃答; -
(3.3) 執(zhí)行
ZEND_CATCH
,檢查拋出的異常對象是否與當(dāng)前catch的類型匹配突硝,檢查的過程為判斷兩個類是否存在父子關(guān)系测摔,如果匹配則表示異常被成功捕獲,將EG(exception)清空解恰,如果沒有則跳到下一個catch的位置重復(fù)步驟(3.3)锋八,如果到最后一個catch仍然沒有命中則在這個catch的位置拋出一個異常(實際還是原來按個異常,只是將拋出的位置轉(zhuǎn)移了當(dāng)前catch的位置)护盈,然后回到步驟(3);
-
(3.1) 首先遍歷當(dāng)前zend_op_array下定義的所有異常捕獲蒋畜,即
-
(4) 當(dāng)前zend_op_array沒能成功捕獲異常挟纱,需要繼續(xù)往上拋:回到調(diào)用位置,將接下來要執(zhí)行的opcode設(shè)置為
ZEND_HANDLE_EXCEPTION
腐宋,比如函數(shù)中拋出了一個異常沒有在函數(shù)中捕獲紊服,則跳到調(diào)用的位置繼續(xù)捕獲,回到步驟(3)胸竞;如果到最終主腳本也沒有被捕獲則將結(jié)束執(zhí)行并導(dǎo)致error錯誤欺嗤。
這個過程最復(fù)雜的地方在于異常匹配、傳遞的過程卫枝,主要為ZEND_HANDLE_EXCEPTION
煎饼、ZEND_CATCH
兩條opcode之間的調(diào)用,當(dāng)拋出一個異常時會終止后面opcode的執(zhí)行校赤,轉(zhuǎn)向執(zhí)行ZEND_HANDLE_EXCEPTION
吆玖,根據(jù)異常拋出的位置定位到最近的一個try的catch位置筒溃,如果這個catch沒有匹配則跳到下一個catch塊,然后再次執(zhí)行ZEND_HANDLE_EXCEPTION
衰伯,如果到最后一個catch仍沒有匹配則將異常拋出前位置EG(opline_before_exception)更新為最后一個catch的位置铡羡,再次執(zhí)行ZEND_HANDLE_EXCEPTION
,由于異常拋出的位置已經(jīng)更新了所以不會再匹配上次檢查過的那個catch意鲸,這個過程實際就是不斷遞歸執(zhí)行ZEND_HANDLE_EXCEPTION
烦周、ZEND_CATCH
;如果當(dāng)前zend_op_array都無法捕獲則將異常拋向上一個調(diào)用棧繼續(xù)捕獲怎顾,下面根據(jù)一個例子具體說明下:
function my_func(){
//...
throw new Exception("This is a exception from my_func()");
}
try{
my_func();
}catch(ErrorException $e){
echo "ErrorException";
}catch(Exception $e){
echo "Exception";
}
my_func()中拋出了一個異常读慎,首先在my_func()中拋出一個異常,然后在my_func()的zend_op_array中檢查是不是能夠捕獲槐雾,發(fā)現(xiàn)沒有夭委,則回到調(diào)用的位置,再次檢查募强,第1次匹配到catch(ErrorException $e)
株灸,檢查后發(fā)現(xiàn)并不匹配,然后跳到下一個catch塊繼續(xù)匹配擎值,第2次匹配到catch(Exception $e)
慌烧,檢查后發(fā)現(xiàn)命中,捕獲成功鸠儿。
上面的過程并沒有提到finally的執(zhí)行時機(jī)屹蚊,首先要明確finally在哪些情況下會執(zhí)行,命中catch的情況比較簡單进每,即在catch statement執(zhí)行完以后跳到finally執(zhí)行汹粤,另外一種情況是如果一個異常在try中但沒有命中任何catch那么其finally也是會被執(zhí)行的,這種情況的finally實際是在步驟(3)中執(zhí)行的田晚,最后一個catch檢查完以后會更新異常拋出位置:EG(opline_before_exception)嘱兼,然后會再次執(zhí)行ZEND_HANDLE_EXCEPTION
,再次檢查時就會發(fā)現(xiàn)沒有命中任何catch但命中finally了(因為異常位置更新了)贤徒,這時候就會將異常對象保存在finally塊中遭京,然后執(zhí)行finally,執(zhí)行完再將異常對象還原繼續(xù)捕獲