php面試題4-實現autoload

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完全夠用,沒必要自己重複造輪子。

發佈了157 篇原創文章 · 獲贊 140 · 訪問量 56萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章