PHP 版本:php-5.6
核心方法: spl_autoload_register
这里说的自动加载为官方提供的一系列 SPL 接口实现。
类加载方式
- 手动加载
__autoload
spl_autoload_register
手动加载
包含:include
include_once
requice
requice_one
include
以下文档也适用于 require。
被包含文件先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照 include_path 指定的目录寻找。如果在 include_path 下没找到该文件则 include 最后才在调用脚本文件所在的目录和当前工作目录下寻找。如果最后仍未找到文件则 include 结构会发出一条警告;这一点和 require 不同,后者会发出一个致命错误。如果定义了路径——不管是绝对路径(在 Windows 下以盘符或者 开头,在 Unix/Linux 下以 / 开头)还是当前目录的相对路径(以 . 或者 .. 开头)——include_path 都会被完全忽略。例如一个文件以 ../ 开头,则解析器会在当前目录的父目录下寻找该文件。
代码示例:
FILE: run.php
1 | 复制代码<?php |
FILE: class.php
1 | 复制代码<?php |
结果:
结论:
include
成功返回值:1
,失败返回值:false
include
失败会有warning
,不会中断进程
include_once
include_once
行为和 include 语句类似,唯一区别是如果该文件中已经被包含过,则不会再次包含。
将 run.php
修改如下:
1 | 复制代码<?php |
结果:
结论:
include_once
重复包含时,会直接返回1
,并忽略此次包含操作,继续执行
require
require
和include
几乎完全一样,但require
在出错时产生E_COMPILE_ERROR
级别的错误。(脚本将会中止运行)
将 run.php
修改如下:
1 | 复制代码<?php |
结果:
结论:
require
包含成功,同include
一样,返回值:1
require
包含失败,直接抛出Fatal error
,进程中止
require_once
require_once
语句和require
语句完全相同,唯一区别是PHP
会检查该文件是否已经被包含过,如果是则不会再次包含。
将 run.php
修改如下:
1 | 复制代码ini_set('display_errors', '1'); |
结果:
结论:
- 成功,返回值:
1
- 重复包含,返回
1
,并忽略此次包含
总结
include
include_once
requice
requice_one
成功时,都会返回 1
,差别在于 包含失败、重复包含 的处理
__autoload
尝试加载未定义的类。此函数将会在
PHP 7.2.0
中弃用。
PHP 代码解释
使用示例:
FILE:foo.php
1 | 复制代码<?php |
FILE:run.php
1 | 复制代码<?php |
结果:
1 | 复制代码➜ load git:(master) ✗ php run.php |
结论:
遇到未包含的类,会触发 __autoload
进行加载,如果所有加载规则中没有此类,则 Fatal error
。
Zend 代码解释
下面,我们来看一下 Zend
引擎是如何触发 __autoload
调用的。
利用 vld 来查看刚才执行过程中产生的 opcode
,结果如下:
我们看到,PHP
运行到第 10 行时,所生成的 opcode
为:INIT_STATIC_METHOD_CALL
,两个操作数都为常量(CONST
)。
根据 opcode 的处理函数对应规则,我们利用 命名法
可以确定,
处理函数为:ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER
源码位置为:vim Zend/zend_vm_execute.h +3819
源码如下:
1 | 复制代码static int ZEND_FASTCALL ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) |
通过以上源码,我们发现关键方法为 zend_fetch_class_by_name
,跟进此方法:
1 | 复制代码zend_class_entry *zend_fetch_class_by_name(const char *class_name, uint class_name_len, const zend_literal *key, int fetch_type TSRMLS_DC) |
我们发现是通过 zend_lookup_class_ex
来获取类,继续跟进:
1 | 复制代码ZEND_API int zend_lookup_class_ex(const char *name, int name_length, const zend_literal *key, int use_autoload, zend_class_entry ***ce TSRMLS_DC) |
我们发现是通过 zend_call_function
出发了自动加载函数,而且看到了加载方法的名字 __autoload
(宏:ZEND_AUTOLOAD_FUNC_NAME
)
zend_call_function
中会做一下检测并调用等,而且我们看到 zend_lookup_class_ex
的返回结果即为 zend_call_function
的返回结果。
接下来我们逐步退出函数调用栈:
假设 zend_call_function
调用失败,返回 FALSE
,
则 zend_lookup_class_ex
返回 FALSE
;
则 zend_fetch_class_by_name
返回 NULL
;
则 ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER
抛出异常 Class ** not found
,如下图所示:
结论
至此,我们通过 PHP 代码
Zend 源码
了解了 __autoload
的调用过程。
我们知道 __autoload
现在已并不推荐使用,
它的缺点也很明显,不支持多个自动加载函数。
spl_autoload_register
将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。如果在你的程序中已经实现了__autoload()函数,它必须显式注册到__autoload()队列中。
PHP 代码解释
使用示例:
FILE:foo.php
(同上 __autoload
)
FILE:foo2.class.php
1 | 复制代码<?php |
FILE:run.php
1 | 复制代码<?php |
结果如下:
我们看到,调用 getFoo2
时,会先调用第一个注册的 autoload
方法,如果没找到对应的类,会产生 warning
并继续调用后边注册的 autoload
方法。
说明了 PHP
内核中为通过 spl_autoload_register
注册的 autoload
方法维护了一个队列,当前文件为包含调用类,便会触发此队列,并依次调用,直到队列结束
或者 找到对应类。
Zend 源码解释
首先,我们看一下 PHP
文件生成的 opcode
我们发现,其方法调用所生成的 opcode
跟 __autoload
一样,
但是我们之前调用了 spl\_autoload\_register,
那么,看一下
spl_autoload_register的源码:
**FILE:
ext/spl/php_spl.c`*\
1 | 复制代码PHP_FUNCTION(spl_autoload_register) |
通过分析源码,我们发现 spl_autoload_register
会把注册的自动加载函数添加到 autoload_functions
中,最后将 autoload_functions
赋值给 EG(autoload_func)
(上方源码倒数第一个 if
判断逻辑中)。
而有印象的同学会发现,EG(autoload_func)
在分析 __autoload
调用源码时出现过(可以划到之前的分析查看),它是执行环境全局结构体中的成员,出现调用大概源码如下:
1 | 复制代码ZEND_API int zend_lookup_class_ex(const char *name, int name_length, const zend_literal *key, int use_autoload, zend_class_entry ***ce TSRMLS_DC) |
分析到这里,我们已经知道了,spl_autoload_register
注册的函数是如何在 PHP
代码调用时被触发的。
感兴趣的同学可以继续查看一下 zend_call_function
的源码,了解具体的调用方式。
结论
通过 spl_autoload_register
注册自动加载函数,会在 Zend
引擎中维护一个 autoload
队列,即可添加多个 autoload
函数,并在 PHP
调用当前文件未知的类时,触发 autoload_func
的调用。
同时,细心的同学也会从 spl_autoload_register
源码中发现,当注册时传入的方法不可调用时,spl_autoload
如果有实现,也会被注册到 autoload
队列中。
写在最后
我们介绍了 include*
require*
,
而且我们从 PHP
应用 和 zend
源码角度,分别分析了 __autoload
spl_autoload_register
的实现和调用过程。
我们可以直到,加载函数推荐使用 spl_autoload_register
来实现。
我们的分析更多的是意在让自己对这些细节加深认识,并进一步深入了解
Zend
源码。
更多使用细节,请参考:
以上为个人分析,如有不适的地方,请多多指教!
本文转载自: 掘金