Laravel学习笔记之IoC Container实例化源码解析

说明:本文主要学习Laravel容器的实例化过程,主要包括Register Base Bindings, Register Base Service Providers , Register Core Container Aliases and Set the Base Path等四个过程。同时并把自己的一点研究心得分享出来,希望对别人有所帮助。

开发环境:Laravel5.3 + PHP7 + OS X10.11

Laravel的入口文件是public/index.php文件,首先第一步加载composer的autoload文件:

// bootstrap/autoload.php
require __DIR__.'/../vendor/autoload.php';

关于composer自动加载原理可看这篇文章:Laravel学习笔记之Composer自动加载

然后开始实例化Application容器得到全局变量$app:

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

输入的是project的根路径,研究下\Illuminate\Foundation\Application的构造函数源码:

    public function __construct($basePath = null)
    {
        $this->registerBaseBindings();

        $this->registerBaseServiceProviders();

        $this->registerCoreContainerAliases();

        if ($basePath) {
            $this->setBasePath($basePath);
        }
    }

Create Application过程中做了4件事:

 1. register base bindings.

 2. register base service providers(\Illuminate\Events\EventServiceProvider and \Illuminate\Routing\RoutingServiceProvider).

 3. register core service aliases 
('app', 'auth', 'auth.driver', 'blade.compiler', 'cache', 'cache.store', 'config', 'cookie', 'encrypter', 'db', 'db.connection', 
'events', 'files', 'filesystem', 'filesystem.disk', 'filesystem.cloud', 'hash', 'translator', 'log', 'mailer', 
'auth.password', 'auth.password.broker', 'queue', 'queue.connection', 'queue.failer', 'redirect', 'redis', 'request', 
'router', 'session', 'session.store', 'url', 'validator', 'view'), and these core service will be registered later.

 4. set the base path, including 
'path' = __DIR__ . '/app', 'path.base' = __DIR__ , 'path.lang' = __DIR__ . '/resources/lang',
'path.config' = __DIR__ . '/config', 'path.public' = __DIR__ . '/public', 'path.storage' = __DIR__ . '/storage', 
'path.database' = __DIR__ . '/database', 'path.resources' = __DIR__ . '/resources', 
'path.bootstrap' = __DIR__ . '/bootstrap'. U can get theses path everywhere in the way, 
e.g.  public_path('/js/app.js') === __DIR__ . '/public/js/app.js';

1. Register Base Bindings

基础绑定主要是绑定当前Application对象进容器,绑定的是同一对象,但给了两个名字:

$this->instance('app', $this);

$this->instance('Illuminate\Container\Container', $this);

OK, 那instance()是如何绑定服务的?
\Illuminate\Foundation\Application是extends from the \Illuminate\Container\Container,看instance()源码:

    /**
     * Register an existing instance as shared in the container.
     *
     * @param  string  $abstract
     * @param  mixed   $instance
     * @return void
     */
    public function instance($abstract, $instance)
    {
        // $abstract如果是string,截取右边的'\', 如\Illuminate\Foundation\Application => Illuminate\Foundation\Application
        $abstract = $this->normalize($abstract);
        
        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);

            $this->alias($abstract, $alias);
        }

        unset($this->aliases[$abstract]);

        $bound = $this->bound($abstract);

        $this->instances[$abstract] = $instance;

        if ($bound) {
            $this->rebound($abstract);
        }
    }

分解代码,看别名的注册:

    if (is_array($abstract)) {
        list($abstract, $alias) = $this->extractAlias($abstract);

        $this->alias($abstract, $alias);
    }
    
        ...
        
    protected function extractAlias(array $definition)
    {
        return [key($definition), current($definition)];
    }   
    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $this->normalize($abstract);
    }     

如果$abstract是数组, e.g. $this->instance(['app' => '\\Illuminate\\Foundation\\Application'], $this),则app是alias name,存入Container class的$aliases[ ]属性中,这样存入值是:

$aliases = [
    'app'=> '\Illuminate\Foundation\Application',
];

然后在注册到属性$instances[ ]中,则上面的绑定代码类似于;

// 这里加个别名
$this->instances['app' => '\Illuminate\Foundation\Application'] = (new \Illuminate\Foundation\Application($path = __DIR__));
$this->instances['Illuminate\Container\Container'] = (new \Illuminate\Foundation\Application($path = __DIR__));

可以PHPUnit测试下别名这个feature:

public function testAlias ()
{
    // make()是从Container中解析出service,与instance正好相反
    $object1 = App::make('app');
    $object2 = App::make('\Illuminate\Foundation\Application');
    $this->assertInstanceOf(\Illuminate\Foundation\Application::class, $object1);
    $this->assertInstanceOf(\Illuminate\Foundation\Application::class, $object2);
}

由于不是单例绑定singleton(),这里$object1与$object2都是\Illuminate\Foundation\Application的对象,但不是同一对象。singleton()和make()稍后讨论下。

同时检查下之前是否已经绑定了,如果已经绑定了,则执行之前rebinding()的回调函数,主要是执行Container的$reboundCallbacks[ ]属性值。Container提供了rebinding()函数供再一次补充绑定(如再给'app'绑定一些之前绑定没有的的行为),PHPUnit测试下:

public function testReboundCallbacks() 
{
    // Arrange
    $container = new Container;
    
    // Actual
    $container->instance('app', function(){
        return 'app1';
    });
    $a = 0
    $container->rebinding('app', function() use (&$a) {
        $a = 1;
    });
    // 再次绑定时,触发上一次rebinding中绑定该'app'的回调
    $container->instance('app', function () {
        return 'app2';
    });
    
    // Assert
    $this->assertEqual(1, $a);
}

Container的作用是供service的绑定和解析,绑定有三种方法:bind(),singleton(),instance();解析是make(),稍后讨论下容器中最重要的这几个feature。

2. Register Base Service Providers

绑定了名为'app','IlluminateContainerContainer'的两个service后(尽管绑定的service相同),看下绑定了两个基础service provider:

$this->register(new \Illuminate\Events\EventServiceProvider($this));
$this->register(new \Illuminate\Routing\RoutingServiceProvider($this));

两个基础的service provider is: IlluminateEventsEventServiceProvider和IlluminateRoutingRoutingServiceProvider。看下是如何注册两个service provider:

    public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }
        
        if (is_string($provider)) {
            $provider = $this->resolveProviderClass($provider);
        }

        if (method_exists($provider, 'register')) {
            $provider->register();
        }
        
        foreach ($options as $key => $value) {
            $this[$key] = $value;
        }

        $this->markAsRegistered($provider);

        // If the application has already booted, we will call this boot method on
        // the provider class so it has an opportunity to do its boot logic and
        // will be ready for any usage by the developer's application logics.
        if ($this->booted) {
            $this->bootProvider($provider);
        }

        return $provider;
    }

首先检查是否已经注册了,如果注册了就直接返回,主要是检查Application class 的$serviceProviders[ ]的值,看下代码:

    if (($registered = $this->getProvider($provider)) && ! $force) {
        return $registered;
    }

    ...

    public function getProvider($provider)
    {
        $name = is_string($provider) ? $provider : get_class($provider);

        return Arr::first($this->serviceProviders, function ($value) use ($name) {
            return $value instanceof $name;
        });
    }    

如果输入的是字符串,就直接new $provider($this)生成对象,所以上面两个注册可以这么写:

$this->register(\Illuminate\Events\EventServiceProvider::class);
$this->register(\Illuminate\Routing\RoutingServiceProvider::class);

然后执行service provider中的register()方法,稍后看下两个base service provider注册了哪些service。

然后把注册过的service provider标记为provided,就是写入到$serviceProviders[ ]中,而开始是先检查$serviceProviders[ ]中,有没有已经注册过的将要注册的service。看下markAsRegistered()源码:

    protected function markAsRegistered($provider)
    {
        $this['events']->fire($class = get_class($provider), [$provider]);

        $this->serviceProviders[] = $provider;

        $this->loadedProviders[$class] = true;
    }

这里还用了刚注册的'events' service来触发该service provider已经注册的事件,并把该service provider写入到已经加载的属性中loadedProviders[ ].

然后检查程序是否已经启动,如果已经启动完成了,再执行每一个service provider中的boot()方法,这里会发现为啥每一个service provider里经常出现register()和boot()方法,并且register()是注册服务的,等所有服务注册完,再去boot()一些东西。当然,这里程序刚刚注册第一个EventServiceProvider,程序离完全启动还早着呢。不过,可以先看下这里的bootProvider()方法源码:

    protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }
    /**
     * Call the given Closure / class@method and inject its dependencies.
     *
     * @param  callable|string  $callback
     * @param  array  $parameters
     * @param  string|null  $defaultMethod
     * @return mixed
     */
    public function call($callback, array $parameters = [], $defaultMethod = null)
    {
        if ($this->isCallableWithAtSign($callback) || $defaultMethod) {
            return $this->callClass($callback, $parameters, $defaultMethod);
        }

        $dependencies = $this->getMethodDependencies($callback, $parameters);

        return call_user_func_array($callback, $dependencies);
    }

重点看下call()这个Container另一个重要的函数,如果这么调用call(EventServiceProvider@register),那就通过Container::callClass()来解析出class和method,然后在调用call(),看下callClass()源码:

protected function callClass($target, array $parameters = [], $defaultMethod = null)
    {
        $segments = explode('@', $target);
        $method = count($segments) == 2 ? $segments[1] : $defaultMethod;

        if (is_null($method)) {
            throw new InvalidArgumentException('Method not provided.');
        }

        // 然后在这样调用call([$class, $method], $parameters)
        return $this->call([$this->make($segments[0]), $method], $parameters);
    }

也就是说,如果call(EventServiceProvider@register)这种方式的话先转化成call([$class, $method], $parameters)来调用,当然要是直接这种方式就不用在转换了。这里是通过[(new EventServiceProvider($app)), 'boot']类似这种方式来调用的。在调用boot()时有依赖怎么办?使用[$class, $method]通过getMethodDependencies($parameters)来获取$dependencies,看下getMethodDependencies($parameters)源码:

    protected function getMethodDependencies($callback, array $parameters = [])
    {
        $dependencies = [];

        foreach ($this->getCallReflector($callback)->getParameters() as $parameter) {
            $this->addDependencyForCallParameter($parameter, $parameters, $dependencies);
        }

        return array_merge($dependencies, $parameters);
    }
    protected function getCallReflector($callback)
    {
        if (is_string($callback) && strpos($callback, '::') !== false) {
            $callback = explode('::', $callback);
        }

        if (is_array($callback)) {
            return new ReflectionMethod($callback[0], $callback[1]);
        }

        return new ReflectionFunction($callback);
    }
    protected function addDependencyForCallParameter(ReflectionParameter $parameter, array &$parameters, &$dependencies)
    {
        if (array_key_exists($parameter->name, $parameters)) {
            $dependencies[] = $parameters[$parameter->name];

            unset($parameters[$parameter->name]);
        } elseif ($parameter->getClass()) {
            $dependencies[] = $this->make($parameter->getClass()->name);
        } elseif ($parameter->isDefaultValueAvailable()) {
            $dependencies[] = $parameter->getDefaultValue();
        }
    }

这里是通过PHP的Reflector Method来获取依赖,依赖如果是对象的话再继续make()自动解析出service,是个外部传进来的值则代入,有默认值传默认值。反射(Reflector)是PHP的一个重要的高级特性,值得研究。
总的来说,在boot()方法中如果有dependency,container会自动解析,不管该dependency是不是某个service。这就是Method Injection,我们知道Dependency Injection有两种:Constructor Injection and Method Injection,这里可看到Method Injection是如何实现的。

OK,然后看下两个service provider注册了些什么?
首先注册EventServiceProvider中提供的service,看有哪些:

public function register()
{
    $this->app->singleton('events', function ($app) {
        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
            return $app->make('Illuminate\Contracts\Queue\Factory');
        });
    });
}

OK,只有一个名为'events'的service注册到容器中了,并且是单例注册的。看下singleton()的源码:

    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }
    
    public function bind($abstract, $concrete = null, $shared = false)
    {
        $abstract = $this->normalize($abstract);

        $concrete = $this->normalize($concrete);

        // 如果是数组,抽取别名并且注册到$aliases[]中,上文已经讨论
        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);

            $this->alias($abstract, $alias);
        }

        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // If the factory is not a Closure, it means it is just a class name which is
        // bound into this container to the abstract type and we will just wrap it
        // up inside its own Closure to give us more convenience when extending.
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        // If the abstract type was already resolved in this container we'll fire the
        // rebound listener so that any objects which have already gotten resolved
        // can have their copy of the object updated via the listener callbacks.
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }
    
    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

singleton()实际上就是$shared = true 的bind()。同时舍弃掉$instances[]中已经注册过的名为$abstract的service,当然别名数组也别忘了舍弃。
如果$concrete没有提供,则使用$abstract自动补全$concrete,并且使用getClosure()封装下做个Closure:

    protected function getClosure($abstract, $concrete)
    {
        // $c 就是$container,即Container Object,会在回调时传递给这个变量
        return function ($c, $parameters = []) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';

            return $c->$method($concrete, $parameters);
        };
    }

$concrete没有提供绑定的情况,如:$this->singleton(IlluminateContainerContainer::class); 只提供了$abstract.

这里,就是向$bindings[ ]中注册下,现在它的值类似这样:

$bindings = [
    'events' => [
        'concrete' => function ($app) {
                        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {return $app->make('Illuminate\Contracts\Queue\Factory');});
                      },
        'shared'   => true,
    ],
];

已经说了singleton()和binding()注册的区别就是'shared'的值不一样,如果是$this->app->binding('events', Closure),则$bindings[ ]值是:

$bindings = [
    'events' => [
        'concrete' => function ($app) {
                        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {return $app->make('Illuminate\Contracts\Queue\Factory');});
                      },
        'shared'   => false,
 
    ],
 
];

OK,看下RoutingServiceProvider中注册了些什么service?
上文说过,Application中register()会调用service provider中的register()方法,看下\Illuminate\Routing\RoutingServiceProvider源码就发现其注册了几个service:'router', 'url', 'redirect', Psr\Http\Message|ServerRequestInterface::class, Psr\Http\Message\ResponseInterface::class, Illuminate\Contracts\Routing\ResponseFactory::class
只有Illuminate\Contracts\Routing\ResponseFactory::class是singleton(),其余是bind(),e.g. 'router' service source code:

$this->app['router'] = $this->app->share(function ($app) {
    return new Router($app['events'], $app);
});

为什么说是bind()?并且$this->app['router']是啥意思?

OK, 看下share()的源码:

public function share(Closure $closure)
{
    return function ($container) use ($closure) {
        static $object;
 
        if (is_null($object)) {
            $object = $closure($container);
        }
 
        return $object;
    };
}

share()仅仅执行$closure()并传入$container,所以上面的'router' service代码类似于:

$this->app['router'] = new Router($app['events'], $app);

$this->app是Container对象,而Container implement ArrayAccess这个Interface,实现对类的属性做数组式访问,所以Container必须实现四个方法:

@link http://php.net/manual/en/arrayaccess.offsetset.php
public function offsetExists($offset);
public function offsetGet($offset);
public function offsetSet($offset, $value);
public function offsetUnset($offset);

这里是对$this->app赋值,所以看下offsetSet()源码:

public function offsetSet($key, $value)
{
    if (! $value instanceof Closure) {
        $value = function () use ($value) {
            return $value;
        };
    }
 
    $this->bind($key, $value);
}

这里是用bind()来绑定到container中,所以上文中说是bind(),而不是其他。所上文的代码类似于这样:

$this->app['router'] = new Router($app['events'], $app);
  
is like:
  
$object = new Router($app['events'], $app);
$this->bind('router', function () use ($object) {return $object});

总的来说,就是通过注册EventServiceProvider and RoutingServiceProvider来绑定了一些service, e.g. 'events', 'router' and so on.

3. Register Core Container Aliases

由于PHP使用namespace来命名class,有时类名很长,所以需要做个别名alias图方便。看下registerCoreContainerAliases()的源码:

    public function registerCoreContainerAliases()
    {
        $aliases = [
            'app'                  => ['Illuminate\Foundation\Application', 'Illuminate\Contracts\Container\Container', 'Illuminate\Contracts\Foundation\Application'],
            'auth'                 => ['Illuminate\Auth\AuthManager', 'Illuminate\Contracts\Auth\Factory'],
            'auth.driver'          => ['Illuminate\Contracts\Auth\Guard'],
            'blade.compiler'       => ['Illuminate\View\Compilers\BladeCompiler'],
            'cache'                => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'],
            'cache.store'          => ['Illuminate\Cache\Repository', 'Illuminate\Contracts\Cache\Repository'],
            'config'               => ['Illuminate\Config\Repository', 'Illuminate\Contracts\Config\Repository'],
            'cookie'               => ['Illuminate\Cookie\CookieJar', 'Illuminate\Contracts\Cookie\Factory', 'Illuminate\Contracts\Cookie\QueueingFactory'],
            'encrypter'            => ['Illuminate\Encryption\Encrypter', 'Illuminate\Contracts\Encryption\Encrypter'],
            'db'                   => ['Illuminate\Database\DatabaseManager'],
            'db.connection'        => ['Illuminate\Database\Connection', 'Illuminate\Database\ConnectionInterface'],
            'events'               => ['Illuminate\Events\Dispatcher', 'Illuminate\Contracts\Events\Dispatcher'],
            'files'                => ['Illuminate\Filesystem\Filesystem'],
            'filesystem'           => ['Illuminate\Filesystem\FilesystemManager', 'Illuminate\Contracts\Filesystem\Factory'],
            'filesystem.disk'      => ['Illuminate\Contracts\Filesystem\Filesystem'],
            'filesystem.cloud'     => ['Illuminate\Contracts\Filesystem\Cloud'],
            'hash'                 => ['Illuminate\Contracts\Hashing\Hasher'],
            'translator'           => ['Illuminate\Translation\Translator', 'Symfony\Component\Translation\TranslatorInterface'],
            'log'                  => ['Illuminate\Log\Writer', 'Illuminate\Contracts\Logging\Log', 'Psr\Log\LoggerInterface'],
            'mailer'               => ['Illuminate\Mail\Mailer', 'Illuminate\Contracts\Mail\Mailer', 'Illuminate\Contracts\Mail\MailQueue'],
            'auth.password'        => ['Illuminate\Auth\Passwords\PasswordBrokerManager', 'Illuminate\Contracts\Auth\PasswordBrokerFactory'],
            'auth.password.broker' => ['Illuminate\Auth\Passwords\PasswordBroker', 'Illuminate\Contracts\Auth\PasswordBroker'],
            'queue'                => ['Illuminate\Queue\QueueManager', 'Illuminate\Contracts\Queue\Factory', 'Illuminate\Contracts\Queue\Monitor'],
            'queue.connection'     => ['Illuminate\Contracts\Queue\Queue'],
            'queue.failer'         => ['Illuminate\Queue\Failed\FailedJobProviderInterface'],
            'redirect'             => ['Illuminate\Routing\Redirector'],
            'redis'                => ['Illuminate\Redis\Database', 'Illuminate\Contracts\Redis\Database'],
            'request'              => ['Illuminate\Http\Request', 'Symfony\Component\HttpFoundation\Request'],
            'router'               => ['Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'],
            'session'              => ['Illuminate\Session\SessionManager'],
            'session.store'        => ['Illuminate\Session\Store', 'Symfony\Component\HttpFoundation\Session\SessionInterface'],
            'url'                  => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
            'validator'            => ['Illuminate\Validation\Factory', 'Illuminate\Contracts\Validation\Factory'],
            'view'                 => ['Illuminate\View\Factory', 'Illuminate\Contracts\View\Factory'],
        ];

        foreach ($aliases as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

给class name注册个别名,并且在相同数组里有着共同的别名,e.g. 'IlluminateFoundationApplication', 'IlluminateContractsContainerContainer' and 'IlluminateContractsFoundationApplication' share the same alias name 'app'.

4. Set the Base Path

Application Constructor里需要传入一个path这个原料来构造类,这里path是这个project的当前绝对路径。同时绑定一些常用的文件夹路径供将来使用,看下构造函数中源码:

public function __construct($basePath)
{
      ...
      
    if ($basePath) {
        $this->setBasePath($basePath);
    }
}
public function setBasePath($basePath)
{
    $this->basePath = rtrim($basePath, '\/');
 
    $this->bindPathsInContainer();
 
    return $this;
}
protected function bindPathsInContainer()
{
    $this->instance('path', $this->path());
    $this->instance('path.base', $this->basePath());
    $this->instance('path.lang', $this->langPath());
    $this->instance('path.config', $this->configPath());
    $this->instance('path.public', $this->publicPath());
    $this->instance('path.storage', $this->storagePath());
    $this->instance('path.database', $this->databasePath());
    $this->instance('path.resources', $this->resourcePath());
    $this->instance('path.bootstrap', $this->bootstrapPath());
}

instance()上文已经讨论过,所以这里的$instances[ ]类似于这样:

$instances = [
    'path'           => __DIR__ . '/app',
    'path.base'      => __DIR__ . '/',
    'path.lang'      => __DIR__ . '/resources/lang',
    'path.config'    => __DIR__ . '/config',
    'path.public'    => __DIR__ . '/public',
    'path.storage'   => __DIR__ . '/storage',
    'path.database'  => __DIR__ . '/database',
    'path.resources' => __DIR__ . '/resources',
    'path.bootstrap' => __DIR__ . '/bootstrap',
];

OK,看下bootstrap/app.php文件,在得到$app这个实例化对象后,再单例绑定Two Kernel and One Exception:

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    RightCapital\Admin\Http\Kernel::class
);
 
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    RightCapital\Admin\Console\Kernel::class
);
 
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    RightCapital\Admin\Exceptions\Handler::class
);

最后,就得到一个塞满好几个service的容器了,而未被实例化前是个空Container.整个的Application的实例化过程分析就OK了。

总结:本文主要学习了Application的实例化过程,主要学习了实例化过程中向这个IoC(Inversion of Control) Container绑定了哪些service,并讨论了绑定的三个方法:bind(),singleton(),instance(),解析方法make()留到单独研究Container时再讨论吧。下次分享下Container学习心得,并写上PHPUnit测试,到时见。

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值