[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`类中。 该函数执行后得到的结果:
![](https://img.kancloud.cn/6a/11/6a115936a9e92e8e1cb2f831c7e1b9e9_522x279.png)
`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几千个文件会是什么样的情形),大大提高了开发效率和简洁代码;另一方面,自动加载是使用到了类的时候才去查找并加载类的文件,实现了按需加载,节约程序开销,提高了程序的性能。