使用弱引用提高效率
随着 PHP 的不断发展和成熟,越来越多的开发人员开始使用 PHP 框架来促进应用程序的快速开发。然而,这种做法的一个必然副产品就是占用内存的对象越来越大、越来越复杂。包含许多属性、其他对象或大小数组的大型对象通常被称为昂贵对象。
使这一趋势造成的潜在内存问题更加复杂的是,所有 PHP 对象的赋值都是通过引用自动完成的。如果没有引用,使用第三方框架就会变得非常麻烦。但是,当通过引用赋值对象时,该对象必须完整地保留在内存中,直到所有引用被销毁。只有在取消设置或覆盖对象后,它才会被完全销毁。
在 PHP 7.4 中,以弱引用支持的形式引入了这一问题的潜在解决方案。PHP 8 通过添加弱映射类扩展了这一新功能。在本节中,您将了解这一新技术的工作原理,以及它如何证明对开发有利。首先来看看弱引用。
利用弱引用
弱引用在 PHP 7.4 中首次引入,并在 PHP 8 中得到改进。该类作为对象创建的包装器,允许开发人员以这样一种方式使用对象引用,即范围外(例如,unset()
)对象不受垃圾回收的保护。
目前,pecl.php.net 上有许多 PHP 扩展程序都支持弱引用。大多数实现都入侵了 PHP 语言核心的 C 语言结构,要么重载对象处理程序,要么操作堆栈和各种 C 指针。在大多数情况下,这样做的最终结果是丧失了可移植性并产生大量分段错误。PHP 8 的实现避免了这些问题。
如果您的程序代码涉及大型对象,而且程序代码可能会运行很长时间,那么掌握 PHP 8 弱引用的使用就非常重要。在了解使用细节之前,让我们先看看类的定义。
检查 WeakReference 类定义
WeakReference
类的正式定义如下:
WeakReference {
public __construct() : void
public static create (object $object) : WeakReference
public get() : object|null
}
如您所见,类的定义非常简单。该类可用于对任何对象进行封装。有了这个包装器,就可以更容易地完全销毁一个对象,而不必担心可能会有一个残留的引用导致该对象仍然存在于内存中。
有关弱引用的背景和性质的更多信息,请访问: https://wiki.php.net/rfc/weakrefs 。文档参考在这里: https://www.php.net/manual/en/class.weakreference.php 。 |
现在让我们来看一个简单的例子,以帮助您理解。
使用弱引用
本例演示了如何使用弱引用。在这个示例中,你将看到当进行普通的对象引用赋值时,即使原始对象被取消设置,它仍然会加载到内存中。另一方面,如果使用弱引用进行对象引用赋值,一旦原始对象被取消设置,它就会从内存中完全删除:
-
首先,我们定义四个对象。请注意,
$obj2
是对$obj1
的正常引用,而$obj4
是对$obj3
的弱引用:// /repo/ch010/php8_weak_reference.php $obj1 = new class () { public $name = 'Fred'; }; $obj2 = $obj1; // normal reference $obj3 = new class () { public $name = 'Fred'; }; $obj4 = WeakReference::create($obj3); // weak ref
-
然后,我们将显示
$obj1
取消设置前后$obj2
的内容。由于$obj1
和$obj2
之间的连接是普通的 PHP 引用,因此由于创建了强引用,$obj1
仍保留在内存中:var_dump($obj2); unset($obj1); var_dump($obj2); // $obj1 still loaded in memory
-
然后,我们对
$obj3
和$obj4
执行同样的操作。请注意,我们需要使用WeakReference::get()
来获取相关对象。一旦取消设置$obj3
,与$obj3
和$obj4
相关的所有信息都将从内存中删除:var_dump($obj4->get()); unset($obj3); var_dump($obj4->get()); // both $obj3 and $obj4 are gone
以下是在 PHP 8 中运行的代码示例的输出:
root@php8_tips_php8 [ /repo/ch10 ]#
php php8_weak_reference.php
object(class@anonymous)#1 (1) {
["name"]=> string(4) "Fred"
}
object(class@anonymous)#1 (1) {
["name"]=> string(4) "Fred"
}
object(class@anonymous)#2 (1) {
["name"]=> string(4) "Fred"
}
NULL
输出结果告诉我们一个有趣的故事!第二次 var_dump()
操作显示,尽管 $obj1
已被取消设置,但由于 $obj2
创建了强引用,它仍像僵尸一样继续存在。如果你正在处理昂贵的对象和复杂的应用程序代码,为了释放内存,你需要在释放内存之前首先找到并销毁所有引用!
另一方面,如果真的需要内存,与其直接进行对象赋值(在 PHP 中是自动通过引用进行的),不如使用 WeakReference::create()
方法创建引用。弱引用具有普通引用的所有功能。唯一的区别是,如果它引用的对象被销毁或退出作用域,弱引用也会自动销毁。
从输出中可以看到,最后一次 var_dump()
操作的结果是 NULL
。这说明对象确实已被销毁。当主对象被取消设置时,它的所有弱引用都会自动消失。现在你已经了解了如何使用弱引用,以及弱引用可能解决的问题,是时候来看看一个新类 WeakMap
了。
使用 WeakMap
PHP 8 中添加了一个新类 WeakMap
,该类可利用弱引用支持。这个新类的功能类似于 SplObjectStorage
。下面是该类的官方定义:
final WeakMap implements Countable, ArrayAccess, IteratorAggregate {
public __construct ( )
public count ( ) : int
abstract public getIterator ( ) : Traversable
public offsetExists ( object $object ) : bool
public offsetGet ( object $object ) : mixed
public offsetSet ( object $object , mixed $value ) : void
public offsetUnset ( object $object ) : void
}
就像 SplObjectStorage
一样,这个新类也是一个对象数组。由于它实现了 IteratorAggregate
,因此可以使用 getIterator()
方法访问内部迭代器。因此,新类不仅提供了传统的数组访问,还提供了 OOP 迭代器访问,可谓两全其美!在详细介绍如何使用 WeakMap
之前,了解 SplObjectStorage
的典型用法非常重要。
使用 SplObjectStorage 实现容器类
SplObjectStorage
类的一个潜在用途是将其作为依赖注入(DI)容器(也称为 服务定位器 或 反转控制容器)的基础。依赖注入容器类旨在创建和保存对象实例,以便于检索。
在这个示例中,我们用从 Laminas\Filter\*
类中提取的昂贵对象数组加载了一个容器类。然后,我们使用容器对样本数据进行消毒,之后取消设置过滤器数组:
-
首先,我们定义了一个基于
SplObjectStorage
的容器类。(稍后,在下一节中,我们将开发另一个基于WeakMap
的容器类,做同样的事情)。下面是UsesSplObjectStorage
类。在__construct()
方法中,我们将配置好的过滤器附加到SplObjectStorage
实例上:// /repo/src/Php7/Container/UsesSplObjectStorage.php namespace Php7\Container; use SplObjectStorage; class UsesSplObjectStorage { public $container; public $default; public function __construct(array $config = []) { $this->container = new SplObjectStorage(); if ($config) foreach ($config as $obj) $this->container->attach( $obj, get_class($obj)); $this->default = new class () { public function filter($value) { return $value; }}; }
-
然后,我们定义了一个
get()
方法,该方法会遍历SplObjectStorage
容器,并在找到时返回过滤器。如果没有找到,则返回一个默认类,该类只需直接传递数据即可:public function get(string $key) { foreach ($this->container as $idx => $obj) if ($obj instanceof $key) return $obj; return $this->default; } }
请注意,在使用
foreach()
循环遍历SplObjectStorage
实例时,我们返回的是值 ($obj
),而不是键。另一方面,如果我们使用的是WeakMap
实例,则需要返回键而不是值!
然后,我们定义一个调用程序,使用新创建的 UsesSplObjectStorage
类来包含过滤器集:
-
首先,我们定义自动加载并使用相应的类:
// /repo/ch010/php7_weak_map_problem.php require __DIR__ . '/../src/Server/Autoload/Loader.php'; loader = new \Server\Autoload\Loader(); use Laminas\Filter\ {StringTrim, StripNewlines, StripTags, ToInt, Whitelist, UriNormalize}; use Php7\Container\UsesSplObjectStorage;
-
接下来,我们定义一个样本数据数组:
$data = [ 'name' => '<script>bad JavaScript</script>name', 'status' => 'should only contain digits 9999', 'gender' => 'FMZ only allowed M, F or X', 'space' => " leading/trailing whitespace or\n", 'url' => 'unlikelysource.com/about', ];
-
然后,我们为所有字段分配必要的筛选器(
$required
),并为某些字段分配特定的筛选器($added
):$required = [StringTrim::class, StripNewlines::class, StripTags::class]; $added = ['status' => ToInt::class, 'gender' => Whitelist::class, 'url' => UriNormalize::class ];
-
然后,我们创建一个过滤器实例数组,用于填充我们的服务容器
UseSplObjectStorage
。请注意,每个过滤器类都会产生大量开销,可以被视为一个昂贵的对象:$filters = [ new StringTrim(), new StripNewlines(), new StripTags(), new ToInt(), new Whitelist(['list' => ['M','F','X']]), new UriNormalize(['enforcedScheme' => 'https']), ]; $container = new UsesSplObjectStorage($filters);
-
现在,我们使用容器类在数据文件中循环检索过滤器实例。
filter()
方法会产生一个特定于该过滤器的净化值:foreach ($data as $key => &$value) { foreach ($required as $class) { $value = $container->get($class)->filter($value); } if (isset($added[$key])) { $value = $container->get($added[$key]) ->filter($value); } } var_dump($data);
-
最后,我们抓取了内存统计数据,作为比较
SplObjectStorage
和WeakMap
使用情况的基础。我们还取消了$filters
,理论上这将释放大量内存。我们运行gc_collect_cycles()
强制执行 PHP 垃圾回收过程,将释放的内存重新释放到内存池中:$mem = memory_get_usage(); unset($filters); gc_collect_cycles(); $end = memory_get_usage(); echo "\nMemory Before Unset: $mem\n"; echo "Memory After Unset: $end\n"; echo 'Difference : ' . ($end - $mem) . "\n"; echo 'Peak Memory Usage : ' . memory_get_peak_usage();
以下是刚刚显示的调用程序在 PHP 8 中运行的结果:
root@php8_tips_php8 [ /repo/ch10 ]#
php php7_weak_map_problem.php
array(5) {
["name"]=> string(18) "bad JavaScriptname"
["status"]=> int(0)
["gender"]=> NULL
["space"]=> string(30) "leading/trailing whitespace or"
["url"]=> &string(32) "https://unlikelysource.com/about"
}
Memory Before Unset: 518936
Memory After Unset: 518672
Difference : 264
Peak Memory Usage : 780168
从输出结果可以看出,我们的容器类运行完美,可以访问任何存储的过滤器类。值得注意的是,在执行 unset($filters)
命令后释放的内存只有 264 字节:并不多!
现在你已经了解了 SplObjectStorage
类的典型用法。现在让我们看看 SplObjectStorage
类的一个潜在问题,以及 WeakMap
如何解决这个问题。
了解 WeakMap 相对于 SplObjectStorage 的优势
SplObjectStorage
的主要问题是,当被分配的对象被取消设置或退出作用域时,它仍然保留在内存中。原因是当对象附加到 SplObjectStorage
实例时,它是通过引用完成的。
如果只处理少量对象,可能不会出现严重问题。如果使用 SplObjectStorage
并分配大量昂贵的对象进行存储,最终可能会导致长期运行的程序出现内存泄漏。另一方面,如果使用 WeakMap
实例进行存储,垃圾回收就可以移除对象,从而释放内存。当你开始将 WeakMap
实例融入常规编程实践时,你最终会得到更高效的代码,占用更少的内存。
有关 WeakMap 的更多信息,请点击此处查看原始 RFC: https://wiki.php.net/rfc/weak_maps 。还可以查看文档: https://www.php.net/weakMap 。 |
现在让我们重写上一节的示例 ( /repo/ch010/php7_weak_map_problem.php ),但这次要使用 WeakMap
:
-
如前面的代码示例所述,我们定义了一个名为
UsesWeakMap
的容器类,用于存放昂贵的过滤器类。该类与上一节所示类的主要区别在于UsesWeakMap
使用WeakMap
而不是SplObjectStorage
进行存储。下面是类的设置和__construct()
方法:// /repo/src/Php7/Container/UsesWeakMap.php namespace Php8\Container; use WeakMap; class UsesWeakMap { public $container; public $default; public function __construct(array $config = []) { $this->container = new WeakMap(); if ($config) foreach ($config as $obj) $this->container->offsetSet( $obj, get_class($obj)); $this->default = new class () { public function filter($value) { return $value; }}; }
-
这两个类的另一个区别是
WeakMap
实现了IteratorAggregate
。不过,这仍然允许我们在get()
方法中使用简单的foreach()
循环:public function get(string $key) { foreach ($this->container as $idx => $obj) if ($idx instanceof $key) return $idx; return $this->default; } }
请注意,在使用
foreach()
循环遍历WeakMap
实例时,我们返回的是key($idx)
,而不是值! -
然后,我们定义一个调用程序,调用自动加载器并使用相应的过滤器类。这个调用程序与上一节程序的最大区别在于,我们使用了基于
WeakMap
的新容器类:// /repo/ch010/php8_weak_map_problem.php require __DIR__ . '/../src/Server/Autoload/Loader.php'; $loader = new \Server\Autoload\Loader(); use Laminas\Filter\ {StringTrim, StripNewlines, StripTags, ToInt, Whitelist, UriNormalize}; use Php8\Container\UsesWeakMap;
-
与上一个示例一样,我们定义了一个样本数据数组并分配了过滤器。此代码与上一示例的第 2 和第 3 步相同,故未显示。
-
然后,我们在数组中创建过滤器实例,作为新容器类的参数。我们使用过滤器数组作为参数来创建容器类实例:
$filters = [ new StringTrim(), new StripNewlines(), new StripTags(), new ToInt(), new Whitelist(['list' => ['M','F','X']]), new UriNormalize(['enforcedScheme' => 'https']), ]; $container = new UsesWeakMap($filters);
-
最后,与上一个示例中的步骤 6 完全相同,我们循环浏览数据并应用容器类中的筛选器。我们还收集并显示内存统计数据。
以下是在 PHP 8 中运行的使用 WeakMap
的修订程序的输出结果:
root@php8_tips_php8 [ /repo/ch10 ]#
php php8_weak_map_problem.php
array(5) {
["name"]=> string(18) "bad JavaScriptname"
["status"]=> int(0)
["gender"]=> NULL
["space"]=> string(30) "leading/trailing whitespace or"
["url"]=> &string(32) "https://unlikelysource.com/about"
}
Memory Before Unset: 518712
Memory After Unset: 517912
Difference : 800
Peak Memory Usage : 779944
如你所料,总体内存使用量略有降低。不过,最大的区别在于取消设置 $filters
后的内存差异。在上一个示例中,差异为 264 字节。而在本例中,使用 WeakMap
产生的差异为 800 字节。这意味着,与使用 SplObjectStorage
相比,使用 WeakMap
有可能释放三倍以上的内存!
关于弱引用和弱映射的讨论到此结束。现在你可以编写更高效、使用更少内存的代码了。存储的对象越大,节省内存的潜力就越大。
总结
在本章中,你不仅学习了新的 JIT 编译器的工作原理,还了解了传统的 PHP 解释-编译-执行循环。使用 PHP 8 并启用 JIT 编译器有可能将 PHP 应用程序的速度提高三倍以上。
在下一节中,您将了解什么是稳定排序,以及 PHP 8 是如何实现这一重要技术的。掌握了稳定排序,你的代码就能以合理的方式生成数据,从而提高客户满意度。
接下来的章节将向你介绍一种利用 SplFixedArray
类大大提高性能和减少内存消耗的技术。之后,您将了解 PHP 8 对弱引用的支持以及新的 WeakMap
类。使用本章所涉及的技术将使你的应用程序执行得更快、运行得更高效、内存消耗得更少。
在下一章中,您将学习如何成功迁移到 PHP 8。