C.2. 类装载 做过Zend Framework 应用性能调优的人都知道,Zend Framework 中类装载的开销是相当大的。从各组件对应的大量类文件,到类名与文件系统非唯一对应的插件的引入,大量include_once和 require_once调用可能导致严重的性能问题。这章将提供一些具体的策略来解决这些问题。 C.2.1. 如何优化include_path? 提高类装载速度的一个优化策略是合理安排include_path。具体而言,你应该做四件事情:使用绝对路径(或绝对路径的相对路径[原文:paths relative to absolute paths]);减少包含路径数量;ZF库的路径要尽量靠前;只在include_path最后包含当前路径。 C.2.1.1. 使用绝对路径 尽管这看起来是一个微不足道的优化,然而如果不这么做,PHP的realpath缓存可能就无法发挥作用,结果导致opcode缓存的效果与你的期望大相径庭。 做到这一点有两个简单的途径。第一种是在php.ini、httpd.conf或.htaccess 中硬编码那些包含路径。第二种是在设置include_path时调用PHP的realpath()函数: $paths = array( realpath(dirname(__FILE__) . '/../library'), '.', ); set_include_path(implode(PATH_SEPARATOR, $paths); 你也可以使用相对路径——如果它是一个绝对路径的相对路径:
define('APPLICATION_PATH', realpath(dirname(__FILE__))); $paths = array( APPLICATION_PATH . '/../library'), '.', ); set_include_path(implode(PATH_SEPARATOR, $paths); 尽管如此,对一个相对路径调用realpath()也很方便。 C.2.1.2. 减少包含路径的数量 包含路径将按照它们的出现顺序逐个扫描。显而易见,如果文件在第一个路径下的查找速度要远远快于它在最后一个路径。因此,一个明显的改善方法是减少 include_path中的路径数目,只包含必需的路径。仔细检查自己在include_path中定义的每一个路径,确保应用用到了该路径下的代码,如果没有,则删除。 另外一个优化方法是合并路径。例如,Zend Framework 遵循PEAR 的命名规范,因此,如果应用同时也使用了PEAR 库(或其它遵循PEAR 命名规范的框架或组件库),则可以把它们放在相同的包含路径下。通常,这只需要在一个公共目录下为各个库建立一个符号链接。 C.2.1.3. 尽早定义Zend Framework库的路径 除了上面的建议,另外一个优化方法是尽量把Zend Framework库的路径放在include_path的前面。在大部分情况下,它应该是include_path中的第一个路径。这样可以保证Zend Framework自身的文件可以在第一次扫描中命中。 C.2.1.4. 把当前路径放在最后或去掉 大部分include_path的例子都包含当前路径,或‘.’。这可以方便脚本包含当前目录下的文件。并且在这些例子中,当前路径通常是在 include_path的开头——即首先尝试当前路径。对于Zend Framework应用,这通常不是开发者所期望的,当前路径放在包含路径的最后或许更适合一些。 Example C.1. 示例: 优化include_path 让我们把上述建议综合在一起。假设我们同时使用Zend Framework 和一些PEAR库——如PHPUnit和Archive_Tar库,并且偶尔也需要从当前目录包含文件。 首先,我们在项目中建立一个库目录。在这个目录中,为Zend Framework库和其它PEAR库建立符号链接:
library Archive/ PEAR/ PHPUnit/ Zend/ 如果需要的话,还可以在此建立项目自身的库目录,这对那些共享库毫无影响。 接下来,我们将在public/index.php 文件中创建包含路径。它会在所有请求中执行生效,不需要在别的地方再次设置。 我们将采用上面的建议:使用通过realpath()获取的绝对路径;把Zend Framework库路径放在前边;合并包含路径;把当前路径放在最后。事实上,示例做得非常漂亮——仅用了两个路径。 $paths = array( realpath(dirname(__FILE__) . '/../library'), '.' ); set_include_path(implode(PATH_SEPARATOR, $paths));
C.2.2. 如何消除非必要的require_once语句 延迟载入是一项仅在需要时载入类的优化技术。——比如实例化一个对象、调用静态类的方法或者引用类的常量或静态属性。PHP通过自动载入机制实现延迟载入,开发者需要定义回调函数,将类名映射到相应的类文件。 然而,如果你在库中仍然使用require_once,那么自动载入的效果就要大打折扣——在Zend Framework中更是如此。因此,现在的问题是:如何消除这些require_once调用,从而发挥自动载入机制的最大性能? C.2.2.1. 使用find和sed去除require_once调用 去除require_once 的一个简便方法是联合使用unix工具find和sed,把每个require_once 语句转换为注释。可以使用下面的命令(%是shell提示符): % cd path/to/ZendFramework/library % find . -name '*.php' -print0 | xargs -0 sed --regexp-extended --in-place 's/(require_once)/// 1/g' 这一行命令(因为阅读方便分为两行)遍历每一个PHP文件,将'require_once' 替换为'// require_once',高效地将每个require_once调用注释掉。 这行命令可以方便地加入到一个自动部署或发布工具中,帮助提升产品的性能。不过需要注意的是,如果你使用了该技术,你就必须使用自动载入功能,这可以在项目的"public/index.php"文件中通过下面代码实现: require_once 'Zend/Loader.php'; // one require_once is still necessary Zend_Loader::registerAutoload(); C.2.3. 如何加快插件的载入 许多组件都支持插件,开发者可以自己开发插件和对应组件协同工作,也可以覆写存在于Zend Framework包中的标准插件。插件给框架增加了灵活性,但是为此付出的代价是:插件的载入需要耗费大量资源。 插件载入器允许开发者注册类前缀/路径对,从而为插件指定非标准的类文件搜索路径。每个前缀允许关联多个路径。在内部,插件载入器首先遍历所有类前缀获取该类前缀关联的路径,然后再遍历这些路径查找文件,然后读入文件,测试查找的类是否已经找到。可以想象,这将产生很多对文件系统的stat调用。 把这个过程乘以使用PluginLoader的组件数,你就可以知道该问题的影响范围了。截止到本文写作时,下面列举的组件使用了PluginLoader: •Zend_Controller_Action_HelperBroker: 助手•Zend_Dojo: 视图助手,form元素和装饰器•Zend_File_Transfer: 适配器•Zend_Filter_Inflector: 过滤器(ViewRenderer动作助手和Zend_Layout使用)•Zend_Filter_Input: 过滤器和验证器(validators)•Zend_Form: 元素,验证器,过滤器,装饰器,验证码(captcha),文件转换适配器•Zend_Paginator: 适配器•Zend_View: 助手,过滤器
如何减少这些调用的次数呢? C.2.3.1. 使用插件载入器的包含文件缓存 Zend Framework 1.7.0为PluginLoader增加了一个包含文件缓存。该功能将记录插件的类文件路径,并构造"include_once"语句写入一个文件,从而可以在启动文件中包含该文件。尽管这么做将使你的代码增加很多include_once调用,但是它可以让插件载入器运行的更快。 PluginLoader的文档:includes a complete example of its use.
C.3. 国际化(i18n)和本地化(l10n) 国际化和本地化站点是扩展用户的好方法,它保证访问者可以获得所需的信息。然而,它也经常会带来一些性能问题。下面这些策略可以用于降低国际化和本地化带来的负担。 C.3.1. 该使用哪个翻译适配器 各种翻译适配器千差万别,有的以功能丰富见长,有的以性能卓越著称。除了因业务限制而只能选择特定适配器之外,该如何选择一个高效率的适配器呢? C.3.1.1. 使用非-XML翻译适配器获取最快的速度 Zend Framework中包含了许多翻译适配器。然而它们多半使用XML格式,这会导致内存和性能的开销。幸运的是,也有一些适配器是基于能够高效解析的其它格式。按照速度从快到慢的顺序,它们是: · Array:这是最快的,它将在包含时被直接解析为PHP的原生格式。 · CSV:使用fgetcsv()解析CSV文件,并转化为PHP的原生格式。 · INI:使用parse_ini_file()解析INI文件,并转化为PHP的原生格式。它的性能和CSV差不多。 · Gettext:Zend Framework中的gettext适配器没有使用gettext扩展,因为该扩展是非线程安全的,并且不能在一个服务器上指定多个locale。因此,它比直接使用gettext扩展要慢,不过gettext格式是二进制的,比解析XML要快。 如果高性能是你的一个关注点的话,建议使用上述的某个适配器。 C.3.2. 如何使翻译和本地化更快 也许因为商业因素,你只能使用基于XML的翻译适配器。或者想让程序更加高效,或者想让本地化操作更快。有什么办法呢? C.3.2.1. 使用翻译和本地化缓存 Zend_Translate和Zend_Locale都实现了缓存功能,它可以明显地提升性能。通常情况下,性能瓶颈在于读入文件,而不是实际的查找操作,使用缓存可以避免翻译和本地化文件的重复读取。 你可以从下面链接中进一步了解翻译和本地化缓存的内容: · Zend_Translate adapter caching · Zend_Locale caching C.4. 视图渲染 如果使用Zend Framework的MVC框架,你可能会使用Zend_View组件。相对于其它视图或模板引擎,Zend_View是相当高效的。因为直接用PHP编写视图脚本,就没有编译自定义标记的负担,也不需要担心编译后脚本的优化。当然, Zend_View也存在自己的问题:扩展的代价很大(视图助手),如果通过许多视图助手来完成关键功能,将会导致很大的性能开销。 C.4.1. 如何加快视图助手的解析 Zend_View中的大部分方法实际上都是通过助手系统的重载来实现的。它赋予Zend_View极大的灵活性,不需要扩展Zend_View,提供应用所需的助手方法就可以,开发者在单独的类中定义助手方法,然后就可以像调用定义在Zend_View中的方法那样使用助手方法。这样既可以保持视图对象的简单性,也可以保证助手只在需要时被创建。 在内部,Zend_View通过插件载入器查找助手类文件。这意味着每一次调用助手,Zend_View需要把助手名字传给插件载入器,由它决定类名,如果未载入则载入助手,然后返回实例化的对象。因为Zend_View在内部缓存了已经加载的助手,所以下次再使用这个助手时,速度将会快很多,然而,如果应用中使用了很多助手,这个过程对系统性能的影响可能会很大。 那接下来的问题是:如何加速助手的解析? C.4.1.1. 使用PluginLoader的包含文件缓存功能 这是最简单经济的方法,具体见C.2.3.1中所述。曾经有实验表明,这个技术在没有opcode缓存情形下可以提高25-30%的性能,如果有opcode缓存,则可以提高到40-65%。 C.4.1.2. 扩展Zend_View来提供常用助手方法 另外一个提高性能的方法是扩展Zend_View,在子类中手动增加应用常用的助手方法。这些助手方法可以简单地作为一个代理,实例化一个相应助手类完成任务,当然也可以自己实现任务的所有操作。 class My_View extends Zend_View{ /** * @var array Registry of helper classes used */ protected $_localHelperObjects = array(); /** * Proxy to url view helper * * @param array $urlOptions Options passed to the assemble method of the Route object. * @param mixed $name The name of a Route to use. If null it will use the current Route * @param bool $reset Whether or not to reset the route defaults with those provided * @return string Url for the link href attribute. */ public function url(array $urlOptions = array(), $name = null, $reset = false, $encode = true ) { if (!array_key_exists('url', $this->_localHelperObjects)) { $this->_localHelperObjects['url'] = new Zend_View_Helper_Url(); $this->_localHelperObjects['url']->setView($view); } $helper = $this->_localHelperObjects['url']; return $helper->url($urlOptions, $name, $reset, $encode); } /** * Echo a message * * Direct implementation. * * @param string $string * @return string */ public function message($string) { return "" . $this->escape($message) . "n"; }} 无论哪种方式,该方法将大量降低助手系统的性能消耗,它完全避免了插件载入器的调用,同时还充分利用了自动加载机制和按需加载的特性。 C.4.2. 如何提高区域视图(view partials)性能 如果开发者在应用中大量使用区域(partials),那么他会经常发现partial() 视图助手会因为克隆视图对象而产生性能瓶颈。有办法提高它的速度吗? C.4.2.1. 仅在真正需要时使用partial() partial()视图助手接收三个参数: · $name: 视图脚本的名字 · $module: 视图脚本所在的模块名,或者当没有第三个参数且是一个对象或数组时,它将作为$model参数。 · $model: 数组或对象,作为干净的数据赋值给视图,用于解析区域视图。 partial()的强大之处在于第二与第三个参数。$module参数允许临时加入指定模块的视图路径,从而解析该模块下的区域视图脚本;$model参数允许你显式地为区域视图指定变量。如果你根本没有用到这两个参数,那么请用render()代替! 基本上,除非你需要为区域视图传入变量并需要一个干净的变量环境,或者渲染另外一个MVC模块的视图,你没有必要使用partial()。相反,你应该使用Zend_View内置的render()方法渲染视图。 C.4.3. 如何提高action()视图助手性能 1.5.0版本引入了action()视图助手,允许开发者分发一个MVC动作并捕获它的输出。它向DRY原则迈近了一大步,促进了代码的复用。但是这也是一个代价高昂的操作。在action()视图助手内部,它会克隆请求和响应对象,调用分发器,然后调用对应的控制器和动作等等。 如何提高它的速度呢? C.4.3.1. 尽量使用ActionStack代替 与action()视图助手同时加入ZF的动作堆栈,由一个动作助手和前端控制器插件组成。它允许你将分发周期中需要调用的附加动作压入一个栈中。如果你在布局视图脚本中使用了action(),它可以用动作堆栈替换,完成视图的渲染,实现响应片段的分离。作为例子,你可以像下面那样实现一个 dispatchLoopStartup() 插件,为每个页面添加一个登录表单输入框: class LoginPlugin extends Zend_Controller_Plugin_Abstract{ protected $_stack; public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request ) { $stack = $this->getStack(); $loginRequest = new Zend_Controller_Request_Simple(); $loginRequest->setControllerName('user') ->setActionName('index') ->setParam('responseSegment', 'login'); $stack->pushStack($loginRequest); } public function getStack() { if (null === $this->_stack) { $front = Zend_Controller_Front::getInstance(); if (!$front->hasPlugin('Zend_Controller_Plugin_ActionStack')) { $stack = new Zend_Controller_Plugin_ActionStack(); $front->registerPlugin($stack); } else { $stack = $front->getPlugin('ActionStack') } $this->_stack = $stack; } return $this->_stack; }} 然后UserController::indexAction()方法就可以通过responseSegment参数指定渲染哪个响应片段。在布局脚本中,你可以简单地输出该响应片段: layout()->login ?> 尽管ActionStack需要一个分发周期,但是它比action()视图助手效率要高,因为它不需要克隆对象及重置它们的状态。此外,它可以确保所有的pre/post分发插件会被调用,如果你是通过前端控制器插件实现ACL,那么这一点就显得特别重要。 C.4.3.2. 查询模型时使用视图助手代替action() 大部分情况下,使用action()是小题大做。如果大部分的业务逻辑都封装在模型中,而你只是简单地查询模型,并将结果传递给视图脚本的话,通过一个视图助手获取模型,查询数据,完成相应的操作,将是一个更高效简洁的方法。 作为一个例子,考虑下面的控制器动作和视图脚本: class BugController extends Zend_Controller_Action{ public function listAction() { $model = new Bug(); $this->view->bugs = $model->fetchActive(); }} // bug/list.phtml:echo "n";foreach ($this->bugs as $bug) { printf("•%s: %sn", $this->escape($bug->id), $this->escape($bug->summary));•}echo "n"; 使用action(),将像下面这样调用: action('list', 'bug') ?> 这可以重构为通过视图助手实现,如下所示: class My_View_Helper_BugList extends Zend_View_Helper_Abstract{ public function direct() { $model = new Bug(); $html = "n"; foreach ($model->fetchActive() as $bug) { $html .= sprintf( "•%s: %sn", • $this->view->escape($bug->id), $this->view->escape($bug->summary) ); } $html .= "n"; return $html; }} 然后像下面这样调用助手: bugList() ?> 这么做有两个好处:消除了action()助手的开销,并且该API更易于理解。