這節(jié)看下 CI 提供的緩存功能,緩存也是以驅(qū)動的方式運行的,由如下幾部分組成
- Cache_apc.php:提供對 php 字節(jié)碼 opcode 和用戶數(shù)據(jù)的緩存。
- Cache_dummy.php:實現(xiàn)了驅(qū)動接口的一個空類,僅僅是為了防止某個驅(qū)動不存在時代碼報錯。
- Cache_file.php:文件緩存。
- Cache_memcached.php:memcache 緩存
- Cahce_redis.php:redis 緩存,不過只是封裝一些基本功能。
- Cache_wincache.php :提供 Windows 上的緩存功能。

關(guān)于 APC
apc 全稱 alternative php cache(可選 php 緩存),其提供對 opcode 和 用戶數(shù)據(jù) 的緩存!
什么是 opcode?
opcode 是 php 腳本編程生成的機器碼,一個 php 腳本的解析過程大致如下

正如你看到的,如果我們能將 Parse 和 Compile 這兩步能省掉的話,直接得到機器碼對于提升 php 腳本的執(zhí)行性能是非常有幫助的,于是在早期 php 版本中出現(xiàn)了提供 Opcode 緩存的擴展 apc!

但是由于該擴展在 php 5.5.* 有嚴重的內(nèi)存問題,就被廢棄了,然后官方出了一個新的 Opcode 緩存 Zend Opcache,在 php 5.5 以后的版本中自帶了該擴展!
apc 除了提供 Opcode 緩存,還提供用戶數(shù)據(jù)緩存,既然 apc 被廢了,于是后面就出了一個替代 apc 的擴展 acpu, 該擴展只被用作緩存用戶數(shù)據(jù),其接口完全兼容 apc!
那什么是用戶數(shù)據(jù)緩存呢?
用戶數(shù)據(jù)緩存就是編寫 PHP 代碼時用 apc_store 和 apc_fetch 函數(shù)操作讀取、寫入的數(shù)據(jù);
apcu 提供的緩存可以看做是一個輕量級 key/value 存儲系統(tǒng),類似 memcache,如果你緩存的用戶數(shù)據(jù)量大的話,還是建議你使用 memcache,redis 等專業(yè)緩存系統(tǒng)!
好了,說完基本概念,現(xiàn)在讓我們寫下這樣一段緩存代碼
$this->load->driver('cache', array('adapter' => 'apc', 'backup' => 'file'));
if ( ! $foo = $this->cache->get('foo'))
{
echo 'Saving to the cache!<br />';
$foo = 'foobarbaz!';
$this->cache->save('foo', $foo, 300);
}
echo $foo;
那它載入驅(qū)動后的運行流行是怎么樣的呢?我們的源碼分析就以此展開!
驅(qū)動抽象類加載
之前分析加載器源碼時我們知道 CI 任何資源的載入都是通過加載器實現(xiàn)的,而緩存驅(qū)動是通過加載器中的 driver() 函數(shù)載入的。
注意:driver() 方法載入的是驅(qū)動抽象類!
driver()
public function driver($library, $params = NULL, $object_name = NULL)
{
//如果加載了多個驅(qū)動,遞歸處理
if (is_array($library))
{
foreach ($library as $driver)
{
$this->driver($driver);
}
return $this;
}
elseif (empty($library))
{
return FALSE;
}
// 該文件中實有兩個類,分別為驅(qū)動抽象層父類和驅(qū)動器的基類,我們后面會分析這個文件的源碼
if ( ! class_exists('CI_Driver_Library', FALSE))
{
// We aren't instantiating an object here, just making the base class available
require BASEPATH.'libraries/Driver.php';
}
// 這里可以看到,CI 默認驅(qū)動的的架構(gòu)是 $driver/$driver,也就是說如果我們傳入的驅(qū)動是 cache,
// 那么解析到的驅(qū)動是 Cache/cache,而之前在加載器源碼分析中我們知道在方法 library()
// 內(nèi)部會從指定的加載目錄($path)中去查找這個驅(qū)動,最后會實例化該驅(qū)動;
if ( ! strpos($library, '/'))
{
$library = ucfirst($library).'/'.$library;
}
return $this->library($library, $params, $object_name);
}
通過上面的源碼看到最終的驅(qū)動是通過加載器的 library() 方法載入的,并在該方法內(nèi)部會實例化 Cache 對象, 這樣整個驅(qū)動抽象層的載入就完成了!
那么驅(qū)動抽象類實例化時到底做了呢?
驅(qū)動抽象類的繼承
驅(qū)動抽象類位于 /libraries/Cache/Cache.php 中,進入該文件首先看到其繼承了一個類 CI_Driver_Library!

那這個 CI_Driver_Library 類是從哪里來的呢?先留個疑問!
接著往下看,我們觀察 CI_Cache 的構(gòu)造方法。
驅(qū)動抽象類的構(gòu)造函數(shù)
// $config 參數(shù)就是 $this->load->driver('cache', array('adapter' => 'apc', 'backup' => 'file')) 時的傳入的第二個的參數(shù),
// adapter 是驅(qū)動器(file,redis,memcache等),backup 是備用的驅(qū)動器,不清楚的話看文檔:
// https://codeigniter.org.cn/user_guide/libraries/caching.html#id2
public function __construct($config = array())
{
$default_config = array(
'adapter',
'memcached'
);
// 將 $config 中的配置項掛到當前類的屬性上,由于當前類的屬性有個前綴,所以給加了個 _
foreach ($default_config as $key)
{
if (isset($config[$key]))
{
$param = '_'.$key;
$this->{$param} = $config[$key];
}
}
//檢測是否給緩存配置了前綴,主要是為了防止多個應用的命名沖突
isset($config['key_prefix']) && $this->key_prefix = $config['key_prefix'];
if (isset($config['backup']) && in_array($config['backup'], $this->valid_drivers))
{
$this->_backup_driver = $config['backup'];
}
// 使用 is_supported() 判斷驅(qū)動器是否可用,不可用的話就降級到備用驅(qū)動器,如果還不可以用的話,就再次降級到 dummy,
// dummy 這玩意就是空殼子,只是簡單實現(xiàn)抽象層的接口并返回true,起到了容錯的作用而已 !
if ( ! $this->is_supported($this->_adapter))
{
if ( ! $this->is_supported($this->_backup_driver))
{
// Backup isn't supported either. Default to 'Dummy' driver.
log_message('error', 'Cache adapter "'.$this->_adapter.'" and backup "'.$this->_backup_driver.'" are both unavailable. Cache is now using "Dummy" adapter.');
$this->_adapter = 'dummy';
}
else
{
// Backup is supported. Set it to primary.
log_message('debug', 'Cache adapter "'.$this->_adapter.'" is unavailable. Falling back to "'.$this->_backup_driver.'" backup adapter.');
$this->_adapter = $this->_backup_driver;
}
}
}
構(gòu)造方法中我們看到只是處理了傳入的參數(shù)解析到當前的屬性上面;然后判斷驅(qū)動器是否可用。
但是,有個問題讓人挺納悶的:
我們至今沒有看到驅(qū)動器的實例化,之前在 driver() 看到載入的驅(qū)動只是將 CI_Cache 這個驅(qū)動抽象類給實例化了,那么驅(qū)動器到底是在哪被被載入并實例化的?
在驅(qū)動器都還沒有實例化的情況下,竟然去根據(jù) is_supported() 判斷是否支持驅(qū)動器,這豈不是明顯的邏輯bug?
如果不是,那么會不會是在 is_supported() 中載入了驅(qū)動器并實例化了呢?
那只能進入 is_supported() 一探究竟了!
is_supported()
public function is_supported($driver)
{
static $support = array();
if ( ! isset($support[$driver]))
{
$support[$driver] = $this->{$driver}->is_supported();
}
return $support[$driver];
}
可以看到 is_supported() 就是在驅(qū)動抽象層對各個驅(qū)動 is_supported() 方法的包裝而已,我們依然沒有看到 驅(qū)動器的實例化!
我就不賣關(guān)子了,其實驅(qū)動器就是在 is_supported() 方法中被實例化的,你肯定會有疑問: "并沒有看見實例化驅(qū)動器的關(guān)鍵字 new 啊"? 是的,你確實沒看見,不見得驅(qū)動器沒被創(chuàng)建!
接下來我要說的我見到了截止目前分析 CI 框架源碼最精妙的一段代碼(哎!深邃的面向?qū)ο蟀?;
先說結(jié)論:驅(qū)動器是通過魔術(shù)方法給載入的 (想對 CI 說 FUCK U)?。?!
那魔術(shù)方法又在哪?
之前在 driver() 函數(shù)中看到有一行如下的代碼,魔術(shù)方法就出在載入的這個 Driver.php 中!
if ( ! class_exists('CI_Driver_Library', FALSE))
{
// We aren't instantiating an object here, just making the base class available
require BASEPATH.'libraries/Driver.php';
}
驅(qū)動器的載入和實例化就是在 Driver.php 實現(xiàn)的!,正如我們剛進入驅(qū)動抽象類 CI_Cache 看到的 CI_Driver_Library 類,這個 CI_Driver_Library 就位于Driver.php 中!
Driver.php 中驅(qū)動器的載入
Driver.php 中有兩個類,CI_Driver_Library 和 CI_Driver:
- 前者被驅(qū)動抽象類 CI_Cache繼承用來載入驅(qū)動器;
- 后者用來將 CI_Cache 這個抽象層的公共屬性和方法分配給每個子驅(qū)動器;
魔術(shù)方法是如何被觸發(fā)的?
我們知道,如果對象找不到某個屬性是就會觸發(fā)魔術(shù)方法,該魔術(shù)方法是在 is_supported() 中被觸發(fā)的!

注意看圖中的紅框部分:
由于 $driver 是驅(qū)動器,明顯在這里驅(qū)動器還沒被實例化,所以該驅(qū)動器是找不到的,那既然找不到,而 CI_Cache 又繼承了 CI_Driver_Library ,那必然會觸發(fā)其內(nèi)部的魔術(shù)方法 __get()!
接下來我們看下 __get() 的內(nèi)容
__get()
public function __get($child)
{
// Try to load the driver
return $this->load_driver($child);
}
注釋說的很清楚了,$child 就是驅(qū)動器的名字,通過 load_driver() 載入的!
load_driver()
/*
* load_driver 就簡單多了,就是從指定的 $path 下尋找到驅(qū)動然后實例化就行了
*/
public function load_driver($child)
{
// 解析得到類名,反正 CI 自己的那套命名空間的的解析總是在 MY_,CI_ 這些命名空間倒來倒去
$prefix = config_item('subclass_prefix');
if ( ! isset($this->lib_name))
{
$this->lib_name = str_replace(array('CI_', $prefix), '', get_class($this));
}
//得到驅(qū)動器的名稱,注意沒前綴
$child_name = $this->lib_name.'_'.$child;
// valid_drivers 在 CI_Cache 中,是一個數(shù)組,定義了驅(qū)動器有哪些
if ( ! in_array($child, $this->valid_drivers))
{
$msg = 'Invalid driver requested: '.$child_name;
log_message('error', $msg);
show_error($msg);
}
//很熟悉的套路了,得到 path,然后會從這些 path 去加載這個驅(qū)動器
$CI = get_instance();
$paths = $CI->load->get_package_paths(TRUE);
// 加載擴展了的驅(qū)動器
$class_name = $prefix.$child_name;
$found = class_exists($class_name, FALSE);
if ( ! $found)
{
// Check for subclass file
foreach ($paths as $path)
{
// Does the file exist?
$file = $path.'libraries/'.$this->lib_name.'/drivers/'.$prefix.$child_name.'.php';
if (file_exists($file))
{
// Yes - require base class from BASEPATH
$basepath = BASEPATH.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
if ( ! file_exists($basepath))
{
$msg = 'Unable to load the requested class: CI_'.$child_name;
log_message('error', $msg);
show_error($msg);
}
// Include both sources and mark found
include_once($basepath);
include_once($file);
$found = TRUE;
break;
}
}
}
// 加載原生的驅(qū)動器
if ( ! $found)
{
// Use standard class name
$class_name = 'CI_'.$child_name;
if ( ! class_exists($class_name, FALSE))
{
// Check package paths
foreach ($paths as $path)
{
// Does the file exist?
$file = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
if (file_exists($file))
{
// Include source
include_once($file);
break;
}
}
}
}
// 如果驅(qū)動抽象和驅(qū)動器都不存在就報錯
if ( ! class_exists($class_name, FALSE))
{
if (class_exists($child_name, FALSE))
{
$class_name = $child_name;
}
else
{
$msg = 'Unable to load the requested driver: '.$class_name;
log_message('error', $msg);
show_error($msg);
}
}
// 看到?jīng)],驅(qū)動器終于被實例化了,實例化后將該驅(qū)動作為屬性掛到驅(qū)動抽象層,
// 然后就可以使用驅(qū)動器的 $this->{$driver}->is_supported() 去檢測該驅(qū)動是否可用了;
$obj = new $class_name();
$obj->decorate($this);
$this->$child = $obj;
return $this->$child;
}
這樣我們之前關(guān)于驅(qū)動器那幾個疑問就可以解答完畢了,可以看到驅(qū)動抽象層中調(diào)用未實例化的驅(qū)動器時觸發(fā)了魔術(shù)方法,進而實現(xiàn)了驅(qū)動器的載入,整個過程非常巧妙但又一開始讓人覺得非常復雜!
另外上面的 load_driver() 倒數(shù)第三行我們看到有個 decorate() 方法,這個方法做了一些畫龍點睛的事情,我們有必要看下!
decorate ()
decorate() 位于 Driver.php 的 CI_Driver 中, CI_Driver 類被各個驅(qū)動器給繼承了!
decorate() 看名字是裝飾的意思,它內(nèi)部其實就是對裝飾器模式的一種應用而已,而裝飾器模式的定義是:
為已有的功能動態(tài)的添加更多功能的一種方式;如果你需要擴展一個類的功能,并且不想增加子類的話,那么裝飾器模式就非常適合你!
public function decorate($parent)
{
$this->_parent = $parent;
$class_name = get_class($parent);
// 利用反射將抽象類 CI_Cache 的公共屬性和方法給遍歷出來,并保存,
// 至于保存起來要干嘛?我們待會說;
// 關(guān)于什么是 PHP 中的反射見這里:http://php.net/manual/zh/book.reflection.php
if ( ! isset(self::$_reflections[$class_name]))
{
$r = new ReflectionObject($parent);
foreach ($r->getMethods() as $method)
{
if ($method->isPublic())
{
$this->_methods[] = $method->getName();
}
}
foreach ($r->getProperties() as $prop)
{
if ($prop->isPublic())
{
$this->_properties[] = $prop->getName();
}
}
self::$_reflections[$class_name] = array($this->_methods, $this->_properties);
}
else
{
list($this->_methods, $this->_properties) = self::$_reflections[$class_name];
}
}
上面的代碼中我們看到,將抽象類的接口通過反射遍歷處理并保存,那么為什么要保存起來呢?
答案是:由于驅(qū)動抽象類和驅(qū)動器是非繼承關(guān)系,我們知道非繼承關(guān)系要保持接口對外的統(tǒng)一行為,如果驅(qū)動器中某個行為不存在,我們必須能夠為其動態(tài)添加該行為,所以看到 decorate() 將抽象層的行為給保存起來了!
那又是如何動態(tài)的為驅(qū)動器添加抽象的行為呢?
答案是:驅(qū)動器通過自身的魔術(shù)方法,當調(diào)用不存在的某個行為,就可以通過魔術(shù)方法調(diào)用保存$_reflections中抽象類的行為,所以你看,這不就是動態(tài)添加上了抽象層的行為么?活生生裝飾器模式的應運!
驅(qū)動器自身的魔術(shù)方法繼承自 CI_driver 類中,該類的這幾個魔術(shù)方法就是對 _reflections 中保存的抽象層行為的操作罷了!
public function __call($method, $args = array())
{
if (in_array($method, $this->_methods))
{
return call_user_func_array(array($this->_parent, $method), $args);
}
throw new BadMethodCallException('No such method: '.$method.'()');
}
public function __get($var)
{
if (in_array($var, $this->_properties))
{
return $this->_parent->$var;
}
}
public function __set($var, $val)
{
if (in_array($var, $this->_properties))
{
$this->_parent->$var = $val;
}
}
驅(qū)動抽象和驅(qū)動器的載入實例化的源碼我們就分析到此了,代碼量雖然不多,可足夠復雜,可以說是解讀 CI 框架源碼以來最難讀的一部分源碼了!
接下來我們看下具體驅(qū)動器對于驅(qū)動抽象層接口實現(xiàn)的細節(jié),驅(qū)動器接口實現(xiàn)部分的源碼也沒多復雜,我們就只是看下 redis,file 這兩個驅(qū)動的接口實現(xiàn)!
File
整個 Cache_file 的實現(xiàn)很簡單,大家有興趣自己看下,幾行代碼一會就掃完了!
// 寫入緩存,注意:是以序列化的方式寫入的,并保存了緩存過期的時間
public function save($id, $data, $ttl = 60, $raw = FALSE)
{
$contents = array(
'time' => time(),
'ttl' => $ttl,
'data' => $data
);
if (write_file($this->_cache_path.$id, serialize($contents)))
{
chmod($this->_cache_path.$id, 0640);
return TRUE;
}
return FALSE;
}
// ------------------------------------------------------------------------
public function get($id)
{
$data = $this->_get($id);
return is_array($data) ? $data['data'] : FALSE;
}
// 讀取緩存,可以看到反序列化后還判斷了時間是否過期
protected function _get($id)
{
if ( ! file_exists($this->_cache_path.$id))
{
return FALSE;
}
$data = unserialize(file_get_contents($this->_cache_path.$id));
if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl'])
{
unlink($this->_cache_path.$id);
return FALSE;
}
return $data;
}
// ------------------------------------------------------------------------
//緩存刪除就是直接將文件干掉了
public function delete($id)
{
return file_exists($this->_cache_path.$id) ? unlink($this->_cache_path.$id) : FALSE;
}
Redis
CI 提供的對 redis 驅(qū)動非常輕量,只是做了一些簡單的包裝而已,缺失了 redis 的很多操作復雜數(shù)據(jù)結(jié)構(gòu)的接口,如果你對 redis 有一些復雜的使用,筆者不建議你使用 CI 自帶的 redis 驅(qū)動,你完全可以自己在封裝一個 redis 庫!
下面是筆者在 CI 中自己封裝了一個 redis 操作類,經(jīng)供參考
<?php
/**
* Created by TCL
* User: Administrator
* Date: 2018/2/3
* Time: 14:15
*/
defined('BASEPATH') OR exit('No direct script access allowed');
/**
* Class Pedis
*/
class Pedis extends Redis
{
/**
* @var null
*/
private static $init = false;
/**
* @var null|Redis
*/
public $redis = null;
/**
* @var null redis 配置文件
*/
private $config = null;
/**
* PRedis constructor.
*/
public function __construct()
{
parent::__construct();
if(self::$init == false)
{
$CI = & get_instance();
$host = $CI->config->item('host');
$port = $CI->config->item('port');
$passwd = $CI->config->item('password');
$this->connect($host, $port);
$this->auth($passwd);
self::$init = true;
}
}
/**
* 關(guān)閉 redis 連接
*/
public function __destruct()
{
if (self::$init)
{
$this->close();
}
}
}
該類繼承 redis 后, 經(jīng)過這樣一層包裝,redis 中所有的接口你都可以通過該類使用!
至此整個緩存驅(qū)動的源碼就分析結(jié)束了!