PHP autoload機制詳解
PHP autoload機制詳解
(1) autoload機制概述
在使用PHP的OO模式開發系統時,通常大家習慣上將每個類的實現都存放在一個單獨的檔案裡,這樣會很容易實現對類進行復用,同時將來維護時也很便利。這 也是OO設計的基本思想之一。在PHP5之前,如果需要使用一個類,只需要直接使用include/require將其包含進來即可。下面是一個實際的例 子:
/* Person.class.php */
<?php
class Person {
var $name, $age;
function __construct ($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
?>
/* no_autoload.php */
<?php
require_once (”Person.class.php”);
$person = new Person(”Altair”, 6);
var_dump ($person);
?>
在這個例子中,no-autoload.php檔案需要使用Person類,它使用了require_once將其包含,然後就可以直接使用Person類來例項化一個物件。
但 隨著專案規模的不斷擴大,使用這種方式會帶來一些隱含的問題:如果一個PHP檔案需要使用很多其它類,那麼就需要很多的require/include語 句,這樣有可能會造成遺漏或者包含進不必要的類檔案。如果大量的檔案都需要使用其它的類,那麼要保證每個檔案都包含正確的類檔案肯定是一個噩夢。
PHP5為這個問題提供了一個解決方案,這就是類的自動裝載(autoload)機制。autoload機制可以使得PHP程式有可能在使用類時才自動包含類檔案,而不是一開始就將所有的類檔案include進來,這種機制也稱為lazy loading。
下面是使用autoload機制載入Person類的例子:
/* autoload.php */
<?php
function __autoload($classname) {
require_once ($classname . “class.php”);
}
$person = new Person(”Altair”, 6);
var_dump ($person);
?>
通常PHP5在使用一個類時,如果發現這個類沒有載入,就會自動執行__autoload()函式,在這個函式中我們可以載入需要使用的類。在我們這個簡 單的例子中,我們直接將類名加上副檔名”.class.php”構成了類檔名,然後使用require_once將其載入。從這個例子中,我們可以看出 autoload至少要做三件事情,第一件事是根據類名確定類檔名,第二件事是確定類檔案所在的磁碟路徑(在我們的例子是最簡單的情況,類與呼叫它們的 PHP程式檔案在同一個資料夾下),第三件事是將類從磁碟檔案中載入到系統中。第三步最簡單,只需要使用include/require即可。要實現第一
步,第二步的功能,必須在開發時約定類名與磁碟檔案的對映方法,只有這樣我們才能根據類名找到它對應的磁碟檔案。
因 此,當有大量的類檔案要包含的時候,我們只要確定相應的規則,然後在__autoload()函式中,將類名與實際的磁碟檔案對應起來,就可以實現 lazy loading的效果。從這裡我們也可以看出__autoload()函式的實現中最重要的是類名與實際的磁碟檔案對映規則的實現。
但現在問題來了,如果在一個系統的實現中,如果需要使用很多其它的類庫,這些類庫可能是由不同的開發人員編寫的,其類名與實際的磁碟檔案的對映規則不盡相 同。這時如果要實現類庫檔案的自動載入,就必須在__autoload()函式中將所有的對映規則全部實現,這樣的話__autoload()函式有可能 會非常複雜,甚至無法實現。最後可能會導致__autoload()函式十分臃腫,這時即便能夠實現,也會給將來的維護和系統效率帶來很大的負面影響。在 這種情況下,難道就沒有更簡單清晰的解決辦法了吧?答案當然是:NO! 在看進一步的解決方法之前,我們先來看一下PHP中的autoload機制是如何實現的。
(2) PHP的autoload機制的實現
我 們知道,PHP檔案的執行分為兩個獨立的過程,第一步是將PHP檔案編譯成普通稱之為OPCODE的位元組碼序列(實際上是編譯成一個叫做 zend_op_array的位元組陣列),第二步是由一個虛擬機器來執行這些OPCODE。PHP的所有行為都是由這些OPCODE來實現的。因此,為了研 究PHP中autoload的實現機制,我們將autoload.php檔案編譯成opcode,然後根據這些OPCODE來研究PHP在這過程中都做了 些什麼:
/* autoload.php 編譯後的OPCODE列表,是使用作者開發的OPDUMP工具
* 生成的結果,可以到網站 http://www.phpinternals.com/ 下載該軟體。
*/
1: <?php
2: // require_once (”Person.php”);
3:
4: function __autoload ($classname) {
0 NOP
0 RECV 1
5: if (!class_exists($classname)) {
1 SEND_VAR !0
2 DO_FCALL ‘class_exists’ [extval:1]
3 BOOL_NOT $0 =>RES[~1]
4 JMPZ ~1, ->8
6: require_once ($classname. “.class.php”);
5 CONCAT !0, ‘.class.php’ =>RES[~2]
6 INCLUDE_OR_EVAL ~2, REQUIRE_ONCE
7: }
7 JMP ->8
8: }
8 RETURN null
9:
10: $p = new Person(’Fred’, 35);
1 FETCH_CLASS ‘Person’ =>RES[:0]
2 NEW :0 =>RES[$1]
3 SEND_VAL ‘Fred’
4 SEND_VAL 35
5 DO_FCALL_BY_NAME [extval:2]
6 ASSIGN !0, $1
11:
12: var_dump ($p);
7 SEND_VAR !0
8 DO_FCALL ‘var_dump’ [extval:1]
13: ?>
在 autoload.php的第10行程式碼中我們需要為類Person例項化一個物件。因此autoload機制一定會在該行編譯後的opcode中有所體 現。從上面的第10行程式碼生成的OPCODE中我們知道,在例項化物件Person時,首先要執行FETCH_CLASS指令。我們就從PHP對 FETCH_CLASS指令的處理過程開始我們的探索之旅。
通過查閱PHP的原始碼(我使用的是PHP 5.3alpha2版本)可以發現如下的呼叫序列:
ZEND_VM_HANDLER(109, ZEND_FETCH_CLASS, …) (zend_vm_def.h 1864行)
=> zend_fetch_class (zend_execute_API.c 1434行)
=>zend_lookup_class_ex (zend_execute_API.c 964行)
=> zend_call_function(&fcall_info, &fcall_cache) (zend_execute_API.c 1040行)
在最後一步的呼叫之前,我們先看一下呼叫時的關鍵引數:
/* 設定autoload_function變數值為”__autoload” */
fcall_info.function_name = &autoload_function; // Ooops, 終於發現”__autoload”了
…
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機制的主要執行過程為:
(1) 檢查執行器全域性變數函式指標autoload_func是否為NULL。
(2) 如果autoload_func==NULL, 則查詢系統中是否定義有__autoload()函式,如果沒有,則報告錯誤並退出。
(3) 如果定義了__autoload()函式,則執行__autoload()嘗試載入類,並返回載入結果。
(4) 如果autoload_func不為NULL,則直接執行autoload_func指標指向的函式用來載入類。注意此時並不檢查__autoload()函式是否定義。
真 相終於大白,PHP提供了兩種方法來實現自動裝載機制,一種我們前面已經提到過,是使用使用者定義的__autoload()函式,這通常在PHP源程式中 來實現;另外一種就是設計一個函式,將autoload_func指標指向它,這通常使用C語言在PHP擴充套件中實現。如果既實現了 __autoload()函式,又實現了autoload_func(將autoload_func指向某一PHP函式),那麼只執行 autoload_func函式。
(3) 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函式了。
(4) autoload效率問題及對策
使 用autoload機制時,很多人的第一反應就是使用autoload會降低系統效率,甚至有人乾脆提議為了效率不要使用autoload。在我們瞭解了 autoload實現的原理後,我們知道autoload機制本身並不是影響系統效率的原因,甚至它還有可能提高系統效率,因為它不會將不需要的類載入到 系統中。
那麼為什麼很多人都有一個使用autoload會降低系統效率的印象呢?實際上,影響autoload機制效率本身恰恰是使用者設計的自動載入函式。如果它 不能高效的將類名與實際的磁碟檔案(注意,這裡指實際的磁碟檔案,而不僅僅是檔名)對應起來,系統將不得不做大量的檔案是否存在(需要在每個 include path中包含的路徑中去尋找)的判斷,而判斷檔案是否存在需要做磁碟I/O操作,眾所周知磁碟I/O操作的效率很低,因此這才是使得autoload機 制效率降低的罪魁禍首!
因此,我們在系統設計時,需要定義一套清晰的將類名與實際磁碟檔案對映的機制。這個規則越簡單越明確,autoload機制的效率就越高。
結論:autoload機制並不是天然的效率低下,只有濫用autoload,設計不好的自動裝載函式才會導致其效率的降低。
相關文章
- PHP autoload 機制詳解PHP
- 詳解 php 反射機制原理PHP反射
- PHP 垃圾回收機制詳解PHP
- PHP AutoLoad 自動載入機制分析實踐PHP
- PHP自動載入__autoload的工作機制PHP
- 詳解 nginx php-fpm 工作機制NginxPHP
- Session機制詳解Session
- AsyncTask機制詳解
- 詳解spl_autoload_register()函式函式
- Redis 事件機制詳解Redis事件
- Java SPI機制詳解Java
- Java 反射機制詳解Java反射
- 回撥機制詳解
- TCP可靠機制詳解TCP
- JavaScript非同步機制詳解JavaScript非同步
- Java-SPI機制詳解Java
- Spring事件機制詳解Spring事件
- js執行機制詳解JS
- Android Handler機制詳解Android
- Fail-Fast機制詳解AIAST
- Django的Form機制詳解DjangoORM
- PHP autoload與include效能比較PHP
- Oracle SCN機制詳細解讀Oracle
- IO多路複用機制詳解
- JS 反射機制及 Reflect 詳解JS反射
- iOS APP簽名機制詳解iOSAPP
- 分頁機制圖文詳解
- linux 核心 RCU機制詳解Linux
- Java併發控制機制詳解Java
- Android 訊息機制詳解Android
- Python垃圾回收機制詳解Python
- C#垃圾回收機制詳解C#
- Java 類載入機制詳解Java
- Django的訊號機制詳解Django
- Spring 事務機制詳解Spring
- Java 動態代理機制詳解Java
- Java介面回撥機制詳解Java
- 詳解Python垃圾回收機制Python