天天看點

PHP自動加載__autoload的工作機制PHP自動加載__autoload的工作機制

PHP自動加載__autoload的工作機制

 PHP的懶加載lazy loading

在 2011年11月12日 那天寫的     已經有 4559 次閱讀了

感謝 

參考或原文

伺服器君一共花費了11.683 ms進行了2次資料庫查詢,努力地為您提供了這個頁面。

試試閱讀模式?希望聽取您的建議   

在使用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機制是如何實作的。

我們知道,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機制的主要執行過程為:

  • 檢查執行器全局變量函數指針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,設計不好的自動裝載函數才會導緻其效率的降低。