Gradle学习笔记

gradle学习笔记(一)

概念

Gradle本身是基于Groovy脚本语言进行构建的,并通过Domain Specific Language(DSL语言)进行描述和控制构建逻辑的。
参考的文档:

  1. 官方文档
  2. 中文翻译文档
  3. Gradle用户指南
  4. Android Studio构建指南
  5. Android Studio Gradle,插件使用指南
  6. Gradle DSL语言API

gradle初探

项目全局build.gradle

文件中最重要的就是buildscript的部分代码。在buildscript中,Gradle制定了使用jcenter代码仓库,同事声明了依赖的Android Gradle插件版本。
allprojects领域中,开发者可以为项目整体配置一些属性。

Module build.gradle

Gradle使用的是DSL语言,它是针对某个领域所设计出来的特定的语言,因为有了领域的限制,要解决的问题就被划定了范围。因此要针对每个特定的领域进行分析即可。

  • apply plugin 领域
    apply plugin 这块领域描述了Gradle所引入的插件。
    apply plugin:'com.android.application'表示该module是一个Android Application。这个插件包含了Android项目相关的所有工具。

  • android 领域
    android{...}这块领域描述了该Android module构建过程中所用到的所有参数。默认情况下,IDE自动创建了compileSdkVersion、buildToolsVersion这两个参数,分别对应变异的SDK版本和ANDROID build tools版本。而在android领域内,系统还默认创建了两个领域---defaultConfig和buildTypes,这两个领域。

  • dependencies 领域
    dependencies{...}这块领域描述了该Android module构建过程中所依赖的所有库,库可以是以jar的形式进行以来,或者是使用Android推荐的aar形式进行依赖。aar相对于jar具有不可比拟的优势,不仅配置以来更加简单,而且可以将图片的资源文件放入aar中供主项目依赖,几乎等同于依赖源码。

Gradle Task

  1. 查看工程有哪些Task:./gradlew task
  2. 各个Task的具体作用与各个Task之间的相互调用关系:./gradlew task -all
  3. assemble task用于组合项目的所有输出,包含了assembleDebugassembleRelease两个Task
  4. check task 用于执行检查任务
  5. build Task 类似一个组合指令,执行了check和assemble的所有工作
  6. clean task 用于清理所有中间编译结果,这个指令使用的非常广泛。

Gradle进阶

构建全局配置

  • 全局参数
    在项目根目录下的build.gradle中,通过ext领域可以指定全局的配置信息,代码如下所示:
ext{
    compileSdkVersion = 23
    buildToolsVersion = "23.0.2"
    minSdkVersion = 14
    targetSdkVersion = 23
    versionCode = 3
    versionName = "1.0.1"
}
  • 引用配置
    在配置好全局参数后,就可以在每个module中使用这些配置了,例如

compileSdkVersion rootProject.ext.compileSdkVersion

方法非常简单,通过rootProject.ext可以引用所有的全局参数。
另外,开发者也可以把ext全局配置卸载allprojects领域中,这样在每个module中就可以直接引用申明的变量了。

allprojects{
    repositores{
        jecnter()
    }
    ext {
        COMPILE_SDK_VERSION = 22
    }
}

这样写的好处是可以将配置进行统一管理。但坏处是如果这样写的话,Gradle的版本更新通知检查机制就无限了。大部分时候,这种写法是利大于弊的。

构建defaultConfig

defaultConfig{
    applicationId "com.xxx.xxx"
    minSdkVersion 14
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
}

这些设置替换了AndroidMainifest文件中的属性。除此之外可以动态控制VersionName的生成。

defaultConfig{
    applicationId "com.xxx.xxx"
    minSdkVersion 14
    targetSdkVersion 23
    versionCode 1
    versionName getCustomVersionName()
}

def getCustomVersionName(){
    ...
}

构建buildTypes

通过创建不同的构建类型,从而生成不同类型的apk,可以帮助开发者完成很多事情。例如实现只有在debug类型下才开启的功能,如调试、Log等功能,以及为不同构建类型实现不同的参数配置,等等。

  • 构建类型基础
buildTypes{
    release{
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'),`proguard-rules.pro`
    }
}

除了系统默认的构建type--debug和release之外,gradle同样支持自定义创建新的构建类型。例如,在脚本中添加一个xys类型,同时设置该类型的applicationIdSuffix的参数为".xxx",代码如下:

buildTypes{
    release{
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'),`proguard-rules.pro`
    }
    xxx{
        applicationIdSuffix ".xys"
    }
}

执行./gradlew build之后再build目录中多生成了一个app-xxx-unsigned.apk,这个就是自定义的新的buildType-xys类型。那么applicationIdSuffix参数的作用是什么呢?在Android系统中,系统是通过包名来区分应用的。如果应用的包名相同,那么就意味着这是一个应用。因此在构建类型的时候,可以指定applicationIdSuffix参数为默认的包名增加一个后缀。例如前面例子中的”.xxx“,以此区分不同的构建类型。类似的方式,还可以给debug版本增加”.debug“的后缀,给release版本增加".release"的后缀。

  • 构建类型buildTypes的继承
buildTypes{
    release{
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'),`proguard-rules.pro`
    }
    xxx.initWith(buildTypes.debug)
    xxx{
        applicationIdSuffix ".xxx"
    }
}

构建signingConfigs

Android Apk使用签名来保证App的合法性。android系统有一个默认的debug签名,debug包会默认使用这个debug签名进行签名。那么当你需要给其他版本设置签名的时候,就需要自己来配置signingConfigs领域

生成签名

生成签名有两种:

  1. 命令
  2. android studio

生成的签名文件是xxx.jks文件。对于企业项目来说,这个key通常是存放在打包服务器上的,那么在gradle脚本中,就需要通过具体的路径来访问。这一点与访问各种配置文件的方式是一样的。

  • 配置签名
    生成了签名文件后,就可以在build.gradle脚本的android领域中配置签名的相关参数
signingConfigs{
    xxx{
        storeFile file("xxx_key.jsk")
        storePassword "12344567"
        keyAlias "xxx"
        keyPassword "1234567"
    }
}

配置的信息就是前面在创建签名时填写的信息。需要注意的是,签名信息一定要包含在一个领域中,你可以给这个领域起一个名字,例如在这里的”xxx“(通常情况下,会使用debug,release这样的签名)。

  • 使用签名
    配置好相关的签名信息后,就可以在构建类型的时候加入签名的设置。这样生成的apk就会包含签名版和未签名版两种,完整的配置如下所示。
signingConfigs{
    xxx{
        storeFile file("xxx_key.jsk")
        storePassword "12344567"
        keyAlias "xxx"
        keyPassword "1234567"
    }
}

buildTypes{
    release{
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'),`proguard-rules.pro`
    }

    xxx{
        signingConfig signingConfigs.xxx
        applicationIdSuffix ".xxx"
    }
}

Anroid领域中的可选配置

在Android领域中,还有一些可选的配置。在具体的开发场景中,开发者可以根据自己的需要进行配置。

  • compileOptions
    配置编译的选项,类似于compileSdkVersion。不是设置Android SDK的选项,而是设置Java的编译选项,通常可以在这里指定Java的编译版本。
compileOptions{
    sourceCompatibility Java Version.VERSION_1_8
    targetCompatibility Java Version.VERSION_1_8
}

指定编译版本,通常是为了使用某些版本中的一些语言新特性。

  • lintOptions

Lint代码检查,这个选项打开,在编译的时候,会因为Lint的error而终止。

构建Proguard

Proguard配置是Android的apk混淆文件配置,但它的作用绝对不仅仅是混淆代码。他同样可以精简代码、资源,优化代码结构。

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

Gradle动态参数配置

Gradle既然是一种脚本配置语言,那么它一定可以通过配置文件动态配置其编译脚本,列入前面在配置签名脚本时,使用的代码如下所示。

signingConfigs{
    xxx {
        storeFile file("xxx_key.jsk")
        storePassword "12344567"
        keyAlias "xxx"
        keyPassword "1234567"
    }
}

使用gradle.properties文件来配置脚本的动态参数。

System.properties方式

在gradle.properties文件中添加以下配置

systemProp.keyAliasPassword=1234567
systemProp.keyAlias=xxx
systemProp.keyStorePassword=1234567
systemProp.keyStore=xxx_key.jks

这些配置实际上就是之前写死的配置参数,只不过这里把它们配置到了systemProp中,那么在build.gradle脚本进行引用的时候,就可以通过System.properties[KEY]获取这些参数。

signingConfigs{
    xxx {
        storeFile file(System.properties['keyStore'])
        storePassword System.properties['keyStorePassword']
        keyAlias System.properties['xxx.keyAlias']
        keyPassword System.properties['xxx.keyAliasPassword']
    }
}

通过project.property(Key)方法,就可以去除对应的Value。这种方式与使用System.properties的方式基本一样。

多渠道打包

所谓多渠道打包,实际上就是在代码层面上标记不同的渠道名,从而便于统计不同的应用市场该apk的下载量。而且有些时候有些暴还可以以从网页的外链接或者一些非市场的渠道进行下载。这些都需要进行统计,因此多渠道打包,变成了打包任务的重中之重。
利用Gradle进行多渠道打包,将开发者从之前繁杂的ant打包中解放出来。Gradle的强大功能,将多渠道打包变得异常简单,只需要在Gradle脚本中进行简单配置,即可完成多渠道打包。

  • 创建渠道占位符
    首先AndroidMainifest文件的Application节点下,创建如下所示的meta-data节点
<meta-data
    android:name="PRODUCT"
    android:value="${CHANNEL_VALUE}"/>

其中”${CHANNEL_VALUE}“就是要进行替换的渠道占位符。

  • 配置Gradle脚本
    在项目的Gradle脚本的android领域中,添加productFlavors领域,并添加定义的渠道名。同时,使用manifestPlaceholders指定要替换渠道占位符的值。
productFlavors{
    product1{
        manifestPlaceholders=[CHANNEL_VALUE:"PRODUCT1"]
    }
    product2{
        manifestPlaceholders=[CHANNEL_VALUE:"PRODUCT2"]
    }
    product3{
        manifestPlaceholders=[CHANNEL_VALUE:"PRODUCT3"]
    }
}

实际上除了渠道名,AndroidMainifest文件中的其他设置,同样可以使用占位符进行配置。只要利用manifestPlaceholders进行替换即可,原理与多渠道类似。这一个技巧可以让项目能够直接在编译脚本--build.gradle中进行动态参数控制,便于统一管理。更进一步,在Module中同样可以进行这些动态参数的控制。例如某些Module的封装,需要配置一些炎症Key作为参数,如果这些Key卸载Module中,Module就是去了通用性。因此借助manifestPlaceholders,开发者可以将动态参数配置到Module中,通过主项目的manifestPlaceholders。可以参考博客

脚本优化

productFlavors{
    product1{
        manifestPlaceholders=[CHANNEL_VALUE:"PRODUCT1"]
    }
    product2{
        manifestPlaceholders=[CHANNEL_VALUE:"PRODUCT2"]
    }
    product3{
        manifestPlaceholders=[CHANNEL_VALUE:"PRODUCT3"]
    }
}
productFlavors.all{flavor->
    flavor.manifestPlaceholders=[CHANNEL_VALUE:name]        
}

增加的productFlavors.all领域对所有的productFlavors进行遍历,并使用其name作为渠道名。这些name实际上就是produce1,Produce2,produce3

生成重命名包

在生成渠道包后,包的明明通常是默认命名,即app-渠道名-buildType.apk。但是通常情况下,项目经理都会要求对报名进行重命名,以满足市场部的需求。那么这时候就可以通过Gradle脚本进行快速重命名,而不需要再使用rename指令或者Python指令或者Python脚本进行修改

application Variants.all{variant ->
    variant.outputs.each{ output ->
        if(output.outputFile != null &&
                        output.outputFile.name.endsWith('.apk') &&
                        'release'.equals(variant.buildType.name)){
                            def apkFile = new File(output.outputFile.getParent(),
                                "XXXApp_${variant.flavorName}_ver${variant.versionName}.apk")
                            output.outputFile = apkFile
                        }
    }

}

将这段脚本放到android领域中即可,当执行gradle build指令时该task也会执行,与多渠道优化的那段代码非常类似,它去除了所有的生成的apk包,并判断其文件是否是apk、是否是release版本。如果是,则重新将其命名为”XXXApp_渠道名_ver版本号.apk“。代码其实非常简单,但难就难在对groovy语言的理解和gradle android插件的熟悉度上。很多系统变量和内置变量。

为不同版本添加不同代码

在开发中,不同的版本通常有不同的代码功能。例如最常用的Log开关,在debug版本中会打印开发日志,而在release版本中需要关闭的。因此,一般会有一个全局的变量开关,根据不同的版本设置不同的值。这一切在gradle脚本的支持下,仅仅变成了一句配置。

buildTypes{
    rlease{
        buildConfigField "boolean","testFlag","true"
        minifyEnabled true
        shrinkResources ture
        proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
    }
    xxx{
        buildConfigFiles "boolean","testFlag","false"
        signingConfig signingConfigs.xxx
        applicationIdSuffix ".xxx"
    }
}

通过制定buildConfigField的三个参数--类型、名称、值,就可以将一个变量设置到不同的buildType中去。打开系统的BuildConfig类,可以看到不同buildType下对应的testFlag的值。该文件对应的路径为/项目/app/build/generated/source/buildConfig/(你也可以通过双击Shift进行快速查找)
直接通过BuildConfig类,就可以获取到不同buildType所对应的值了。如果是String类型的变量,在写入字符串的时候,需要加入转义字符。

buildConfigField "String","myname","\"abs\""

除了Java代码可以使用这种方式进行添加之外,资源文件同样可以进行分版本设置属性值。例如要给不同的版本设置不同的AppName

defaultConfig{
    ....
    resVaule("string","app_name","XXXApp")
}
buildTypes{
    release{
        ...
        resVaule("string","app_name","XXXAppRelease")
    }
    debug{
        ...
        resVaule("string","app_name","XXXAppDebug")
    }
}
defbuildTime(){
    return new Date().format("yyyy-MM-dd HH:mm:ss")
}
defaultConfig{
    resValue "String","build_time",buildTime()
}

在上面的代码中,定义了一个buildTime方法,并赋值给自定义的build_time变量。这时候不需要在Java代码中增加变量,即可直接引用已经编译到R文件中的变量build_time,代码如下:

Log.d("test",getString(R.string.build_time))

Gradle多项目依赖

  • build.gradle:控制每个module的编译过程。
  • gradle.properties:设置Gradle脚本中的参数。
  • local.properties:Gradle的SDK相关环境变量配置。
  • settings.gradle:配置Gradle的多项目管理。

使用Gradle上传aar到Maven库

开发者可以将自己开发的库项目上传到Maven库,供其他程序调用。上传的方式为通过脚本进行提交

uploadArchives{
    repositories{
        mavenDeployer{
            pom.groupId = GROUPID
            pom.artifactId = ARTIFACTID
            if(System.properties['isRelease'].toBooleans()){
                pom.version = VERSION
                repository(url: nexusReleases){
                    authentication(userName:nexusUsername,password:nexusPassword)
                }
            }else{
                pom.version = "${VERSION}-SNAPSHOT"
                repository(url:nexusSnapshots){
                    authentication(userName:nexusUsername,password:nexusPassword)
                }
            }
            pom.project{
                descriptoin 'xxxx'
            }
        }
    }
}

同时,还需要在gradle.properties文件中进行参数的配置

GROUP_ID = com.xxxx.cccc
ARTIFACT_ID = aaaa
VERSION = 1.x.xxx
RELEASE_REPOSITORY_URL = maven url
nexusUsername = username
nexusPassword = password
systemProp.isRelease = true

Gradle 依赖管理

  • 强制刷新配置
compile('com.xxx.xxx:3.0.1-SNAPSHOT@aar'){
    transitive = true
}

如果增加一个属性transitive并让其值为true,则代表会强制刷新远程库,避免远程库更新后本地未刷新的问题。

Gradle依赖传递

在使用Gradle aar文件时,京城会发生这样的情况,主项目A依赖库项目B,库项目B依赖库项目C和jar包D。这时候主项目在引用库项目B时,写成如下所示的方式。

compile 'com.xxx.xxxx:xxxx:1.0.0-SNAPSHOT'

这样的写法也是一般引用库项目的标准写法,其表示B项目及其依赖的所有项目,即C和D。那么如果C或者D出现重复依赖的问题,或者主项目只想依赖库项目B而不像依赖库项目B所以来的项目,则可以使用@aar关键字关闭依赖传递,使用方法如下所示。

compile 'com.xxx.xxxx:xxxx:1.0.0-SNAPSHOT@aar'

如果这样引用库项目B,则不会进行依赖传递。但要注意的是,libs目录下的jar文件时不受影响的,开发者在使用过程中需要非常注意。
另外,还可以使用exclude module排除一个库中引用的其他库,例如aar库A依赖了B和C,此时可以通过以下的方式进行依赖。

compile('com.xxx.yyy:aaa:1.1.1'){
    exclude module:'com.xxx.yyy.bbb:1.1.2'
}

传递依赖问题是使用Gradle时一定会遇到的问题,不仅仅是依赖传递的库会冲突,而且也会发生资源冲突的问题。因此遇到Gradle编译错误的时候,一定要仔细分析错误的原因,找到冲突的根本原因从而去解决问题

Gradle依赖统一管理

Gradle引用依赖非常简单,但一旦涉及多module,每个module的依赖管理就变得非常麻烦。这就和编程中使用的变量一样,每个module中都引用自己的依赖-局部变量,这样就造成多个module有多个局部变量,不利于项目管理。因此,最好是使用类似全局变量的方式来进行统一的管理。
在根目录的build.gralde脚本中配置如下所示的代码。

ext{
    android=[compileSdkVersion:23,
            buildToolsVersion:'23.0.2']

    dependencies = [supportv7:'com.android.support:appcompat-v7:23.2.0']
}

在全局Gradle脚本中,指定了android和dependencies两个列表,并在其中配置了统一的参数和对应的值。这样在每个module中,一颗通过代码使用全局的依赖配置。

android{
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    buildToolsVersion rootProject.ext.android.buildToolsVersion
}
dependencies{
    compile rootProject.ext.dependencies.supportv7
}

更进一步,开发者还可以把这些全局参数抽取出来,写到一个单独的配置文件中。例如,比这在项目根目录下创建一个config.gradle文件,并写入如下所示的代码

ext{
    android = [compileSdkVersin:23,
                buildToolsVersion:'23.0.2']
    dependencies=[supportv7:'com:android.support:appcompat-v7:23.2.0']
}

这里就要把ext全局参数抽取出来了。下一步,在根目录下的build.grdle文件中,使用代码加载这个配置文件,代码如下所示。

apply from:'config.gradle'

这样就可以在所有的子module中使用这些参数了,通过这种统一的依赖管理方式,可以统一所有module的依赖配置,避免使用不同版本的依赖库而导致的冲入,而且也利于项目的管理。

Gradle使用技巧

  • Debug模式禁用掉Lint./gradlew build -x lint,其中-x参数表示排除掉一个Task,即Lint。通过这种方式可以实现禁止Lint的执行。
  • Debug模式禁用AAPTaaptOptions.cruncherEnabled = false
  • 官方文档

Gradle加速

Gradle在编译时会执行大量的Task,同时生成很多中间文件。因此磁盘IO会造成编译速度缓慢。解决该问题的最好办法就是为电脑更换固态硬盘,增加磁盘的IO速度。同时尽量减少本地库项目的依赖,多实用aar进行依赖。
在gradle.properties文件中增加如下代码

org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true

同时,在build.gradle中增加如下代码

dexOptions{
    incremental true
        javaMaxHeapSize "4g"
}

gradle.properties文件中的代码,表示开启Gradle的多线程和多核心支持。而build.gradle中的代码,表示开启Gradle的增量编译,增加编译的内存资源的4G。

Gradle自定义插件

Gradle提供了强大的插件自定义功能,可以在某些情况下通过自定义插件实现自己的一些功能。官方文档

在Gradle中创建自定义插件,Gradle提供了以下三种方式。

  • 在build.gradle脚本中直接使用
  • 在buildSrc中使用
  • 在独立Module中使用

Gradle插件可以在IDEA中进行开发,也开一在Android Studio中进行研发。他们唯一不同就是IDEA提供了Gradle开发的插件,比较方便创建文件和目录。而在Android studio中,开发者需要手动创建。

Gradle 打包配置

编译时报重复文件的错误

// 1. pickFirsts:当出现重复文件,会使用第一个匹配的文件打包进入。
// 2. merges:当出现重复文件,合并重复的文件打入APK,两个文件会进行拼接
// 3. excludes:打包的时候排除匹配的文件

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

推荐阅读更多精彩内容