读懂Android Studio中的Gradle文件

Gradle是基于Groovy的动态DSL,而Groovy是基于JVM的,Groovy的语法和Java很类似。

Closure

Groovy语言有一个很重要的概念,就是Closures(闭包)。Closure是一段代码块,它可以被赋值给一个变量,还可以接受参数。

把一个闭包赋值给一个变量:

def tmpClosure = {println "This is a Closure"}
tmpClosure()

闭包接受参数:

def tmpClosure = {String str -> println str}
tmpClosure("This is a Closure")

省略括号

在Groovy中,如果函数只有一个参数,那调用这个函数时就可以省略括号;如果有多个参数,且最后一个参数是闭包,那闭包可以写在括号外面。

只有一个参数:

void compileSdkVersion(int apiLevel)
compileSdkVersion 22 //相当于compileSdkVersion(22)

void afterEvaluate(Closure closure)
afterEvaluate{println "afterEvaluate"} //相当于afterEvaluate({println "afterEvaluate"}) 
//这里的println本身也省略了括号

有多个参数,且最后一个参数是闭包:

MavenPom addFilter(String name, Closure filter)
addFilter("add"){println "addFilter"}// 相当于addFilter("add", {println "addFilter"})

Groovy语言还有很多很强的特性,甚至可以很轻易的为final类添加新的方法,一个类还可以直接复制另一个的全部方法,有兴趣的话可以自己再深入了解。这里主要是为了能读懂Android Studio的Gradle文件,就点到为止了。

Android Studio中的Gradle文件

了解了Groovy语言的这个特点后,我们再来看Android Studio中的Gradle文件。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath 'com.novoda:bintray-release:0.4.0'
    }
    // Workaround for the following test coverage issue. Remove when fixed:
    // https://code.google.com/p/android/issues/detail?id=226070
    configurations.all {
        resolutionStrategy {
            force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
            force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
        }
    }
}

因为Groovy的函数可以省略括号,我们就可以知道其实buildscript就是参数是一个Closure的函数而已。在Groovy中,只接收Closure作为参数的函数称为Script Block。在Gradle的javadoc中我们可以找到这个方法

void buildscript(@DelegatesTo(value=ScriptHandler.class,strategy=1) Closure configureClosure)

在这里configureClosure被委托给了ScriptHandler,可以理解为ScripHandle就是这个闭包的执行上下文。

在Closure中有三个关键字,this,ownerdelegate。this指向创建该闭包的类的实例对象或类本身。对于owner来讲,它的含义基本上跟this的含义一样,只是除了一种情况,如果该闭包是在其他的闭包中定义的,那么owner是指向定义它的闭包对象。对于delegate来讲,它的含义大多数情况下是跟owner的含义一样,除非它被显示的修改。更详细的介绍可以查看官方文档

可以理解为闭包中没有定义的变量和函数前面都自动加上delegate,这个delegate指向执行上下文,delegate可以被修改,在这里就通过@DelegatesTo被修改成指向ScriptHandler的实例。所以ScriptHandler的实例可以调用buildscript里面的repositories和buildscript函数,也就是说ScriptHandler本身有这两个方法:repositories{}和dependencies{}

再看classpath ‘com.android.tools.build:gradle:2.3.1’其实也是一个省略了括号的函数调用,相当于:

classpath('com.android.tools.build:gradle:2.3.1')。

Gradle到底是什么

Gradle是一个工具,也是一个编程框架。我们编写Gradle脚本实际上就是在调用Gradle的API。在Gradle中,每个待编译的工程都称为一个Project。每个Project在构建时都有一系列的Task。通俗地来讲,build.gradle用来配置Project以及定义Project里面的Task,也就是它要做哪些工作,这些Task主要由build.gradle引入的插件来定义。

在Android Studio的工程结构中,一个大的工程下面可能会有多个小工程,其中分为library和application,每个小工程下面都有gradle文件,定义了这个工程编译时需要做的工作。在大工程的根目录下会有一个build.gradle和setting.gradle。build.gradle主要用来配置小工程,setting.gradle则声明了这个大工程下面有哪些小工程。

//setting.gradle
include ':library'
include ':library-core'
include ':library-dash'
include ':library-hls'
include ':library-smoothstreaming'
include ':library-ui'

关于配置构建文件,安卓官方也给出了很详细的文档,大家可以看看:配置构建变体

Project

对于每个待构建的项目,Gradle都会为之创建一个Project对象,这个Project对象会和build.gradle绑定起来,build.gradle的执行过程就是Project被配置的过程。在build script中没有定义的属性和函数都属于Project对象。

构建脚本的顶层语句块都会被委托给Project的实例。所以可以在脚本中使用Project的方法。

Any method you call in your build script which is not defined in the build script, is delegated to the Project object.
Any property you access in your build script, which is not defined in the build script, is delegated to the Project object.

前边的buildscript{}就是顶层语句块。在Project的文档可以找到这个Script Block:buildscript{}。看看关于Project的官方文档:Project

关于build.gradle文件的编译:

When Gradle executes a script, it compiles the script into a class which implements Script.This means that all of the properties and methods declared by the Scriptinterface are available in your script.

Grade在执行脚本的时候会将其编译成一个继承Script接口的类,所以Script接口的方法在你的脚本文件中也都可以调用。可以下载Groovy Console,解压后的bin文件夹有个groovyConsole.bat文件,双击可以执行,把脚本粘贴进去,ctrl+t就会显示出编译后的代码:

apply plugin: 'com.android.application'
apply plugin: 'com.hochan.apkdist'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "com.example.c00426275.myapplication"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    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:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.android.support:design:26.+'
    testCompile 'junit:junit:4.12'
}

在初始化的过程中,Gradle会为每个需要被构建的工程创建一个Project对象,接下来:

  • 创建Settings实例;
  • 如果有settings.gradle文件,则交给Settings运行;
  • 利用Settings来确定Project的层级结构;
  • 最后运行build.gradle文件,先运行本身的,再运行子项目的。

Any property or method which your script uses is delegated through to the associated Project object. This means, that you can use any of the methods and properties on the Project interface directly in your script.

这里也说明了我们在build.gradle中用到的属性和方法都是委托给Project 的,所以只要Project有的方法我们都能用。

默认情况下Project中定义了许多Script Block。Gradle插件允许我们自己定义新的Script Blocks。比如下面的android{}就是通过com.android.application插件扩展了Project对象,添加了android Script block。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "com.example.c00426275.myapplication"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

在这里可以看到添加了哪些方法:Class AppExtension

Project查找方法的规则

上面说了顶层script block都会交给Project来执行,Project查找方法主要有以下几种:

  • Project对象本身定义的方法

  • 脚本文件中定义的方法

  • 被插件添加的extension(extension的名字可以做为方法名)

  • 被插件添加的convension方法

  • 工程中的task。task的名字可以作为方法名

  • 父工程中的方法

拿上面android{}这个script block来说明Project是如何处理android{}里面的闭包的:

首先为了在脚本中使用android,就必须引进插件:

apply plugin: 'com.android.application'

如果写过自定义插件我们就会知道一般在插件的apply()函数里我们会为Project添加一个新的extension:

class MyCustomPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {

        project.extensions.create('customplugin', ApkDistExtension);
        .....
    }
}

这里就给Project添加了一个名字为customplugin类型为ApkDistExtension的新的extension。我们就知道Android插件实际上也添加了一个名为“android”类型为AppExtension的extension。当Project要执行android{}时就会把闭包放到AppExtension的上下文中执行,闭包的delegate被设置为AppExtension,所以你会发现这个闭包里的属性和方法在AppExtension中都是存在的。这也是为什么我们在Android Studio中点进android会跳到AppExtension的原因。

Task

A project is essentially a collection of Task objects. Each task performs some basic piece of work, such as compiling classes, or running unit tests, or zipping up a WAR file.

Project就是一系列Task的集合,比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。

Task又包含了一系列Action,Action就是闭包,可以理解为一段可以执行的代码。在Task执行时,Action会依次被执行。可以通过doFirst(Action)和doLast(Action)往Task中添加Action,用来定义此Task执行之前或者执行之后要执行的代码。

The action is simply a closure containing some Groovy code to execute.

因为 build.gradle文件中的task非常多,先执行哪个后执行那个需要一种逻辑来保证。这种逻辑就是依赖逻辑,几乎所有的Task 都需要依赖其他 task 来执行,没有被依赖的task 会首先被执行。

Task一般由插件来定义。在新建工程的app模块的build.gradle文件的第一行,往往都是如下这句:

apply plugin: 'com.android.application'

这句话的意思就是应用“com.android.application“这个插件来构建app模块,app模块就是Gradle中的一个Project。也就是说,这个插件负责定义并执行Java源码编译、资源文件编译、打包等一系列Task。实际上"com.android.application"整个插件中定义了如下4个顶级任务:

  • assemble: 构建项目的输出(apk)
  • check: 进行校验工作
  • build: 执行assemble任务与check任务
  • clean: 清除项目的输出

当我们执行一个任务时,会自动执行它所依赖的任务。比如,执行assemble任务会执行assembleDebug任务和assembleRelease任务,这是因为一个Android项目至少要有debug和release这两个版本的输出。

一次构建将会经历下列三个阶段:

  • 初始化阶段:Project实例在这儿创建,如果有多个模块,即有多个build.gradle文件,多个Project将会被创建。
  • 配置阶段:在该阶段,build.gradle脚本将会执行,为每个Project创建和配置所有的tasks。
  • 执行阶段:这一阶段,gradle会决定哪一个tasks会被执行,哪一个tasks会被执行完全依赖开始构建时传入的参数和当前所在的文件夹位置有关。
task clean {
    println "This is the clean task!!!"
}

执行这个Task:

D:\Android\Workspace\MyApplication>gradlew clean
This is the clean task!!!
NDK is missing a "platforms" directory.
If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to C:\Users\c00426275\AppData\Local\Android\Sdk\ndk-bundle.
If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.

Incremental java compilation is an incubating feature.
:clean UP-TO-DATE
:app:clean

BUILD SUCCESSFUL

Total time: 2.499 secs

可以看到一开始就打印出信息了。这是因为我们执行这个Task时会首先有一个构建的过程,这时候会创建我们定义的Task,但还没有执行。根据Task的定义:

    /**
     * <p>Creates a {@link Task} with the given name and adds it to this project. Before the task is returned, the given
     * closure is executed to configure the task.</p> <p/> <p>After the task is added to the project, it is made
     * available as a property of the project, so that you can reference the task by name in your build file.  See <a
     * href="#properties">here</a> for more details</p>
     *
     * @param name The name of the task to be created
     * @param configureClosure The closure to use to configure the created task.
     * @return The newly created task object
     * @throws InvalidUserDataException If a task with the given name already exists in this project.
     */
    Task task(String name, Closure configureClosure);

这种创建Task的方式在返回创建的Task之前会先执行闭包里面的代码块,所以在配置阶段就会打印出信息。再看下面一种:

task clean << {
    println "This is the clean task!!!"
}

执行这个Task返回:

D:\Android\Workspace\MyApplication>gradlew clean
The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.
        at build_e3y087gd9cjyjxfqd534yqax9.run(D:\Android\Workspace\MyApplication\build.gradle:21)
NDK is missing a "platforms" directory.
If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to C:\Users\c00426275\AppData\Local\Android\Sdk\ndk-bundle.
If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.

Incremental java compilation is an incubating feature.
:clean
This is the clean task!!!
:app:clean

BUILD SUCCESSFUL

Total time: 3.25 secs

事实上这种创建Task的方式相当于:

task clean{
    doLast {
        println "This is the clean task!!!"
    }
}

只有在执行到这个Task之后才会执行doLast{}里面的内容,所以在执行阶段才打印信息。

整合一下:

task clean{
    println "clean!!!"
    doFirst{
        println "doFirstA!!!"
    }
    doFirst{
        println "doFirstB!!!"
    }
    doLast {
        println "doLastA!!!"
    }
    doLast{
        println("doLastB!!!")
    }
}

执行Task:

D:\Android\Workspace\MyApplication>gradlew clean
clean!!!
NDK is missing a "platforms" directory.
If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to C:\Users\c00426275\AppData\Local\Android\Sdk\ndk-bundle.
If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.

Incremental java compilation is an incubating feature.
:clean
doFirstB!!!
doFirstA!!!
doLastA!!!
doLastB!!!
:app:clean

BUILD SUCCESSFUL

Total time: 3.111 secs

Task的创建

Task主要分为两种,一种是直接在build script创建的Task,像我们在上面创建的那种;还有一种Task是加强版的Task,这种Task有自己的属性和方法,我们可以在build script中创建这种Task的时候在闭包中对这个Task的属性进行配置。

task copy(type: Copy) {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}

这里的from、to、include就是Copy这个Task类中本来定义好的属性方法。

    /**
     * {@inheritDoc}
     */
    public AbstractCopyTask from(Object... sourcePaths) {
        getMainSpec().from(sourcePaths);
        return this;
    }

上面那种创建Task的方式相当于:

Copy myCopy = task(myCopy, type: Copy)
myCopy.from 'resources'
myCopy.into 'target'
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')

我们也可以通过继承DefaultTask来自定义我们自己的Task。

常见的Script Block

下面大概介绍一下在build.gradle文件中常见的几个Script Block。

repositories{}

Repository 是 文件的集合,这些文件,通过group、name和version 组织起来。在使用上,主要体现为jar 和 xml文件。Gradle 通过这些Repository 找到外部依赖(external dependencies)。Gradle 并不默认指定任何仓库。它支持很多中央仓库,如maven、ivy,通过文件访问或者通过HTTP 访问。下面举例说明:

1、使用本地maven 仓库:

repositories {
  mavenLocal()()
}

2、使用远程maven 仓库:

repositories {
    maven {
        url "http://repo.mycompany.com/maven2"
    }
}

为了找到dependency,Gradle会按照你在文件中(build.gradle)仓库的顺序寻找所需依赖(如jar文件),如果在某个仓库中找到了,那么将不再其它仓库中寻找。

buildscript{}

在我们新建完一个安卓项目后常常在build.gradle中可以看到这样的代码:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

或者是这样的:

repositories {
     jcenter()
}

这里有三个repositories,分别是buildscript块级、allprojects块级和根块级的。它们有什么区别呢?事实上在buildscript中的声明是gradle脚本自身需要使用的资源,可以声明的资源包括依赖项、第三方插件、maven仓库地址等。allprojects块的repositories用于多项目构建,为所有项目提供共同所需依赖包。而在build.gradle文件中直接声明的依赖项、仓库地址等信息是项目自身需要的资源。gradle在执行脚本时,会优先执buildscript代码块中的内容,然后才会执行剩余的build脚本。

buildTypes{}

android {
    ......
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        staging {
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            buildConfigField "String", "API_URL",
                    "\"http://staging.example.com/api\""
        }
    }
}

buildTypes{}这里想不明白,为什么Android Studio显示buildTypes()的参数是:

void buildTypes(Action<? super NamedDomainObjectContainer<BuildType>> action)

可是脚本里{}不是闭包吗?

最后貌似还是找到了答案:In general, would “action” (not configuration) closures be the first choice?

也就是当我们这么定义一个函数时:

class SomeTask extends DefaultTask {
  void thing(Action<Thing> action) {
    action.execute()
  }
}

在它执行的时候其实是这样的:

class SomeTask extends DefaultTask {
    void thing(Closure<?> closure) {
    thing(new org.gradle.api.internal.ClosureBackedAction(closure))
  }
    void thing(Action<Thing> action) {
    action.execute(thing)
  }
}

所以在调用的时候可以直接传个闭包过去。

作者是这么说的:

Most of the objects that are exposed at the DSL level in Gradle have been decorated at runtime (this is via class level byte code injection). One of the things this does is add 'Closure' overrides for action methods.

还说:

We haven't quite closed out this feature yet which is why it's under documented, which we are sorely aware of.(还没完善这项功能所以现在有关文字介绍有限.)

allprojects{}和subprojects{}

一个工程里面会包含rootProject和subProjects,allProjects会包含这两者,而subProjects就只包含subProjects。

以subprojects{}来说,其闭包会被代理到每个subProject来执行,subProject会被当成参数传递给这个闭包作为代理,根据Groovy的语法,如果闭包只有一个参数,那这个参数可以省略不谢,且可用it来代替:

subprojects {
    apply plugin: "groovy"

    rootProject.dependencies {
        it.runtime project(path)
    }

    it.repositories {
        mavenCentral()
    }

    it.dependencies {
        compile 'org.codehaus.groovy:groovy-all:2.4.7'
        compile gradleApi()
        compile 'org.yaml:snakeyaml:1.14'
    }
}

这里面的it就代表传进来的subproject,但其实repositories 和dependencies 的it都可以像apply那样省略不写。

所以如果我们的工程有多个自工程,而且有相同的依赖或其他配置,可以通过subProjects来进行复用设置。

最后

这篇博客主要是为了做一个笔记,记下自己这几天学习Gradle的过程中对Gradle一些基本概念和编译过程的理解。引用的英文都是来自Gradle的官方文档,有的是javadoc,有些是来自用户手册。其中很多理解的内容来自网上的博客。这篇博客可能有自己理解错误的,也有自己还理解不到位的,如有疑问或指教欢迎与我交流。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,832评论 25 707
  • 参考资料:http://gold.xitu.io/post/580c85768ac247005b5472f9htt...
    zhaoyubetter阅读 10,984评论 0 6
  • Gradle对于很多开发者来说有一种既熟悉又陌生的感觉,他是离我们那么近,以至于我每天做项目都需要他,但是他又是离...
    阿_希爸阅读 9,573评论 10 199
  • 这篇文章讲给大家带来gradle打包系列中的高级用法-自己动手编写gradle插件。我们平常在做安卓开发时,都会在...
    呆萌狗和求疵喵阅读 15,976评论 22 80
  • 好开心呀,好想吃颗伤心补一补。
    小坨花阅读 215评论 0 1