PHP的autoload機制的實現(xiàn)解析

在使用PHP的OO模式開發(fā)系統(tǒng)時蓬网,通常大家習(xí)慣上將每個類的實現(xiàn)都存放在一個單獨的文件里,這樣會很容易實現(xiàn)對類進(jìn)行復(fù)用鹉勒,同時將來維護(hù)時也很便利

一帆锋、autoload機制概述?

在使用PHP的OO模式開發(fā)系統(tǒng)時,通常大家習(xí)慣上將每個類的實現(xiàn)都存放在一個單獨的文件里禽额,這樣會很容易實現(xiàn)對類進(jìn)行復(fù)用锯厢,同時將來維護(hù)時也很便利皮官。這也是OO設(shè)計的基本思想之一。在PHP5之前实辑,如果需要使用一個類捺氢,只需要直接使用include/require將其包含進(jìn)來即可。?

下面是一個實際的例子:?

代碼如下:

/* Person.class.php */?


class Person {?

var $name, $age;?

function __construct ($name, $age)?

{?

$this->name = $name;?

$this->age = $age;?

}?

}?

?>?

/* no_autoload.php */?


require_once ("Person.class.php");?

$person = new Person("Altair", 6);?

var_dump ($person);?

在這個例子中剪撬,no-autoload.php文件需要使用Person類摄乒,它使用了require_once將其包含,然后就可以直接使用Person類來實例化一個對象残黑。?

但隨著項目規(guī)模的不斷擴(kuò)大馍佑,使用這種方式會帶來一些隱含的問題:如果一個PHP文件需要使用很多其它類,那么就需要很多的require/include語句梨水,這樣有可能會造成遺漏或者包含進(jìn)不必要的類文件挤茄。如果大量的文件都需要使用其它的類,那么要保證每個文件都包含正確的類文件肯定是一個噩夢冰木。?

PHP5為這個問題提供了一個解決方案,這就是類的自動裝載(autoload)機制笼恰。autoload機制可以使得PHP程序有可能在使用類時才自動包含類文件踊沸,而不是一開始就將所有的類文件include進(jìn)來,這種機制也稱為lazy loading社证。?

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

代碼如下:

/* autoload.php */?

function __autoload($classname) {?

require_once ($classname . "class.php");?

}?

$person = new Person("Altair", 6);?

var_dump ($person);?

?>?

通常PHP5在使用一個類時逼龟,如果發(fā)現(xiàn)這個類沒有加載,就會自動運行__autoload()函數(shù)追葡,在這個函數(shù)中我們可以加載需要使用的類腺律。在我們這個簡單的例子中,我們直接將類名加上擴(kuò)展名”.class.php”構(gòu)成了類文件名宜肉,然后使用require_once將其加載匀钧。從這個例子中,我們可以看出autoload至少要做三件事情谬返,第一件事是根據(jù)類名確定類文件名之斯,第二件事是確定類文件所在的磁盤路徑(在我們的例子是最簡單的情況,類與調(diào)用它們的PHP程序文件在同一個文件夾下)遣铝,第三件事是將類從磁盤文件中加載到系統(tǒng)中佑刷。第三步最簡單,只需要使用include/require即可酿炸。要實現(xiàn)第一步瘫絮,第二步的功能,必須在開發(fā)時約定類名與磁盤文件的映射方法填硕,只有這樣我們才能根據(jù)類名找到它對應(yīng)的磁盤文件麦萤。?

因此,當(dāng)有大量的類文件要包含的時候,我們只要確定相應(yīng)的規(guī)則频鉴,然后在__autoload()函數(shù)中栓辜,將類名與實際的磁盤文件對應(yīng)起來,就可以實現(xiàn)lazy loading的效果垛孔。從這里我們也可以看出__autoload()函數(shù)的實現(xiàn)中最重要的是類名與實際的磁盤文件映射規(guī)則的實現(xiàn)坝冕。?

但現(xiàn)在問題來了耕蝉,如果在一個系統(tǒng)的實現(xiàn)中,如果需要使用很多其它的類庫,這些類庫可能是由不同的開發(fā)人員編寫的拗慨,其類名與實際的磁盤文件的映射規(guī)則不盡相同。這時如果要實現(xiàn)類庫文件的自動加載沪袭,就必須在__autoload()函數(shù)中將所有的映射規(guī)則全部實現(xiàn)艇抠,這樣的話__autoload()函數(shù)有可能會非常復(fù)雜,甚至無法實現(xiàn)讯榕。最后可能會導(dǎo)致__autoload()函數(shù)十分臃腫骤素,這時即便能夠?qū)崿F(xiàn),也會給將來的維護(hù)和系統(tǒng)效率帶來很大的負(fù)面影響愚屁。在這種情況下济竹,難道就沒有更簡單清晰的解決辦法了吧?答案當(dāng)然是:NO! 在看進(jìn)一步的解決方法之前霎槐,我們先來看一下PHP中的autoload機制是如何實現(xiàn)的送浊。?

二、PHP的autoload機制的實現(xiàn)?

我們知道丘跌,PHP文件的執(zhí)行分為兩個獨立的過程袭景,第一步是將PHP文件編譯成普通稱之為OPCODE的字節(jié)碼序列(實際上是編譯成一個叫做zend_op_array的字節(jié)數(shù)組),第二步是由一個虛擬機來執(zhí)行這些OPCODE闭树。PHP的所有行為都是由這些OPCODE來實現(xiàn)的耸棒。因此,為了研究PHP中autoload的實現(xiàn)機制蔼啦,我們將autoload.php文件編譯成opcode榆纽,然后根據(jù)這些OPCODE來研究PHP在這過程中都做了些什么:?

代碼如下:

/* autoload.php 編譯后的OPCODE列表,是使用作者開發(fā)的OPDUMP工具?

* 生成的結(jié)果捏肢,可以到網(wǎng)站 http://www.phpinternals.com/ 下載該軟件奈籽。?

*/?


// 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' [extval:1]?

3 BOOL_NOT $0 =>RES[~1]?

4 JMPZ ~1, ->8?

require_once ($classname. ".class.php");?

5 CONCAT !0, '.class.php' =>RES[~2]?

6 INCLUDE_OR_EVAL ~2, REQUIRE_ONCE?

}?

7 JMP ->8?

}?

8 RETURN null?

$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?

var_dump ($p);?

7 SEND_VAR !0?

8 DO_FCALL 'var_dump' [extval:1]?

?>?

在autoload.php的第10行代碼中我們需要為類Person實例化一個對象。因此autoload機制一定會在該行編譯后的opcode中有所體現(xiàn)鸵赫。從上面的第10行代碼生成的OPCODE中我們知道衣屏,在實例化對象Person時,首先要執(zhí)行FETCH_CLASS指令辩棒。我們就從PHP對FETCH_CLASS指令的處理過程開始我們的探索之旅狼忱。?

通過查閱PHP的源代碼(我使用的是PHP 5.3alpha2版本)可以發(fā)現(xiàn)如下的調(diào)用序列:?

代碼如下:

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行)?

在最后一步的調(diào)用之前膨疏,我們先看一下調(diào)用時的關(guān)鍵參數(shù):?

代碼如下:

/* 設(shè)置autoload_function變量值為"__autoload" */?

fcall_info.function_name = &autoload_function; // Ooops, 終于發(fā)現(xiàn)"__autoload"了?

...?

fcall_cache.function_handler = EG(autoload_func); // autoload_func !?

zend_call_function是Zend Engine中最重要的函數(shù)之一,其主要功能是執(zhí)行用戶在PHP程序中自定義的函數(shù)或者PHP本身的庫函數(shù)钻弄。zend_call_function有兩個重要的指針形參數(shù)fcall_info, fcall_cache佃却,它們分別指向兩個重要的結(jié)構(gòu),一個是zend_fcall_info, 另一個是zend_fcall_info_cache窘俺。zend_call_function主要工作流程如下:如果fcall_cache.function_handler指針為NULL饲帅,則嘗試查找函數(shù)名為fcall_info.function_name的函數(shù),如果存在的話瘤泪,則執(zhí)行之灶泵;如果fcall_cache.function_handler不為NULL,則直接執(zhí)行fcall_cache.function_handler指向的函數(shù)对途。?

現(xiàn)在我們清楚了赦邻,PHP在實例化一個對象時(實際上在實現(xiàn)接口,使用類常數(shù)或類中的靜態(tài)變量实檀,調(diào)用類中的靜態(tài)方法時都會如此)惶洲,首先會在系統(tǒng)中查找該類(或接口)是否存在,如果不存在的話就嘗試使用autoload機制來加載該類膳犹。而autoload機制的主要執(zhí)行過程為:?

(1) 檢查執(zhí)行器全局變量函數(shù)指針autoload_func是否為NULL湃鹊。?

(2) 如果autoload_func==NULL, 則查找系統(tǒng)中是否定義有__autoload()函數(shù),如果沒有镣奋,則報告錯誤并退出。?

(3) 如果定義了__autoload()函數(shù)怀愧,則執(zhí)行__autoload()嘗試加載類侨颈,并返回加載結(jié)果。?

(4) 如果autoload_func不為NULL芯义,則直接執(zhí)行autoload_func指針指向的函數(shù)用來加載類哈垢。注意此時并不檢查__autoload()函數(shù)是否定義。?

真相終于大白扛拨,PHP提供了兩種方法來實現(xiàn)自動裝載機制耘分,一種我們前面已經(jīng)提到過,是使用用戶定義的__autoload()函數(shù)绑警,這通常在PHP源程序中來實現(xiàn)求泰;另外一種就是設(shè)計一個函數(shù),將autoload_func指針指向它计盒,這通常使用C語言在PHP擴(kuò)展中實現(xiàn)渴频。如果既實現(xiàn)了__autoload()函數(shù),又實現(xiàn)了autoload_func(將autoload_func指向某一PHP函數(shù))北启,那么只執(zhí)行autoload_func函數(shù)卜朗。?

三拔第、SPL autoload機制的實現(xiàn)?

SPL是Standard PHP Library(標(biāo)準(zhǔn)PHP庫)的縮寫。它是PHP5引入的一個擴(kuò)展庫场钉,其主要功能包括autoload機制的實現(xiàn)及包括各種Iterator接口或類蚊俺。SPL autoload機制的實現(xiàn)是通過將函數(shù)指針autoload_func指向自己實現(xiàn)的具有自動裝載功能的函數(shù)來實現(xiàn)的。SPL有兩個不同的函數(shù)spl_autoload, spl_autoload_call逛万,通過將autoload_func指向這兩個不同的函數(shù)地址來實現(xiàn)不同的自動加載機制泳猬。?

spl_autoload是SPL實現(xiàn)的默認(rèn)的自動加載函數(shù),它的功能比較簡單泣港。它可以接收兩個參數(shù)暂殖,第一個參數(shù)是$class_name,表示類名当纱,第二個參數(shù)$file_extensions是可選的呛每,表示類文件的擴(kuò)展名,可以在$file_extensions中指定多個擴(kuò)展名坡氯,護(hù)展名之間用分號隔開即可晨横;如果不指定的話,它將使用默認(rèn)的擴(kuò)展名.inc或.php箫柳。spl_autoload首先將$class_name變?yōu)樾懯中危缓笤谒械膇nclude path中搜索$class_name.inc或$class_name.php文件(如果不指定$file_extensions參數(shù)的話),如果找到悯恍,就加載該類文件库糠。你可以手動使用spl_autoload(“Person”, “.class.php”)來加載Person類。實際上涮毫,它跟require/include差不多瞬欧,不同的它可以指定多個擴(kuò)展名。?

怎樣讓spl_autoload自動起作用呢罢防,也就是將autoload_func指向spl_autoload艘虎?答案是使用spl_autoload_register函數(shù)。在PHP腳本中第一次調(diào)用spl_autoload_register()時不使用任何參數(shù)咒吐,就可以將autoload_func指向spl_autoload野建。?

通過上面的說明我們知道,spl_autoload的功能比較簡單恬叹,而且它是在SPL擴(kuò)展中實現(xiàn)的候生,我們無法擴(kuò)充它的功能。如果想實現(xiàn)自己的更靈活的自動加載機制怎么辦呢绽昼?這時陶舞,spl_autoload_call函數(shù)閃亮登場了。?

我們先看一下spl_autoload_call的實現(xiàn)有何奇妙之處绪励。在SPL模塊內(nèi)部肿孵,有一個全局變量autoload_functions唠粥,它本質(zhì)上是一個HashTable,不過我們可以將其簡單的看作一個鏈表停做,鏈表中的每一個元素都是一個函數(shù)指針,指向一個具有自動加載類功能的函數(shù)晤愧。spl_autoload_call本身的實現(xiàn)很簡單,只是簡單的按順序執(zhí)行這個鏈表中每個函數(shù)蛉腌,在每個函數(shù)執(zhí)行完成后都判斷一次需要的類是否已經(jīng)加載官份,如果加載成功就直接返回,不再繼續(xù)執(zhí)行鏈表中的其它函數(shù)烙丛。如果這個鏈表中所有的函數(shù)都執(zhí)行完成后類還沒有加載舅巷,spl_autoload_call就直接退出,并不向用戶報告錯誤河咽。因此钠右,使用了autoload機制,并不能保證類就一定能正確的自動加載忘蟹,關(guān)鍵還是要看你的自動加載函數(shù)如何實現(xiàn)飒房。?

那么自動加載函數(shù)鏈表autoload_functions是誰來維護(hù)呢?就是前面提到的spl_autoload_register函數(shù)媚值。它可以將用戶定義的自動加載函數(shù)注冊到這個鏈表中狠毯,并將autoload_func函數(shù)指針指向spl_autoload_call函數(shù)(注意有一種情況例外,具體是哪種情況留給大家思考)褥芒。我們也可以通過spl_autoload_unregister函數(shù)將已經(jīng)注冊的函數(shù)從autoload_functions鏈表中刪除嚼松。?

上節(jié)說過,當(dāng)autoload_func指針非空時锰扶,就不會自動執(zhí)行__autoload()函數(shù)了惜颇,現(xiàn)在autoload_func已經(jīng)指向了spl_autoload_call,如果我們還想讓__autoload()函數(shù)起作用應(yīng)該怎么辦呢少辣?當(dāng)然還是使用spl_autoload_register(__autoload)調(diào)用將它注冊到autoload_functions鏈表中。?

現(xiàn)在回到第一節(jié)最后的問題羡蛾,我們有了解決方案:根據(jù)每個類庫不同的命名機制實現(xiàn)各自的自動加載函數(shù)漓帅,然后使用spl_autoload_register分別將其注冊到SPL自動加載函數(shù)隊列中就可了。這樣我們就不用維護(hù)一個非常復(fù)雜的__autoload函數(shù)了痴怨。?

四忙干、autoload效率問題及對策?

使用autoload機制時,很多人的第一反應(yīng)就是使用autoload會降低系統(tǒng)效率浪藻,甚至有人干脆提議為了效率不要使用autoload捐迫。在我們了解了autoload實現(xiàn)的原理后,我們知道autoload機制本身并不是影響系統(tǒng)效率的原因爱葵,甚至它還有可能提高系統(tǒng)效率施戴,因為它不會將不需要的類加載到系統(tǒng)中反浓。?

那么為什么很多人都有一個使用autoload會降低系統(tǒng)效率的印象呢?實際上赞哗,影響autoload機制效率本身恰恰是用戶設(shè)計的自動加載函數(shù)雷则。如果它不能高效的將類名與實際的磁盤文件(注意,這里指實際的磁盤文件肪笋,而不僅僅是文件名)對應(yīng)起來月劈,系統(tǒng)將不得不做大量的文件是否存在(需要在每個include path中包含的路徑中去尋找)的判斷,而判斷文件是否存在需要做磁盤I/O操作藤乙,眾所周知磁盤I/O操作的效率很低猜揪,因此這才是使得autoload機制效率降低的罪魁禍?zhǔn)??

因此,我們在系統(tǒng)設(shè)計時坛梁,需要定義一套清晰的將類名與實際磁盤文件映射的機制而姐。這個規(guī)則越簡單越明確,autoload機制的效率就越高罚勾。autoload機制并不是天然的效率低下毅人,只有濫用autoload,設(shè)計不好的自動裝載函數(shù)才會導(dǎo)致其效率的降低尖殃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丈莺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子送丰,更是在濱河造成了極大的恐慌缔俄,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件器躏,死亡現(xiàn)場離奇詭異俐载,居然都是意外死亡,警方通過查閱死者的電腦和手機登失,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進(jìn)店門遏佣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人揽浙,你說我怎么就攤上這事状婶。” “怎么了馅巷?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵膛虫,是天一觀的道長。 經(jīng)常有香客問我钓猬,道長稍刀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任敞曹,我火速辦了婚禮账月,結(jié)果婚禮上综膀,老公的妹妹穿的比我還像新娘。我一直安慰自己捶障,他們只是感情好僧须,可當(dāng)我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著项炼,像睡著了一般担平。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锭部,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天暂论,我揣著相機與錄音,去河邊找鬼拌禾。 笑死取胎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的湃窍。 我是一名探鬼主播闻蛀,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼您市!你這毒婦竟也來了觉痛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤茵休,失蹤者是張志新(化名)和其女友劉穎薪棒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榕莺,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡俐芯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了钉鸯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吧史。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唠雕,靈堂內(nèi)的尸體忽然破棺而出贸营,到底是詐尸還是另有隱情,我是刑警寧澤及塘,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站锐极,受9級特大地震影響笙僚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灵再,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一肋层、第九天 我趴在偏房一處隱蔽的房頂上張望亿笤。 院中可真熱鬧,春花似錦栋猖、人聲如沸净薛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肃拜。三九已至,卻和暖如春雌团,著一層夾襖步出監(jiān)牢的瞬間燃领,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工锦援, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留猛蔽,地道東北人。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓灵寺,卻偏偏與公主長得像曼库,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子略板,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容