Gradle 入门之理解脚本与构建过程

什么是Gradle

Gradle是一个开源、通用构建工具,可以用于几乎任何类型项目的构建。对于初学者而言需要理解的是构建并不等同于编译,构建是指由我们的源码、资源等经过一系列的操作,最终生成可发布的产物,编译往往只是构建过程的一个小环节。以Android的构建为例,我们最初的输入包括Java/Kt 代码、资源、Native代码等,Java代码的编译(javac)只是构建环节的一小步,整个过程还包括很多其他步骤,比如资源编译、资源合并、字节码处理(Transform)、Dex生成、无用资源去除、Apk组装、签名等等。

一些构建工具的对比:
Ant: 没有依赖管理,早期我们使用eclipse开发时候是需要手动导入jar包的
Maven: xml配置,在约定优于配置的路上走的太远,不鼓励自己定义任务

Gradle:

  • 承袭约定优于配置的思想(提供一些约定的插件实现),且提供更加灵活的定义构建过程的方式
  • 支持依赖管理(基于 Apache Ivy ),支持已有的 maven 和 ivy 仓库
  • 使用groovy /kotlin 脚本构建,有丰富的DSL来描述构建
  • 支持多工程构建

Gradle的通用体现在其本身并没有对软件的构建过程做限制,但是这并不意味着我们我们需要自己多很多工作才能完成项目的构建,Gradle本身已经给我提供了一些常见类型项目的构建插件,比如构建Java Library的java-library ,我们也可以自己定义一些插件来构建项目。

Groovy基础

目前Gradle的DSL分别有Kotlin和Groovy两种,后缀分别是.gradle 和 .gradle.kts ,Kotlin大家比较熟悉,这里主要介绍一下Groovy的一些常见特性,以便能更好看懂gradle脚本。Groovy很多特性和Kotlin相似,但是相比Kotlin,Groovy和Java更加接近。

各种省略

  • 语句后面的分号是可以省略的
  • 语句中的return都是可以省略的
  • 方法声明的参数类型可以省略
  • 方法调用时,括号也是可以省略的

可以像java一样声明具体的类型,也可以使用def关键字来声明。

int a = 0
String getString() {
    return "hello"
}

def b = 1
def getString2() {
    return "hello"
}
def getString2() {
    "hello"
}
def printText(str) {
    println "root $str"
}

println 'hello'   <=>   println('hello')
method 1,2  <=>  method(1,2)

字符串

字符串声明常用的可以用单引号和双引号两种。单引号标记的字符是一个纯粹的字符串常量,双引号声明的字符串内可以使用$来引用变量。

image.png

List/Map

List:

def test = [100, "hello", true]
println test[0] 

Map:

语法key和value用冒号分隔,pair之间用逗号分隔。另外,用这种语法创建出来的是LinkedHashMap实例。

def map = ['plugin': 'kotlin-android', 'from': rootProject.file('gradle/global/library.gradle')]

如果key是普通的字符串,还可以进一步省略掉引号,如下所示:

def map = [plugin: 'kotlin-android', from: rootProject.file('gradle/global/library.gradle')]

如果方法的参数是Map,那么传入时可以省略方括号

apply函数的原型如下:

public void apply(Map<String, ?> options)

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android', from: rootProject.file('gradle/global/library.gradle')

像访问属性一样调用getter/setter方法

这点和Kotlin也非常类似。Groovy在访问Java或者Groovy定义的类的getter/setter方法时候,可以当做像在访问属性一样。

比如在build.gradle里如下代码:

version = 1.0
group = "TestGroup"

对应的如下方法:

void setVersion(Object version);
void setGroup(Object group);

闭包

Grovvy的闭包和Kotlin的Lambda非常相似。

{ v,j ->
  语句
}

作为函数的最后一个参数时可以写在括号函数外面。

task ('hello') {
   println 'hello'
}

task函数原型为:

Task task(String name, Closure configureClosure)

闭包的实例类型是Closure,Groovy的闭包不能直观看出来这个闭包有哪些参数 ,或者这个闭包的委托是什么,需要自己去查。

闭包委托

和Kotlin的扩展函数很像,是Groovy 脚本的核心奥义。

class One {
    def printText(str) {
        println "Printed in One: $str"
    }
}
class Two {
    def printText(str) {
        println "Printed in Two: $str"
    }
}

def printClosure = {
    printText "I come from a closure"
}

printClosure.delegate = new One()
printClosure() // will print "Printed in One: I come from a closure
printClosure.delegate = new Two()
printClosure() // will print "Printed in Two: I come from a closure

可以在闭包内直接调用其委托对象的方法和属性,对对象进行配置。

我们可以看到printClosure调用了不同委托的printText方法,这个特性用的比较多。

task ('hello') {
    doLast {
        println 'hello'
    }
}

task后面的闭包里,能调用Task的doLast方法,是因为这个闭包的委托对象是创建的Task对象。

https://www.cnblogs.com/zqlxtt/p/5741297.html

入门基础

Grale需要需要运行在1.8版本的JVM上。

安装(一般不需要)

Mac可以通过HomeBrew安装:

brew install gradle

或者到官网手动下载

$ mkdir /opt/gradle
$ unzip -d /opt/gradle gradle-6.0.1-bin.zip
$ ls /opt/gradle/gradle-6.0.1
LICENSE  NOTICE  bin  getting-started.html  init.d  lib  media

并配置系统环境变量

export PATH=$PATH:/opt/gradle/gradle-6.0.1/bin

通过gradle -v来验证是否安装完成

 gradle -v
------------------------------------------------------------
Gradle 6.0.1
------------------------------------------------------------

Build time:   2019-11-18 20:25:01 UTC
Revision:     fad121066a68c4701acd362daf4287a7c309a0f5

Kotlin:       1.3.50
Groovy:       2.5.8
Ant:          Apache Ant(TM) version 1.10.7 compiled on September 1 2019
JVM:          1.8.0_181 (Oracle Corporation 25.181-b13)
OS:           Mac OS X 10.14.6 x86_64

Gradle Wrapper

Gradle Wrapper是对Gradle的一层包装,主要是可以帮助团队统一Gralde的版本,避免大家版本不一致带来的不必要问题。当我执行./graldew命令时候会检查有没有当前版本的gradle,如果没有的话会帮我们下载。

Gradle Wrapper通过Gradle内置的wrapper task可以生成,执行

gradle wrapper

生成如下的文件:

├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar 
│       └── gradle-wrapper.properties
├── gradlew 
└── gradlew.bat

gradlew / gradlew.bat : shell / 批处理脚本,分别用于类Unix和Windows操作系统,用法gradle一样

gradle-wrapper.jar : 具体的业务逻辑,./gradlew 执行时候通过执行这个jar包去执行gradle的小左

gradle-wrapper.properties :配置文件

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

distributionUrl: gradle的下载地址,可以自己其他下载地址。另外这个压缩包后缀有-all和-bin两种,-bin是只有可执行文件,-all是带有源码和doc的

zipStoreBase:存放下载gradle压缩包的主目录。从系统中读取GRADLE_USER_HOME这个环境变量,如果没有配置的话就是在用户目录下的.gradle中 ,即~/.gradle/

zipStorePath: 存放下载gradle压缩包的相对于zipStoreBase的相对路径,默认情况下即~/.gradle/wrapper/dists

distributionBase:和zipStoreBase类似

distributionPath:和zipStorePath类似,只是用来存储gradle压缩包解压后的产物

依赖库下载后也会存储于GRADLE_USER_HOME/caches下,时间久了可以清理一波

Gradle命令基础

使用帮助命令来了解有哪些命令或者参数:

gradle -h

比如常用的:

-v Gradle版本信息

--offline 离线编译

--stop 停止Daemon进程

-P 设置工程属性,例如-Pmyprop=myvalue

查看所有可执行的Task

gradle tasks

image.png

生成一个java libary的工程目录结构

gradle init --type java-library

生成Gradle wrapper

gradle wrapper

help Task,可以用来查看其他Task的具体信息,比如

gradle help --task wrapper

image.png

一次调用多个Task,比如先clean后编译依次顺序跟多个task即可

gradle clean assembleDebug

Task名字可以缩写,比如执行installDouyinCnDebug可以所以写为iDCD,由每个驼峰的第一个字母组成,

前提是没有其他Task和这个Task的缩写相同,否则就必须写全名。

gradle iDCD

多项目构建时执行某个特定Project(AS中的Module)的Task,更多细节可以见Project和Task的path

gradle :app:clean
./gradlew :app-lite:clean :app-lite:assembleDouyinCnDebug -Dorg.gradle.debug=true --no-daemon

构建核心模型

Gradle构建的两个基本概念是Project和Task。Project就是指一个工程,可以简单理解为对应于AS的一个module,一个Project由一系列的Task组成。Gradle的构建过程根据这些Task的依赖关系生成有向无环图(DAG),执行Task时候根据有向无环图来决定哪些任务需要被执行,按照什么顺序执行。

两个Task依赖图的例子,左边是一个抽象的例子,右边是标准Java构建任务依赖图

image.png

Gradle的构建阶段

一次Gradle构建分为三个阶段:

  • Initialization

Gradle支持单工程和多工程构建,在初始化阶段,Gradle决定哪些工程要参与构建,并且给这些工程都创建一个Project实例。这个阶段settings.gradle会被执行。

  • Configuration

所有参与构建的工程的build脚本(默认是build.gradle)会被执行,构建Task的有向无环图。每次触发gradle命令时都会经历这个阶段,不要再此阶段做耗时操作。

  • Execution

根据从gradle 命令里传进来的task决定哪些Task应该被执行,然后按顺序执行这些Task。

多工程构建

工程构建分为单工程构建和多工程构建,灵活的多工程构建是Gradle的一大卖点。多工程中每个工程都对应一个Project对象,整个多工程结构是Project的一棵树,rootProject是树的根节点。

image.png

以上图工程结构为例,gradle_test(名字被修改过)即为rootProject,其他的工程main,javalib,kotlinapp,internal为gradle_test的子工程,internal还有自己的子工程library1。

多工程构建的关键就是settings.gradle文件,这个文件主要用来决定哪些工程需要参与构建,单工程构建是不需要这个文件的(也可以有)。

另外每个工程(包含rootProject)都有一个build.gradle文件,每个build.gradle文件对应一个Project对象,这些脚本主要是用来做每个工程的具体配置。

Settings

settings.gradle在initialization阶段执行,用来决定哪些工程参与构建。

.gradle脚本本身就像是一个闭包,这个闭包被委托给了相应的对象。对于build.gradle脚本,属性和方法调用被委托给Project对象,对于setings.gralde脚本方法和属性的调用被委托给Settings对象。

Settings API

public interface Settings extends PluginAware, ExtensionAware{
    //添加参与构建的工程
    void include(String... projectPaths);
    //通过path获取添加参与构建的工程描述,可以对相关信息进行设置
    ProjectDescriptor project(String path) throws UnknownProjectException;

    ...    
}

默认实现类是DefaultSettings.

示例:

rootProject.name = "gradle_test"
println("$rootProject.name")

include ':javalib'
project(':javalib').buildFileName = "javalib.gradle"

include ':main'
project(':main').projectDir = file("./app")
include ':internal:library1'
include 'kotlinapp'

println(rootProject.children)

Project和Task的path

一个Project的path以:开头,最开头的这个:代表的是rootProject 。之后的工程名之间以冒号分割,后面一个是前面一个的子工程。

:javalib 表示的根工程下的javalib
:internal:library1  表示的是根工程下的internal下的library1工程,其中library1是internal的子工程,internal是rootProject的子工程。这个inlcude会一下子把这两个都添加进去。

Task的path是在工程的path后面加上Task的名字,比如:main:hello。当我们的多个工程下都定义有相同名字的Task时候,我们在根工程下执行 ./gradlew hello,所有参与构建的工程的hello这个Task都会被执行。如果我们想指定具体工程的Task,执行./gradlew :main:hello:main:hello这个可以认为是这个Task的绝对路径,在任意子工程的目录下这个都是可以索引到这个task的 。另外也可以进入到这个工程的目录下(假设这个子工程在根工程的目录下)执行 ../gradlew hello,这个时候使用的是相对路径。

Project

build.gradle 对应一个 Project 对象,在脚本中的配置方法其实都对应着 Project 中的API。build脚本在在Configure阶段被执行,因此里面不要写太耗时的操作。

Project API

属性

File bd = getBuildDir()
println "buildDir = ${bd.getAbsolutePath()}"
//获取Project的名字
String name = getName()
println "project name = $name"
//设置Project的描述信息
setDescription "测试"
String desc = getDescription()
println "project description = $desc"
//获取Project的路径
String path = getPath()
println "project path = $path"

直接执行 gradle 命令,可以看到在配置阶段输出以下结果:

> Configure project :main
buildDir = /Users/wangpengfei/AndroidStudioProjects/New/gradle/app/build
project name = main
project description = 测试
project path = :main

创建task

见下文Task部分

文件操作

创建目录

File mkDir = mkdir("${buildDir}/test");
println "是否创建成功:${mkDir.exists()}"

定位文件

//定位单个文件,参数可以是相对路径、绝对路径
File testDir = file("${buildDir}/test")
println "文件是否存在:${testDir.exists()}"
//文件集合,Gradle里用 FileCollection 来表示
FileCollection fileCollection = files("${buildDir}/test", "${buildDir}/test2")
fileCollection.each {File f ->
    println f.name
}

删除文件

//删除 build 目录下所有文件
delete("${buildDir}")

文件树

Gradle里用 ConfigurableFileTree 来表示文件树,文件树会返回某个目录及其子目录下所有的文件。

比如常用的

implementation fileTree(dir: 'libs', include: ['*.jar'])

详细说明:

//1.通过一个基准目录创建文件树,参数可以是相对目录,也可以是绝对目录,与file()方法一样println "通过基准目录来创建文件树"
ConfigurableFileTree fileTree1 = fileTree("build")//添加包含规则
fileTree1.include "*.txt", "*/*.txt"//添加排除规则
fileTree1.exclude "*.java"
fileTree1.each { f ->
    println f    
}

//2.通过闭包来创建文件树
ConfigurableFileTree fileTree2 = fileTree("build") {
    include "*/*.txt", "*.java"
    exclude "*.txt"
}
fileTree2.each { f ->
    println f    
}
//3.通过map配置来创建文件树,可配置的选项有:dir: ''、include: '[]、exclude: []、includes: []、excludes: []
def fileTree3 = fileTree(dir: "build", includes: ["*/*.txt", "*.java"])
fileTree3 = fileTree(dir: "build", exclude: "*.java")
fileTree3.each { f ->
    println f    
}

构建脚本配置

buildscript

buildscript用来添加编译时需要依赖的一些第三方插件,只需要在根工程配置即可。gradle自带的插件不需要配置,比如java-library。

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

apply

apply(options: Map<String, ?>)

我们通过该方法使用插件或者是其他脚本,options里主要选项有:

  • from: 使用其他脚本,值可以为 Project.uri(Object) 支持的路径
  • plugin:使用其他插件,值可以为插件id或者是插件的具体实现类

例如:

//脚本路径
apply from: rootProject.file('gradle/global/library.gradle')
//插件id
apply plugin: 'kotlin-android'
//插件实现类
apply plugin: com.plugin.CustomPlugin

rootProject统一配置

根工程的build.gradle会最早执行,因此可以所有的子工程添加一些通用的配置。

 // 针对所有项目进行配置
allprojects(config: Closure)
// 针对所有子项目进行配置
subprojects(config: Closure)

通过path定位并获取该 Project 对象
project(path: String): Project
// 通过path定位一个Project,并进行配置
project(path: String, config: Closure): Project

allproject与allprojects的区别是包不包含根工程自身。

我们修改根目录 build.gradle 文件如下:

println "-----root file config-----"
//配置 app 项目
project(":app") {
    ext {
        appParam = "test app"
    }
}
//配置所有的项目
allprojects {
    ext {
        allParam = "test all project"
    }   
}
//配置子项目
subprojects {
    ext {
        subParam = "test sub project"
    }
}
println "allParam = ${allParam}"

修改 app/build.gradle 文件如下:

println "-----app config-----"
println "appParam = ${appParam}"
println "allParam = ${allParam}"
println "subParam = ${subParam}"

修改 library/build.gradle 文件如下:

println "-----library config-----"
println "allParam = ${allParam}"
println "subParam = ${subParam}"

运行结果如下:

-----root file config-----
allParam = test all project
-----app config-----
appParam = test app
allParam = test all project
subParam = test sub project
-----library config-----
allParam = test all project
subParam = test sub project

工程属性

Gradle属性

在与 build.gradle 文件同级目录下,定义一个名为 gradle.properties 文件,里面定义的键值对,可以在 Project 中直接访问。rootProject下定义的属性在子工程下也可以访问,当然子工程自己定义的优先级要。

//gradle.properties里定义属性值
company="hangzhouheima"
username="hjy"

在 build.gradle 文件里可以这样直接访问:

println "company = ${company}"
println "username = ${username}"

扩展属性

还可以通过 ext 命名空间来定义属性,我们称之为扩展属性。

ext {
  username = "hjy"
  age = 30
}
println username
println ext.age
println project.username
println project.ext.age

必须注意,默认的扩展属性,只能定义在 ext 命名空间下面。对扩展属性的访问方式,以上几种都支持。

Task

一个 Task 是 Gradle 里项目构建的原子执行单元,Gradle 通过将一个个Task串联起来完成具体的构建任务。

Task API

Actions

一个 Task 是由一序列 Action 组成的,当运行一个 Task 的时候,这个 Task 里的 Action 序列会按顺序依次执行。

上文我们提过在build.gradle可以调用Project的方法来创建Task。比如以下方法:

//其中configureClosure的委托是Task对象,可以做一些配置,这个闭包Configuration阶段执行
Task task(String name, Closure configureClosure);
Task task(String name) throws InvalidUserDataException;

task添加到Project之后,可以像访问Project的一个属性一样访问它,如下:

task task1 {
    //Configuration阶段执行
    println "configure task1"
}
task 'task2'{
     println "configure task2"
}

task1.doFirst {
    println "task1 doFirst"
}
task1.doLast {
    println "task1 doLast"
}
task2.doLast {
    println "task2 doLast
}

Task 里的 Action 只会在Execution 阶段时执行,可以通过 doFirst、doLast 来为 Task 增加 Action

  • doFirst:在最前面添加Action
  • doLast:在最后面添加Action

执行gradle task1

> Configure project :
configure task1
configure task2
> Task : task1
task1 doFirst
task1 doLast

从上面例子中可以看到,所有 Task 的配置代码都会运行,而 Task Actions 则只有该 Task 运行时才会执行。

操作符重载

doLast有一种等价操作叫做leftShift,leftShift可以缩写为 << ,下面几种写法效果是一模一样的:

task1.doLast {
    println "task1 doLast"
}
task1 << {
    println "task1 doLast<<"
}
task1.leftShift {
    println "task1 doLast leftShift"
}

创建Task的常见写法

task myTask1 {
    doLast {
        println "doLast in task1"
    }
}

task myTask2 << {
    println "doLast in task2"
}
//采用 Project.task(String name) 方法来创建
project.task("myTask3").doLast {
    println "doLast in task3"
}
//采用 TaskContainer.create(String name) 方法来创建
project.tasks.create("myTask4").doLast {
    println "doLast in task4"
}
project.tasks.create("myTask5") << {
    println "doLast in task5"
}

初次接触这些写法,头都是大的,Gradle太灵活了,个人觉得记住最常用的即可,看到类似写法能看懂就行了。

Task的属性

在 Gradle 中定义 Task 的时候,可以指定更多的参数,如下所示:

<style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="VkLfQSh23b-1627750088520" data-version="3" data-is-embed="true"><colgroup><col width="155"><col width="206"><col width="274"></colgroup>
| 参数名 | 含义 | 默认值 |
| name | task的名字 | 必须指定,不能为空 |
| type | task的父类 | 默认值为org.gradle.api.DefaultTask |
| group | task所属的分组名 | null |
| description | task的描述 | null |
| dependsOn | task依赖的task集合 | 无 |
| constructorArgs | 构造函数参数 | 无 |
| overwrite | 是否替换已经存在的同名task | fasle` |</byte-sheet-html-origin>

task myTask2 << {
    println "doLast in task2"
}
task myTask3 << {
    println "doLast in task3, this is old task"
}
task myTask3(description: "这是task3的描述", group: "myTaskGroup", dependsOn: [myTask1, myTask2], overwrite: true) << 
{
    println "doLast in task3, this is new task"
}

执行 gradle myTask3,结果如下:

> Task :myTask2
doLast in task2
> Task :myTask3
doLast in task3, this is new task

执行命令 gradle -q tasks --all,查看下 task 信息,节选我们创建的 task 信息如下:

MyTaskGroup tasks
------------
myTask3 - 这是task3的描述

Other tasks
-----------
myTask2

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