Gradle 庖丁解牛(构建源头源码浅析)

http://blog.csdn.net/yanbober/article/details/60584621

是一个基于 Groovy 的框架了,也就是说我们得按照他的约束来玩了,和我们平时 Android 开发使用框架类似,一旦引入框架,我们就得按照框架的写法来规规矩矩的编写。Gradle 这个框架只负责流程约定,处理细节是我们自己的事,就像我们编译 Android App 时基于 Gradle 框架流程引入 apply plugin: 'com.android.application' 构建插件一样,具体做事是我们插件再约束的,插件又对我们简化了配置,我们只用基于 Gradle 框架和相关插件进行构建配置。

了所以简单粗暴的解释就是, Groovy 是一门语言,DSL 就是一种特定领域的配置文件,Gradle 就是基于 Groovy 的一种框架,就像我们以前做 Android 开发使用 Ant 构建一样,build.xml 就可以粗略的理解为 Ant 的 DSL 配置,所以我们编写 build.xml 时会相对觉得挺轻松(和后来的 Gradle 还是没法比的)。

搞清了他们之间的关系后我们就要深入看看为啥是这样了,因为关于 Gradle 构建现在中文网上的教程要么是教你如何配置 DSL 属性,要么就是教你如何使用 Groovy 去拓展自己的特殊 task,或者是教你怎么编写插件,却几乎没人提到 Gradle 这个构建框架的本质,所以使用起来总是心里发慌,有种不受控制的感觉。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

3 Gradle 源码源头分析

还记得我们前一小节讲的 gradlew 么,追踪溯源就从它开始,以前我也是出于好奇就去看了下它,发现这个 shell 脚本前面其实就是干了一堆没事干的事,大招就在它的最后一句,如下:

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
 
 
  • 1
  • 1

对的,大招就是 GradleWrapperMain,这货还支持通过 “$@” 传递参数,想想我们平时执行的 gradlew 命令,这下明白了吧,既然这样我们就拿他开涮,去看看他的源码,如下:

public class GradleWrapperMain {
    ......
    //执行 gradlew 脚本命令时触发调用的入口。
    public static void main(String[] args) throws Exception {
        //不多说,正如上面分析 gradlew 作用一样,去工程目录下获取 wrapper.jar 和 properties 文件。
        File wrapperJar = wrapperJar();
        File propertiesFile = wrapperProperties(wrapperJar);
        File rootDir = rootDir(wrapperJar);
        //解析gradlew 输入传递的参数等,反正就是一堆准备工作
        CommandLineParser parser = new CommandLineParser();
        ......
        //憋大招的两行
        WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(propertiesFile);
        wrapperExecutor.execute(
                args,
                new Install(logger, new Download(logger, "gradlew", wrapperVersion()), new PathAssembler(gradleUserHome)),
                new BootstrapMainStarter());
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上面 WrapperExecutor.forWrapperPropertiesFile 方法实质就是通过 Java Properties 去解析 gradle/wrapper/gradle-wrapper.properties 文件里的配置,譬如 distributionUrl 等;接着执行 wrapperExecutor.execute 方法,args 参数就是我们执行 gradlew 脚本时传递的参数,Install 实例就是管理本地本项目是否有 wrapper 指定版本的 gradle,木有就去下载解压等等,BootstrapMainStarter 实例就是 wrapper 执行 gradle 真实入口的地方,所以我们接着看看 execute 这个方法,如下:

public void execute(String[] args, Install install, BootstrapMainStarter bootstrapMainStarter) throws Exception {
    //config 就是 gradle-wrapper.properties 解析的配置
    File gradleHome = install.createDist(config);
    bootstrapMainStarter.start(args, gradleHome);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

到这里如果本地没有 wrapper 包装的 Gradle,就会下载解压等,然后准备一堆货,货备足后就调用了前面说的 BootstrapMainStarter 的 start 方法,如下:

public class BootstrapMainStarter {
    public void start(String[] args, File gradleHome) throws Exception {
        File gradleJar = findLauncherJar(gradleHome);
        URLClassLoader contextClassLoader = new URLClassLoader(new URL[]{gradleJar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
        Thread.currentThread().setContextClassLoader(contextClassLoader);
        Class<?> mainClass = contextClassLoader.loadClass("org.gradle.launcher.GradleMain");
        Method mainMethod = mainClass.getMethod("main", String[].class);
        mainMethod.invoke(null, new Object[]{args});
        if (contextClassLoader instanceof Closeable) {
            ((Closeable) contextClassLoader).close();
        }
    }
    ......
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

不解释,快上车,真的 Gradle 要现身了,Wrapper 的使命即将终结,我们把重点转到 org.gradle.launcher.GradleMain 的 main 方法,如下:

public class GradleMain {
    public static void main(String[] args) throws Exception {
        new ProcessBootstrap().run("org.gradle.launcher.Main", args);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

GG了,莫慌,我们的重点不是看懂 Gradle 的每一句代码,我们需要捡自己需要的重点,这货设置各种 ClassLoader 后最终还是调用了 org.gradle.launcher.Main 的 run 方法,实质就是 EntryPoint 类的 run 方法,因为 Main 类是 EntryPoint 类的实现类,而 EntryPoint 的 run 方法最主要做的事情就是创建了一个回调监听接口,然后调用了 Main 重写的 doAction 方法,所以我们去到 Main 的 doAction 看看,如下:

public class Main extends EntryPoint {
    ......
    protected void doAction(String[] args, ExecutionListener listener) {
        createActionFactory().convert(Arrays.asList(args)).execute(listener);
    }

    CommandLineActionFactory createActionFactory() {
        return new CommandLineActionFactory();
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这货实质调用了 CommandLineActionFactory 实例的 convert 方法得到 Action 实例,然后调用了 Action 的 execute 方法,我去,真特么绕的深,这弯溜的,我们会发现 CommandLineActionFactory 里的 convert 方法实质除过 log 记录准备外干的惟一一件事就是创建其内部类 WithLogging 的对象,这时候我们可以发现 Action 的 execute 方法实质就是调用了 WithLogging 的 execute 实现,如下:

public void execute(ExecutionListener executionListener) {
    //executionListener 是前面传入的回调实现实例
    //各种解析config,譬如参数的--内容等等,不是我们的重点
    //各种初始化、log启动等等,不是我们的重点
    ......
    //大招!!!外面new WithLogging实例时传入的参数!!!
    action.execute(executionListener);
    ......
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这不,最终还是执行了 new ExceptionReportingAction( 
new JavaRuntimeValidationAction( 
new ParseAndBuildAction(loggingServices, args)), 
new BuildExceptionReporter(loggingServices.get(StyledTextOutputFactory.class), loggingConfiguration, clientMetaData())));
对象的 execute 方法(上面的 action 就是这个对象),关于这个对象的创建我们只用关注 new JavaRuntimeValidationAction( 
new ParseAndBuildAction(loggingServices, args))
 这个参数即可,这也是一个 Action,实例化后在 ExceptionReportingAction 的 execute 调用了他的 execute,而 ParseAndBuildAction 的 execute 又被 JavaRuntimeValidationAction 的 execute 触发,有点包装模式的感觉,所以我们直接关注 ParseAndBuildAction 的实例化和 execute 方法,因为其他不是我们的重点,如下:

private class ParseAndBuildAction implements Action<ExecutionListener> {
    ......
    public void execute(ExecutionListener executionListener) {
        List<CommandLineAction> actions = new ArrayList<CommandLineAction>();
        //给你一个默认的 help 和 version 的 CommandLineAction 加入 actions 列表
        actions.add(new BuiltInActions());
        //创建一个 GuiActionsFactory 和 BuildActionsFactory 加入 actions 列表
        createActionFactories(loggingServices, actions);
        //依据参数给各个添加到列表的 CommandLineAction 对象进行配置
        CommandLineParser parser = new CommandLineParser();
        for (CommandLineAction action : actions) {
            action.configureCommandLineParser(parser);
        }
        //依据这几个参数获取创建一个可用的 Action<? super ExecutionListener>
        Action<? super ExecutionListener> action;
        try {
            ParsedCommandLine commandLine = parser.parse(args);
            //如果输入的命令中包含 gui 参数则创建 GuiActionsFactory 的 action 备用。
            //如果输入的命令中包含 help 或者 version 参数则创建 BuiltInActions 的 action 备用。
            //其他参数的则创建 BuildActionsFactory 的 action 备用。
            action = createAction(actions, parser, commandLine);
        } catch (CommandLineArgumentException e) {
            action = new CommandLineParseFailureAction(parser, e);
        }
        //执行我们创建的备用 action。。。。
        action.execute(executionListener);
    }
    ......
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

既然我们是追主线分析(关于执行命令中带 help、version、gui 的情况我们就不分析了,也比较简单,当我们执行 gradle –help 或者 gradle –gui 时打印的 help 或者弹出的 GUI 是 BuiltInActions 或者 GuiActionsFactory ,比较简单,不作分析),我们看核心主线 BuildActionsFactory 的 createAction 方法和通过 createAction 方法生成的 Runnable 的 run 方法即可(所谓的主线就是我们执行 gradle taskName 命令走的流程,譬如 gradle asseambleDebug 等),如下是 BuildActionsFactory 的 createAction 方法:

public Runnable createAction(CommandLineParser parser, ParsedCommandLine commandLine) {
    //命令各种转换包装
    Parameters parameters = parametersConverter.convert(commandLine, new Parameters());
    ......
    //三种判断,哪个中了就返回,runXXX系列方法实质都调用了runBuildAndCloseServices方法,只是参数不同而已
    if (parameters.getDaemonParameters().isEnabled()) {
        return runBuildWithDaemon(parameters.getStartParameter(), parameters.getDaemonParameters(), loggingServices);
    }
    if (canUseCurrentProcess(parameters.getDaemonParameters())) {
        return runBuildInProcess(parameters.getStartParameter(), parameters.getDaemonParameters(), loggingServices);
    }
    return runBuildInSingleUseDaemon(parameters.getStartParameter(), parameters.getDaemonParameters(), loggingServices);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

既然上面都判断最后调用都是类同的,那我们就假设调用了 runBuildInProcess 方法吧,如下:

private Runnable runBuildInProcess(StartParameter startParameter, DaemonParameters daemonParameters, ServiceRegistry loggingServices) {
    //创建client,这是个神奇的设计思路,大招!
    ServiceRegistry globalServices = ServiceRegistryBuilder.builder()
            .displayName("Global services")
            .parent(loggingServices)
            .parent(NativeServices.getInstance())
            .provider(new GlobalScopeServices(startParameter.isContinuous()))
            .build();
    //上面说的,BuildActionsFactory的createAction方法最后都是调用这个方法,只是传递参数不同而已!
    return runBuildAndCloseServices(startParameter, daemonParameters, globalServices.get(BuildExecuter.class), globalServices);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

此刻您可憋住了,别小看这么简单的一个方法,这玩意麻雀虽小五脏俱全啊,在我第一次看这部分源码时是懵逼的,好在看见了相关类的注释才恍然大悟,至于为啥我们现在来分析下。先说说 globalServices 对象的构建吧,其实和 createGlobalClientServices() 这个方法类似,随意咯,我们就随便看个,如下:

    private ServiceRegistry createGlobalClientServices() {
        return ServiceRegistryBuilder.builder()
                .displayName("Daemon client global services")
                .parent(NativeServices.getInstance())
                .provider(new GlobalScopeServices(false))
                .provider(new DaemonClientGlobalServices())
                .build();
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

看起来就是构造了一个 clientSharedServices 对象,然后交给下面的 runBuildAndCloseServices 方法使用,对的,就是这样的,只是这个 createGlobalClientServices() 方法真的很懵逼,ServiceRegistryBuilder 的 build() 方法实质是实例化了一个 DefaultServiceRegistry 对象,然后通过构造方法传递了 parent(NativeServices.getInstance()) 实例,通过 addProvider(provider) 方法传递了 provider(new GlobalScopeServices(false)) 和 provider(new DaemonClientGlobalServices()) 实例,巧妙的地方就在 DefaultServiceRegistry 类的注释上面,大家一定要先看注释,ServiceRegistryBuilder 的 build() 最后调用的是 DefaultServiceRegistry 对象的 addProvider 方法,实质调用的是 DefaultServiceRegistry 的 findProviderMethods(provider) 方法,如下:

private void findProviderMethods(Object target) {
    Class<?> type = target.getClass();
    RelevantMethods methods = getMethods(type);
    //把target自己和所有父类中以create开头的方法通过new DecoratorMethodService(target, method)包装加入到ownServices列表。
    for (Method method : methods.decorators) {
        ownServices.add(new DecoratorMethodService(target, method));
    }
    //把target自己和所有父类中以create开头的方法通过new DecoratorMethodService(target, method)包装加入到ownServices列表。
    for (Method method : methods.factories) {
        ownServices.add(new FactoryMethodService(target, method));
    }
    //把target自己和所有父类中叫cofigure的方法都反射调用一把。
    for (Method method : methods.configurers) {
        applyConfigureMethod(method, target);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这时咱们先回过头看看前面说的 createGlobalClientServices() 方法,我们发现其中的 NativeServices、FileSystemServices、DaemonClientGlobalServices 都没有 config 方法,但是有一堆 createXXX 的方法,这些都会被加入 ownServices 列表,但是 GlobalScopeServices 类却有 config 方法,如下:

public class GlobalScopeServices {
    ......
    //该方法在DefaultServiceRegistry的findProviderMethods(provider)中被反射调用。
    //registration是DefaultServiceRegistry的newRegistration()方法返回的ServiceRegistration匿名实现对象。
    //classLoaderRegistry就是一个
    void configure(ServiceRegistration registration, ClassLoaderRegistry classLoaderRegistry) {
        //getAll根据PluginServiceRegistry.class传入后的一系列规则,找到所有factory方法。
        final List<PluginServiceRegistry> pluginServiceFactories = new DefaultServiceLocator(classLoaderRegistry.getRuntimeClassLoader(), classLoaderRegistry.getPluginsClassLoader()).getAll(PluginServiceRegistry.class);
        for (PluginServiceRegistry pluginServiceRegistry : pluginServiceFactories) {
            registration.add(PluginServiceRegistry.class, pluginServiceRegistry);
            if (pluginServiceRegistry instanceof GradleUserHomeScopePluginServices) {
                registration.add(GradleUserHomeScopePluginServices.class, (GradleUserHomeScopePluginServices) pluginServiceRegistry);
            }
            pluginServiceRegistry.registerGlobalServices(registration);
        }
    }
    ......
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

到这里你可以暂时松口气了,上面 config 等等一堆都是在做准备,说白了就是各种列表注册都放好,然后 DefaultServiceRegistry 的 get(…) 系列方法实质都是通过 doGet(Type serviceType) 的 invok 方法调用等。接着让我们把目光移到上面运行 gradle taskName 命令时分析的 BuildActionsFactory 的 runBuildInProcess(…) 方法,我们在该方法中调用 runBuildAndCloseServices(…) 方法时第三个参数传递的是 globalServices.get(BuildExecuter.class), 也就是调用了 DefaultServiceRegistry 的 get(BuildExecuter.class) 方法,刚刚说了 DefaultServiceRegistry 的 get(…) 实质就是 invoke 一个方法,在这里 serviceType 参数是 BuildExecuter.class,所以调用的就是 ToolingGlobalScopeServices 的 createBuildExecuter(…) 方法,ToolingGlobalScopeServices 是 LauncherServices 的 registerGlobalServices(…) 方法实例化的,而 LauncherServices implements PluginServiceRegistry 又是刚刚上面分析 GlobalScopeServices 的 configure(…) 方法里被实例化添加的,LauncherServices 的 registerGlobalServices(…) 方法也是在那调用的。

绕了一圈真是折腾,突然发现累觉不爱,觉得 Gradle 框架中 DefaultServiceRegistry 类的设计真的是挺颠覆我认知的,虽然没啥黑科技,但是这个设计思路真的是坑爹,很容易绕懵逼,好在后来整明白了,真想说句 ZNMKD。好了,牢骚也发完了,下面该正题了,我们接着上面说的去看看 LauncherServices 内部 ToolingGlobalScopeServices 的 createBuildExecuter(…) 方法,一贯做法,关注主线,咱们会发现这方法最主要的就是层层简单代理模式包装实例化 BuildActionExecuter 对象,最关键的就是实例化了 InProcessBuildActionExecuter 对象,这个对象就一个 execute 方法(这个 execute 方法被调用的地方就在上面分析的 BuildActionsFactory 的 runBuildInProcess 方法返回的 RunBuildAction 对象的 run 方法中,RunBuildAction 对象的 run 方法又是前面分析的源头触发的),如下:

public class InProcessBuildActionExecuter implements BuildActionExecuter<BuildActionParameters> {
    ......
    //BuildActionsFactory的runBuildInProcess方法返回的RunBuildAction对象的run方法触发该方法调用。
    //参数都是RunBuildAction中传递的,RunBuildAction的参数又是前面分析的BuildActionsFactory的runBuildInProcess方法传递。
    public Object execute(BuildAction action, BuildRequestContext buildRequestContext, BuildActionParameters actionParameters, ServiceRegistry contextServices) {
        //创建DefaultGradleLauncher对象,真是活菩萨啊,总算看到光明了!
        GradleLauncher gradleLauncher = gradleLauncherFactory.newInstance(action.getStartParameter(), buildRequestContext, contextServices);
        try {
            gradleLauncher.addStandardOutputListener(buildRequestContext.getOutputListener());
            gradleLauncher.addStandardErrorListener(buildRequestContext.getErrorListener());
            GradleBuildController buildController = new GradleBuildController(gradleLauncher);
            buildActionRunner.run(action, buildController);
            return buildController.getResult();
        } finally {
            gradleLauncher.stop();
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

赞,到此真想说句真是不容易啊!!!总算看见光明了,我 Gradle 的大 GradleLauncher,为啥这么称呼呢,因为你看了 GradleLauncher 实现你会有种柳暗花明又一村的感觉,真的,你会觉得前面这些复杂的初始化就是为了等到这个实例的到来,因为 GradleLauncher 实现里就真真切切的告诉你了 Gradle 构建的三大生命周期——-初始化、配置、执行,不信你看:

public class DefaultGradleLauncher implements GradleLauncher {
    //大名鼎鼎的 gradle 构建三步生命周期!!!!有种亲爹的感觉!
    private enum Stage {
        Load, Configure, Build
    }
    ......
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

到此构建源头就分析完了,下一篇会接着这里分析 DefaultGradleLauncher 及后续真正开始构建的流程,所以这时候你回过头会发现 Gradle 其实也就那么回事,也是代码写的(哈哈,找打!),只是这一篇我们只分析了 Gradle 框架自身初始化(非构建生命周期的初始化,要区分)的核心流程。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

4 总结

为了 Gradle 庖丁解牛系列做铺垫,本篇主要进行了 Gradle 基础铺垫说明总结,后面对 Gradle 框架自身初始化(非构建生命周期的初始化,要区分)的核心流程进行了分析,通过本文我们至少应该知道如下:

  • Gradle 只是一个构建框架,而且大多数代码是 Java 和 Groovy 编写。
  • gradlew 是 gradle 的一个兼容包装工具。
  • 执行 gradle 或者 gradlew 命令开始进行构建生命周期前做的第一步是对 Gradle 框架自身的初始化(本文浅析内容)。

有了这一篇的铺垫,下面几篇我们会以此分析继续,不过主线都是基于执行一个 gradle taskName 命令,所以如果想看懂这个系列文章,建议先补习下 Gradle 构建基础。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值