摘要:任務是加載類的初始化頂級命名空間與文件路徑映射初始化和注冊。在實際情況下可能會出現這樣的情況。值得注意的是這個函數返回的是一個匿名函數,為什么呢原因就是類中的等等都是的。。。關于匿名函數的綁定功能。
前言
在開始之前,歡迎關注我自己的博客:www.leoyang90.cn
上一篇文章,我們討論了 PHP 的自動加載原理、PHP 的命名空間、PHP 的 PSR0 與 PSR4 標準,有了這些知識,其實我們就可以按照 PSR4 標準寫出可以自動加載的程序了。然而我們為什么要自己寫呢?尤其是有 Composer 這神一樣的包管理器的情況下?
Composer 自動加載概論Composer 是 PHP 的一個依賴管理工具。它允許你申明項目所依賴的代碼庫,它會在你的項目中為你安裝他們。詳細內容可以查看 Composer 中文網。
Composer Composer 將這樣為你解決問題:
你有一個項目依賴于若干個庫。
其中一些庫依賴于其他庫。
你聲明你所依賴的東西。
Composer 會找出哪個版本的包需要安裝,并安裝它們(將它們下載到你的項目中)。
例如,你正在創建一個項目,你需要一個庫來做日志記錄。你決定使用 monolog。為了將它添加到你的項目中,你所需要做的就是創建一個 composer.json 文件,其中描述了項目的依賴關系。
{ "require": { "monolog/monolog": "1.2.*" } }
然后我們只要在項目里面直接use MonologLogger即可,神奇吧!
簡單的說,Composer 幫助我們下載好了符合 PSR0 或 PSR4 標準的第三方庫,并把文件放在相應位置;幫我們寫了 _autoload() 函數,注冊到了 spl_register() 函數,當我們想用第三方庫的時候直接使用命名空間即可。
??
那么當我們想要寫自己的命名空間的時候,該怎么辦呢?很簡單,我們只要按照 PSR4 標準命名我們的命名空間,放置我們的文件,然后在 composer 里面寫好頂級域名與具體目錄的映射,就可以享用 composer 的便利了。
當然如果有一個非常棒的框架,我們會驚喜地發現,在 composer 里面寫頂級域名映射這事我們也不用做了,框架已經幫我們寫好了頂級域名映射了,我們只需要在框架里面新建文件,在新建的文件中寫好命名空間,就可以在任何地方 use 我們的命名空間了。
下面我們就以 laravel 框架為例,講一講 composer 是如何實現 PSR0 和 PSR4 標準的自動加載功能。
首先,我們先大致了解一下 Composer 自動加載所用到的源文件。
啟動autoload_real.php:自動加載功能的引導類。任務是 composer 加載類的初始化(頂級命名空間與文件路徑映射初始化)和注冊( spl_autoload_register() )。
ClassLoader.php:composer 加載類。composer 自動加載功能的核心類。
autoload_static.php:頂級命名空間初始化類,用于給核心類初始化頂級命名空間。
autoload_classmap.php:自動加載的最簡單形式,有完整的命名空間和文件目錄的映射;
autoload_files.php:用于加載全局函數的文件,存放各個全局函數所在的文件路徑名;
autoload_namespaces.php:符合 PSR0 標準的自動加載文件,存放著頂級命名空間與文件的映射;
autoload_psr4.php:符合 PSR4 標準的自動加載文件,存放著頂級命名空間與文件的映射;
laravel 框架的初始化是需要 composer 自動加載協助的,所以 laravel 的入口文件 index.php 第一句就是利用 composer 來實現自動加載功能。
require __DIR__."/../bootstrap/autoload.php";
咱們接著去看 bootstrap 目錄下的 autoload.php:
define("LARAVEL_START", microtime(true)); require __DIR__."/../vendor/autoload.php";
再去vendor目錄下的autoload.php:
require_once __DIR__ . "/composer" . "/autoload_real.php"; return ComposerAutoloaderInit 832ea71bfb9a4128da8660baedaac82e::getLoader();
為什么框架要在 bootstrap/autoload.php 轉一下?個人理解,laravel 這樣設計有利于支持或擴展任意有自動加載的第三方庫。
好了,我們終于要看到了 Composer 真正要顯威的地方了。autoload_real 里面就是一個自動加載功能的引導類,這個類不負責具體功能邏輯,只做了兩件事:初始化自動加載類、注冊自動加載類。
到 autoload_real 這個文件里面去看,發現這個引導類的名字叫 ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e,為什么要叫這么古怪的名字呢?因為這是防止用戶自定義類名跟這個類重復沖突了,所以在類名上加了一個 hash 值。
其實還有一個原因,那就是composer運行加載多個ComposerAutoloaderInit類。在實際情況下可能會出現這樣的情況:vendor/modelA/vendor/composer。也就是說第三方庫中也存在著一個composer,他有著自己所依賴的各種庫,也是通過composer來加載。這樣的話就會有兩個ComposerAutoloaderInit類,那么就會觸發redeclare的錯誤。給ComposerAutoloaderInit加上一個hash,那么就可以實現多個class loader 的加載。
在 vendor 目錄下的 autoload.php 文件中我們可以看出,程序主要調用了引導類的靜態方法 getLoader(),我們接著看看這個函數。
public static function getLoader() { /***************************經典單例模式********************/ if (null !== self::$loader) { return self::$loader; } /***********************獲得自動加載核心類對象********************/ spl_autoload_register(array("ComposerAutoloaderInit 832ea71bfb9a4128da8660baedaac82e", "loadClassLoader"), true, true); self::$loader = $loader = new ComposerAutoloadClassLoader(); spl_autoload_unregister(array("ComposerAutoloaderInit 832ea71bfb9a4128da8660baedaac82e", "loadClassLoader")); /***********************初始化自動加載核心類對象********************/ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined("HHVM_VERSION"); if ($useStaticLoader) { require_once __DIR__ . "/autoload_static.php"; call_user_func(ComposerAutoloadComposerStaticInit 832ea71bfb9a4128da8660baedaac82e::getInitializer($loader)); } else { $map = require __DIR__ . "/autoload_namespaces.php"; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . "/autoload_psr4.php"; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . "/autoload_classmap.php"; if ($classMap) { $loader->addClassMap($classMap); } } /***********************注冊自動加載核心類對象********************/ $loader->register(true); /***********************自動加載全局函數********************/ if ($useStaticLoader) { $includeFiles = ComposerAutoloadComposerStaticInit 832ea71bfb9a4128da8660baedaac82e::$files; } else { $includeFiles = require __DIR__ . "/autoload_files.php"; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire 832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file); } return $loader; }
從上面可以看出,我把自動加載引導類分為5個部分。
第一部分——單例第一部分很簡單,就是個最經典的單例模式,自動加載類只能有一個,多次加載影響效率,可能會引起重復require同一個文件。
if (null !== self::$loader) { return self::$loader; }第二部分——構造 ClassLoader 核心類
第二部分 new 一個自動加載的核心類對象。
/***********************獲得自動加載核心類對象********************/ spl_autoload_register(array("ComposerAutoloaderInit 832ea71bfb9a4128da8660baedaac82e", "loadClassLoader"), true, true); self::$loader = $loader = new ComposerAutoloadClassLoader(); spl_autoload_unregister(array("ComposerAutoloaderInit 832ea71bfb9a4128da8660baedaac82e", "loadClassLoader"));
loadClassLoader() 函數:
public static function loadClassLoader($class) { if ("ComposerAutoloadClassLoader" === $class) { require __DIR__ . "/ClassLoader.php"; } }
從程序里面我們可以看出,composer 先向 PHP 自動加載機制注冊了一個函數,這個函數 require 了 ClassLoader 文件。成功 new 出該文件中核心類 ClassLoader() 后,又銷毀了該函數。
為什么不直接 require,而要這么麻煩?原因和ComposerAutoloaderInit加上hash一樣,如果直接require,那么會造成ClassLoader類的重復定義。所以有人建議這樣:
if (!class_exists("ComposerAutoloadClassLoader", false)) { require __DIR__ . "/ClassLoader.php"; } static::$loader = $loader = new ComposerAutoloadClassLoader();
其實這樣可以更加直觀。但是class_exists有個缺點,那就是opcache緩存有個bug,class_exists即使為真,程序仍然會進入if條件進行require,這樣仍然造成了重復定義的問題。
那為什么不跟引導類一樣用個 hash 呢?這樣就可以多次定義這個ClassLoader類了。原因就是這個類是可以復用的,框架允許用戶使用這個類,如果用hash用戶就完全沒辦法用ClassLoader了。
所以最終的解決方案就是利用spl_autoload_register來加載,這樣只要ClassLoader只要被聲明過,spl_autoload_register就不會調用,也就不會require。
可見這簡單的幾行代碼其實內幕很深的。詳細可見
github 的相關 issue:Unable to run tests with phpunit and composer installed globally #1248
github 相關解決方案 PR : Allow loading of multiple composer autoloaders concurrently, fixes #1248 #1313
/***********************初始化自動加載核心類對象********************/ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined("HHVM_VERSION"); if ($useStaticLoader) { require_once __DIR__ . "/autoload_static.php"; call_user_func(ComposerAutoloadComposerStaticInit 832ea71bfb9a4128da8660baedaac82e::getInitializer($loader)); } else { $map = require __DIR__ . "/autoload_namespaces.php"; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . "/autoload_psr4.php"; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . "/autoload_classmap.php"; if ($classMap) { $loader->addClassMap($classMap); } }
這一部分就是對自動加載類的初始化,主要是給自動加載核心類初始化頂級命名空間映射。初始化的方法有兩種:(1)使用 autoload_static 進行靜態初始化;(2)調用核心類接口初始化。
autoload_static 靜態初始化靜態初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虛擬機。為什么要多帶帶要求 php5.6 版本以上呢?原因就是這種靜態加載加速機制是 opcache 緩存針對靜態數組優化的,只支持 php5.6 以上的版本。hhvm 是 php 另一個虛擬機,當然沒有辦法支持 opcache 緩存。
github相關 PR: Speedup autoloading on PHP 5.6 & 7.0+ using static arrays
我們深入 autoload_static.php 這個文件發現這個文件定義了一個用于靜態初始化的類,名字叫 ComposerStaticInit832ea71bfb9a4128da8660baedaac82e,仍然為了避免沖突加了 hash 值和多次復用。這個類很簡單:
class ComposerStaticInit832ea71bfb9a4128da8660baedaac82e{ public static $files = array(...); public static $prefixLengthsPsr4 = array(...); public static $prefixDirsPsr4 = array(...); public static $prefixesPsr0 = array(...); public static $classMap = array (...); public static function getInitializer(ClassLoader $loader) { return Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixesPsr0; $loader->classMap = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$classMap; }, null, ClassLoader::class); } }
這個靜態初始化類的核心就是 getInitializer() 函數,它將自己類中的頂級命名空間映射給了 ClassLoader 類。值得注意的是這個函數返回的是一個匿名函數,為什么呢?原因就是 ClassLoader 類中的 prefixLengthsPsr4、prefixDirsPsr4 等等都是 private的。。。普通的函數沒辦法給類的 private 成員變量賦值。利用匿名函數的綁定功能就可以將把匿名函數轉為 ClassLoader 類的成員函數。
關于匿名函數的 綁定功能。
接下來就是頂級命名空間初始化的關鍵了。
public static $classMap = array ( "AppConsoleKernel" => __DIR__ . "/../.." . "/app/Console/Kernel.php", "AppExceptionsHandler" => __DIR__ . "/../.." . "/app/Exceptions/Handler.php", "AppHttpControllersAuthForgotPasswordController" => __DIR__ . "/../.." . "/app/Http/Controllers/Auth/ForgotPasswordController.php", "AppHttpControllersAuthLoginController" => __DIR__ . "/../.." . "/app/Http/Controllers/Auth/LoginController.php", "AppHttpControllersAuthRegisterController" => __DIR__ . "/../.." . "/app/Http/Controllers/Auth/RegisterController.php", ... )
簡單吧,直接命名空間全名與目錄的映射,沒有頂級命名空間。。。簡單粗暴,也導致這個數組相當的大。
PSR0頂級命名空間映射:public static $prefixesPsr0 = array ( "P" => array ( "Prophecy" => array ( 0 => __DIR__ . "/.." . "/phpspec/prophecy/src", ), "Parsedown" => array ( 0 => __DIR__ . "/.." . "/erusev/parsedown", ), ), "M" => array ( "Mockery" => array ( 0 => __DIR__ . "/.." . "/mockery/mockery/library", ), ), "J" => array ( "JakubOnderkaPhpConsoleHighlighter" => array ( 0 => __DIR__ . "/.." . "/jakub-onderka/php-console-highlighter/src", ), "JakubOnderkaPhpConsoleColor" => array ( 0 => __DIR__ . "/.." . "/jakub-onderka/php-console-color/src", ), ), "D" => array ( "DoctrineCommonInflector" => array ( 0 => __DIR__ . "/.." . "/doctrine/inflector/lib", ), ), );
為了快速找到頂級命名空間,我們這里使用命名空間第一個字母作為前綴索引。這個映射的用法比較明顯,假如我們有 Parsedown/example 這樣的命名空間,首先通過首字母 P,找到
"P" => array ( "Prophecy" => array ( 0 => __DIR__ . "/.." . "/phpspec/prophecy/src", ), "Parsedown" => array ( 0 => __DIR__ . "/.." . "/erusev/parsedown", ), )
這個數組,然后我們就會遍歷這個數組來和 Parsedown/example 比較,發現第一個 Prophecy 不符合,第二個 Parsedown 符合,然后得到了映射目錄:(映射目錄可能不止一個)
array ( 0 => __DIR__ . "/.." . "/erusev/parsedown", )
我們會接著遍歷這個數組,嘗試 _DIR_ ."/.." . "/erusev/parsedown/Parsedown/example.php 是否存在,如果不存在接著遍歷數組(這個例子數組只有一個元素),如果數組遍歷完都沒有,就會加載失敗。
PSR4標準頂級命名空間映射數組:public static $prefixLengthsPsr4 = array( "p" => array ( "phpDocumentorReflection" => 25, ), "S" => array ( "SymfonyPolyfillMbstring" => 26, "SymfonyComponentYaml" => 23, "SymfonyComponentVarDumper" => 28, ... ), ... ); public static $prefixDirsPsr4 = array ( "phpDocumentorReflection" => array ( 0 => __DIR__ . "/.." . "/phpdocumentor/reflection-common/src", 1 => __DIR__ . "/.." . "/phpdocumentor/type-resolver/src", 2 => __DIR__ . "/.." . "/phpdocumentor/reflection-docblock/src", ), "SymfonyPolyfillMbstring" => array ( 0 => __DIR__ . "/.." . "/symfony/polyfill-mbstring", ), "SymfonyComponentYaml" => array ( 0 => __DIR__ . "/.." . "/symfony/yaml", ), ... )
PSR4 標準頂級命名空間映射用了兩個數組,第一個和 PSR0 一樣用命名空間第一個字母作為前綴索引,然后是頂級命名空間,但是最終并不是文件路徑,而是頂級命名空間的長度。為什么呢?因為前一篇 文章 我們說過,PSR4 標準的文件目錄更加靈活,更加簡潔。PSR0 中頂級命名空間目錄直接加到命名空間前面就可以得到路徑 (Parsedown/example => _DIR_ ."/.." . "/erusev/parsedown/Parsedown/example.php),而 PSR4 標準卻是用頂級命名空間目錄替換頂級命名空間(Parsedown/example => _DIR_ ."/.." . "/erusev/parsedown/example.php),所以獲得頂級命名空間的長度很重要。
具體的用法:假如我們找 SymfonyPolyfillMbstringexample 這個命名空間,和 PSR0 一樣通過前綴索引和字符串匹配我們得到了
"SymfonyPolyfillMbstring" => 26,
這條記錄,鍵是頂級命名空間,值是命名空間的長度。拿到頂級命名空間后去 $prefixDirsPsr4 數組獲取它的映射目錄數組:(注意映射目錄可能不止一條)
"SymfonyPolyfillMbstring" => array ( 0 => __DIR__ . "/.." . "/symfony/polyfill-mbstring", )
然后我們就可以將命名空間 SymfonyPolyfillMbstringexample 前26個字符替換成目錄 _DIR_ . "/.." . "/symfony/polyfill-mbstring,我們就得到了 _DIR_ . "/.." . "/symfony/polyfill-mbstring/example.php,先驗證磁盤上這個文件是否存在,如果不存在接著遍歷。如果遍歷后沒有找到,則加載失敗。
??
自動加載核心類 ClassLoader 的靜態初始化完成!!!
如果PHP版本低于5.6或者使用HHVM虛擬機環境,那么就要使用核心類的接口進行初始化。
//PSR0標準 $map = require __DIR__ . "/autoload_namespaces.php"; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } //PSR4標準 $map = require __DIR__ . "/autoload_psr4.php"; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . "/autoload_classmap.php"; if ($classMap) { $loader->addClassMap($classMap); }PSR0 標準
autoload_namespaces:
return array( "Prophecy" => array($vendorDir . "/phpspec/prophecy/src"), "Parsedown" => array($vendorDir . "/erusev/parsedown"), "Mockery" => array($vendorDir . "/mockery/mockery/library"), "JakubOnderkaPhpConsoleHighlighter" => array($vendorDir . "/jakub-onderka/php-console-highlighter/src"), "JakubOnderkaPhpConsoleColor" => array($vendorDir . "/jakub-onderka/php-console-color/src"), "DoctrineCommonInflector" => array($vendorDir . "/doctrine/inflector/lib"), );
PSR0 標準的初始化接口:
public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } }
很簡單,PSR0 標準取出命名空間的第一個字母作為索引,一個索引對應多個頂級命名空間,一個頂級命名空間對應多個目錄路徑,具體形式可以查看上面我們講的 autoload_static 的 $prefixesPsr0。如果沒有頂級命名空間,就只存儲一個路徑名,以便在后面嘗試加載。
PSR4標準autoload_psr4
return array( "XdgBaseDir" => array($vendorDir . "/dnoegel/php-xdg-base-dir/src"), "WebmozartAssert" => array($vendorDir . "/webmozart/assert/src"), "TijsVerkoyenCssToInlineStyles" => array($vendorDir . "/tijsverkoyen/css-to-inline-styles/src"), "Tests" => array($baseDir . "/tests"), "SymfonyPolyfillMbstring" => array($vendorDir . "/symfony/polyfill-mbstring"), ... )
PSR4 標準的初始化接口:
public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ("" !== $prefix[$length - 1]) { throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } }
PSR4 初始化接口也很簡單。如果沒有頂級命名空間,就直接保存目錄。如果有命名空間的話,要保證頂級命名空間最后是,然后分別保存(前綴=》頂級命名空間,頂級命名空間=》頂級命名空間長度),(頂級命名空間=》目錄)這兩個映射數組。具體形式可以查看上面我們講的 autoload_static的prefixLengthsPsr4、 $prefixDirsPsr4。
傻瓜式命名空間映射autoload_classmap:
public static $classMap = array ( "AppConsoleKernel" => __DIR__ . "/../.." . "/app/Console/Kernel.php", "AppExceptionsHandler" => __DIR__ . "/../.." . "/app/Exceptions/Handler.php", ... )
addClassMap:
public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } }
這個最簡單,就是整個命名空間與目錄之間的映射。
結語其實我很想接著寫下下去,但是這樣會造成篇幅過長,所以我就把自動加載的注冊和運行放到下一篇文章了。我們回顧一下,這篇文章主要講了:(1)框架如何啟動 composer 自動加載;(2)composer 自動加載分為5部分;
其實說是5部分,真正重要的就兩部分——初始化與注冊。初始化負責頂層命名空間的目錄映射,注冊負責實現頂層以下的命名空間映射規則。
Written with StackEdit.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/22928.html
前言 在開始之前,歡迎關注我自己的博客:www.leoyang90.cn上一篇 文章我們講到了 Composer 自動加載功能的啟動與初始化,經過啟動與初始化,自動加載核心類對象已經獲得了頂級命名空間與相應目錄的映射,換句話說,如果有命名空間 AppConsoleKernel,我們已經知道了 App 對應的目錄,接下來我們就要解決下面的就是 ConsoleKernel這一段。 注冊 我們先回顧...
摘要:今天來寫寫這個框架的類加載機制版本原理在項目啟動時,通過注冊了要使用的類的自動加載處理方法,在類第一次被使用的時候,類文件通過該方法被引入,然后類才得以使用源碼分析在的入口文件,我們找到我們隨著這個路徑我們找打了這個主要內容如下其中是為了注 今天來寫寫Symfony2.8 這個框架的類加載機制 版本 Symfony 2.8 原理 在項目啟動時,Symfony 通過spl_autoloa...
摘要:索性讀一下它的源碼。行載入類載入類,這個類比較重要,實現了自動加載。注冊錯誤和異常處理機制加載慣例配置文件接下來我們看一下自動加載的實現方法。所以借助此函數可以達到自動加載。博客鏈接解讀源碼一自動加載 聽說 TP5 已經 RC4 了,曾經在 RC3 的時候用它寫過一個小東西。官方說從 RC4 以后改動不是太大。索性讀一下它的源碼。然后順便記錄一下,如有錯漏,請路過大神多多指正! 入口 ...
摘要:源碼分析自動加載系統會調用方法注冊自動加載,在這一步完成后,所有符合規范的類庫包括依賴加載的第三方類庫都將自動加載。是通過加載對應的文件進行注冊加載的。 源碼分析 自動加載 系統會調用 Loader::register()方法注冊自動加載,在這一步完成后,所有符合規范的類庫(包括Composer依賴加載的第三方類庫)都將自動加載。 系統的自動加載由下面主要部分組成: 1. 注冊系統的自...
摘要:如果遍歷后沒有找到,則加載失敗。在之后碰到了之后直接拿來用,提高系統自動加載的性能。這里我們就講完了注冊自動加載。使用自動加載我們在中定義了我們自動加載函數式方法。 繼 生命周期的第二篇,大家盡可放心,不會隨便鴿文章的 第一篇中,我們提到了入口腳本,也說了,里面注冊了自動加載的功能 本文默認你有自動加載和命名空間的基礎。如果沒有請 看此篇文章 php 類的自動加載與命名空間 自動加載...
閱讀 768·2021-09-26 09:55
閱讀 2058·2021-09-22 15:44
閱讀 1473·2019-08-30 15:54
閱讀 1324·2019-08-30 15:54
閱讀 2668·2019-08-29 16:57
閱讀 517·2019-08-29 16:26
閱讀 2490·2019-08-29 15:38
閱讀 2122·2019-08-26 11:48