php autoload機制學習

在使用PHP的OO模式開發系統時,一般你們習慣上將每一個類的實現都存放在一個單獨的文件裏,這樣會很容易實現對類進行復用,同時未來維護時也很便利。這也是OO設計的基本思想之一。在PHP5以前,若是須要使用一個類,只須要直接使用include/require將其包含進來便可。下面是一個實際的例子:php

01 /* Person.class.php */
02 <?php
03 class Person {
04     var $name$age;
05
06     function __construct ($name$age)
07     {
08         $this->name = $name;
09         $this->age = $age;
10     }
11 }
12 ?>
13
14 /* no_autoload.php */
15 <?php
16 require_once ("Person.class.php");
17
18 $person new Person(」Altair」, 6);
19 var_dump ($person);
20 ?>

在這個例子中,no-autoload.php文件須要使用Person類,它使用了require_once將其包含,而後就能夠直接使用Person類來實例化一個對象。數組

但隨着項目規模的不斷擴大,使用這種方式會帶來一些隱含的問題:若是一個PHP文件須要使用不少其它類,那麼就須要不少的require/include語句,這樣有可能會形成遺漏或者包含進沒必要要的類文件。若是大量的文件都須要使用其它的類,那麼要保證每一個文件都包含正確的類文件確定是一個噩夢。函數

PHP5爲這個問題提供了一個解決方案,這就是類的自動裝載(autoload)機制。autoload機制可使得PHP程序有可能在使用類時才自動包含類文件,而不是一開始就將全部的類文件include進來,這種機制也稱爲lazy loading。工具

下面是使用autoload機制加載Person類的例子:oop

1 /* autoload.php */
2 <?php
3 function __autoload($classname) {
4     require_once ($classname "class.php");
5 }
6
7 $person new Person("Altair", 6);
8 var_dump ($person);
9 ?>

一般PHP5在使用一個類時,若是發現這個類沒有加載,就會自動運行__autoload()函數,在這個函數中咱們能夠加載須要使用的類。在咱們這個簡單的例子中,咱們直接將類名加上擴展名」.class.php」構成了類文件名,而後使用require_once將其加載。從這個例子中,咱們能夠看出autoload至少要作三件事情,第一件事是根據類名肯定類文件名,第二件事是肯定類文件所在的磁盤路徑(在咱們的例子是最簡單的狀況,類與調用它們的PHP程序文件在同一個文件夾下),第三件事是將類從磁盤文件中加載到系統中。第三步最簡單,只須要使用include/require便可。要實現第一 步,第二步的功能,必須在開發時約定類名與磁盤文件的映射方法,只有這樣咱們才能根據類名找到它對應的磁盤文件。fetch

所以,當有大量的類文件要包含的時候,咱們只要肯定相應的規則,而後在__autoload()函數中,將類名與實際的磁盤文件對應起來,就能夠實現lazy loading的效果。從這裏咱們也能夠看出__autoload()函數的實現中最重要的是類名與實際的磁盤文件映射規則的實現。網站

但如今問題來了,若是在一個系統的實現中,若是須要使用不少其它的類庫,這些類庫多是由不一樣的開發人員編寫的,其類名與實際的磁盤文件的映射規則不盡相同。這時若是要實現類庫文件的自動加載,就必須在__autoload()函數中將全部的映射規則所有實現,這樣的話__autoload()函數有可能會很是複雜,甚至沒法實現。最後可能會致使__autoload()函數十分臃腫,這時即使可以實現,也會給未來的維護和系統效率帶來很大的負面影響。在這種狀況下,難道就沒有更簡單清晰的解決辦法了吧?答案固然是:NO! 在看進一步的解決方法以前,咱們先來看一下PHP中的autoload機制是如何實現的。ui

咱們知道,PHP文件的執行分爲兩個獨立的過程,第一步是將PHP文件編譯成普通稱之爲OPCODE的字節碼序列(其實是編譯成一個叫作zend_op_array的字節數組),第二步是由一個虛擬機來執行這些OPCODE。PHP的全部行爲都是由這些OPCODE來實現的。所以,爲了研究PHP中autoload的實現機制,咱們將autoload.php文件編譯成opcode,而後根據這些OPCODE來研究PHP在這過程當中都作了些什麼:this

01 /* autoload.php 編譯後的OPCODE列表,是使用做者開發的OPDUMP工具
02 * 生成的結果,能夠到網站 http://www.phpinternals.com/ 下載該軟件。
03 */
04 1: <?php
05 2:  // require_once ("Person.php");
06 3: 
07 4:  function __autoload ($classname) {
08         0  NOP               
09         0  RECV                1
10 5:   if (!class_exists($classname)) {
11         1  SEND_VAR            !0
12         2  DO_FCALL            'class_exists' [extval:1]
13         3  BOOL_NOT            $0 =>RES[~1]    
14         4  JMPZ                ~1, ->8
15 6:    require_once ($classname. 「.class.php」);
16         5  CONCAT              !0, ‘.class.php’ =>RES[~2]    
17         6  INCLUDE_OR_EVAL     ~2, REQUIRE_ONCE
18 7:   }
19         7  JMP                 ->8
20 8:  }
21         8  RETURN              null
22 9: 
23 10:  $p new Person(’Fred’, 35);
24         1  FETCH_CLASS         ‘Person’ =>RES[:0]    
25         2  NEW                 :0 =>RES[$1]    
26         3  SEND_VAL            ‘Fred’
27         4  SEND_VAL            35
28         5  DO_FCALL_BY_NAME     [extval:2]
29         6  ASSIGN              !0, $1
30 11: 
31 12:  var_dump ($p);
32         7  SEND_VAR            !0
33         8  DO_FCALL            ‘var_dump’ [extval:1]
34 13: ?>

在autoload.php的第10行代碼中咱們須要爲類Person實例化一個對象。所以autoload機制必定會在該行編譯後的opcode中有所體 現。從上面的第10行代碼生成的OPCODE中咱們知道,在實例化對象Person時,首先要執行FETCH_CLASS指令。咱們就從PHP對 FETCH_CLASS指令的處理過程開始咱們的探索之旅。spa

經過查閱PHP的源代碼(我使用的是PHP 5.3alpha2版本)能夠發現以下的調用序列:

1 ZEND_VM_HANDLER(109, ZEND_FETCH_CLASS, …) (zend_vm_def.h 1864行)
2     => zend_fetch_class (zend_execute_API.c 1434行)
3     =>zend_lookup_class_ex (zend_execute_API.c 964行)
4     => zend_call_function(&fcall_info, &fcall_cache) (zend_execute_API.c 1040行)

在最後一步的調用以前,咱們先看一下調用時的關鍵參數:

1 /* 設置autoload_function變量值爲」__autoload」 */
2  fcall_info.function_name = &autoload_function;  // Ooops, 終於發現」__autoload」了
3  
4  fcall_cache.function_handler = EG(autoload_func); // autoload_func !

zend_call_function是Zend Engine中最重要的函數之一,其主要功能是執行用戶在PHP程序中自定義的函數或者PHP自己的庫函數。zend_call_function有兩個 重要的指針形參數fcall_info, fcall_cache,它們分別指向兩個重要的結構,一個是zend_fcall_info, 另外一個是zend_fcall_info_cache。zend_call_function主要工做流程以下:若是 fcall_cache.function_handler指針爲NULL,則嘗試查找函數名爲fcall_info.function_name的函 數,若是存在的話,則執行之;若是fcall_cache.function_handler不爲NULL,則直接執行 fcall_cache.function_handler指向的函數。

如今咱們清楚了,PHP在實例化一個對象時(實際上在實現接口, 使用類常數或類中的靜態變量,調用類中的靜態方法時都會如此),首先會在系統中查找該類(或接口)是否存在,若是不存在的話就嘗試使用autoload機 制來加載該類。而autoload機制的主要執行過程爲:

  • 檢查執行器全局變量函數指針autoload_func是否爲NULL。

  • 若是autoload_func==NULL, 則查找系統中是否認義有__autoload()函數,若是沒有,則報告錯誤並退出。

  • 若是定義了__autoload()函數,則執行__autoload()嘗試加載類,並返回加載結果。

  • 若是autoload_func不爲NULL,則直接執行autoload_func指針指向的函數用來加載類。注意此時並不檢查__autoload()函數是否認義。

真相終於大白,PHP提供了兩種方法來實現自動裝載機制,一種咱們前面已經提到過,是使用用戶定義的__autoload()函數,這一般在PHP源程序中 來實現;另一種就是設計一個函數,將autoload_func指針指向它,這一般使用C語言在PHP擴展中實現。若是既實現了 __autoload()函數,又實現了autoload_func(將autoload_func指向某一PHP函數),那麼只執行 autoload_func函數。

SPL autoload機制的實現

SPL是Standard PHP Library(標準PHP庫)的縮寫。它是PHP5引入的一個擴展庫,其主要功能包括autoload機制的實現及包括各類Iterator接口或類。 SPL autoload機制的實現是經過將函數指針autoload_func指向本身實現的具備自動裝載功能的函數來實現的。SPL有兩個不一樣的函數 spl_autoload, spl_autoload_call,經過將autoload_func指向這兩個不一樣的函數地址來實現不一樣的自動加載機制。

spl_autoload 是SPL實現的默認的自動加載函數,它的功能比較簡單。它能夠接收兩個參數,第一個參數是$class_name,表示類名,第二個參 數$file_extensions是可選的,表示類文件的擴展名,能夠在$file_extensions中指定多個擴展名,護展名之間用分號隔開即 可;若是不指定的話,它將使用默認的擴展名.inc或.php。spl_autoload首先將$class_name變爲小寫,而後在全部的 include path中搜索$class_name.inc或$class_name.php文件(若是不指定$file_extensions參數的話),若是找 到,就加載該類文件。你能夠手動使用spl_autoload(」Person」, 「.class.php」)來加載Person類。實際上,它跟require/include差很少,不一樣的它能夠指定多個擴展名。

怎樣讓spl_autoload自動起做用呢,也就是將autoload_func指向spl_autoload?答案是使用 spl_autoload_register函數。在PHP腳本中第一次調用spl_autoload_register()時不使用任何參數,就能夠將 autoload_func指向spl_autoload。

經過上面的說明咱們知道,spl_autoload的功能比較簡單,並且它是在SPL擴展中實現的,咱們沒法擴充它的功能。若是想實現本身的更靈活的自動加載機制怎麼辦呢?這時,spl_autoload_call函數閃亮登場了。

咱們先看一下spl_autoload_call的實現有何奇妙之處。在SPL模塊內部,有一個全局變量autoload_functions,它本質上是 一個HashTable,不過咱們能夠將其簡單的看做一個鏈表,鏈表中的每個元素都是一個函數指針,指向一個具備自動加載類功能的函數。 spl_autoload_call自己的實現很簡單,只是簡單的按順序執行這個鏈表中每一個函數,在每一個函數執行完成後都判斷一次須要的類是否已經加載, 若是加載成功就直接返回,再也不繼續執行鏈表中的其它函數。若是這個鏈表中全部的函數都執行完成後類尚未加載,spl_autoload_call就直接 退出,並不向用戶報告錯誤。所以,使用了autoload機制,並不能保證類就必定能正確的自動加載,關鍵仍是要看你的自動加載函數如何實現。

那麼自動加載函數鏈表autoload_functions是誰來維護呢?就是前面提到的spl_autoload_register函數。它能夠將用戶定 義的自動加載函數註冊到這個鏈表中,並將autoload_func函數指針指向spl_autoload_call函數(注意有一種狀況例外,具體是哪 種狀況留給你們思考)。咱們也能夠經過spl_autoload_unregister函數將已經註冊的函數從autoload_functions鏈表 中刪除。

當autoload_func指針非空時,就不會自動執行__autoload()函數了,如今 autoload_func已經指向了spl_autoload_call,若是咱們還想讓__autoload()函數起做用應該怎麼辦呢?固然仍是使 用spl_autoload_register(__autoload)調用將它註冊到autoload_functions鏈表中。

如今回到第一節最後的問題,咱們有了解決方案:根據每一個類庫不一樣的命名機制實現各自的自動加載函數,而後使用spl_autoload_register分別將其註冊到SPL自動加載函數隊列中就可了。這樣咱們就不用維護一個很是複雜的__autoload函數了。

autoload效率問題及對策

使用autoload機制時,不少人的第一反應就是使用autoload會下降系統效率,甚至有人乾脆提議爲了效率不要使用autoload。在咱們瞭解了 autoload實現的原理後,咱們知道autoload機制自己並非影響系統效率的緣由,甚至它還有可能提升系統效率,由於它不會將不須要的類加載到系統中。

那麼爲何不少人都有一個使用autoload會下降系統效率的印象呢?實際上,影響autoload機制效率自己偏偏是用 戶設計的自動加載函數。若是它不能高效的將類名與實際的磁盤文件(注意,這裏指實際的磁盤文件,而不只僅是文件名)對應起來,系統將不得不作大量的文件是 否存在(須要在每一個include path中包含的路徑中去尋找)的判斷,而判斷文件是否存在須要作磁盤I/O操做,衆所周知磁盤I/O操做的效率很低,所以這纔是使得autoload機制效率下降的罪魁禍首!

所以,咱們在系統設計時,須要定義一套清晰的將類名與實際磁盤文件映射的機制。這個規則越簡單越明確,autoload機制的效率就越高。

結論:autoload機制並非自然的效率低下,只有濫用autoload,設計很差的自動裝載函數纔會致使其效率的下降。

(原文地址:http://www.nowamagic.net/php/php_Autoload.php

相關文章
相關標籤/搜索