1. 程式人生 > >CI框架原始碼解析二之引導檔案CodeIgniter.php

CI框架原始碼解析二之引導檔案CodeIgniter.php

         上篇解析入口檔案寫到載入 core/CodeIgniter.php框架核心檔案,啟動框架。CodeIgniter.php檔案被稱為BOOTSTRAP,也就是引導檔案,這裡也就是CI框架的核心了。其實把CodeIgniter.php這個檔案的程式碼執行一次,就是整個CI應用都完成了一次完整的運作流程了。其中會載入一些元件,引入很多外部檔案,等等。所以建議在閱讀此檔案程式碼的時候,第一遍先閱讀它的大概流程,也就是說不必進入相應的元件、函式檔案中去。第二遍看的時候才具體看那些函式、元件裡面是怎麼實現的。這個當然要看個人需要咯~

          通過解析,博主總結CodeIgniter.php檔案一共完成已下幾種工作:

① 載入框架常量、函式庫以及框架初始化

② 載入核心類元件

③ 路由的設定與判斷

④ 解析請求的類,並呼叫請求的方法

⑤ 輸出

1、載入框架常量、函式庫以及框架初始化

1) 執行前工作

      判斷常量、設定版本號等。

    /**
     * 這個BASEPATH,就是在入口檔案(index.php)裡面定義的那個BASEPATH~
     * 如果沒有定義BASEPATH,那麼直接退出,下面程式都不執行。
     * 其實除了入口檔案index.php開頭沒有這句話之外,所有檔案都會有這句話
     * 也就是說,所有檔案都不能單獨執行,一定是index.php在執行過程中把這些檔案通
     * 過某種方式引進來執行,所以只有入口檔案index.php才能被訪問。
     */
    defined('BASEPATH') OR exit('No direct script access allowed');
    
    //設定版本號
    define('CI_VERSION', '3.1.0');

2) 載入框架常量

    if (file_exists(APPPATH . 'config/' . ENVIRONMENT . '/constants.php')) {
        require_once(APPPATH . 'config/' . ENVIRONMENT . '/constants.php');
    }
    require_once(APPPATH . 'config/constants.php');

        載入配置的常量。這個配置檔案裡面預設已經有一些和檔案有關的常量。下面這個判斷可以看出一開始我們在index.php裡面定義的那個ENVIRONMENT的作用之一,如果是定義某個環境,會呼叫相應的配置檔案,這樣就可以使得應用在相應的環境中執行。不僅僅是這個常量的配置檔案是這樣子,以後你會發現,其實全部配置檔案都是先判斷當前環境再引入。方便切換,只需在index.php裡面改一下ENVIRONMENT的值。

        當然啦,如果壓根沒有這個環境下的配置檔案,就會呼叫預設的。CI手冊上也有說,各種環境下的相同的配置檔案,可以直接放在/config/下,而不需要每個環境的目錄下都有。

3) 載入全域性函式庫

    //載入全域性函式庫
    require_once(BASEPATH . 'core/Common.php');

4) 進行全域性變數安全處理

    if (!is_php('5.4')) {
        //上篇博文已經講述過ini_set()、ini_get()函式的功能
        ini_set('magic_quotes_runtime', 0);
        if ((bool)ini_get('register_globals')) {
            $_protected = array(
                '_SERVER',
                '_GET',
                '_POST',
                '_FILES',
                '_REQUEST',
                '_SESSION',
                '_ENV',
                '_COOKIE',
                'GLOBALS',
                'HTTP_RAW_POST_DATA',
                'system_path',
                'application_folder',
                'view_folder',
                '_protected',
                '_registered'
            );
            $_registered = ini_get('variables_order');
            foreach (array('E' => '_ENV', 'G' => '_GET', 'P' => '_POST', 'C' => '_COOKIE', 'S' => '_SERVER') as $key => $superglobal) {
                if (strpos($_registered, $key) === FALSE) {
                    continue;
                }
                foreach (array_keys($$superglobal) as $var) {
                    if (isset($GLOBALS[$var]) && !in_array($var, $_protected, TRUE)) {
                        $GLOBALS[$var] = NULL;
                    }
                }
            }
        }
    }

        如果低於php5.4版本,將進行全域性變數安全處理。當開啟了register_globals,這就意味著EGPCS中的變數可以直接用變數名訪問,這些全域性變數是儲存在$GLOBALS陣列中的,這是個隱患,雖然5.4及之後消除了,但考慮相容以前,需要手工清除這些全域性變數。那麼挑選了最重要的需要特別保護的一些變數名,也就是$_protected陣列的值。凡是EGPCS中涉及到變數名稱在$_protected陣列中的,一律清空。

5) 自定義錯誤、異常和程式完成的函式

    set_error_handler('_error_handler');
    set_exception_handler('_exception_handler');
    register_shutdown_function('_shutdown_handler');

a、設定錯誤處理:set_error_handler('_error_handler')。處理函式原型:function _error_handler($severity, $message, $filepath, $line)。程式本身原因或手工觸發trigger_error("A custom error has been triggered");
b、設定異常處理:set_exception_handler('_exception_handler')。處理函式原型:function _exception_handler($exception)。當用戶丟擲異常時觸發throw new Exception('Exception occurred');
c、千萬不要被shutdown迷惑:register_shutdown_function('_shutdown_handler')可以這樣理解呼叫條件:當頁面被使用者強制停止時、當程式程式碼執行超時時、當php程式碼執行完成時。

6) 檢查核心class是否被擴充套件

    if (!empty($assign_to_config['subclass_prefix'])) {
        get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix']));
    }

        其中,$assign_to_config應該是定義在入口檔案Index.php中的配置陣列. 通常情況下,CI的核心元件的名稱均以”CI_”開頭,而如果更改了或者擴充套件CI的核心元件,則應該使用不同的subclass_prefix字首如MY_ ,這種情況下,應該通過$assign_to_config[‘subclass_prefix’]指定你的擴充套件核心的字首名,便於CI的Loader元件載入該類,或者可能出現找不到檔案的錯誤。另外,subclass_prefix配置項預設是位於APPPATH/Config/config.php配置檔案中的,這段程式碼同樣告訴我們,index.php檔案中的subclass_prefix具有更高的優先權(也就是,如果兩處都設定了subclass_prefix,index.php中的配置項會覆蓋配置檔案Config.php中的配置)。

7) 載入composer

    //載入composer
    if ($composer_autoload = config_item('composer_autoload')) {
        if ($composer_autoload === TRUE) {
            file_exists(APPPATH . 'vendor/autoload.php')
                ? require_once(APPPATH . 'vendor/autoload.php')
                : log_message('error', '$config[\'composer_autoload\'] is set to TRUE but ' . APPPATH . 'vendor/autoload.php was not found.');
        } elseif (file_exists($composer_autoload)) {
            require_once($composer_autoload);
        } else {
            log_message('error', 'Could not find the specified $config[\'composer_autoload\'] path: ' . $composer_autoload);
        }
    }
        注:這個內容比較大,知識比較多,以後有機會單獨開篇來介紹。

到這裡,CI框架的基本環境配置初始化已經算是完成了,接下來,CodeIgniter會藉助一系列的元件,完成更多的需求。

2、載入核心類元件

        通常,CI框架中不同的功能均由不同的元件來完成(如Log元件主要用於記錄日誌,Input元件則用於處理使用者的GET,POST等資料)這種模組化的方式使得各元件之間的耦合性較低,從而也便於擴充套件。CI中主要的核心元件如下所示:

a) BM:指BenchMark,是CI的基準點元件,主要用於mark各種時間點、記錄記憶體使用等引數,便於效能測試和追蹤。
b) 鉤子類->EXT:CI的擴充套件元件,前面已經介紹過,用於在不改變CI核心的基礎上改變或者增加系統的核心執行功能。Hook鉤子允許你在系統執行的各個掛鉤點(hook point)新增自定義的功能和跟蹤,如pre_system,pre_controller,post_controller等預定義的掛鉤點。以下所有的$EXT->_call_hook("xxx");均是call特定掛鉤點的程式(如果有的話)。
c) 配置類->CFG:Config配置管理元件。主要用於載入配置檔案、獲取和設定配置項等。
d) utf8類->UNI:用於對UTF-8字符集處理的相關支援。其他元件如INPUT元件,需要改元件的支援。
e) URL類->URI:解析URI(Uniform Rescource Identifier)引數等.這個元件與RTR元件關係緊密。(似乎URI與Router走到哪裡都是好基友)。
 f) 路由類->RTR:路由元件。通過URI元件的引數解析,決定資料流向(路由)。
g) OUTPUT類->OUT:最終的輸出管理元件,掌管著CI的最終輸出(海關啊)。
h) 安全類->SEC:安全處理元件。畢竟安全問題永遠是一個大問題。
i) 輸入及過濾類->IN:用於獲取輸入以及表單驗證。
j) 語言類->LANG:用於設定框架語言。

3、路由的設定與判斷

    <pre name="code" class="php">    $e404 = FALSE;
    $class = ucfirst($RTR->class);
    $method = $RTR->method;
    
    if (empty($class) OR !file_exists(APPPATH . 'controllers/' . $RTR->directory . $class . '.php')) {
        $e404 = TRUE;
    } else {
        require_once(APPPATH . 'controllers/' . $RTR->directory . $class . '.php');
        if (!class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method)) {
            $e404 = TRUE;
        } elseif (method_exists($class, '_remap')) {
            $params = array($method, array_slice($URI->rsegments, 2));
            $method = '_remap';
        }
        elseif (!in_array(strtolower($method), array_map('strtolower', get_class_methods($class)), TRUE)) {
            $e404 = TRUE;
        }
    }

CI認為下面這幾種情況認為是404,如果找不到就呼叫show_404()函式:    1) 請求的class不存在:! class_exists($class)    2) 請求私有方法:!$method[0] === '_'    3) 請求基類方法:method_exists('CI_Controller', $method)    4)請求的方法不存在:! in_array(strtolower($method), array_map('strtolower', get_class_methods($class)), TRUE)

        如果請求的條件滿足上面3箇中的任何一個,則被認為是不合法的請求(或者是無法定位的請求),因此會被CI定向到404頁面(值得注意的是,如果設定了404_override,並且404_override的class存在,並不會直接呼叫show_404並退出,而是會像正常的訪問一樣,例項化:$CI = new $class();)

        其中有一段程式碼是關於404處理,在這裡就不把程式碼貼上來了,在文章的最後我會將整個檔案的程式碼(註釋版)貼出來。        走到這裡,CI的Controller總算是載入完了(累趴)。不過且慢,還有不少事情要做:

    if ($method !== '_remap') {
        $params = array_slice($URI->rsegments, 2);
    }

        檢查_remap,_remap這個東西類似於CI的rewrite,可以將你的請求定位到其他的位置。這個方法是應該定義在你的應用程式控制器的;現在,所有的請求都會被定位到改控制器的index()中去了。如果_remap不存在,則呼叫實際控制器的$method方法:call_user_func_array(array(&$CI, $method), $params);

4、解析請求的類,並呼叫請求的方法

    $CI = new $class();
    //鉤子,不想多說了
    $EXT->call_hook('post_controller_constructor');
    call_user_func_array(array(&$CI, $method), $params);

        call_user_func_array 呼叫回撥函式,並把一個數組引數作為回撥函式的引數,call_user_func_array 函式和 call_user_func 很相似,只是 使 用了陣列 的傳遞引數形式,讓引數的結構更清晰。

5、輸出

    if ($EXT->call_hook('display_override') === FALSE) {
        //這裡,把$this->load->view();裡面緩衝的輸出結果輸出,基本上一個流程總算完成了。
        $OUT->_display();
    }

        最終輸出,$this->load->view()之後,並不會直接輸出,而是放在了快取區。$Out->_display之後,才會設定快取,並最終輸出。

        至此,終於可以鬆一口氣了。到現在,CI的核心流程總算是走完了(雖然還有很多細節的問題,但不管怎麼說,大樹的枝幹已經有了,樹葉的細節,可以慢慢新增)。在結束本文之前,我們來梳理一下CI的核心執行流程:


        最後,貼一下整個CodeIgniter.php檔案的原始碼(註釋版):

    <?php
    
    /**
     * =======================================
     * Created by Pocket Knife Technology.
     * User: ZhiHua_W
     * Date: 2016/10/15 0032
     * Time: 上午 9:14
     * Project: CodeIgniter框架—原始碼分析
     * Power: Analysis for CodeIgniter.php
     * =======================================
     */
    
    /**
     * 這個BASEPATH,就是在入口檔案(index.php)裡面定義的那個BASEPATH~
     * 如果沒有定義BASEPATH,那麼直接退出,下面程式都不執行。
     * 其實除了入口檔案index.php開頭沒有這句話之外,所有檔案都會有這句話
     * 也就是說,所有檔案都不能單獨執行,一定是index.php在執行過程中把這些檔案通
     * 過某種方式引進來執行,所以只有入口檔案index.php才能被訪問。
     */
    defined('BASEPATH') OR exit('No direct script access allowed');
    
    //設定版本號
    define('CI_VERSION', '3.1.0');
    
    /**
     * 載入框架常量:這個配置檔案裡面預設已經有一些和檔案有關的常量
     * 根據定義的環境,載入對應的環境目錄下的常量,
     * 如果與系統常量衝突,最終以系統常量為準,所以環境常量無法覆蓋系統常量。
     * 這樣做主要為了快速設定特定環境下的特定常量。
     */
    if (file_exists(APPPATH . 'config/' . ENVIRONMENT . '/constants.php')) {
        require_once(APPPATH . 'config/' . ENVIRONMENT . '/constants.php');
    }
    require_once(APPPATH . 'config/constants.php');
    
    //載入全域性函式庫
    require_once(BASEPATH . 'core/Common.php');
    
    //進行全域性變數安全處理
    if (!is_php('5.4')) {
        //上篇博文已經講述過ini_set()、ini_get()函式的功能
        ini_set('magic_quotes_runtime', 0);
        /**
         * 此處存在的知識點
         * PHP變數解析順序:ini_get('variables_order'),同時也聲明瞭接收哪種型別傳送過來的變數;
         * 當程式中使用了$_REQUEST接收變數,設定順序EGPCS(Environment,GET,POST,Cookie,Server)就很重要,注意是從右向左覆蓋。
         * php配置檔案給出了配置提示
         * Default Value: "EGPCS";
         * Development Value: "GPCS";
         * Production Value: "GPCS";
         * PHP 5.4.0 廢除了register_globals,magic_quotes以及安全模式。因此這一段是專門針對PHP5.4之前的版本的。
         * 當開啟了register_globals,這就意味著EGPCS中的變數可以直接用變數名訪問,
         * 這些全域性變數是儲存在$GLOBALS陣列中的,這是個隱患,雖然5.4及之後消除了,但考慮相容以前,
         * 需要手工清除這些全域性變數。那麼挑選了最重要的需要特別保護的一些變數名,
         * 也就是$_protected陣列的值。凡是EGPCS中涉及到變數名稱在$_protected陣列中的,一律清空。
         */
        if ((bool)ini_get('register_globals')) {
            $_protected = array(
                '_SERVER',
                '_GET',
                '_POST',
                '_FILES',
                '_REQUEST',
                '_SESSION',
                '_ENV',
                '_COOKIE',
                'GLOBALS',
                'HTTP_RAW_POST_DATA',
                'system_path',
                'application_folder',
                'view_folder',
                '_protected',
                '_registered'
            );
            $_registered = ini_get('variables_order');
            foreach (array('E' => '_ENV', 'G' => '_GET', 'P' => '_POST', 'C' => '_COOKIE', 'S' => '_SERVER') as $key => $superglobal) {
                if (strpos($_registered, $key) === FALSE) {
                    continue;
                }
                foreach (array_keys($$superglobal) as $var) {
                    if (isset($GLOBALS[$var]) && !in_array($var, $_protected, TRUE)) {
                        $GLOBALS[$var] = NULL;
                    }
                }
            }
        }
    }
    
    /**
     * 自定義錯誤、異常和程式完成的函式
     * 1、設定錯誤處理:set_error_handler('_error_handler')。處理函式原型:function _error_handler($severity, $message, $filepath, $line)。
     * 程式本身原因或手工觸發trigger_error("A custom error has been triggered");
     * 2、設定異常處理:set_exception_handler('_exception_handler')。處理函式原型:function _exception_handler($exception)。
     * 當用戶丟擲異常時觸發throw new Exception('Exception occurred');
     * 3、千萬不要被shutdown迷惑:register_shutdown_function('_shutdown_handler')可以這樣理解呼叫條件:當頁面被使用者強制停止時、當程式程式碼執行超時時、當php程式碼執行完成時。
     */
    set_error_handler('_error_handler');
    set_exception_handler('_exception_handler');
    register_shutdown_function('_shutdown_handler');
    
    /**
     * 這個subclass_prefix是什麼?
     * 這是一個非常棒的東西!!有了它我們就可以輕易擴充套件CI框架~
     * 具體core/Common.php 中的load_class()
     */
    if (!empty($assign_to_config['subclass_prefix'])) {
        //這個get_config($replace)就是從配置檔案裡面讀取資訊,這裡是讀取config/config.php中的配置資訊
        //這個引數$replace的作用是什麼呢?就是臨時把修改配置檔案的意思,注意並沒有從改變檔案的值,這個改變只是
        //停留在記憶體的層面上。
        //而$assign_to_config['xxx'];是在index.php中定義的一個配置資訊陣列,這個配置陣列要優先權要大於配置檔案當中的。
        //所以這個判斷的作用是,看看有沒有在index.php裡面定義了 $assign_to_config['subclass_prefix'],如果有的話,
        //就那把配置檔案中的$config['subclass_prefix']的值改成這個。
        get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix']));
    }
    
    //載入composer
    if ($composer_autoload = config_item('composer_autoload')) {
        if ($composer_autoload === TRUE) {
            file_exists(APPPATH . 'vendor/autoload.php')
                ? require_once(APPPATH . 'vendor/autoload.php')
                : log_message('error', '$config[\'composer_autoload\'] is set to TRUE but ' . APPPATH . 'vendor/autoload.php was not found.');
        } elseif (file_exists($composer_autoload)) {
            require_once($composer_autoload);
        } else {
            log_message('error', 'Could not find the specified $config[\'composer_autoload\'] path: ' . $composer_autoload);
        }
    }
    
    /**
     * 載入Benchmark,它很簡單,就是計算任意兩點之間程式的執行時間
     * Benchmark是基準點的意思。
     * 其實這個類的功能很簡單,只是用來計算程式執行消耗的時間和記憶體而已。
     * 以後經常在某個地方冒出來句$BM->mark('xxx');這個作用就是記錄執行到當前位置的時候的時間點。
     * 通過兩個時間點,就可以計算出時間了。
     */
    $BM =& load_class('Benchmark', 'core');
    $BM->mark('total_execution_time_start');
    $BM->mark('loading_time:_base_classes_start');
    
    /**
     * 取得Hooks元件。
     * 這個hook也是非常非常棒的一個東西!它可以讓我們很好地擴充套件和改造CI
     * 可以這樣理解,一個應用從執行到結束這個期間,CI為我們保留了一些位置,在這些位置上面可以讓開發人員放上所謂的
     * “鉤子”(其實就是一段程式啦!),在應用執行過程中,當執行到有可以放鉤子的位置的時候,先檢測開發人員有沒有
     * 實現這裡的鉤子,如果有就執行它。
     * 有些地方甚至可以用自己寫的鉤子程式替代CI框架本來的程式。
     */
    $EXT =& load_class('Hooks', 'core');
    
    //這裡就有一個鉤子啦。大概意思是:整個應用系統開始動了,這裡要不要先讓開發人員來一段程式?
    //如果你定義了pre_system這個鉤子,那麼,其實它就是在這個位置執行的。
    $EXT->call_hook('pre_system');
    
    //取得配置元件。
    $CFG =& load_class('Config', 'core');
    
    //如果有在index.php定義配置陣列,那麼就丟給配置元件CFG,以後就由CFG來保管了配置資訊了。
    if (isset($assign_to_config) && is_array($assign_to_config)) {
        foreach ($assign_to_config as $key => $value) {
            $CFG->set_item($key, $value);
        }
    }
    
    //重要的字符集相關的東西,處理框架字符集問題
    $charset = strtoupper(config_item('charset'));
    ini_set('default_charset', $charset);
    //根據php擴充套件啟用情況設定了MB_ENABLED,ICONV_ENABLED兩個常量
    //多位元組支援也是一個重要的話題
    if (extension_loaded('mbstring')) {
        define('MB_ENABLED', TRUE);
        @ini_set('mbstring.internal_encoding', $charset);
        mb_substitute_character('none');
    } else {
        define('MB_ENABLED', FALSE);
    }
    
    if (extension_loaded('iconv')) {
        define('ICONV_ENABLED', TRUE);
        @ini_set('iconv.internal_encoding', $charset);
    } else {
        define('ICONV_ENABLED', FALSE);
    }
    
    if (is_php('5.6')) {
        ini_set('php.internal_encoding', $charset);
    }
    
    //負載相容性特徵
    //重寫系統元件的一些方法,包括: mbstring/ hash/password/ standard
    require_once(BASEPATH . 'core/compat/mbstring.php');
    require_once(BASEPATH . 'core/compat/hash.php');
    require_once(BASEPATH . 'core/compat/password.php');
    require_once(BASEPATH . 'core/compat/standard.php');
    
    //取得UTF-8元件,這裡暫時不用管它。
    $UNI =& load_class('Utf8', 'core');
    
    //取得URI元件。
    $URI =& load_class('URI', 'core');
    
    //取得URI的好基友RTR
    //RTR的這個_set_routing();其實做了非常多的事情。在後面的解析Router.php也會解說。非常重要。
    $RTR =& load_class('Router', 'core', isset($routing) ? $routing : NULL);
    
    //輸出元件。這個輸出元件有什麼用?輸出不是$this->load->view()麼?其實它們兩個也是好基友。
    $OUT =& load_class('Output', 'core');
    
    //下面是輸出快取的處理,這裡允許我們自己寫個hook來取替代CI原來Output類的快取輸出
    //如果快取命中則輸出,並結束整個CI的單次生命週期。如果沒有命中快取,或沒有啟用快取,那麼將繼續向下執行。
    if ($EXT->call_hook('cache_override') === FALSE && $OUT->_display_cache($CFG, $URI) === TRUE) {
        exit;
    }
    
    //取得安全元件(安全元件暫時不詳講,因為對於CI一個運作流程來說,它不是必要的。CI的安全處理以後會作為一個新話題來探討)
    //防xss攻擊啊,csrf攻擊啊
    $SEC =& load_class('Security', 'core');
    
    //取得安全元件的好基友INPUT元件。(主要是結合安全元件作一些輸入方面的安全處理,$this->input->post()這些常用的操作都是
    $IN =& load_class('Input', 'core');
    
    //語言元件。
    $LANG =& load_class('Lang', 'core');
    
    //引入控制器父類檔案。這裡和其它元件的引入方式不一樣,用load_class();因為我們最終用到的並不是這個父類,
    //而是我們自己寫在application/controllers/下的某個由uri請求的控制器。
    require_once BASEPATH . 'core/Controller.php';
    
    //定義get_instance();方法,通過呼叫CI_Controller::get_instance()可以實現單例化,
    //呼叫此函式可方便以後直接取得當前應用控制器。
    function &get_instance()
    {
        return CI_Controller::get_instance();
    }
    
    //和其它元件一樣,控制器父類同樣可以通過字首的方式進行擴充套件。
    //其實如果能夠進入這裡,說明了上面的$RTR->_set_routing();在_validate_request()的時候一定是在請求預設控制器。
    //引入自定義擴充套件controller
    if (file_exists(APPPATH . 'core/' . $CFG->config['subclass_prefix'] . 'Controller.php')) {
        require_once APPPATH . 'core/' . $CFG->config['subclass_prefix'] . 'Controller.php';
    }
    
    //這裡mark一下,說明CI的所需要的基本的類都載入完了。
    $BM->mark('loading_time:_base_classes_end');
    
    /**
     * 下面主要進行一些方法上的驗證。
     * 因為畢竟我們是通過URI直接呼叫控制器裡面的方法的,其實這是個很危險的事情。
     * 必須要保證我們原本沒想過要通過URI訪問的方法不能訪問。
     *
     * CI裡面規定以_下劃線開頭的方法,一般是作為非公開的方法,即使方法定義為public。
     * 其實不僅僅是CI這麼做,把非公開的方法名以_開頭,是很好的一種規範。
     * 第二個就是父類CI_Controller裡面的方法也是不允許通過URI訪問的。
     * 如果URI請求這樣的方法,那麼會作為404處理。
     */
    $e404 = FALSE;
    $class = ucfirst($RTR->class);
    $method = $RTR->method;
    //CI認為下面這幾種情況認為是404,如果找不到就呼叫show_404()函式:
    //1) 請求的class不存在:! class_exists($class)
    //2) 請求私有方法:!$method[0] === '_'
    //3) 請求基類方法:method_exists('CI_Controller', $method)
    //4)請求的方法不存在:! in_array(strtolower($method), array_map('strtolower', get_class_methods($class)), TRUE)
    if (empty($class) OR !file_exists(APPPATH . 'controllers/' . $RTR->directory . $class . '.php')) {
        $e404 = TRUE;
    } else {
        require_once(APPPATH . 'controllers/' . $RTR->directory . $class . '.php');
        if (!class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method)) {
            $e404 = TRUE;
        } elseif (method_exists($class, '_remap')) {
            $params = array($method, array_slice($URI->rsegments, 2));
            $method = '_remap';
        } elseif (!in_array(strtolower($method), array_map('strtolower', get_class_methods($class)), TRUE)) {
            $e404 = TRUE;
        }
    }
    //404處理
    if ($e404) {
        if (!empty($RTR->routes['404_override'])) {
            if (sscanf($RTR->routes['404_override'], '%[^/]/%s', $error_class, $error_method) !== 2) {
                $error_method = 'index';
            }
            $error_class = ucfirst($error_class);
            if (!class_exists($error_class, FALSE)) {
                if (file_exists(APPPATH . 'controllers/' . $RTR->directory . $error_class . '.php')) {
                    require_once(APPPATH . 'controllers/' . $RTR->directory . $error_class . '.php');
                    $e404 = !class_exists($error_class, FALSE);
                } elseif (!empty($RTR->directory) && file_exists(APPPATH . 'controllers/' . $error_class . '.php')) {
                    require_once(APPPATH . 'controllers/' . $error_class . '.php');
                    if (($e404 = !class_exists($error_class, FALSE)) === FALSE) {
                        $RTR->directory = '';
                    }
                }
            } else {
                $e404 = FALSE;
            }
        }
        if (!$e404) {
            $class = $error_class;
            $method = $error_method;
            $URI->rsegments = array(
                1 => $class,
                2 => $method
            );
        } else {
            show_404($RTR->directory . $class . '/' . $method);
        }
    }
    //這個_remap也是一個非常棒的東西!如果有定義,它會優先被呼叫,在這個方法裡面我們可以根據$method和引數,隨意
    //做任何處理和加工。即使那個$method不存在。
    if ($method !== '_remap') {
        $params = array_slice($URI->rsegments, 2);
    }
    
    //這裡又一個鉤子,這個鉤子的位置往往都是在一些特殊的位置的,像這裡就是發生在例項化控制器前。
    $EXT->call_hook('pre_controller');
    
    // Mark一下
    $BM->mark('controller_execution_time_( ' . $class . ' / ' . $method . ' )_start');
    //折騰了這麼久,終於例項化我們想要的控制器了
    //終於呼叫了!!!!!!!!!!!就在這裡。
    //不過,不是打擊你,雖然我們請求的控制器的那個方法被呼叫了,但是實際上,我們想要的輸出並沒有完全輸出來。
    //這就是因為$this->load->view();並不是馬上輸出結果,而是把結果放到緩衝區,然後最後Output類把它衝出來。
    $CI = new $class();
    
    //鉤子,不想多說了
    $EXT->call_hook('post_controller_constructor');
    //現在,所有的請求都會被定位到改控制器的index()中去了。如果_remap不存在,則呼叫實際控制器的$method方法
    //call_user_func_array 呼叫回撥函式,並把一個數組引數作為回撥函式的引數,call_user_func_array 函式和 call_user_func 很相似
    //只是使用了陣列的傳遞引數形式,讓引數的結構更清晰。
    call_user_func_array(array(&$CI, $method), $params);
    
    // Mark一下
    $BM->mark('controller_execution_time_( ' . $class . ' / ' . $method . ' )_end');
    $EXT->call_hook('post_controller');
    if ($EXT->call_hook('display_override') === FALSE) {
        //這裡,把$this->load->view();裡面緩衝的輸出結果輸出,基本上一個流程總算完成了。
        $OUT->_display();
    }
    //呼叫post_system的hook
    $EXT->call_hook('post_system');