[转]ThinkPHP6.0核心分析--自动加载原理
in ThinkPHP with 0 comment

[转]ThinkPHP6.0核心分析--自动加载原理

in ThinkPHP with 0 comment

版权信息

转载自v2ex hubqin 源地址

说明

段落引用类自动加载已然是现代化框架必备的基础设施,它让我们只要设置好命名空间跟文件夹的对应关系,在使用到类的时候,就会自动去加载对应的类的文件。自动加载的核心是实现一个自动加载的方法,我们只要在该方法中添加命名空间到文件的映射规则,当到程序遇到「不认识」的类时,就会自动触发该方法,自动去找到对应的类并加载之。 接下来,我们来分析框架的自动加载是如何实现的。

从入口文件出发

入口文件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类中。 该函数执行后得到的结果: 转thinkphp.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;
    }
}

小结

自动加载所完成的工作有:

总的来说,自动加载一方面接管了我们手动写一堆 require 或 include 的工作(想像一下,要require或include几千个文件会是什么样的情形),大大提高了开发效率和简洁代码;另一方面,自动加载是使用到了类的时候才去查找并加载类的文件,实现了按需加载,节约程序开销,提高了程序的性能。