Gradle实战

前置文章 :Gradle学习总结

通过下面几个案例来演示下Gradle的强大之处,因为在一般的开发中我们只是简单的配置下build.gradle文件(添加依赖),并不能体现它的强大,也不知道它还有那些用途。

目录

案例一:单独定义一个Gradle文件,存放应用中的所有配置变量和依赖,以便统一管理

common.gradle
//用来存放应用中的所有配置变量,统一管理,而不再是每个moudle里都自己写一份,修改起来更加的方便
ext {//扩展Project的变量

  android = [compileSdkVersion   : 25,
             buildToolsVersion   : '25.0.0',
             applicationId       : 'com.gradle.demo',
             minSdkVersion       : 16,
             targetSdkVersion    : 23,
             versionCode         : 1,
             versionName         : '1.0.0',
             multiDexEnabled     : true,
             manifestPlaceholders: [UMENG_CHANNEL_VALUE: 'study']]

  signConfigs = ['storeFile'    : 'sign.jks',
                 'storePassword': '123456',
                 'keyAlias'     : 'alias',
                 'keyPassword'  : '123456']

  java = ['javaVersion': JavaVersion.VERSION_1_7]


  dependence = ['libSupportV7'           : 'com.android.support:appcompat-v7:25.0.0',
                'libSupportMultidex'     : 'com.android.support:multidex:1.0.1',
                'libPullAlive'           : ':lib_pullalive',
                'libCircleImageView'     : 'de.hdodenhof:circleimageview:2.1.0',
                'libSystembarTint'       : 'com.readystatesoftware.systembartint:systembartint:1.0.3',
                'libUmengAnalytics'      : 'com.umeng.analytics:analytics:latest.integration',
                'libUniversalImageLoader': 'com.nostra13.universalimageloader:universal-image-loader:1.9.5',
                'libOkhttp'              : 'com.squareup.okhttp3:okhttp:3.3.0',
                'libAutoScrollViewPager' : 'cn.trinea.android.view.autoscrollviewpager:android-auto-scroll-view-pager:1.1.2',
                'libSlidableActivity'    : 'com.r0adkll:slidableactivity:2.0.5',
                'libAndfix'              : 'com.alipay.euler:andfix:0.5.0@aar',
                'libLogger'              : 'com.orhanobut:logger:+',
                'libTinker'              : "com.tencent.tinker:tinker-android-lib:1.7.7",
                'libTinkerAndroid'       : "com.tencent.tinker:tinker-android-anno:1.7.7"]
}
定义好文件后我们在根工程中引入该文件
//引入文件
apply from: this.file('common.gradle')
buildscript {
    //配置工程的仓库地址
    repositories {
        jcenter()
    }
    //配置工程的"插件"依赖地址
    dependencies {
        classpath "com.android.tools.build:gradle:2.2.2"
    }
}
引入文件后,我们在app工程build.gradle文件中使用
apply plugin: 'com.android.application'
android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    buildToolsVersion rootProject.ext.android.buildToolsVersion

    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        multiDexEnabled rootProject.ext.android.multiDexEnabled //突破应用方法数65535的一个限制
    }

    signingConfigs {
        //签名打包
        release {
            storeFile file(rootProject.ext.signConfigs.storeFile)
            storePassword rootProject.ext.signConfigs.storePassword
            keyAlias rootProject.ext.signConfigs.keyAlias
            keyPassword rootProject.ext.signConfigs.keyPassword
        }
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }

    lintOptions {
        abortOnError false
        lintConfig file("lint.xml")
    }

    //recommend
    dexOptions {
        jumboMode = true
    }

    compileOptions {
        sourceCompatibility rootProject.ext.java.javaVersion
        targetCompatibility rootProject.ext.java.javaVersion
    }

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs'] //修改so库存放位置
        }
    }
}

//为应用程序添加第三方库依赖
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile rootProject.ext.dependence.libSupportV7
    compile rootProject.ext.dependence.libSupportMultidex
    compile project(rootProject.ext.dependence.libPullAlive)
    compile rootProject.ext.dependence.libCircleImageView
    compile rootProject.ext.dependence.libSystembarTint
    //添加友盟统计
    compile rootProject.ext.dependence.libUmengAnalytics
    compile rootProject.ext.dependence.libUniversalImageLoader
    compile rootProject.ext.dependence.libOkhttp
    //okttp依赖
    //compile 'com.github.chrisbanes:PhotoView:1.3.0'
    compile(rootProject.ext.dependence.libAutoScrollViewPager) {
        exclude module: 'support-v4' //排除依赖
        exclude group: 'com.android.support' //排除该组下所有的依赖
        transitive false //禁止传递依赖
    }
    compile rootProject.ext.dependence.libSlidableActivity
    //滑动关闭Activity库
    compile rootProject.ext.dependence.libAndfix
    //阿里热修复andfix
    compile rootProject.ext.dependence.libLogger
    //日志库logger
    //Tinker相关依赖
    compile(rootProject.ext.dependence.libTinker) {
        changing = true //每次都从服务端拉取
    }
    provided(rootProject.ext.dependence.libTinkerAndroid) { changing = true }
}

这是配置后,我们的app工程的build.gradle文件更有语义性,版本变化后也便于管理,同时还可以复用。

案例二:自动维护版本发布文档

我们APP每次迭代需要记录APP的版本信息和对应的变化信息,以某一特定的格式记录,方便以后对比管理。下面先看一下版本发布文档的样式:(当然你也可以定义自己想要的样式)

<releases>
  <release>
    <versionCode>100</versionCode>
    <versionName>1.0.0</versionName>
    <versionInfo>App的第1个版本,上线了一些最基础核心的功能.</versionInfo>
  </release>

<release>
  <versionCode>101</versionCode>
  <versionName>1.0.1</versionName>
  <versionInfo>App的第2个版本,修复了XXXXXBug,优化XXXX功能</versionInfo>
</release>
  .................. 省略 ...................
<release>
  <versionCode>110</versionCode>
  <versionName>1.1.0</versionName>
  <versionInfo>第10个版本。。。</versionInfo>
</release>
</releases>

有了这样的文档我们可以很方便的对比每个版本的差异,我们可以写一个脚本来自动生成这样的文档,而不需要手动维护。

releaseinfo.gradle
import groovy.xml.MarkupBuilder

/**
 * 描述:版本发布文档自动维护脚本
 * 流程描述: 
 *           1、将版本相关信息解析出来
 *           2、将解析出的数据生成xml格式数据
 *           3、写入到已有的文档数据中
 **/
ext {
    versionName = rootProject.ext.android.versionName
    versionCode = rootProject.ext.android.versionCode
    versionInfo = 'App的第2个版本,上线了一些最基础核心的功能.'
    destFile = file('releases.xml')//指定输出文件
    if (destFile != null && !destFile.exists()) {
        destFile.createNewFile()
    }
}
//创建一个Task,并指定输入输出
task writeTask {
    inputs.property('versionCode', this.versionCode)
    inputs.property('versionName', this.versionName)
    inputs.property('versionInfo', this.versionInfo)
    outputs.file this.destFile
    doLast {
        //将输入的内容写入到输出文件中去
        def data = inputs.getProperties()
        File file = outputs.getFiles().getSingleFile()
        def versionMsg = new VersionMsg(data)
        //将实体对象写入到xml文件中
        def sw = new StringWriter()
        def xmlBuilder = new MarkupBuilder(sw)
        if (file.text != null && file.text.size() <= 0) {
            //没有内容
            xmlBuilder.releases {
                release {
                    versionCode(versionMsg.versionCode)
                    versionName(versionMsg.versionName)
                    versionInfo(versionMsg.versionInfo)
                }
            }
            //直接写入
            file.withWriter { writer -> writer.append(sw.toString())
            }
        } else {
            //已有其它版本内容
            xmlBuilder.release {
                versionCode(versionMsg.versionCode)
                versionName(versionMsg.versionName)
                versionInfo(versionMsg.versionInfo)
            }
            //插入到最后一行前面
            def lines = file.readLines()
            def lengths = lines.size() - 1
            file.withWriter { writer ->
                lines.eachWithIndex { line, index ->
                    if (index != lengths) {
                        writer.append(line + '\r\n')
                    } else if (index == lengths) {
                        writer.append('\r\r\n' + sw.toString() + '\r\n')
                        writer.append(lines.get(lengths))
                    }
                }
            }
        }
    }
}
//信息实体类
class VersionMsg {
    String versionCode
    String versionName
    String versionInfo
}
//通过输入输出来指定Task的执行顺序
task readTask {
    //指定输入文件为上一个task的输出
    inputs.file this.destFile
    doLast {
        //读取输入文件的内容并显示
        def file = inputs.files.singleFile
        println file.text
    }
}

task taskZ {
    dependsOn writeTask, readTask//通过依赖指定Task的执行顺序
    doLast {
        println '输入输出任务结束'
    }
}

//将文档复制到指定的文件中
task handleReleaseFile {
    def srcFile = file('releases.xml')
    def destDir = new File(this.buildDir, 'generated/release/')
    doLast {
        println '开始解析对应的xml文件...'
        destDir.mkdir()
        def releases = new XmlParser().parse(srcFile)
        releases.release.each { releaseNode ->
            //解析每个release结点的内容
            def name = releaseNode.versionName.text()
            def versionCode = releaseNode.versionCode.text()
            def versionInfo = releaseNode.versionInfo.text()
            //创建文件并写入结点数据
            def destFile = new File(destDir, "release-${name}.txt")
            destFile.withWriter { writer -> writer.write("${name} -> ${versionCode} -> ${versionInfo}")
            }
        }
    }

}

当版本改变的时候,我们只需要修改versionInfo 对应的内容,然后执行Task就可以生成对应的文档了,不过上述脚本还是需要我们手动修改versionInfo 的内容,我们可以把versionInfo 的内容从服务器获取(获取版本信息),这样的话就不需要我们手动修改了,groovy对网络访问这块没有特殊的扩展,所以我们用java的请求网络的方式实现即可。

案例三:项目发送到远程maven库

//是否需要把项目发送到远程maven库
ext.publishToRemote = true
ext.publishDefaultArtifact = !"true".equals(project.getProperties().get("org.gradle.parallel"))
ext.publishApk = false
ext.isApplication = false

if (!project.getBuildFile().exists()) {
    return;
}

apply plugin: 'maven'
apply plugin: 'maven-publish'

configurations {
    providedCompile
    compile.extendsFrom providedCompile
}

repositories {
//  mavenLocal() :maven本地仓库
    maven {
        url "maven仓库地址"
    }
}


if (!project.getRootProject().hasProperty("aarMap")) {
    project.getRootProject().ext.set("aarMap", new HashSet<String>())
}

def deployVersion = System.getProperty('deployVersion')
//配置完成之后执行
project.afterEvaluate {
    if (project.plugins.hasPlugin("com.android.library")) {
        project.getRootProject().aarMap.add(project.name)
    }

    ext.isApplication = (project.plugins.hasPlugin("com.android.application"))

    tasks.whenTaskAdded { task ->
        if (task.name.startsWith("generatePomFileForMavenPublication")) {
            task.doFirst {
                project.publishing.publications.maven(MavenPublication) {
                    if (!components.hasWithName("java") && !isApplication) {

                        File f = file("${project.buildDir}/outputs/awb/${project.name}-release.awb");

                        if (!f.exists()) {
                            f = file("${project.buildDir}/outputs/aar/${project.name}-release.aar");
                        }
                        if (!f.exists()) {
                            f = file("${project.buildDir}/outputs/awb/${project.name}-debug.awb");
                        }
                        if (!f.exists()) {
                            f = file("${project.buildDir}/outputs/aar/${project.name}-debug.aar");
                        }

                        artifact f.getPath()
                    }
                }
            }
        }

        if (isApplication && !publishApk) {
            if (task.name.startsWith("publish")) {
                task.setEnabled(false)
            }
        }
    }
}


def HashMap getAccount() {
    HashMap accountMap = new HashMap()
    def parsedSettingsXml
    def settingsFile = '/Users/root/Downloads/android/apache-maven-3.3.9/conf/settings.xml'
    def defaultSettingsFile = System.getProperty("user.home") + "/.m2/settings.xml"

    if (file(settingsFile).exists() || file(defaultSettingsFile).exists()) {
        if (file(settingsFile).exists()) {
            parsedSettingsXml = (new XmlParser()).parse(settingsFile);
        } else if (file(defaultSettingsFile).exists()) {
            parsedSettingsXml = (new XmlParser()).parse(defaultSettingsFile);
        }

        parsedSettingsXml.servers[0].server.each { server ->
            if ("releases" == server.id.text()) {
                accountMap.put("id", server.id.text())
                accountMap.put("username", server.username.text())
                accountMap.put("password", server.password.text())
            }
        }

    } else {
        accountMap.put("id", "releases")
        accountMap.put("username", "admin")
        accountMap.put("password", "admin123")
    }
    return accountMap
}

publishing {
    if (null != deployVersion) {
        version = deployVersion
    }

    publications {
        maven(MavenPublication) {
            version version
            task sourceJar(type: Jar) {
                classifier = 'source'
                version = version
                try {
                    if (components.hasWithName("java")) {
                        from sourceSets.main.allJava
                    } else {
                        from android.sourceSets.main.java.srcDirs
                    }
                } catch (Throwable e) {
                }
            }
            artifact sourceJar
            if (components.hasWithName("java") || components.hasWithName("android")) {

                if (components.hasWithName("java")) {
                    from components.java
                } else if (!isApplication) {
                    from components.android
                }

                pom.withXml {
                    asNode().dependencies.'*'.each {
                        if (it.scope.text() == 'runtime') {
                            if (project.configurations.providedCompile.allDependencies.find { dep -> dep.name == it.artifactId.text() }) {
                                it.scope*.value = 'provided'
                            } else if (project.configurations.compile.allDependencies.find { dep -> dep.name == it.artifactId.text() }) {
                                it.scope*.value = 'compile'
                            }
                        }
                    }
                }

                pom.withXml {
                    asNode().dependencies.'*'.findAll() {
                        it.groupId.text() == groupId && project.getRootProject().aarMap.contains(it.artifactId.text())
                    }.each { it.appendNode('type', 'aar') }
                }

                if (!components.hasWithName("java") && !isApplication) {
                    pom.withXml {
                        def dependenciesNode = asNode().dependencies[0]
                        if (getGradle().startParameter.toString().contains("assembleDebug")) {
                            configurations.debugCompile.allDependencies.each {
                                if (it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) {

                                    def dependencyNode = dependenciesNode.appendNode('dependency')
                                    dependencyNode.appendNode('groupId', it.group)
                                    dependencyNode.appendNode('artifactId', it.name)
                                    dependencyNode.appendNode('version', it.version)
                                    if (it.group == groupId && project.getRootProject().aarMap.contains(it.name)) {
                                        dependencyNode.appendNode('type', 'aar')
                                    }
                                }
                            }
                        } else {
                            configurations.releaseCompile.allDependencies.each {
                                if (it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) {

                                    def dependencyNode = dependenciesNode.appendNode('dependency')
                                    dependencyNode.appendNode('groupId', it.group)
                                    dependencyNode.appendNode('artifactId', it.name)
                                    dependencyNode.appendNode('version', it.version)
                                    if (it.group == groupId && project.getRootProject().aarMap.contains(it.name)) {
                                        dependencyNode.appendNode('type', 'aar')
                                    }

                                }
                            }
                        }
                    }
                }

            } else if (!isApplication) {

                pom.withXml {

                    def dependenciesNode = asNode().appendNode('dependencies')

                    def providedCompiles = new HashSet();

                    configurations.providedCompile.allDependencies.each {
                        if (it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) {
                            def dependencyNode = dependenciesNode.appendNode('dependency')
                            dependencyNode.appendNode('groupId', it.group)
                            dependencyNode.appendNode('artifactId', it.name)
                            dependencyNode.appendNode('version', it.version)
                            dependencyNode.appendNode('scope', 'provided')
                            if (it.group == groupId && project.getRootProject().aarMap.contains(it.name)) {
                                dependencyNode.appendNode('type', 'aar')
                            }
                            providedCompiles.add(it.group + "." + it.name)
                        }
                    }

                    configurations.compile.allDependencies.each {
                        if (it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) {

                            if (!providedCompiles.contains(it.group + "." + it.name)) {
                                def dependencyNode = dependenciesNode.appendNode('dependency')
                                dependencyNode.appendNode('groupId', it.group)
                                dependencyNode.appendNode('artifactId', it.name)
                                dependencyNode.appendNode('version', it.version)
                                if (it.group == groupId && project.getRootProject().aarMap.contains(it.name)) {
                                    dependencyNode.appendNode('type', 'aar')
                                }
                            }
                        }
                    }

                    if (getGradle().startParameter.toString().contains("assembleDebug")) {
                        configurations.debugCompile.allDependencies.each {
                            if (it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) {
                                if (!providedCompiles.contains(it.group + "." + it.name)) {
                                    def dependencyNode = dependenciesNode.appendNode('dependency')
                                    dependencyNode.appendNode('groupId', it.group)
                                    dependencyNode.appendNode('artifactId', it.name)
                                    dependencyNode.appendNode('version', it.version)
                                    if (it.group == groupId && project.getRootProject().aarMap.contains(it.name)) {
                                        dependencyNode.appendNode('type', 'aar')
                                    }
                                }
                            }
                        }
                    } else {
                        configurations.releaseCompile.allDependencies.each {
                            if (it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null) {

                                if (!providedCompiles.contains(it.group + "." + it.name)) {
                                    def dependencyNode = dependenciesNode.appendNode('dependency')
                                    dependencyNode.appendNode('groupId', it.group)
                                    dependencyNode.appendNode('artifactId', it.name)
                                    dependencyNode.appendNode('version', it.version)
                                    if (it.group == groupId && project.getRootProject().aarMap.contains(it.name)) {
                                        dependencyNode.appendNode('type', 'aar')
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    repositories {
        mavenLocal()
        if (publishToRemote) {
            if (version.endsWith("-SNAPSHOT")) {
                maven {
                    url "maven仓库远程地址"
                    credentials {
                        username = "admin"
                        password = "admin123"
                    }
                }
            } else {
                def accountMap = getAccount();
                maven {
                    url "maven仓库本地地址"
                    credentials {
                        username = accountMap.get("username")
                        password = accountMap.get("password")
                    }
                }
            }
        }
    }
}

案例四:多渠道打包

渠道包就是要在安装包中添加渠道信息,也就是channel,对应不同的渠道,例如:小米市场、360市场、应用宝市场等 。我们要在安装包中添加不同的标识,应用在请求网络的时候携带渠道信息,方便后台做运营统计(这就是添加渠道信息的用处)。

在AndroidMainfest.xml配置相应的渠道
<meta-data android:value="UMENG_CHANNEL"         
      android:name="${UMENG_CHANNEL_VALUE}"/>  <!--动态更改渠道号-->
在build.gradle中配置渠道信息和自动替换脚本
//多渠道打包
productFlavors {
    xiaomi {}
    huawei {}
    yingyongbao {}
    wandoujia {}
}

//自动替换清单文件中的渠道号
productFlavors.all {
    flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
默认配置
defaultConfig {
    applicationId "com.gradle.demo"
    minSdkVersion 11
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    multiDexEnabled true //突破应用方法数65535的限制
}

所有渠道默认使用这一配置,如果渠道有特殊需求,可以在productFlavors对应的渠道号中单独配置。

打包后自动修改APK的名字
//release包的命名格式为:产品名_版本号_渠道名.apk
//debug包的命名格式为:产品名_版本号-debug.apk
applicationVariants.all { variant ->
    variant.outputs.each { output ->
        def outputFile = output.outputFile
        if (null != outputFile && outputFile.name.endsWith('.apk')) {
            File outputDir = new File(outputFile.parent);
            def baseName = PRODUCT_NAME + "${defaultConfig.versionName}" + "_" + variant.productFlavors[0].name
            def newApkName
            if (variant.buildType.name.equals('release')) {
                newApkName = baseName + '.apk'
            } else if (variant.buildType.name.equals('debug')) {
                def debugName = PRODUCT_NAME + "${defaultConfig.versionName}"
                newApkName = debugName + "-debug.apk"
            }
            output.outputFile = new File(outputDir, newApkName)
        }
    }
}
完整build.gradle文件内容如下:
apply plugin: 'com.android.application'

//产品名
def PRODUCT_NAME = "BuglyDemo"

android {
    //添加签名文件配置
    signingConfigs {
        mysigns {
            keyAlias 'alias'
            keyPassword '123456'
            storeFile file('sign.jks')
            storePassword '123456'
        }
    }

    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    defaultConfig {
        applicationId "com.gradle.demo"
        minSdkVersion 11
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        multiDexEnabled true //突破应用方法数65535的限制
    }

    //多渠道打包
    productFlavors {
        xiaomi {}
        huawei {}
        yingyongbao {}
        wandoujia {}
    }

    //自动替换清单文件中的渠道号
    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }


    buildTypes {
        release {
            minifyEnabled false //是否启用混淆
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            debuggable false
            jniDebuggable false
            signingConfig signingConfigs.mysigns
            renderscriptDebuggable false
            minifyEnabled false
            pseudoLocalesEnabled false
            zipAlignEnabled true
        }
    }

//release包的命名格式为:产品名版本号渠道名.apk
//debug包的命名格式为:产品名_版本号-debug.apk
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (null != outputFile && outputFile.name.endsWith('.apk')) {
                File outputDir = new File(outputFile.parent);
                def baseName = PRODUCT_NAME + "${defaultConfig.versionName}" + "_" + variant.productFlavors[0].name
                def newApkName
                if (variant.buildType.name.equals('release')) {
                    newApkName = baseName + '.apk'
                } else if (variant.buildType.name.equals('debug')) {
                    def debugName = PRODUCT_NAME + "${defaultConfig.versionName}"
                    newApkName = debugName + "-debug.apk"
                }
                output.outputFile = new File(outputDir, newApkName)
            }
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.1.1'
    testCompile 'junit:junit:4.12'
    //添加友盟统计库依赖
    compile 'com.umeng.analytics:analytics:latest.integration'
}

打包工具:ApkChannelPackag

关注微信公众号获取更多相关资源

Android行动派

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值