Android Gradle 学习之二:重命名APK

如果只是想看怎么重命名apk,只看前两段就可以了。如果想从源码角度了解一下,那么可以先看下上一篇Android Gradle 学习之一:源码下载

先来看下在gradle中怎么修改生成的apk的名字,在module的build.gradle文件中写如下代码:

applicationVariants.all { variant ->
        variant.outputs.all { output ->
            if (output.outputFileName != null && output.outputFileName.endsWith('.apk')) {
                def fileName = "CustomGradle-v${versionName}-${variant.buildType.name}.apk"
                output.outputFileName = fileName
            }
        }
    }

这段代码会根据variant将apk命名成自己想要名称,因为我没有设置flavor,所以最后全量build生成的apk的名字为:

CustomGradle-v1.0-debug.apk

CustomGradle-v1.0-releas.apk

CustomGradle-v1.0-androidTest.apk

重命名APK的方法相信百度一下很多地方都能够查得到,其实官方也给了例子展示了如何改名。也可能只是测试项目不是例子,因为源码gradle的测试工程里面,文件在:</your/gradle/source>/tools/base/build-system/integration-test/test-projects/renamedApk/build.gradle

官方的写法是这样的:

android.applicationVariants.all { variant ->
    variant.outputs.all { output ->
        try {
            outputFileName = new File(output.outputFile.parent, "foo")
            throw new RuntimeException("setting an absolute path to outputFileName not caught")
        } catch (GradleException e) {
            // expected
        }
        outputFileName = "${variant.name}.apk"
    }
}

其实类似,但是注意下细节就会发现,官方的outputFileName前面没有加output。输出下这个闭包的delegate可以发现,variant.outputs.all方法把闭包的delegate设置成了他的成员,所以output.outputFileName这个调用和去掉output直接写outputFileName这个调用是一样的,都是拿到output的成员变量“outputFileName”。delegate是groovy闭包的一个用法,自行查阅吧

其实我的问题并非解决如何修改输出的apk名,每次遇到类似的需求的时候,百度一下就能找到改名的方法,但是看到这些办法似懂非懂,让自己写还写不出来,如果遇到其他需求感觉就不知道怎样修改gradle了。所以本篇主要是从源码的角度来看下为什么要要在gradle里面加上这些代码能够修改打包之后的apk。

1. InstallDebug

在gradle task工具栏里面能够看到一个installDebug的task,他的任务就是安装编译好的apk。想要安装apk就肯定需要知道文件名和文件路径,那我们先从这个task入手,看看他是怎样拿到要安装的文件的apk的。

源代码位于</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/InstallVariantTask.java

找到带有@TaskAction注解的方法install()就是InstallVariantTask的主要执行体,代码如下:

@TaskAction
    public void install() throws DeviceException, ProcessException {
        TaskDependency mustRunAfter = getMustRunAfter();
        final ILogger iLogger = getILogger();
        DeviceProvider deviceProvider = new ConnectedDeviceProvider(adbExe.get(),
                getTimeOutInMs(),
                iLogger);
        deviceProvider.init();

        try {
            BaseVariantData variantData = getVariantData();
            GradleVariantConfiguration variantConfig = variantData.getVariantConfiguration();

            List<OutputFile> outputs =
                    ImmutableList.copyOf(
                            ExistingBuildElements.from(
                                    InternalArtifactType.APK,
                                    BuildableArtifactUtil.singleFile(apkDirectory)));
            System.out.println("apkDirectory = " + apkDirectory);
            for (OutputFile opf : outputs) {
                System.out.println("INstallVariantTask opf.getOutputFile().getPath() = " + opf.getOutputFile().getPath() + " opf = " + opf);
            }
            install(
                    getProjectName(),
                    variantConfig.getFullName(),
                    deviceProvider,
                    variantConfig.getMinSdkVersion(),
                    getProcessExecutor(),
                    getSplitSelectExe(),
                    outputs,
                    variantConfig.getSupportedAbis(),
                    getInstallOptions(),
                    getTimeOutInMs(),
                    getLogger());
        } finally {
            deviceProvider.terminate();
        }
    }

里面还有个install方法,这个就是执行真正的安装命令,里面比较复杂就不细说了。大概就是用DeviceConnector执行了一个“pm install -r -t "/data/local/tmp/CustomGradle-v1.0-debug.apk”的命令。

install上面几行System.out.println是我自己加的代码

编译下android gradle,然后在我们的测试工程里面执行installDebug,就能看到apkDirectory的输出了。(怎么编译怎么调试看我的第一篇博客)输出如下:

apkDirectory = FinalBuildableArtifact(APK, com.android.build.gradle.internal.scope.VariantBuildArtifactsHolder@61163dff, [<path/to/CustomGradle>/app/build/outputs/apk/debug])
INstallVariantTask opf.getOutputFile().getPath() = <path/to/CustomGradle>/app/build/outputs/apk/debug/CustomGradle-v1.0-debug.apk opf = BuildOutput{apkData=DefaultApkData(_type=MAIN, _filters=[], _versionCode=1, _versionName=1.0, _filterName=null, _outputFileName=CustomGradle-v1.0-debug.apk, _fullName=debug, _baseName=debug, _enabled=true), path=<path/to/CustomGradle>/app/build/outputs/apk/debug/CustomGradle-v1.0-debug.apk, properties=}

apkDirectory是在这个文件下面的CreationAction里面设置的,这个是一个固定的目录并能通过gradle文件配置。所以我们要找的都是在outputs这个临时变量里。他是通过ExistingBuildElements获得的,ExistingBuildElements看起来是个挺重要的类,里面很多task都会用到这个类的方法。看一下这个from函数

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/scope/ExistingBuildElements.kt

private const val METADATA_FILE_NAME = "output.json"

/**
         * create a {@link BuildElement} from a previous task execution metadata file.
         * @param elementType the expected element type of the BuildElements.
         * @param from the folder containing the metadata file.
         */
        @JvmStatic
        fun from(elementType: ArtifactType, from: File): BuildElements {

            val metadataFile = getMetadataFileIfPresent(from)
            return loadFrom(elementType, metadataFile)
        }


@JvmStatic
        fun getMetadataFileIfPresent(folder: File): File? {
            val outputFile = getMetadataFile(folder)
            return if (outputFile.exists()) outputFile else null
        }

        @JvmStatic
        fun getMetadataFile(folder: File): File {
            return File(folder, METADATA_FILE_NAME)
        }

        private fun loadFrom(
            elementType: ArtifactType?,
                metadataFile: File?): BuildElements {
            if (metadataFile == null || !metadataFile.exists()) {
                val elements: Collection<BuildOutput> = ImmutableList.of()
                return BuildElements(ImmutableList.of())
            }
            try {
                FileReader(metadataFile).use { reader ->
                    return BuildElements(load(metadataFile.parentFile.toPath(),
                        elementType,
                        reader))
                }
            } catch (e: IOException) {
                return BuildElements(ImmutableList.of<BuildOutput>())
            }
        }



        @JvmStatic
        fun load(
                projectPath: Path,
                outputType: ArtifactType?,
                reader: Reader): Collection<BuildOutput> {
            val gsonBuilder = GsonBuilder()

            gsonBuilder.registerTypeAdapter(ApkData::class.java, ApkDataAdapter())
            gsonBuilder.registerTypeAdapter(
                    ArtifactType::class.java,
                    OutputTypeTypeAdapter())
            val gson = gsonBuilder.create()
            val recordType = object : TypeToken<List<BuildOutput>>() {}.type
            val buildOutputs = gson.fromJson<Collection<BuildOutput>>(reader, recordType)
            // resolve the file path to the current project location.
            return buildOutputs
                    .asSequence()
                    .filter { outputType == null || it.type == outputType }
                    .map { buildOutput ->
                        BuildOutput(
                                buildOutput.type,
                                buildOutput.apkData,
                                projectPath.resolve(buildOutput.outputPath),
                                buildOutput.properties)
                    }
                    .toList()
        }

大概意思就是,传入一个apkDirectory的目录路径,在这个目录下找到output.json,读取里面的json构造出BuildElements类。

所以通过看InstallVariantTask的代码可以了解到,生成的apk的目录是固定的,apk的文件名是通过目录下的output.json文件指定的。

那么接下来就需要来找output.json这个文件的生成。

2. BuildElements、ProcessApplicationManifest

output.json文件的生成位于BuildElements.kt里面,代码:

@Throws(IOException::class)
    fun save(folder: File): BuildElements {
        val persistedOutput = persist(folder.toPath())
        FileWriter(ExistingBuildElements.getMetadataFile(folder)).use { writer ->
            writer.append(persistedOutput)
        }
        return this
    }

/**
     * Persists the passed output types and split output to a [String] using gson.
     *
     * @param projectPath path to relativize output file paths against.
     * @return a json String.
     */
    fun persist(projectPath: Path): String {
        val gsonBuilder = GsonBuilder()
        gsonBuilder.registerTypeAdapter(ApkData::class.java, ExistingBuildElements.ApkDataAdapter())
        gsonBuilder.registerTypeAdapter(
            InternalArtifactType::class.java, ExistingBuildElements.OutputTypeTypeAdapter()
        )
        gsonBuilder.registerTypeAdapter(
            AnchorOutputType::class.java,
            ExistingBuildElements.OutputTypeTypeAdapter()
        )
        val gson = gsonBuilder.create()

        // flatten and relativize the file paths to be persisted.
        return gson.toJson(elements
            .asSequence()
            .map { buildOutput ->
                BuildOutput(
                    buildOutput.type,
                    buildOutput.apkData,
                    projectPath.relativize(buildOutput.outputPath),
                    buildOutput.properties
                )
            }
            .toList())
    }
private const val METADATA_FILE_NAME = "output.json"

@JvmStatic
        fun getMetadataFile(folder: File): File {
            return File(folder, METADATA_FILE_NAME)
        }

调用save方法的地方有很多,也都位于各个task里面。但是大多数的save操作都是从另一个目录下的output.json取出来再save到task指定的目录下。那么第一个调用save保存ouput.json文件的task叫ProcessApplicationManifest。

    private OutputScope outputScope;

    @Override
    protected void doFullTaskAction() throws IOException {


        ...


        for (ApkData apkData : outputScope.getApkDatas()) {

            ...

            System.out.println(" apkData.getOutputFileName() = " + apkData.getOutputFileName());
            mergedManifestOutputs.add(
                    new BuildOutput(
                            InternalArtifactType.MERGED_MANIFESTS,
                            apkData,
                            manifestOutputFile,
                            properties));
            ...

        }
        new BuildElements(mergedManifestOutputs.build())
                .save(getManifestOutputDirectory().get().getAsFile());

        ...

    }


public static class CreationAction
            extends AnnotationProcessingTaskCreationAction<ProcessApplicationManifest> {

        public CreationAction(
                @NonNull VariantScope scope,
                // TODO : remove this variable and find ways to access it from scope.
                boolean isAdvancedProfilingOn) {
            super(
                    scope,
                    scope.getTaskName("process", "Manifest"),
                    ProcessApplicationManifest.class);
            this.variantScope = scope;
            this.isAdvancedProfilingOn = isAdvancedProfilingOn;
        }

        @Override
        public void configure(@NonNull ProcessApplicationManifest task) {
            super.configure(task);
           
            ...

            final BaseVariantData variantData = variantScope.getVariantData();

            ...

            task.outputScope = variantData.getOutputScope();

            ...

        }
}

代码太多,这里就只放一些关键的片段。我们想要的的apk的名称来自apkData。从头说一下apkData的来源

在构造ProcessApplicationManifest的CreationAction类时,传入了一个variantScope

在执行ProcessApplicationManifest的configure时,调用variantScope.getOutputScope得到outputScope并传给ProcessApplicationManifest这个task

ProcessApplicationManifest执行时通过getApkDatas得到所有的apkData,然后保存到文件里面。

看到这里,可以了解到重命名的根源是来自variantScope里面的apkData数据。后面就来找下variantScope的来源。

3. TaskManager、BasePlugin

来看下VariantManager里面的代码:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/VariantManager.java

    @NonNull private final List<VariantScope> variantScopes;

    /** Variant/Task creation entry point. */
    public List<VariantScope> createAndroidTasks() {
        variantFactory.validateModel(this);
        variantFactory.preVariantWork(project);

        if (variantScopes.isEmpty()) {
            populateVariantDataList();
        }

        // Create top level test tasks.
        taskManager.createTopLevelTestTasks(!productFlavors.isEmpty());

        for (final VariantScope variantScope : variantScopes) {
            createTasksForVariantData(variantScope);
        }

        taskManager.createSourceSetArtifactReportTask(globalScope);

        taskManager.createReportTasks(variantScopes);

        return variantScopes;
    }

variantScopes是VariantManager的成员没变量,createAndroidTasks返回variantScopes,函数顾名思义就是在创建我们在android studio里面用到的各种task。我们不需要了解里面的每个variantScope是如何创建的,现在只需要知道是apkData的数据结构关系。apkData最终是存储在OutputScope的sortedApkDatas这个列表里面,也要记住variantData这个变量,下面会用到。

(图例 <数据类型> : 变量名)

|____VariantManager : variantManager
| |____List<VariantScope> : variantScopes
| | |____VariantScope
| | | |____BaseVariantData : variantData
| | | | |____OutputScopeFactory : outputFactory
| | | | | |____OutputScope : outputSupplier
| | | | | | |____ImmutableList<ApkData> : sortedApkDatas

 

再看下调用createAndroidTasks的地方,位于BasePlugin里面:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/BasePlugin.java


    @VisibleForTesting
    final void createAndroidTasks() {

        ...

        List<VariantScope> variantScopes = variantManager.createAndroidTasks();

        ApiObjectFactory apiObjectFactory =
                new ApiObjectFactory(
                        globalScope.getAndroidBuilder(),
                        extension,
                        variantFactory,
                        project.getObjects());
        for (VariantScope variantScope : variantScopes) {
            BaseVariantData variantData = variantScope.getVariantData();
            apiObjectFactory.create(variantData);
        }

        ...

    }

来到BasePlugin,这已经是AndroidGradle插件比较根源的位置了。在他的createAndroidTasks函数里面调用了VariantManager的createAndroidTasks方法,拿到了variantScopes列表。然后遍历所有的variantScope,每个variantScope得到variantData(上面提到,这是存储apkData的地方)并通过apiOjectFactory进行创建。创建什么呢,看下ApiObjectFactory里面的代码:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/ApiObjectFactory.java

    public BaseVariantImpl create(BaseVariantData variantData) {

        ...

        BaseVariantImpl variantApi =
                variantFactory.createVariantApi(
                        objectFactory,
                        androidBuilder,
                        variantData,
                        readOnlyObjectProvider);
        if (variantApi == null) {
            return null;
        }

        ...

        createVariantOutput(variantData, variantApi); // 这是重点

        try {
            // Only add the variant API object to the domain object set once it's been fully
            // initialized.
            extension.addVariant(variantApi); // 这个是重点
        } catch (Throwable t) {
            // Adding variant to the collection will trigger user-supplied callbacks
            throw new ExternalApiUsageException(t);
        }

        return variantApi;
    }


    private void createVariantOutput(BaseVariantData variantData, BaseVariantImpl variantApi) {
        variantData.variantOutputFactory =
                new VariantOutputFactory(
                        (variantData.getType().isAar())
                                ? LibraryVariantOutputImpl.class
                                : ApkVariantOutputImpl.class,
                        objectFactory,
                        extension, // 重点
                        variantApi, // 重点
                        variantData.getTaskContainer(),
                        variantData
                                .getScope()
                                .getGlobalScope()
                                .getDslScope()
                                .getDeprecationReporter());
        GradleVariantConfiguration config = variantData.getVariantConfiguration();
        variantData
                .getOutputScope()
                .getApkDatas()
                .forEach(
                        apkData -> {
                            apkData.setVersionCode(config.getVersionCodeSerializableSupplier());
                            apkData.setVersionName(config.getVersionNameSerializableSupplier());
                            variantData.variantOutputFactory.create(apkData); // 重点,代码在下面
                        });
    }

一步步来看吧,ApiObjectFactory的create方法里面创建了一个BaseVariantImpl,然后在createVariantOutput方法里面给他塞了一堆数据。再看下createVariantOutput方法里面,extension是重点重的重点,后面再讲,variantApi就是在create方法里面创建的BaseVariantImpl变量。variantData是从BasePlugin里面传进来的通过variantManager的createAndroidTasks方法得到的list遍历的变量,刚才也有提到,apkData就在这个里面。variantData.getOutputScope().getApkDatas()看下这个调用,再对照着刚才的数据结构。这就是拿到了所有的apkDatas。foreach,遍历所有的apkData,然后调用variantData.variantOutputFactory.create(apkData);

variantOutputFactory就是上面刚创建的变量,他的create方法的代码在下面:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/VariantOutputFactory.java

public class VariantOutputFactory {

    ...

    @Nullable private final BaseVariantImpl variantPublicApi;
    @NonNull private final AndroidConfig androidConfig;

    ...

    public VariantOutput create(ApkData apkData) {
        BaseVariantOutput variantOutput =
                objectFactory.newInstance(targetClass, apkData, taskContainer, deprecationReporter); // 构造函数,并把apkData存到自己的成员变量里面
        androidConfig.getBuildOutputs().add(variantOutput); // 后面讲,这是修改apk名的另一种方法
        if (variantPublicApi != null) {
            variantPublicApi.addOutputs(ImmutableList.of(variantOutput)); // 重点
        }
        return variantOutput;
    }
}

variantPublicApi就是在createVariantOutput传进来的variantApi,也就是ApiObjectFactory.create方法里面创建的变量。

androidConfig就是上面提到的最重点extension。

这里创建了variantOutput一个变量,类型是ApkVariantOutputImpl,在他的构造函数里面把传入的apkData赋给了自己的成员变量。variantPublicApi.addOutputs(ImmutableList.of(variantOutput));这个方法又将创建的variantOutput塞进了variantPublicApi里面。

看下addOutputs的代码:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/api/BaseVariantImpl.java

    @NonNull protected final NamedDomainObjectContainer<BaseVariantOutput> outputs;

    public void addOutputs(@NonNull List<BaseVariantOutput> outputs) {
       this.outputs.addAll(outputs);
    }

outputs是一个BaseVariantOuput的container。

这样我们得到了一个variantApi的变量,记得在ApiObjectFactory的create代码里面有一句extension.addVariant(variantApi);extension是AppExtension类型的变量,addVariant的代码如下:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/AppExtension.java

    private final DefaultDomainObjectSet<ApplicationVariant> applicationVariantList
            = new DefaultDomainObjectSet<ApplicationVariant>(ApplicationVariant.class);

    @Override
    public void addVariant(BaseVariant variant) {
        applicationVariantList.add((ApplicationVariant) variant);
    }

至此我们再来重新看下从AppExtension开始的数据结构

|____AppExtension : extension
| |____DefaultDomainObjectSet<ApplicationVariant> : applicationVariantList
| | |____ApplicationVariant : variantPublicApi
| | | |____NamedDomainObjectContainer<BaseVariantOutput>: outputs
| | | | |____ApkVariantOutputImpl : variantOutput
| | | | | |____ApkData : apkData

extension下面再讲

applicationVariantList是一个Set,extension创建的时候创建的。

variantPublicApi是ApiObjectFactory新创建的

outputs是一个Container,variantPublicApi的成员变量,是通过project创建一个container

variantOutput是VariantOutputFactory新创建的

apkData就是我们要找的变量,他和variantScope下的apkData是同一个引用。重点圈一下:他和variantScope下的apkData是同一个引用

也就是说我们不管是通过variantScope修改apkData还是通过extension修改apkData效果都是一样的。

4. AppExtension

上面提到了extension是重点,他的类型是AppExtension。也许你是第一次看到这个名字,但是只要你写过Android工程,肯定会经常用到这个类,只是可能你不知道而已。我们看一下一个最进本的android的build.gradle是怎么写的

apply plugin: 'com.android.application'

android { // 这个就是AppExtension
    compileSdkVersion 29
    defaultConfig {
        applicationId "com.xxx.customgradle"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            if (output.outputFileName != null && output.outputFileName.endsWith('.apk')) {
                def fileName = "CustomGradle-v${versionName}-${variant.buildType.name}.apk"
                output.outputFileName = fileName
            }
        }
    }
}

是不是很熟悉,其实第三行的"android"可以理解为数据类型为AppExtension的变量。里面的compileSdkVersion、defaultConfig、buildTypes都是AppExtension类的方法。applicationVariants也是他的方法,原方法名是getApplicationVariant(相关知识自行查阅groovy语法吧)。

那么现在应该就知道为什么重命名的gradle代码要这么写了吧。对比下上一节列出来的数据结构,以及源代码的各个get方法的调用,最终output.outputFileName = fileName就是给apkData赋值了新的名字。

5. 另一种重命名方法

这种方法是我在写这篇文章时突然看到的方法,试了一下确实可以。再把VariantOutputFactory.java的代码贴一下:

</your/gradle/source>/tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/VariantOutputFactory.java

public class VariantOutputFactory {

    ...

    @Nullable private final BaseVariantImpl variantPublicApi;
    @NonNull private final AndroidConfig androidConfig;

    ...

    public VariantOutput create(ApkData apkData) {
        BaseVariantOutput variantOutput =
                objectFactory.newInstance(targetClass, apkData, taskContainer, deprecationReporter); // 构造函数,并把apkData存到自己的成员变量里面
        androidConfig.getBuildOutputs().add(variantOutput); // 看这里
        if (variantPublicApi != null) {
            variantPublicApi.addOutputs(ImmutableList.of(variantOutput));
        }
        return variantOutput;
    }
}

有一句“androidConfig.getBuildOutputs().add(variantOutput);”,我们已知androidConfig就是上面提到的extension,variantOutput里面是存有apkData数据的,而且apkData的引用也是和variantScope的apkData是同一个引用。那么似乎我们也可以用buildOutputs来修改apk的名字。如下android工程的build.gradle:

android {
    compileSdkVersion 29
    defaultConfig {
        applicationId "com.hw.customgradle"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    buildOutputs.all { output ->
        output.apkData.outputFileName = "123.apk" // 重名成成123.apk
    }
}

编译之后,确实可行。

6.总结

分析源码的过程,我确实是经历了从入门到放弃的再到最后苦苦挣扎的阶段。讲真,个人觉得androidgradle的代码写的真不怎么样。从他的数据结构的管理,到代码风格,命名风格有很多都能让人抓狂。

不过分析完这些也确实掌握了一些androidgradle的内部原理,或许以后再有一些编译android工程的问题的时候不会再摸不着头脑不知所措了吧

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值