安卓 gradle 编译流程

./gradlew installDebug 命令行启动*

创建项目的时候 IDE 在项目的根目录会生成 gradlew 可执行文件,gradlew
中很多逻辑是判断系统类型的,gradlew.bat 是
windows下的实现思路一样的。这里的主要的逻辑:

  1. 先定义了 java 程序执行需要的 CLASSPATH 目录

  2. 然后把 JAVACMD 设置为 java 的入口文件

  3. 最后把开发者定义的 JAVA_OPTS、DEFAULT_JVM_OPTS、进程名都拼接为一串参数执行

  4. 文件最后一行的 exec 开始打包,可以替换为 echo 看具体执行的命令是啥

gradle-wrapper.jar 是 gradle 的入口引导

上文中提到的 CLASSPATH 是 gradle 的入口 jar 实际目录在项目根目录
./gradle/wrapper/ 下,并不是 gradle 的编译主逻辑,根据 gradlew
可执行文件指定的入口类 org.gradle.wrapper.GradleWrapperMain

在里面找到了 main 函数,主要逻辑是:

  1. 加载项目目录下的 gradle.properties 到环境变量

  2. 解析同目录下的 gradle-wrapper.properties 用 Install 下载指定的url 到本地

  3. BootstrapMainStarter 加载真正的 org.gradle.launcher.GradleMain
    进行打包,如果看源码去目录
    ~/.gradle/wrapper/dists/gradle-6.7.1-bin/bwlcbys1h7rz3272sye1xwiv6/gradle-6.7.1/lib

这样设计的好处应该是把 gradle 版本和项目解耦,方便替换 gradle
的版本,如果想自定义 gradle 版本或者因为国内访问 url
超时,可以直接替换配置文件的 distributionUrl

GradleUserHomeLookup 里主要声明了默认目录 ~/.gradle

WrapperExecutor 并不是线程池,gradle 代码特色是只要切换一层逻辑就会有
xxxExecutor,Install 和 BootstrapMainStarter 实际是串行的

这里使用 org.gradle.debug=true 打断点不生效,因为这里是纯 java 进程没有处理任何
gradle 参数,应该使用 java
-agentlib:jdwp=transport=dt_socket,server=y,address=5006 -classpath
./gradle/wrapper/gradle-wrapper.jar org.gradle.wrapper.GradleWrapperMain
installdebug 和 jdb -attach
localhost:5006

两个命令行进行断点调试

启动 Daemon 进程

  1. GradleMain.main-> ProcessBootstrap.run 这是一个通用的替换类加载器的反射

  2. org.gradle.launcher.Main.run-> .doAction

  3. DefaultCommandLineActionFactory.convert->WithLogging.execute

  4. ExceptionReportingAction.execute

  5. NativeServicesInitializingAction.execute

  6. DefaultCommandLineActionFactory.ParseAndBuildAction.execute

    1. DefaultCommandLineActionFactory.BuiltInActions.createAction
    2. BuildActionsFactory.createAction 区分是否要使用守护进程执行
    3. RunBuildAction.run 使用的 BuildActionExecuter 是上一步提供的
    4. DaemonClient.execute通过 DefaultDaemonConnector 连接 ExecutorFactory
      执行池,只管 dispatch 发送参数,DefaultDaemonStarter 才开始处理 gradle
      任务,这里判断 org.gradle.daemon.debug 添加了调试功能,端口固定 5005
  7. DefaultExecActionFactory.newExec->DefaultExecHandleBuilder

  8. DefaultExecHandle.start

  9. ProcessBuilder.start 开启了新的线程

  10. GradleDaemon.main > ProcessBootstrap.run 是新进程的入口,这里可以用
    org.gradle.debug=true 打断点调试了

  11. DaemonMain.doAction 创建 Daemon 和 DaemonServices,同时在 DaemonServices
    的构造方法初始化 GlobalScopeServices 内部使用 DefaultServiceLocator 加载
    META-INF/services/”配置的所有 PluginServiceRegistry,确定了 gradle
    能处理哪些任务

  12. DaemonServerConnector
    的实现类DaemonTcpServerConnector(TcpIncomingConnector).start()
    当每次执行./gradlew installDebug 的时候 TcpIncomingConnector 会收到
    ConnectCompletion 再包装成 SynchronizedDispatchConnection(SocketConnection)
    分发给 DefaultIncomingConnectionHandler.handle 切换线程转到
    ConnectionWorker.run ,经过DefaultDaemonConnection.receive 读取到Command
    实际类型为
    org.gradle.launcher.daemon.protocol.Build(ExecuteBuildAction(StartParameterInternal(DefaultTaskExecutionRequest(args))))
    里面包含里命令的字符串,然后handleCommand
    处理数据。这是一个死循环,这个进程前面的逻辑只执行一次

  13. DaemonCommandExecuter.executeCommand-> DaemonCommandExecution. proceed

  14. 顺序执行下面的 action,都是在 DaemonServices.createDaemonCommandActions 创建
    Daemon的时候准备好的,实例创建一次执行多次。前面都是验证是否可以执行的,最后一个
    ExecuteBuild 触发 BuildExecuter。这种一层套一层的设计思路在 gradle
    源码中十分普遍

    1. HandleStop
    2. HandleInvalidateVirtualFileSystem
    3. HandleCancel
    4. HandleReportStatus
    5. ReturnResult
    6. StartBuildOrRespondWithBusy
    7. EstablishBuildEnvironment
    8. LogToClient
    9. LogAndCheckHealth
    10. ForwardClientInput
    11. RequestStopIfSingleUsedDaemon
    12. ResetDeprecationLogger
    13. WatchForDisconnection
    14. ExecuteBuild
  15. 继续往下走主流程,又是一层套一层的执行逻辑,代码风格上 16
    点像装饰,这条像代理。我猜不是同一个开发写的,经过同一个老板 review
    的,代码风格不一样但是执行逻辑完全一样

    1. SetupLoggingActionExecuter
    2. SessionFailureReportingActionExecuter
    3. StartParamsValidatingActionExecuter
    4. GradleThreadBuildActionExecuter
    5. SessionScopeLifecycleBuildActionExecuter
      这前面的都只创建一个实例,在LauncherServices$ToolingGlobalScopeServices.createBuildExecuter
      创建,所有的参数保存在BuildSessionState 中
    6. SubscribableBuildActionExecuter
      后面的每次执行到任务创建LauncherServices$ToolingBuildSessionScopeServices.
      createBuildExecuter
    7. ContinuousBuildActionExecuter
    8. BuildTreeScopeLifecycleBuildActionExecuter ,所有的参数保存在 BuildTreeState
    9. InProcessBuildActionExecuter 由 DefaultIncludedBuildRegistry.createRootBuild
      方法把前面收到的 ExecuteBuildAction 转换为
      GradleBuildController(DefaultGradleLauncher(BuildScopeServices(DefaultServiceRegistry.OwnServices(BuildDefinition(StartParameter)))))
  16. LauncherServices.createActionExecuter 创建了下面这些Runner

    1. RunAsBuildOperationBuildActionRunner
    2. BuildCompletionNotifyingBuildActionRunner
    3. FileSystemWatchingBuildActionRunner
    4. ValidatingBuildActionRunner
    5. BuildOutcomeReportingBuildActionRunner
    6. ChainingBuildActionRunner
    7. ExecuteBuildActionRunner

ServiceRegistry 和 PluginServiceRegistry 所有逻辑的起始

  1. ServiceRegistry 的主要实现类有
    DaemonServices、DaemonClientServices、ConnectorServiceRegistry
    都是随进程存活

    1. findProviderMethods 把当前实现类的所有方法转成 FactoryMethodService 存到
      allServices.providersByType<返回类型,FactoryMethodService > 里。
    2. get(Class) 直接拿到上面的 FactoryMethodService extends SingletonService
      如果第一次调用 create 一个实例,再次调用返回缓存。因为这个 Daemon
      进程不销毁,无论多少次构建任务都只创建一次实例
    3. getFactory(Class) 和上面一点很像,支持创建出来的是个池子,比如
      ConnectorServiceRegistry
  2. PluginServiceRegistry 的主要实现类有
    DependencyServices、LauncherServices、ExecutionServices、MavenPublishServices、XcodeServices
    分别对应了 gradle
    的不同能力,每种能力对不同的生命周期可以添加不同的处理方式,生命周期从长到短依次为:

    1. registerGlobalServices 全局进程内唯一 GlobalScopeServices.configure 执行
    2. registerGradleUserHomeServices 不是安卓项目目录, 是 gradle 的目录,和
      Global 基本一样,正常不会搞多个 gradle 目录,在
      BuildSessionState(GradleUserHomeScopeServiceRegistry) 根据传入的传入的 file
      目录缓存在 map 里,GradleUserHomeScopeServices.configure 执行
    3. registerBuildSessionServices 每一次执行命令 ,在 BuildSessionState
      构造方法同时初始化 BuildSessionScopeServices.configure 执行,相比
      GradleUserHome 不会缓存
    4. registerBuildTreeServices 每个命令的任务树 在 BuildTreeState
      构造方法同时初始化 BuildTreeScopeServices.configure 执行
    5. registerBuildServices 任务树的 build 阶段,随 DefaultGradleLauncher
      初始化执行 BuildScopeServices 并注册 ServiceRegistration
    6. registerGradleServices 随 GradleScopeServices 初始化执行
    7. registerSettingsServices 随 DefaultSettings 初始化的 SettingsScopeServices
      内部执行,DefaultGradleLauncher.prepareSettings 阶段执行
    8. registerProjectServices 随 DefaultProject 初始化的 ProjectScopeServices
      内部执行,DefaultGradleLauncher.prepareProjects 阶段执行,需要注意每个
      android 项目下的 module 和 DefaultProject 一一对应,每个 build.gradle
      都对应不同的 DefaultProject,根据层级有父子关系

gradle 里的 execute 大部分不是创建线程执行,只是因为当前逻辑需要分层。可能是要加 log,可能是为了抓
Exception 不让进程崩溃,也可能是为了统计执行时间等等

gradle 里的接口定义的十分多,为有能力的开发者提供了足够的扩展性,现有的实现类一般都以
Default+接口名

GradleBuildController.execute -> DefaultGradleLauncher.doClassicBuildStages 分为 4 个主要步骤

  1. 初始化 setting.gradle 根据 include 的目录层级创建树状关系的
    DefaultProjectDescriptor

    1. prepareSettings
    2. BuildOperationFiringSettingsPreparer$LoadBuild.run
    3. DefaultSettingsPreparer.prepareSettings
    4. DefaultSettingsLoader.findAndLoadSettings
    5. BuildOperationSettingsProcessor.process
    6. RootBuildCacheControllerSettingsProcessor.process
    7. SettingsEvaluatedCallbackFiringSettingsProcessor.process
    8. ScriptEvaluatingSettingsProcessor.process
      1. 创建 setting.gradle 对应的 DefaultSettings :SettingsFactory.createSettings
        -> DependencyInjectingInstantiator.newInstance ->
        ClassBuilderImpl.generate
      2. 为 DefaultSettings 赋值 :applySettingsScript ->
        BuildOperationScriptPlugin.apply ->
        DefaultScriptPluginFactory$ScriptPluginImpl.apply 解析 gradle
        文件,主要是赋值 rootProjectDescriptor
  2. 每一个 build.gradle 初始化一个 DefaultProject

    1. prepareProjects
    2. BuildOperationFiringProjectsPreparer$ConfigureBuild.run
    3. BuildTreePreparingProjectsPreparer.prepareProjects -> 这里初始化了 buildSrc 自定义插件
      1. 创建 build.gradle 对应的 DefaultProject :NotifyingBuildLoader.load ->
        ProjectPropertySettingBuildLoader.load ->
        InstantiatingBuildLoader.createProjects -> ProjectFactory.createProject ->
        DependencyInjectingInstantiator.newInstance -> ClassBuilderImpl.generate 和
        settings.gradle 同级目录的 build.gradle 是 rootProject,在
        InstantiatingBuildLoader.createProjects -> createChildProjectsRecursively
        每一个 module 都会创建一个 childProject
      2. 解析 build.gradle 为 DefaultProject 赋值:prepareProjects ->
        DefaultProjectsPreparer.prepareProjects ->
        TaskPathProjectEvaluator.configureHierarchy -> DefaultProject.evaluate ->
        LifecycleProjectEvaluator$EvaluateProject.run ->
        这里解析plugin,包含安卓的和自定义的,所有的需要的 task 都在这里添加到
        project 里,ConfigureActionsProjectEvaluator.evaluate 包含三个步骤
        1. PluginsProjectConfigureActions.execute
          1. HelpTasksAutoApplyAction.execute
          2. KotlinScriptingModelBuildersRegistrationAction.execute
          3. BuildInitAutoApplyAction.execute
          4. WrapperPluginAutoApplyAction.execute
        2. BuildScriptProcessor.execute -> BuildOperationScriptPlugin.apply ->
          DefaultScriptPluginFactory$ScriptPluginImpl.apply
        3. DelayedConfigurationActions
  3. 构建任务树生成 TaskGraph

    1. prepareTaskExecution
    2. BuildOperationFiringTaskExecutionPreparer.prepareForTaskExecution
    3. BuildOperationFiringTaskExecutionPreparer$CalculateTaskGraph.run ->
      populateTaskGraph
    4. DefaultTaskExecutionPreparer.prepareForTaskExecution ->
      DefaultBuildConfigurationActionExecuter.select
      1. ExcludedTaskFilteringBuildConfigurationAction.configure
        过滤配置的不需要执行的 task
      2. DefaultTasksBuildExecutionAction.configure 如果没带任何参数执行默认的 task
      3. TaskNameResolvingBuildConfigurationAction.configure ->
        DefaultTaskExecutionGraph(DefaultExecutionPlan).addEntryTasks -> doAddNodes
        -> LocalTaskNode.resolveDependencies -> getDependencies ->
        TaskDependencyResolver.resolveDependenciesFor ->
        CompositeResolvedArtifactSet.visitDependencies ->
        FailureCollectingTaskDependencyResolveContext.add 判断是否为
        TaskDependencyContainer 递归添加所有依赖的 task
      4. DefaultTaskExecutionGraph.populate
      5. DefaultExecutionPlan.determineExecutionPlan
        根据依赖关系生成任务先后执行的队列,可以说是 gradle 的核心
  4. 执行构建任务

    1. runWork
    2. BuildOperationFiringBuildWorkerExecutor.execute
    3. BuildOperationFiringBuildWorkerExecutor$ExecuteTasks.run
    4. DeprecateUndefinedBuildWorkExecutor.execute
    5. IncludedBuildLifecycleBuildWorkExecutor.execute
    6. DefaultBuildWorkExecutor.execute
      1. DryRunBuildExecutionAction.execute 使用 --dry-run 参数调过所有 task
        的执行(调试用)
      2. SelectedTaskExecutionAction.execute 执行 DefaultTaskExecutionGraph
    7. DefaultTaskExecutionGraph.execute -> executeWithServices
    8. DefaultPlanExecutor.process
    9. DefaultPlanExecutor.startAdditionalWorkers
      开启多线程后面的方法都是多线程执行的
    10. DefaultPlanExecutor$ExecutorWorker.run -> executeNextNode 拆分每一个 Node
    11. DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute
    12. DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute
      1. WorkNodeExecutor.execute 如果需要同步执行 实现接口 SelfExecutingNode
      2. LocalTaskNodeExecutor.execute
    13. EventFiringTaskExecuter.execute 返回值 TaskExecuterResult 放到 context 里
    14. CatchExceptionTaskExecuter
    15. SkipOnlyIfTaskExecuter.execute 跳过 enable=false 的任务
    16. SkipTaskWithNoActionsExecuter.execute 跳过不包含任何 action 的任务
    17. ResolveTaskExecutionModeExecuter.execute
    18. FinalizePropertiesTaskExecuter.execute
    19. CleanupStaleOutputsExecuter.execute
    20. ExecuteActionsTaskExecuter.execute -> executeIfValid
    21. DefaultWorkExecutor.execute 返回值 return new TaskExecuterResult 参数放到 context 里,下面的这堆都从 ExecutionGradleServices.createWorkExecutor 里创建
      1. LoadExecutionStateStep.execute 这里开始切换了代码风格,Task 不放参数里
      2. MarkSnapshottingInputsStartedStep.execute
      3. SkipEmptyWorkStep.execute 调过没有输入的 task
      4. ValidateStep.execute
      5. CaptureStateBeforeExecutionStep.execute
      6. ResolveCachingStateStep.execute
      7. MarkSnapshottingInputsFinishedStep.execute
      8. ResolveChangesStep.execute
      9. SkipUpToDateStep.execute 检查是否有变化的文件,调过没有变化的task
      10. RecordOutputsStep.execute
      11. StoreExecutionStateStep.execute
      12. CacheStep.execute -> executeWithoutCache
      13. BroadcastChangingOutputsStep.execute
      14. SnapshotOutputsStep.execute
      15. CreateOutputsStep.execute
      16. TimeoutStep.execute -> executeWithoutTimeout 检查是否配置了这个任务的超时时间
      17. CancelExecutionStep.execute
      18. ResolveInputChangesStep.execute
      19. CleanupOutputsStep.execute
      20. ExecuteStep.execute -> executeWithPreviousOutputFiles
      21. ExecuteActionsTaskExecuter$TaskExecution.execute -> executeWithPreviousOutputFiles -> executeActions 反射到 task 的实现类执行

下载 dependencies 依赖库

  1. prepareProjects 解析 build.gradle 阶段
    DefaultScriptPluginFactory$ScriptPluginImpl.apply 继续往下看

  2. DefaultPluginRequestApplicator.applyPlugins ->
    defineScriptHandlerClassScope

  3. DefaultScriptHandler.getScriptClassPath

  4. DefaultScriptClassPathResolver.resolveClassPath

  5. CompositeBuildClassPathInitializer.execute

  6. DefaultConfiguration$ConfigurationArtifactCollection.getArtifacts ->
    ensureResolved

    1. DefaultConfiguration$ConfigurationFileCollection.getSelectedArtifacts ->
      resolveToStateOrLater -> resolveExclusively -> resolveGraphIfRequired ->
      ErrorHandlingConfigurationResolver((ShortCircuitEmptyConfigurationResolver(DefaultConfigurationResolver(DefaultArtifactDependencyResolver)))).resolveGraph
      -> DependencyGraphBuilder.attachToTargetRevisionsSerially ->
      EdgeState.attachToTargetConfigurations -> calculateTargetConfigurations ->
      ComponentState.getMetadata -> resolve -> ClientModuleResolver. resolve ->
      RepositoryChainComponentMetaDataResolver.resolveModule -> findBestMatch
      配置了多个仓库会挨个遍历找到第一个 Resolved 的
    2. DefaultLenientConfiguration.select
    3. DefaultVisitedArtifactResults.select
    4. CompositeResolvedArtifactSet.of
  7. DefaultLenientConfiguration.visitArtifacts

  8. ParallelResolveArtifactSet$VisitingSet.visit

  9. ParallelResolveArtifactSet$VisitingSet$StartVisitAction.execute

  10. CompositeResolvedArtifactSet.startVisit

  11. ArtifactBackedResolvedVariant$SingleArtifactSet.startVisit
    如果需要下载到本地

  12. DefaultBuildOperationQueue.add 内部有线程池 下面的调用都为异步执行

  13. DownloadArtifactFile.run

  14. DefaultResolvedArtifact.getFile

  15. DefaultArtifactSet$LazyArtifactSource.create

  16. RepositoryChainArtifactResolver.resolveArtifact ->

  17. getLocalAccess() 加载本地缓存

    1. CachingModuleComponentRepository$LocateInCacheRepositoryAccess.resolveArtifact -> resolveArtifactFromCache 返回值 cache 是 null 或 isMissing
      都会走到下载依赖
    2. InMemoryModuleArtifactCache.lookup
    3. DefaultModuleArtifactCache.lookup -> super.lookup ->
      getPersistentCache().get(key);
    4. WritableArtifactCacheLockingManager$CacheLockingPersistentCache.useCache
    5. DefaultCacheFactory$ReferenceTrackingCache($DirCacheReference(DefaultPersistentDirectoryStore(DefaultCacheAccess))).useCache
    6. DefaultMultiProcessSafePersistentIndexedCache(BTreePersistentIndexedCache($HeaderBlock.getRoot().load().StateCheckBlockStore(StateCheckBlockStore(FreeListBlockStore(CachingBlockStore(FileBackedBlockStore(FileBackedBlockStore$BlockImpl))))).read())).get
      主要逻辑是读 module-artifact.bin 缓存找到确切的缓存文
  18. getRemoteAccess() 如果本地缓存没读到 下载依赖并缓存

    1. CachingModuleComponentRepository$ResolveAndCacheRepositoryAccess.resolveArtifact
    2. ExternalResourceResolver$RemoteRepositoryAccess.resolveArtifact
    3. DefaultExternalResourceArtifactResolver.resolveArtifact ->
      downloadStaticResource -> downloadByUrl
    4. DefaultCacheAwareExternalResourceAccessor.getResource ->
      ProducerGuard$AdaptiveProducerGuard.guardByKey
    5. BuildOperationFiringExternalResourceDecorator.getMetaData
    6. AccessorBackedExternalResource(DefaultExternalResourceConnector(HttpResourceAccessor)).getMetaData
    7. HttpClientHelper.performHead -> performRawHead -> performRequest ->
      executeGetOrHead -> performHttpRequest -> getClient().execute
      这里使用了HttpClient 开启了同步下载流程

安卓编译流程-是 gradle 的一种插件

  1. JavaCompile 只编译改过的类

    1. IncrementalInputsTaskAction.doExecute
    2. JavaCompile.performIncrementalCompilation -> performCompilation
    3. CompileJavaBuildOperationReportingCompiler.execute
    4. IncrementalResultStoringCompiler.execute
    5. SelectiveCompiler.execute ->
      JavaRecompilationSpecProvider.provideRecompilationSpec ->
      JavaRecompilationSpecProvider.processOtherChanges ->
      SourceFileChangeProcessor.processChange ->
      PreviousCompilation.getDependents -> ClassSetAnalysis.getRelevantDependents
      根据上次编译结果有依赖关系的类都会重新编译
    6. ModuleApplicationNameWritingCompiler.execute
    7. AnnotationProcessorDiscoveringCompiler.execute
    8. NormalizingJavaCompiler.execute -> delegateAndHandleErrors
    9. JdkJavaCompiler.execute
    10. ResourceCleaningCompilationTask.call
    11. AnnotationProcessingCompileTask.call
    12. IncrementalCompileTask.call
    13. JavacTaskImpl. call 这里就是 jdk 的 javac
  2. 执行自定义插件
    ………… 后面还有很多 Android build tools 流程,未完待续

编译流程的运用

  1. 自定义插件

    1. AnnoationProcessor 是 javac 的一种代码注入技术,在 JavaCompile 期间处理。比如ButterKnife

      1. 定义 javax.annotation.processing.AbstractProcessor 的实现类
      2. 添加 src/main/resources/META-INF/services/javax.annotation.processing.Processor
      3. 添加 src/main/resources/META-INF/gradle/incremental.annotation.processors
      4. 引用 annotationProcessor project(path: ‘:compiler’)
      5. 简单的Demo https://github.com/dingshaoran/WMRouter
    2. GradlePlugin 安卓编译流程的一部分 后面也会讲一下具体实现流程

  2. 提高编译速度

    1. gradle.properties 里添加配置
      org.gradle.unsafe.configuration-cache=true。DefaultGradleLauncher.doBuildStages
      里判断了 DefaultConfigurationCache.canLoad -> isConfigurationCacheEnabled
      -> startParameter.isEnabled -> isConfigurationCache
    2. 用 nexus 搭建自己的 maven 仓库,把 repositories
      所有的地址都配进去,不直接使用多个 repo 防止
      RepositoryChainComponentMetaDataResolver.findBestMatch 遍历多个 repo
    3. gradle.properties 里添加配置 org.gradle.caching=true
    4. 遵循 Gradle 的编码原则,相比普通的 javac gradle 实现了增量编译,如果不按照 gradle 规则会影响打包速度 ⁣https://docs.gradle.org/7.1/userguide/java_plugin.html#sec:incremental_compile

带着问题看博客理解的更透彻

  1. Task、Transfrom 有什么关系
  2. 其他问题写在评论区
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值