入门客AI创业平台(我带你入门,你带我飞行)
博文笔记

理解Yii2类的延迟加载

创建时间:2016-11-19 投稿人: 浏览次数:1089

在Yii中,所有类、接口、Traits都可以使用类的自动加载机制实现在调用前自动加载。Yii借助了PHP的类自动加载机制高效实现了类的定位、导入,这一机制兼容 PSR-4 的标准。在Yii中,类仅在调用时才会被加载,特别是核心类,其定位非常快,这也是Yii高效高性能的一个重要体现。

自动加载机制的实现

先看一自动加载的简单例子

<?php

//Yii框架也根据这种机制实现了类的延迟加载,做得更加地道(使用spl_autoload_register()自动完成类的加载)。
//======下方的1 2 3 4为执行顺序
//4.当PHP发现实例化时不认识Class1,它会把Class1的名字传递给my_loader函数的$class参数当中,那么$class变量就代表了Class1。(同理,当PHP实例化Class2时也如此)
function my_loader($class){
    //3.但是这里不仅加载了Class1,还加载了Class2,导致类还是会被多余加载。怎样解决这个问题?
    /*require("classClass1.php");
    require("classClass2.php");*/
    //5.所以可以通过$class对require代码进行优化:把Class1改为$class变量,因为具有转义的意思,所以需要2个\来代表1个。
    require("class\".$class.".php");
}
//2.这是由于PHP准备报错时,这个函数告诉PHP先不报错,先去运行一下my_loader。然后PHP真的去运行my_loader函数,把里面的Class1类文件加载进来了。也就不会报错了。
spl_autoload_register("my_loader");
$is_girl = !empty($_GET["sex"]) && htmlspecialchars($_GET["sex"]) == 2 ? true : false;//http://127.0.0.1/test/test.php?sex=2
if ($is_girl) {
    echo "this is a girl!";
    $Class1 = new Class1; //1.当PHP运行到这一行,不认识Class1并且在这里也没有加载类文件,这时候PHP八成会报错,但实际上并没有报错。
} else {
    echo "not a girl!";
    $Class2 = new Class2;
}


//测试类
class Class1
{
    public function __construct() {
        echo "<br/>";
        echo "I am class1";
    }
}
class Class2
{
    public function __construct() {
        echo "<br/>";
        echo "I am class1";
    }    
}

理解了上面的例子后再看下发的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 = yiihelpersArrayHelper::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 yiiwebApplication($config);
$application->run();

这个文件主要看点在于第三方autoloader与Yii 实现的autoloader的顺序。不管第三方的代码是如何使用 spl_autoload_register() 来注册自己的autoloader的,只要Yii 的代码在最后面,就可以确保其可以将自己的autoloader插入到整个autoloder 栈的最前面,从而在需要时最先被调用。(解释:栈是先进后出的,所以Yii将自己的autoloader放在后面)

类的映射表机制

类的映射(class map)表机制:Yii框架也提供了类的映射表机制去进一步地更快加载类(常用的类,不建议不常用的类也放在$classMap中,会让在这个数组里查找相应的类的速率降低同时会占用更大的内存)
//使用类的映射表去加载Order类,使用Yii全局类里的$classMap["参数key:加载的类的全名",""]数组,因为是要根据它的名字去加载它的绝对路径,所以这个数组的值就是Order类所在类文件的绝对路径。

Yii::$classMap["appmodelsOrder"] = "C:wampwwwmoocyiiasicmodelsxOrder.php";
//实例化Order活动记录
$order = new Order;

接下来,看看Yii是如何调用 spl_autoload_register() 注册autoloader的, 这要看 Yii.php 里发生了些什么:

<?php
require(__DIR__ . "/BaseYii.php");
class Yii extends yiiBaseYii
{
}

// 重点看这个 spl_autoload_register
spl_autoload_register(["Yii", "autoload"], true, true);

// 下面的语句读取了一个映射表
Yii::$classMap = include(__DIR__ . "/classes.php");

Yii::$container = new yiidiContainer;

这段代码,调用了 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 [
  "yiiaseAction" => YII2_PATH . "/base/Action.php",
  "yiiaseActionEvent" => YII2_PATH . "/base/ActionEvent.php",

  ... ...

  "yiiwidgetsPjaxAsset" => YII2_PATH . "/widgets/PjaxAsset.php",
  "yiiwidgetsSpaceless" => 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 yiiaseApplication;

// 无效的类名,因为没有 @foo 或 @foo/bar 的根别名,要提前定义好
use fooarSomeClass;

使用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完全够用,没必要自己重复造轮子。

至于Composer如何自动加载类文件,这里就不过多的占用篇幅了。可以看看 Composer的文档 。

组件的延时加载

组件的延迟加载:比如用户给Yii框架的项目发送了一个请求,index.php入口脚本文件最先处理这个请求->再把请求交给应用主体app处理(在处理请求之前,把它自己给实例化出来,实例化过程当中会去加载组件<-组件components[包含:session/request/response…组件])->app加载完组件之后再把请求交给Controller处理(控制器在处理请求时可以使用app加载过来的组件)。

那么所谓的组件延迟加载就是:看起来好像是app预先加载了components里的组件,然后在Controller中直接拿过来用。实际上app并没有真正的加载components里的组件,而是在Controller里真正使用到某一个组件(如session)时才加载进来,也就是说把这个组件的加载过程由app的初始化延迟到Controller真正的使用某一个组件时。
实际上这个session组件在调用这行代码之前是不存在的,只有调用了这行代码,程序知道我们想使用session组件才会去加载进来。
具体加载流程:当访问app应用主体里的session属性时,会触及PHP当中的__get()方法,在__get()方法里才会真正的把session组件加载进来,加载完之后会把session组件返回出来(通过$session变量接收)。

$session = Yii::$app->session;

参考文章:http://www.digpage.com/autoload.html

声明:该文观点仅代表作者本人,入门客AI创业平台信息发布平台仅提供信息存储空间服务,如有疑问请联系rumenke@qq.com。