ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[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几千个文件会是什么样的情形),大大提高了开发效率和简洁代码;另一方面,自动加载是使用到了类的时候才去查找并加载类的文件,实现了按需加载,节约程序开销,提高了程序的性能。