[TOC]
# 說明
類自動加載已然是現代化框架必備的基礎設施,它讓我們只要設置好命名空間跟文件夾的對應關系,在使用到類的時候,就會自動去加載對應的類的文件。自動加載的核心是實現一個自動加載的方法,我們只要在該方法中添加命名空間到文件的映射規則,當到程序遇到「不認識」的類時,就會自動觸發該方法,自動去找到對應的類并加載之。 接下來,我們來分析框架的自動加載是如何實現的。
# 從入口文件出發
入口文件`public/index.php`開頭有:
```
require __DIR__ . '/../vendor/autoload.php';
```
`autoload.php` 中的代碼:
```
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitxxx::getLoader();
```
> 由于原類名較長,讓我們約定,類名后面有一長串 hash 字串的,都以‘xxx’代替,所以這里將類名標記為`ComposerAutoloaderInitxxx`。
第一行引入了 `autoload_real.php` 文件, 它里面定義了`ComposerAutoloaderInitxxx` 類,以及該類的若干靜態方法。我們從第二行語句展開分析。
# getLoader 方法代碼及分析
```
public static function getLoader()
{
// 檢查$loaders是否有值,有則直接返回
// 相當于單例模式
if (null !== self::$loader) {
return self::$loader;
}
/*
|---------------------------------------------------------
| 將 `ComposerAutoloaderInitxxx` 類的`loadClassLoader`方法注冊為一個
| `__autoload`函數的實現,無法注冊成功則拋出錯誤,且添加到自動加載函數隊
| 列前面(即使用的類找不到時,自動調用`loadClassLoader`方法實現自動加載,
| 具體實現見后面該方法分析)
|---------------------------------------------------------
*/
spl_autoload_register(array('ComposerAutoloaderInitxxx', 'loadClassLoader'), true, true);
/*
|---------------------------------------------------------
| 這里實例化一個ClassLoader類,并賦值到$loader成員。
| \Composer\Autoload\ClassLoader()按照字面的路徑是找不到該類的,
| 所以會觸發`loadClassLoader`方法實現自動加載。
| `loadClassLoader`方法的代碼如下:
| public static function loadClassLoader($class)
| {
| if ('Composer\Autoload\ClassLoader' === $class) {
| require __DIR__ . '/ClassLoader.php';
| }
| }
| 所以這里成功將ClassLoader.php文件加載進來
|---------------------------------------------------------
*/
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
// 得到 $loader 之后去掉前面注冊的自動加載實現
spl_autoload_unregister(array('ComposerAutoloaderInitxxx', 'loadClassLoader'));
// 靜態初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虛擬機
$useStaticLoader = PHP_VERSION_ID >= 50600
&& !defined('HHVM_VERSION')
&& (!function_exists('zend_loader_file_encoded')
|| !zend_loader_file_encoded());
// 一般 $useStaticLoader == true
if ($useStaticLoader) {
// 加載 autoload\_static.php 文件
require_once __DIR__ . '/autoload_static.php';
// 調用上一步加載的文件中的類的 getInitializer 方法
// getInitializer 方法的分析見后面的(A)部分
call_user_func(\Composer\Autoload\ComposerStaticInitxxx::getInitializer($loader));
} else {
//使用“非靜態”的初始化方式,結果和前面分支的靜態初始化方法是一樣的
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
// register 方法將 classLoader 方法加入自動加載函數隊列
// 只要程序遇到不認識的類,就會使用該隊列中的函數去查找類對應的文件
// 最后將找到的文件 require 加載進來
// 查找不到會做一個標記,下次查找時就可以直接識別該類
// 的文件是找不到的,直接返回false。后面展開分析該函數,在(B)部分
$loader->register(true);
// 加載全局函數(分靜態加載和非靜態加載,結果是一樣的)
// 一般全局助手函數都在這里加載
// $files成員變量是一個數組,包含'文件標識(哈希值)=>文件路徑'的鍵值對
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitxxx::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
// 注意到 composerRequirexxx 方法定義在本類的之外,封裝了require函數,
// require進來的文件里面的變量,其作用域被包裹在`composerRequirexxx`中,
// 防止require進來的文件含有$this或self而產生調用混淆或錯誤,
// 而且該函數實現了require_once的效果,效率更高。分析見(C)部分。
composerRequirexxx($fileIdentifier, $file);
}
return $loader;
}
```
## (A)getInitializer 方法分析
```
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitxxx::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitxxx::$prefixDirsPsr4;
$loader->fallbackDirsPsr0 = ComposerStaticInitxxx::$fallbackDirsPsr0;
}, null, ClassLoader::class);
}
```
在PHP中,Closure類的摘要如下:
```
Closure {
__construct ( void )
public static bind ( Closure $closure , object $newthis [, mixed $newscope = 'static' ] ) : Closure
public bindTo ( object $newthis [, mixed $newscope = 'static' ] ) : Closure
}
```
其中,`bind`方法的做作用是:復制一個閉包,綁定指定的$this對象和類作用域。這里將一個閉包綁定到`ClassLoader`類,使得該類的私有成員變量可以被賦值,從而將`ComposerStaticInitxxx`類定義的有關空間命名映射的幾個變量(包括:prefixLengthsPsr4、prefixDirsPsr4、fallbackDirsPsr0)搬到`ClassLoader`類中。 該函數執行后得到的結果:

`ClassLoader`的成員變量實現了初始化,即它們保存了各種形式的命名空間到文件夾路徑的映射。
## (B) register 方法分析
```
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
```
該方法將`loadClass`方法加入自動加載函數隊列,也就是當使用的類找不到時,觸發該方法去查找相應的類,注意到上面的第二個參數為`true`,說明是優先使用該方法作為自動加載的方法。那么,類的文件是如何被加載的,我們要到`loadClass`方法去尋找答案。`loadClass`方法代碼如下:
```
public function loadClass($class)
{
// 如果查找到文件
if ($file = $this->findFile($class)) {
// 將文件加載進來
includeFile($file);
return true;
}
}
```
實際上,答案在 `findFile` 方法:
```
public function findFile($class)
{
// class map lookup
// 如果classMap中有該類的文件映射,則直接返回對應的文件
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
// 如果這個類已經被標為沒有授權或者找不到,則直接返回false
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
// 如果有APCU緩存文件
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
//使用psr4、psr0標準查找,**后面著重分析該方法**
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
```
### findFileWithExtension 方法分析
```
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
// 將‘\’轉為‘/’并加上后綴
// 以下分析,假設$class = app\Request
// 即要查找app\Request類對應的文件
// 假設系統的DIRECTORY_SEPARATOR == ‘/’
// 則app\Request被轉為 app/Request.php
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0]; // 開頭為 a
// prefixLengthsPsr4數組中,有'a' => [ 'app\' => 4]
// 這時,該條件為true(php數組key不區分大小寫)
// ( prefixLengthsPsr4將命名空間用首字母歸類,相當于建了一個索引,
// 可以實現快速查找,如,這里如果沒有找到‘a’作為開頭的
// 就可以不用繼續查找,而是換別的查找方法。)
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class; // app\Request
// 計算字符串中最后一個‘\’的位置,并賦值給$lastPos,并判斷是否存在‘\’
// 對于 app\Request,$lastPos = 3
while (false !== $lastPos = strrpos($subPath, '\\')) {
// 從字符串開頭算起,取$lastPos個字符
// 這里得到$subPath=app'
$subPath = substr($subPath, 0, $lastPos);
// $search == 'app\'
$search = $subPath . '\\';
// 查找prefixDirsPsr4數組對應key是否有值,其key-value值如下:
/*
'app\' => [
[0] => your-project-dir\vendor\composer/../../app
]
*/
// 也就是說app\ 對應項目根目錄的app文件夾
if (isset($this->prefixDirsPsr4[$search])) {
// $pathEnd == '\Request.php'
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
// 逐個檢查prefixDirsPsr4['app\']下的文件路徑是否包含需要的文件
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
// \vendor\composer/../../app\Request.php
// 也就是得到app目錄下的Request.php文件
return $file;
}
}
}
}
}
// 原理類似,其他類型不再展開分析
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
```
最后,如果能找到類對應的文件,則返回文件路徑,在`loadClass`方法中執行`includeFile($file)`將文件加載進來。
## (C)composerRequirexxx 方法分析
在 `autoload_real.php` 文件中,有一個方法是定義在類的外部的,該方法代碼:
```
function composerRequirexxx($fileIdentifier, $file)
{
//文件標識為空才加載文件,實現了require_once的效果
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
//`$file`里面的變量,其作用域被包裹在 `composerRequirexxx`
// 避免$file里面的$this,self等變量穿透到外部
require $file;
// 將文件標識為已加載過的
// 下次需要加載到該文件時,如果該文件已經加載過,就不用再加載
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
```
# 小結
自動加載所完成的工作有:
* 實例化`ClassLoader`類,并初始化其成員變量
* 將`loadClass`方法加入自動加載函數隊列,且該方法實現了classMap,psr4,psr0等方式的文件路徑查找。當程序遇到不認識的類時,會調用該方法進行文件的加載
* 實現全局函數的加載
總的來說,自動加載一方面接管了我們手動寫一堆 require 或 include 的工作(想像一下,要require或include幾千個文件會是什么樣的情形),大大提高了開發效率和簡潔代碼;另一方面,自動加載是使用到了類的時候才去查找并加載類的文件,實現了按需加載,節約程序開銷,提高了程序的性能。