首頁  >  文章  >  後端開發  >  autoload與spl_autoload自動載入比較分析

autoload與spl_autoload自動載入比較分析

伊谢尔伦
伊谢尔伦原創
2017-07-01 09:48:35934瀏覽

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()函數,在這個函數中我們可以加載需要使用的類別。在我們這個簡單的例子中,我們直接將類別名稱加上擴展名" title="擴展名">擴展名”.class.php」構成了類別文件名,然後使用require_once將其載入。從這個例子中,我們可以看出autoload至少要做三件事情,第一件事是根據類別名稱確定類別檔案名,第二件事是確定類別檔案所在的磁碟路徑(在我們的例子是最簡單的情況,類別與呼叫它們的PHP程式檔案在同一個資料夾下),第三件事是將類別從磁碟檔案載入到系統中。一步,第二步的功能,必須在開發時約定類別名稱與磁碟檔案的映射方法,只有這樣我們才能根據類別名稱找到它對應的磁碟檔案。檔案要包含的時候,我們只要確定對應的規則,然後在autoload()函數中,將類別名稱與實際的磁碟檔案對應起來,就可以實現lazy loading的效果。函數的實作中最重要的是類別名稱與實際的磁碟檔案映射規則的實作。類別庫可能是由不同的開發人員編寫的,其類別名稱與實際的磁碟檔案的映射規則不盡相同。 )函數中將所有的映射規則全部實現,這樣的話autoload()函數有可能會非常複雜,甚至無法實現。而係統效率帶來很大的負面影響。的autoload機制是如何實現的。的過程,第一步是將PHP檔案編譯成普通稱為OPCODE的字節碼序列(實際上是編譯成一個叫做zend_op_array的位元組數組),第二步是由一個虛擬機器來執行這些OPCODE。 PHP的所有行為都是由這些OPCODE來實現的。 :
/* autoload.php 編譯後的OPCODE列表,是使用作者開發的OPDUMP工具
*/

程式碼如下:

 <?php
 // require_once (”Person.php”);

 function autoload ($classname) {
0 NOP                
0 RECV                1
    if (!class_exists($classname)) {
1 SEND_VAR            !0
2 DO_FCALL            ‘class_exists&#39; [extval:1]
3 BOOL_NOT            $0 =>RES[~1]     
4 JMPZ                ~1, ->8
     require_once ($classname. “.class.php”);
5 CONCAT              !0, ‘.class.php&#39; =>RES[~2]     
6 INCLUDE_OR_EVAL     ~2, REQUIRE_ONCE
    }
7 JMP                 ->8
 }
8 RETURN              null

 $p = new Person(&#39;Fred&#39;, 35);
1 FETCH_CLASS         ‘Person&#39; =>RES[:0]     
2 NEW                 :0 =>RES[$1]     
3 SEND_VAL            ‘Fred&#39;
4 SEND_VAL            35
5 DO_FCALL_BY_NAME     [extval:2]
6 ASSIGN              !0, $1

 var_dump ($p);
7 SEND_VAR            !0
8 DO_FCALL            ‘var_dump&#39; [extval:1]
 ?>


在 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是可选的,表示类文件的扩展名" title="扩展名">扩展名,可以在$file_extensions中指定多个扩展名" title="扩展名">扩展名,护展名之间用分号隔开即 可;如果不指定的话,它将使用默认的扩展名" title="扩展名">扩展名.inc或.php。spl_autoload首先将$class_name变为小写,然后在所有的 include path中搜索$class_name.inc或$class_name.php文件(如果不指定$file_extensions参数的话),如果找 到,就加载该类文件。你可以手动使用spl_autoload(”Person”, “.class.php”)来加载Person类。实际上,它跟require/include差不多,不同的它可以指定多个扩展名" title="扩展名">扩展名。

怎麼 樣讓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,設計不好的自動裝載函數才會導致其效率的降低。

以上是autoload與spl_autoload自動載入比較分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn