Yii框架宣稱自己的類加載方式很高效,是真正的“用時加載”,那究竟特別在哪裏?今天研究了一下源碼,發現其實是在代碼級加了一層“路徑緩存”。
Yii2 的自動加載原理
我們知道,要實現自己的autoload方法,需要採用spl_autoload_register()
函數註冊一個autoload方法,Yii註冊的這個方法是YiiBase::autoload()
,稍後再講解這個方法的邏輯。另外,Yii一般都用Yii::import($pathAlias, $forceInclude=false)
來加載相應的類(這個方法直接調用了YiiBase::import()
),這個方法配合YiiBase::autoload()
就能實現“用時加載”了。
先說import的大致邏輯:
1、檢查self::$_imports
數組是否存在相應的$pathAlias
,如果有說明已經加載過了,直接返回類名或者目錄名;否則繼續第2步;
2、根據路徑別名獲得實際的路徑名,並根據路徑別名最後一部分是否是“*”可以知道要加載的路徑別名是否是一個文件,如果是文件,去第3步;否則去第4步;
3、如果是$forceInclude
是true,則立即require這個文件,並在$_imports
數組中增加一項$alias => $className;
否則在數組$classMap
中緩存一項$className => $realPath
;
4、對於路徑,會在數組$_includePaths
中緩存這個路徑,並且在$_imports
數組中增加一項$alias => $realPath
;
5、結束。
因爲$forceInclude
默認都爲false,所以import不會立即加載相應的類,等到使用時才真正加載,這是YiiBase::autoload
的工作。
autoload的大致邏輯:
1、檢查類名是否已緩存在$classMap
或$_coreClasses
數組中,如果是則直接require相應的文件路徑,$_coreClasses
是框架自有類的映射表;否則去第2步;
2、檢測YiiBase::$enableIncludePath
是否爲false,如果是則去第3步,否則直接include($className . '.php')
3、遍歷$includePaths
數組,將目錄名拼接上類名,檢查是否爲合法的php文件,如果是則include,然後跳出循環
4、結束。
需要注意的是,文檔指出:如果要與其他類庫一起使用,必須將$enableIncludePath
置爲false,以便在Yii::autoload()
失敗時,其他類庫的autoload
方法有機會執行。
//$enableIncludePath
是否要依靠PHP包含路徑到自動加載類文件。默認爲true. 如果你的宿主環境不允許你改變PHP包含路徑,可以設置爲false, 或者你想添加另外的自動加載器到默認的Yii 自動加載器.
官方描述文檔
在Yii中,所有類、接口、Traits都可以使用類的自動加載機制實現在調用前自動加載。Yii藉助了PHP的類自動加載機制高效實現了類的定位、導入,這一機制兼容 PSR-4 的標準。在Yii中,類僅在調用時纔會被加載,特別是核心類,其定位非常快,這也是Yii高效高性能的一個重要體現。
自動加載機制的實現
Yii的類自動加載,依賴於PHP的 spl_autoload_register() , 註冊一個自己的自動加載函數(autoloader),並插入到自動加載函數棧的最前面,確保Yii的autoloader會被最先調用。
類自動加載的這個機制的引入要從入口文件 index.php 開始說起:
<?php
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');
// 這個是第三方的autoloader
require(__DIR__ . '/../../vendor/autoload.php');
// 這個是Yii的Autoloader,放在最後面,確保其插入的autoloader會放在最前面
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
// 後面不應再有autoloader了
require(__DIR__ . '/../../common/config/aliases.php');
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../common/config/main.php'),
require(__DIR__ . '/../../common/config/main-local.php'),
require(__DIR__ . '/../config/main.php'),
require(__DIR__ . '/../config/main-local.php')
);
$application = new yii\web\Application($config);
$application->run();
這個文件主要看點在於第三方autoloader與Yii 實現的autoloader的順序。不管第三方的代碼是如何使用 spl_autoload_register()
來註冊自己的autoloader的,只要Yii 的代碼在最後面,就可以確保其可以將自己的autoloader插入到整個autoloder 棧的最前面,從而在需要時最先被調用。
接下來,看看Yii是如何調用 spl_autoload_register()
註冊autoloader的, 這要看 Yii.php 裏發生了些什麼:
<?php
require(__DIR__ . '/BaseYii.php');
class Yii extends \yii\BaseYii
{
}
// 重點看這個 spl_autoload_register
spl_autoload_register(['Yii', 'autoload'], true, true);
// 下面的語句讀取了一個映射表
Yii::$classMap = include(__DIR__ . '/classes.php');
Yii::$container = new yii\di\Container;
這段代碼,調用了 spl_autoload_register(['Yii', 'autoload', true, true])
,將 Yii::autoload()
作爲autoloader
插入到棧的最前面了。並將 classes.php 讀取到 Yii::$classMap
中,保存了一個映射表。
在上面的代碼中,Yii類是裏面沒有任何代碼,並未對 BaseYii::autoload() 進行重載,所以,這個 spl_autoload_register() 實際上將 BaseYii::autoload()
註冊爲autoloader
。如果,你要實現自己的autoloader,可以在 Yii 類的代碼中,對 autoload()
進行重載。
在調用 spl_autoload_register()
進行autoloader
註冊之後,Yii將 calsses.php 這個文件作爲一個映射表保存到 Yii::$classMap
當中。這個映射表,保存了一系列的類名與其所在PHP文件的映射關係,比如:
return [
'yii\base\Action' => YII2_PATH . '/base/Action.php',
'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',
... ...
'yii\widgets\PjaxAsset' => YII2_PATH . '/widgets/PjaxAsset.php',
'yii\widgets\Spaceless' => YII2_PATH . '/widgets/Spaceless.php',
];
這個映射表以類名爲鍵,以實際類文件爲值,Yii所有的核心類都已經寫入到這個 classes.php 文件中,所以,核心類的加載是最便捷,最快的。現在,來看看這個關鍵先生 BaseYii::autoload()
public static function autoload($className)
{
if (isset(static::$classMap[$className])) {
$classFile = static::$classMap[$className];
if ($classFile[0] === '@') {
$classFile = static::getAlias($classFile);
}
} elseif (strpos($className, '\\') !== false) {
$classFile = static::getAlias('@' . str_replace('\\', '/',
$className) . '.php', false);
if ($classFile === false || !is_file($classFile)) {
return;
}
} else {
return;
}
include($classFile);
if (YII_DEBUG && !class_exists($className, false) &&
!interface_exists($className, false) && !trait_exists($className,
false)) {
throw new UnknownClassException(
"Unable to find '$className' in file: $classFile. Namespace missing?");
}
}
從這段代碼來看Yii類自動加載機制的運作原理:
檢查 $classMap[$className]
看看是否在映射表中已經有擬加載類的位置信息;
如果有,再看看這個位置信息是不是一個路徑別名,即是不是以 @ 打頭, 是的話,將路徑別名解析成實際路徑。 如果映射表中的位置信息並非一個路徑別名,那麼將這個路徑作爲類文件的所在位置。 類文件的完整路徑保存在 $classFile ;
如果 $classMap[$className]
沒有該類的信息, 那麼,看看這個類名中是否含有 \ , 如果沒有,說明這是一個不符合規範要求的類名,autoloader直接返回。 PHP會嘗試使用其他已經註冊的autoloader進行加載。 如果有 \ ,認爲這個類名符合規範,將其轉換成路徑形式。 即所有的 \ 用 / 替換,並加上 .php 的後綴。
將替換後的類名,加上 @ 前綴,作爲一個路徑別名,進行解析。 從別名的解析過程我們知道,如果根別名不存在,將會拋出異常。 所以,類的命名,必須以有效的根別名打頭:
// 有效的類名,因爲@yii是一個已經預定義好的別名
use yii\base\Application;
// 無效的類名,因爲沒有 @foo 或 @foo/bar 的根別名,要提前定義好
use foo\bar\SomeClass;
使用PHP的 include() 將類文件加載進來,實現類的加載。
從其運作原理看,最快找到類的方式是使用映射表。 其次,Yii中所有的類名,除了符合規範外,還需要提前註冊有效的根別名。
運用自動加載機制
在入口腳本中,除了Yii自己的autoloader,還有一個第三方的autoloader:
require(__DIR__ . '/../../vendor/autoload.php');
這個其實是Composer提供的autoloader。Yii使用Composer來作爲包依賴管理器,因此,建議保留Composer的autoloader,儘管Yii的autoloader也能自動加載使用Composer安裝的第三方庫、擴展等,而且更爲高效。但考慮到畢竟是人家安裝的,人家還有一套自己專門的規則,從維護性、兼容性、擴展性來考慮,建議保留Composer的autoloader。
如果還有其他的autoloader,一定要在Yii的autoloader註冊之前完成註冊,以保證Yii的autoloader總是最先被調用。
如果你有自己的autoloader,也可以不安裝Yii的autoloaer,只是這樣未必能有Yii的高效,且還需要遵循一套類似的類命名和加載的規則。就個人的經驗而言,Yii的autoloader完全夠用,沒必要自己重複造輪子。