Android 开发必备知识:我和 Gradle 有个约会

0、讲个故事

0.1 Ant,我还真以为你是只蚂蚁

真正开始近距离接触编程其实是在2012年,年底的时候带我的大哥说,咱们这个 app 发布的时候手动构建耗时太久,研究一下 ant 脚本吧。

那个时候连 HashMap 都不知道是啥,可想开发经验几乎为零,一个小小的 ant 脚本看得我真是深深地感受到了这个世界充满的恶意。好在后来硬着头皮搞明白了什么 target 之类的鬼东西,不然就没有然后了。

0.2 Maven,你们真的会读这个单词么

Maven /`meivn/

接触 Maven,完全是因为读陈雄华的《Spring 实战》,他的源码居然是用 Maven 构建的,结果 Spring 学得一塌糊涂,Maven 我倒是用顺手了。。

跟 Ant 一样,Maven 可以用来构建 Java 工程;跟 Ant 一样,Maven 的配置用 xml 来描述;但,Maven 可以管理依赖,它可以让你做到“想要什么,就是一句话的事儿”。比如我想要个 gson,Maven 说可以,你记下来我带会儿构建的时候给你去取。

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.4</version>
</dependency>

真是让你当大爷呢。不过,Maven 这家伙学起来有点儿费劲,很多初学的时候在搭建环境的时候就被搞死了——你以为是因为 Maven 的学习曲线陡峭吗?当然不是,是因为当初 Maven 的中央仓库被 x 了,所以你就天天看着 cannot resovle dependencies 玩就好了。

后来 OSChina 傍上了阿里这个爸爸,就有了 maven.oschina.net。我去年找工作落定之后,想着做点儿什么的时候,发现 maven.oschina.net 估计被阿里爸爸关禁闭,死了几天,现在又活过来了。那又怎样呢,反正中央仓库被 x 的事情也已经成为过去。

0.3 Gradle,你爹是不是 Google!!

13年的时候,我兴奋地跟前面提到的大哥说 Maven 是个好同志的时候,大哥说,Google 推荐用 Gradle……所以,我想 Gradle,你爹是不是 Google……或者至少是个干爹吧。

其实这都不重要了,毕竟 Gradle 实在是好用。比起前面两位的 xml 配置的手段,直接用代码的方式上阵必然是灵活得多。不仅如此,Gradle 居然可以使用 Maven 仓库来管理依赖,就像是一个简易版的 Maven 一样,如果不是看不到 pom 文件,你都还以为你仍然在使用 Maven(当然,由于你在用 Maven 的仓库,所以你自然也是离不开 Maven 的)。哦,你是 Ant 用户啊,那也没关系啊,不信你看:

task helloTAS << {
    ant.echo(message: 'Hello TAS.')
}

1、用 Gradle 构建

1.1 工程结构

如图所示,这是一个不能更普通的 android 的 gradle 工程了。

  • 根目录下面的 settings.gradle 当中主要是用来 include 子模块的,比如我们这个工程有一个叫做 app 的子模块,那么 settings.gradle 的内容如下:

include ':app'
  • 根目录下面的 build.gradle 包含一些通用的配置,这些配置可以在各个子模块当中使用。
  • gradle.properties 文件包含的属性,会成为 project 的 properties 的成员,例如我们添加了属性 hello,

hello=Hello Tas!

  • 然后我们在 build.gradle 当中创建 task:
task hello << {
    println hello
    println project.getProperties().get("hello")
 }

输出地结果是一样的:

14:28:11: Executing external task 'hello'...
Configuration on demand is an incubating feature.
:app:hello
Hello Tas!
Hello Tas!

BUILD SUCCESSFUL

Total time: 0.54 secs
14:28:12: External task execution finished 'hello'.
  • local.properties 这个文件在 android 工程当中会遇到,我们通常在其中设置 android 的 sdk 和 ndk 路径。当然,这个 android studio 会帮我们设置好的。为了更清楚地了解这一点,我把 android 的 gradle 插件的部分源码摘录出来:

SDK.groovy,下面的代码主要包含了加载 sdk、ndk 路径的操作。

private void findLocation() {
 if (TEST_SDK_DIR != null) {
    androidSdkDir = TEST_SDK_DIR
    return
}

 def rootDir = project.rootDir
 def localProperties = new File(rootDir, FN_LOCAL_PROPERTIES)
 if (localProperties.exists()) {
    Properties properties = new Properties()
    localProperties.withInputStream { instr ->
    properties.load(instr)
    }
    def sdkDirProp = properties.getProperty('sdk.dir')

    if (sdkDirProp != null) {
        androidSdkDir = new File(sdkDirProp)
    } else {
        sdkDirProp = properties.getProperty('android.dir')
        if (sdkDirProp != null) {
            androidSdkDir = new File(rootDir, sdkDirProp)
            isPlatformSdk = true
        } else {
            throw new RuntimeException(
 "No sdk.dir property defined in local.properties file.")
        }
    }

    def ndkDirProp = properties.getProperty('ndk.dir')
    if (ndkDirProp != null) {
        androidNdkDir = new File(ndkDirProp)
    }

 } else {
    String envVar = System.getenv("ANDROID_HOME")
    if (envVar != null) {
        androidSdkDir = new File(envVar)
    } else {
        String property = System.getProperty("android.home")
        if (property != null) {
            androidSdkDir = new File(property)
        }
    }

    envVar = System.getenv("ANDROID_NDK_HOME")
    if (envVar != null) {
        androidNdkDir = new File(envVar)
        }
    }
 } 

BasePlugin.groovy,通过这两个方法,我们可以在 gradle 脚本当中获取 sdk 和 ndk 的路径

File getSdkDirectory() {
    return sdk.sdkDirectory
}

File getNdkDirectory() {
    return sdk.ndkDirectory
}

例如:

task hello << {
    println android.getSdkDirectory()
}
14:37:33: Executing external task 'hello'...
Configuration on demand is an incubating feature.
:app:hello
/Users/benny/Library/Android/sdk

BUILD SUCCESSFUL

Total time: 0.782 secs
14:37:35: External task execution finished 'hello'.

上面给出的只是最常见的 hierarchy 结构,还有 flat 结构,如下图1为 flat 结构,2为 hierarchy 结构。有兴趣的话可以 Google 一下。

1.2 几个重要的概念

这一小节的出场顺序基本上跟 build.gradle 的顺序一致。

1.2.1 Repository 和 Dependency

如果你只是写 Android 程序,那么依赖问题可能还不是那么的烦人——如果你用 Java 写服务端程序,那可就是一把辛酸一把泪了。

仓库的出现,完美的解决了这个问题,我们在开发时只需要知道依赖的 id 和版本,至于它存放在哪里,我不关心;它又依赖了哪些,构建工具都可以在仓库中帮我们找到并搞定。这一切都是那么自然,要不要来一杯拿铁,让代码构建一会儿?

据说在 Java 发展史上,涌现出非常多的仓库,不过最著名的当然是 Maven 了。Maven 通过 groupId 和 artifactId 来锁定构件,再配置好版本,那么 Maven 仓库就可以最终锁定一个确定版本的构件供你使用了。比如我们开头那个例子,

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.4</version>
</dependency>

Maven 就凭这么几句配置就可以帮你搞定 gson-2.4.jar,不仅如此,它还会按照你的设置帮你把 javadoc 和 source 搞定。妈妈再也不用担心我看不到构件的源码了。

那么这个神奇的 Maven 仓库在哪儿呢? Maven Central,中央仓库,是 Maven 仓库的鼻祖,其他的大多数仓库都会对它进行代理,同时根据需求添加自己的特色库房。简单说几个概念:

  • 代理仓库:要租房,去搜房网啊。你要去驾校报名,我是驾校代理,你找我,我去找驾校。具体到这里,还有点儿不一样,一旦有人从代理仓库下载过一次特定得构件,那么这个构件会被代理仓库缓存起来,以后就不需要找被代理的仓库下载了。
  • 私有仓库:中国特色社会主义。走自己的路,你管我啊?公司内部的仓库里面有几个 hosted 的仓库,这些仓库就是我们公司内部特有的,里面的构件也是我们自己内部的同事上传以后供团队开发使用的。
  • 本地仓库:大隐隐于市。跟代理仓库的道理很像,只不过,这个仓库是存放在你自己的硬盘上的。

说起来,Andoid sdk 下面有个 extra 目录,里面的很多依赖也是以Maven 仓库的形式组织的。不过这是 Google 特色嘛,人家牛到不往 Maven 的中央仓库上传,真是没辙。

1.2.2 SourceSets

源码集,这里面主要包含你的各种类型的代码的路径,比如 ‘src/main/java’ 等等。

1.2.3 Properties

前面我们其实也稍稍有提到,这个 properties 其实是 gradle 的属性,在 gradle 源码当中,我们找到 Project.java 这个接口,可以看到:

   /**
 * <p>Determines if this project has the given property. See <a href="#properties">here</a> for details of the
 * properties which are available for a project.</p>
 *
 * @param propertyName The name of the property to locate.
 * @return True if this project has the given property, false otherwise.
 */
boolean hasProperty(String propertyName);

/**
 * <p>Returns the properties of this project. See <a href="#properties">here</a> for details of the properties which
 * are available for a project.</p>
 *
 * @return A map from property name to value.
 */
Map<String, ?> getProperties();

/**
 * <p>Returns the value of the given property.  This method locates a property as follows:</p>
 *
 * <ol>
 *
 * <li>If this project object has a property with the given name, return the value of the property.</li>
 *
 * <li>If this project has an extension with the given name, return the extension.</li>
 *
 * <li>If this project's convention object has a property with the given name, return the value of the
 * property.</li>
 *
 * <li>If this project has an extra property with the given name, return the value of the property.</li>
 *
 * <li>If this project has a task with the given name, return the task.</li>
 *
 * <li>Search up through this project's ancestor projects for a convention property or extra property with the
 * given name.</li>
 *
 * <li>If not found, a {@link MissingPropertyException} is thrown.</li>
 *
 * </ol>
 *
 * @param propertyName The name of the property.
 * @return The value of the property, possibly null.
 * @throws MissingPropertyException When the given property is unknown.
 */
Object property(String propertyName) throws MissingPropertyException;

/**
 * <p>Sets a property of this project.  This method searches for a property with the given name in the following
 * locations, and sets the property on the first location where it finds the property.</p>
 *
 * <ol>
 *
 * <li>The project object itself.  For example, the <code>rootDir</code> project property.</li>
 *
 * <li>The project's {@link Convention} object.  For example, the <code>srcRootName</code> java plugin
 * property.</li>
 *
 * <li>The project's extra properties.</li>
 *
 * </ol>
 *
 * If the property is not found, a {@link groovy.lang.MissingPropertyException} is thrown.
 *
 * @param name The name of the property
 * @param value The value of the property
 */
    void setProperty(String name, Object value) throws 
 MissingPropertyException;

不难知道,properties 其实就是一个 map,我们可以在 gradle.properties 当中定义属性,也可以通过 gradle 脚本来定义:

setProperty('hello', 'Hello Tas again!')

使用方法我们前面已经提到,这里就不多说了。

1.2.4 Project和Task

如果你用过 ant,那么 project 基本上类似于 ant 的 project 标签,task 则类似于 ant 的 target 标签。我们在 build.gradle 当中编写的

task hello << {
......
}

实际上,是调用

Task Project.task(String name) throws InvalidUserDataException;

创建了一个 task,并通过 << 来定义这个 task 的行为。我们看到 task 还有如下的重载:

Task task(String name, Closure configureClosure);

所以下面的定义也是合法的:

task('hello2',{
    println hello
})

简单说,project 就是整个构建项目的一个逻辑实体,而 task 就是这个项目的具体任务点。更多地介绍可以参见官网的文档,和 gradle 的源码。


2、发布构件

发布构件,还是依赖仓库,我们仍然以 Maven 仓库为例,私有仓库多数采用 sonatype。

2.1 UI 发布

如果管理员给你开了这个权限,你会在 UI 上面看到 upload artifact 的 tab,选择你要上传的构件,配置好对应的参数,点击上传即可。

2.2 使用 Maven 插件

这里的意思是使用 Maven 的 gradle 插件,在构建的过程中直接上传。构建好的构件需要签名,请下载 GPG4WIN (windows),或者 GPGTOOLS(mac),生成自己的 key。

直接上代码:

gradle.properties

sonatypeUsername=你的用户名
sonatypePassword=你的密码
signing.keyId=你的keyid
signing.password=你的keypass
#注意,通常来讲是这个路径。
# mac/linux
signing.secretKeyRingFile=/Users/你的用户名/.gnupg/secring.gpg

# Window XP and earlier (XP/2000/NT)
# signing.secretKeyRingFile=C:\\Documents and Settings\\<username>\\Application Data\\GnuPG\\secring.gpg

# Windows Vista and Windows 7
# signing.secretKeyRingFile=C:\\Users\\<username>\\AppData\\Roaming\\gnupg\\secring.gpg


projectName=你的构件名称
group=你的构件groupid
artifactId=你的构件artifactid
# 版本号,采用三位数字的形式,如果是非稳定版本,请务必添加SNAPSHOT
version=0.0.1-SNAPSHOT

build.gradle

apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'signing'

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"

    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 21
        versionCode 1
        versionName "0.2"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    ......
}

def isSnapshot = version.endsWith('-SNAPSHOT')
def sonatypeRepositoryUrl
if(isSnapshot) {
    sonatypeRepositoryUrl = "http://example/nexus/content/repositories/thirdparty-snapshots/"
} else {
    sonatypeRepositoryUrl = "http://example/nexus/content/repositories/thirdparty/"
}

sourceSets {
    main {
        java {
            srcDir 'src/main/java'
        }
    }
}

task sourcesJar(type: Jar) {
    from sourceSets.main.allSource
    classifier = 'sources'
}

artifacts {
    //archives javadocJar
    archives sourcesJar
}

signing {
    if(project.hasProperty('signing.keyId') && project.hasProperty('signing.password') &&
        project.hasProperty('signing.secretKeyRingFile')) {
    sign configurations.archives
    } else {
        println "Signing information missing/incomplete for ${project.name}"
    }
}

uploadArchives {
    repositories {
        mavenDeployer {

            if(project.hasProperty('preferedRepo') && project.hasProperty('preferedUsername')
            && project.hasProperty('preferedPassword')) {
                
                configuration = configurations.archives
                repository(url: preferedRepo) {
                
                    authentication(userName: preferedUsername, password: preferedPassword)
                    }
                
            } else if(project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) {
                
                beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
                
                repository(url: sonatypeRepositoryUrl) {
                    authentication(userName: sonatypeUsername, password: sonatypePassword)
                
                }
            } else {
            println "Settings sonatypeUsername/sonatypePassword missing/incomplete for ${project.name}"
            }

            pom.artifactId = artifactId
            pom.project {
                name projectName
                packaging 'aar'

                developers {
                    developer {
                        id 'wecar'
                        name 'wecar'
                    }
                }
            }
        }
    }
}

然后运行 gradle uploadArchives 就可以将打包的 aar 发布到公司的 Maven 仓库当中了。jar包的方式类似,这里就不在列出了。

2.3 使用 Maven 命令

这个可以通过 mvn 在 cmdline 直接发布构件,命令使用说明:

mvn deploy:deploy-file -Durl=file://C:\m2-repo \
       -DrepositoryId=some.id \
       -Dfile=your-artifact-1.0.jar \
       [-DpomFile=your-pom.xml] \
       [-DgroupId=org.some.group] \
       [-DartifactId=your-artifact] \
       [-Dversion=1.0] \
       [-Dpackaging=jar] \
       [-Dclassifier=test] \
       [-DgeneratePom=true] \
       [-DgeneratePom.description="My Project Description"] \
       [-DrepositoryLayout=legacy] \
       [-DuniqueVersion=false]

当然这里仍然有个认证的问题,我们需要首先在 maven 的 settings 配置当中加入:

<servers>
        <server>
            <id>helloworld</id>
            <username>your name</username>
            <password>your password</password>
        </server>
</servers>

然后我们就可以使用命令上传了:

mvn deploy:deploy-file -DgroupId=com.tencent.test -DartifactId=test -
Dversion=1.0.0 -Dpackaging=aar -Dfile=test.aar -
Durl=http://example/nexus/content/repositories/thirdparty -
DrepositoryId=helloworld

3、插件

3.1 什么是插件

插件其实就是用来让我们偷懒的。如果没有插件,我们想要构建一个 Java 工程,就要自己定义 sourceSets,自己定义 classpath,自己定义构建步骤等等。

简单地说,插件其实就是一组配置和任务的合集。

gradle 插件的存在形式主要由三种,

  • gradle 文件中直接编写,你可以在你的 build.gradle 当中写一个插件来直接引入:

apply plugin: GreetingPlugin

class GreetingPlugin implements Plugin<Project{
    void apply(Project project) {
       project.task('hello') << {
            println "Hello from the GreetingPlugin"
        }
    }
}
  • buildSrc工程,这个就是在你的工程根目录下面有一个标准的 Groovy 插件工程,目录是 buildSrc,你可以直接引用其中编写的插件。
  • 独立的工程,从结构上跟 buildSrc 工程是一样的,只不过这种需要通过发布到仓库的形式引用。通常我们接触的插件都是这种形式。

详细可以参考:Chapter 61. Writing Custom Plugins

3.2 常见的插件

目前接触到的插件,有下面这么几种:

  • java,构建 java 工程
  • war,发布 war 包用,构建 web 工程会用到
  • groovy,构建 groovy 工程
  • com.android.application,构建 Android app 工程
  • com.android.library,构建 Android library,通常输出 aar
  • sign,签名
  • maven,发布到 maven 仓库
  • org.jetbrains.intellij,构建 intellij 插件工程

3.3 自己动手写一个插件

创建一个普通的 groovy 工程(java 工程也没有关系),创建 src/main/groovy 目录,编写下面的代码:

package com.example.wecar.plugin

import org.gradle.api.Plugin
import org.gradle.api.internal.project.ProjectInternal

class GreetingPlugin implements Plugin<ProjectInternal> {

    void apply(ProjectInternal project) {
        project.task('hello') << {
            println 'hello'
        }
    }
}

在 src/main/resources 创建 META-INF/gradle-plugins 目录,创建 greetings.properties 文件:

implementation-class=com.example.wecar.plugin.GreetingPlugin

其中 greettings 就是你的插件 id。

build.gradle

group 'com.example.wecar.plugin'
version '1.1-SNAPSHOT'

buildscript {
    repositories {
        mavenLocal()
    }
}

apply plugin: 'groovy'
apply plugin: 'java'

repositories {
    mavenCentral()
}

sourceSets {
    main {
        groovy {
            srcDirs = [
                'src/main/groovy',
                'src/main/java'
            ]
        }  // compile everything in src/ with groovy
        java { srcDirs = []}// no source dirs for the java compiler

    }
}

dependencies {
    //tasks.withType(Compile) { options.encoding = "UTF-8" }
    compile gradleApi()
}

// custom tasks for creating source jars
task sourcesJar(type: Jar, dependsOn:classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
}

// add source jar tasks as artifacts
artifacts { archives sourcesJar }

// upload to local
uploadArchives {
    repositories{
        mavenLocal()
    }
}

运行 uploadArchives 发布到本地仓库,那么就可以找到我们自己的插件了,由于当中没有指定 artifactId,那么我们的插件的 artifactId 就是我们的工程名称,比如这里是 deployplugin。

那么我们要怎么引入这个插件呢?

首先要再 buildScript 增加依赖:

buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
        classpath 'com.example.wecar.plugin:deployplugin:1.1-SNAPSHOT'
    }
}

然后:

apply plugin: 'greetings'

这样我们的 task “hello” 就被引入了。


4、Gradle 运行慢?

用过 Gradle 的朋友多少会感觉到这货有时候会比较慢。我们可以通过下面的三个手段加速你的 Gradle。

  • 不用中央仓库。如果你的 repository 配置的是 mavenCentral,放开它吧,全世界的人都在琢磨着怎么虐它,你就不要瞎掺和了。试试 jCenter。
  • 升级最新的 Gradle 版本。
  • 开启Gradle的电动小马达。在 gradle.properties(眼熟?没错,就是它!!)

里面添加下面的配置:

如果你的任务没有时序要求,那么打开这个选项可以并发处理多个任务,充分利用硬件资源。。嗯,如果你的是单核 CPU。。当我没说。。 org.gradle.parallel=true 这个也可以在命令行通过参数的形式启动,3个小时有效。守护进程可以使编译时间大大缩短 org.gradle.daemon=true 这个看需求吧,Gradle 是运行在 Java 虚拟机上的,这个指定了这个虚拟机的堆内存初始化为256M,最大为1G。如果你内存只有2G,那当我没说。。 org.gradle.jvmargs=-Xms256m -Xmx1024m

当然,建议的方式是在你的用户目录下面的 .gradle/ 下面创建一个 gradle.properties,免得坑你的队友……

本文系腾讯Bugly独家内容,转载请在文章开头显眼处注明注明作者和出处“腾讯Bugly(http://bugly.qq.com)”

腾讯Bugly 最专业的质量跟踪平台

精神哥、小萝莉,为您定期分享应用崩溃解决方案

本文分享自微信公众号 - 腾讯Bugly(weixinBugly)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2015-12-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏情情说

《深入实践Spring Boot》阅读笔记之一:基础应用开发

上上篇「1718总结与计划」中提到,18年要对部分项目拆分,进行服务化,并对代码进行重构。公司技术委员会也推荐使用spring boot,之前在各个技术网站中也...

36690
来自专栏IT可乐

Spring详解(一)------概述

  本系列教程我们将对 Spring 进行详解的介绍,相信你在看完后一定能够有所收获。 1、什么是 Spring ?   Spring是一个开源框架,Sprin...

24370
来自专栏Java社区

spring是什么?它能够做什么?新手必看

21730
来自专栏玄魂工作室

Hacker基础之Linux篇:系统简介特别番之Kali Linux

0x01. 前言 作为安全界的明星,Kali可以说是无人不知无人不晓,当然,除了Kali之外,我们还有许多的可以选择的工具,比如: ParrotSec ? ...

45560
来自专栏程序员互动联盟

【专业技术第十一讲】Eclipse和myeclipse区别

存在问题: 初学java环境配置这个Eclipse和MyEclipse分不清?十分不解 解决方案: Eclipse 是一个开放源代码的、基于Java的可扩展开发...

754110
来自专栏Java进阶架构师

这么说吧,NIO很简单,其实就是个牛逼IO,吹牛逼用的

NIO 也能算是一种思想,非阻塞IO通信思想,Netty就是基于NIO思想的NIO框架,

26830
来自专栏H2Cloud

linux下IPC latency 进程间通讯延迟测试结果

详文请见  VLoveIYhttp://ffown.sinaapp.com/?p=40 CPU name : Intel(R) Xeon(R) CPU E540...

55640
来自专栏landv

windows server 2012 R2 远程桌面授权模式尚未配置

windows server 2012 R2 远程桌面授权模式尚未配置,远程桌面服务将在120天内停止工作。如何破解这个宽限期,目前企业7位协议号码均不包含20...

924100
来自专栏Gaussic

Spring In Action 4(Spring实战第四版)翻译与理解 第一章 付诸行动

        这几天笔者阅读了一下Spring In Action 4,感觉还是有一定的收获的。在之前的项目上,只会简单地使用Spring MVC,对于很多概...

57720
来自专栏向治洪

老司机带你玩转web service

当大型需求被数个公司分割开来,各公司系统相互交换数据的问题就会接踵而来。毕竟是多家不同的公司的产品,研发开发语言、采用技术框架基本上是百花齐放。怎样让自家系统提...

31290

扫码关注云+社区

领取腾讯云代金券