Grade脚本学习

前言

学习gradle这个想法在我开始使用AndroidStudio之后就有了,基本上每个开始使用AndroidStudio的人都会被它折磨一段时间,各种各样的build failed,但是由于种种原因没有深入学习,每次都是出来问题再去网上查一下。这段时间终于有时间把gradle拿出来好好学习一下,其实感觉过程是很痛苦的,主要是gradle中涉及的api实在是太多了,以及各种DSL,导致整个Grade的学习一度陷入僵局,直到现在也不敢说对gradle有太深入的研究,也算是对gradle做一个介绍

Groovy基础

说实话对于Groovy我没有太多了解,我感觉前期了解个大概就好了,如果想要学习的细致一点,这里推荐一篇文章,我上面的很多内容也是来自这里Groovy脚本基础全攻略
Gradle是一个构建工具,就像Ant使用xml进行配置一样,Gradle使用Groovy语言进行配置,Groovy源自Java,但是Groovy抛弃了java中很多文法,Groovy更像一种脚本语言。执行Groovy脚本时,Groovy会先将其编译成Java类字节码,然后通过Jvm来执行这个Java类。 作为一种动态语言,Groovy的语法与我们平时用的java还是有很大不同的,不过习惯之后其实和java还是很类似的(这句话说的还是很违心的 - -!)

变量定义

Groovy作为动态语言,可以在变量定义时不指定类型,直接使用def关键字

def x = 1
//当然也可以在定义时指定变量类型
def int y = 1
//省略def也是可以的,但一般不推荐
x = 1

这里定义了一个int型的变量,当然def这个关键字也不是必须的,可以直接写成

x = 1

也是同样定义了一个int型的变量。当然建议大家还是把def加上,以免混乱

  • 还有一点大家可能也注意到了,groovy中可以不使用”;”作为一行代码的结束符
  • groovy的变量定义没有java中的类似public等修饰符,这是因为groovy中默认的修饰符就是public的,可以添加private等修饰符

字符串

Groovy支持多种字符串形式,最常用的是单引号和双引号

def name = 'Groovy!'
def body1 = 'Test ${name}'
def body2 = "Test ${name}"
assert name == 'Groovy!'
//$表示占位符,但是单引号字符串不支持占位符
assert body1 == 'Test ${name}'
//使用双引号字符串可以支持占位符
assert body2 == 'Test Groovy!'

在groovy中插值占位符我们可以用或者
来标示,用于一般替代字串或者表达式,
主要用于A.B的形式,除了上面两种字符串,groovy还支持三引号

def aMultilineString = '''line one
line two
line three'''

新的行被转换为“\n”,其他所有的空白字符都被完整的按照文本原样保留

闭包

Groovy的闭包(closure)是一个非常重要的概念,闭包是可以用作方法参数的代码块,Groovy的闭包更象是一个代码块或者方法指针,代码在某处被定义然后在其后的调用处执行。闭包在gradle中使用也是很多的,弄懂闭包对于gradle 的学习很重要
定义一个闭包

  { [closureParameters -> ] statements }
  //[closureparameters ->是可选的逗号分隔的参数列表,参数类似于方法的参数列表,这些参数可以是类型化或非类型化的。

如下给出几个有效的闭包定义例子:

//最基本的闭包
{ item++ }                                          
//使用->将参数与代码分离
{ -> item++ }                                       
//使用隐含参数it(后面有介绍)
{ println it }                                      
//使用明确的参数it替代
{ it -> println it }                                
//使用显示的名为参数
{ name -> println name }                            
//接受两个参数的闭包
{ String x, int y ->                                
    println "hey ${x} the value is ${y}"
}
//包含一个参数多个语句的闭包
{ reader ->                                         
   def line = reader.readLine()
   line.trim()
}

其实groovy和java没有本质上的区别,最大的区别在书写形式上,在内容上的区别如下

  • groovy会自动导入部分java常用包

  • 对于重载方法,java会在编译时选择相应方法而groovy实在运行是,具体如下

    int method(String arg) { 
        return 1;
    }
    int method(Object arg) { 
        return 2;
    }
    Object o = "Object";
    int result = method(o);
    

    在java中结果是assertEquals(2, result);
    在groovy中结果是assertEquals(1, result);

  • groovy中{}是用于闭包的,所以数组初始化只能使用int[] array = [1,2,3]
    的形式
  • java中变量定义不写修饰默认就是package-private的,groovy默认就是public,要定义package-private必须使用@PackageScope 注解,比如@PackageScope String name

更多更详细的区别,见Differences with Java
groovy中对java的api有一些扩展,groovy主要对java的io文件操作以及集合类api进行了拓展,详情见The Groovy Development Kit

Gradle

Gradle命令行配置

不像很多其他的工具,Gradle提供了一个简单的东西让我们能够快速使用命令行,那就是GradleWrapper,在一个拥有GradleWapper的工程里,我们不用配置任何信息,就能直接使用Gradle的命令:

./gradlew <task> //在Linux和Mac上
gradlew <task> //在windows上

当然代价就是Gradle会去联网下载对应的Gradle版本,如果网速如果慢的的话就GG了,当然Wrapper还有其他好处,它可以让工程统一构建版本,使用统一版本的构建工具来运行
由于我的龟一样的网速,这里我通过配置Gradle命令行来运行gradle,把gradle版本定死了,首先在系统变量中配置GRAGLE_HOME

***\Android Studio\gradle\gradle-2.14.1

这里我们直接使用了AndroidStudio下的gradle插件,然后在Path中配置

%GRADLE_HOME%\bin\

打开命令行输入gradle tasks,可以看到一系列gradle下可执行的任务。说明gradle配置成功了

Gradle基础

Gradle脚本是配置脚本,脚本执行时,他配置了一个特定类型的对象,比如一个build脚本执行时,会生成一个Project 的对象,这个对象被称作脚本的委托对象

脚本类型 委托对象 说明
Build script Project 可以通过这个对象去获取配置文件中的相关信息
Init script Gradle gradle构建过程有且仅有一个gradle对象,可通过Project.getGradle()获得
Settings script Settings 在配置过程中,通过这个文件来确定构建的模块

除了上面几个委托对象,Gradle中还有有一个重要的对象:
Task task是gradle中真正可执行的对象,grade的构建过程是由很多task来完成的,每个task都属于一个project,我们的编译打包过程其实就是在执行task

Task

task可以直接通过gradle命令来执行,上面使用gradle tasks
看到的一系列task就是gradle已经预先提供好的任务,接下来我们同过自己写一个简单的task来更加清楚的认识一下task
新建一个build.gradle文件

task hello << {
     println "hello world!"
}

运行gradle tasks,可以看到最下面多了几行

Other tasks
-----------
hello

这便是我们上面定义的hello,可以直接通过gradle tasks来运行

>gradle hello
:hello
hello world!

这里我们的task只是简单的在命令行打印了一句话而已,但是task其实可以做更多的操作,我们的工程的构建过程其实也就是在执行task而已,只是task做了更多的工作,同时还依赖了其他更多的task

定义一个Task

task的定义很简单,使用task关键字就可以定义一个task

task myTask

这就是一个简单的定义,这个task可以运行,但是什么都没有做,我们可以对myTask对象进行操作,把任务内容加进去

myTask.doLast {
    println 'Hello MyTask'
}

当然我们也可以直接使用task myTask { configure closure }这种方式进行定义,具体如下

task myTask{
    doLast{
        println 'Hello MyTask'
    }
}

这样的写法和上面表达了同样的意思,然而gradle支持我们简写成下面这种形式

task myTask << {
    println 'Hello MyTask'
}
Task间的依赖

task之间可以有依赖关系,这个依赖关系主要通过3个关键字来完成,分别是dependsOnshouldRunAfter,finalizedBy

  • dependsOn是依赖关系A dependsOn B,A执行必须要B任务先执行
  • finalizedBy紧接着完成 A dependsOn B,A执行之后 B就要接着执行
  • shouldRunAfter约束性没有那么强, A shouldRunAfter B,A在B之后执行,但不一定B执行完了A就一定执行, 但是A一定会在B之后执行

这三个关键字的用法都是一样的,只是意义不同, 我们这里借dependsOn
介绍一下依赖关键字的用法
build.gradle

task hello { 
    doLast { 
        println 'Hello world!'
     }
}
task intro(dependsOn: hello) { 
    doLast { 
        println "I'm Gradle" 
    }
}

执行结果如下

> gradle -q intro
Hello world!
I'm Gradle

-q是屏蔽日志信息,只显示最终结果,从结果中可以看到,我们执行了intro,但是intro依赖了hello,因此hello在intro前执行了,这种依赖关系还可以使用属性来定义
build.gradle

 task intro() {
    dependsOn 'hello' 
    doLast { 
        println "I'm Gradle" 
    }
}

或者直接使用task对象来声明依赖关系

task hello { 
    doLast {
         println 'Hello world!' 
    }
}
task intro() { 
    doLast { println "I'm Gradle" }}intro.dependsOn hello

这三种方法都可以用来声明task之间的依赖关系

Task Type

我们知道Gradle使用了groovy,而groovy又是基于java的,所以Gradle的很多行为都可以用类似java的形式来说明,task本质上其实就是一种特殊的类,上面我们定义的task都是继承自DefaultTask这个Task基类的,除了这个基类Gradle还提供了很多扩展类,用于实现很多扩展功能,我们也可以直接基础这些类,在Task定义过程中我们可以让我们的task直接继承自这些类,这个过程通过task type来实现。
具体实现方法如下:

task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }

官方已经定义了很多种Tasktype,更具体可以参考官方文档Task types,这里我们使用Copy这个Type来进行介绍

Gradle生命周期

Gradle是基于依赖的编程语言,我们可以定义task以及task之间的依赖,Gradle来保证task按照我们给定的依赖顺序来执行,为此Gradle需要在task执行之前生成并维护一个task的DAG。Gradle在执行过程中会经历三个生命周期:
Initialzation

  • Grade支持多项目构建,在这个时期,Gradle需要确定哪些project需要进行build,并为每个需要进行build的project创建一个Project对象

Configuration

  • 在Configuration时期,每个Project对应的build.gradle文件都会被解析,相应的属性都会加入到对应的Project对象中

Execution

  • Gradle中Task的执行时期,Gradle保证每个task都能按照指定的顺序执行
Gradle生命周期

Initialzation

Gradle支持单项目构建和多项目构建(Multi-project builds),构建的类型在Initialzation时期就已经决定了,在Initialzation时期,Gradle会去寻找和解析settings.gradle文件。Gradle寻找settings文件的顺序如下:

  • 在命令执行的当前目录去找
  • 如果没有找到,去上级目录中找
  • 如果还没有找到,Gradle认为这是一个单项目构建
  • 如果寻找到settings文件,Gradle会检查当前项目是否在settings定义的多项目层级中(即是否在include声明过),如果没有,同样认为当前项目是一个单项目构建

在多项目构建中,一般的文件结构为一个树形,一个root project,一个或多个subproject,subproject也可能会有subproject,如果根据settings确定为一个多项目构建时,首先初始化的是根节点,然后是settings中配置的各个子节点(这一时期只是生成project对象,并不会对project对象进行和配置)

Configuration

在Configuration时期,每一个subproject对象都会得到配置,每个subproject的配置过程相对独立,但是我们可以通过root project来实现为每个subproject配置统一的属性,这一过程通过Configuration injection来实现。 实现ConfigurationInjection在Gradle中主要通过几个DSL:project、allprojects和subprojects,这里我们借官方文档的一个小例子做一下说明,有兴趣也可以去研究下官方原版
Project目录如下

water/ 
    build.gradle 
    settings.gradle 
    bluewhale/ 
    krill/

settings.gradle

include 'bluewhale', 'krill'

这个和我们之前看到的类似include ':bluewhale',':krill'是一样的,只是省略了冒号
build.gradle

Closure cl = { task -> println "I'm $task.project.name" }
    task('hello').doLast(cl)
    project(':bluewhale') { 
        task('hello').doLast(cl)
    }

命令行执行gradle -q hello,结果如下

> gradle -q hello
I'm water
I'm bluewhale

这里我们做了什么?首先定义了一个闭包,打印task执行时的项目名,然后在当前build.gradle中定义了一个task"Hello",并把闭包传入task的任务中,同时为bluewhale的gradle添加了同样一个task。
ConfigurationInjection使得我们可以在root project中直接通过代码配置subproject中的属性(不仅仅只是task),allprojects和subproject也是类似的,不同的是allproject可以为所有project生成相同配置,而subproject会剔除root project,具体看例子
在water中使用如下build.gradle

allprojects {
     task hello {
         doLast { task -> 
            println "I'm $task.project.name" 
          }
     }
}

执行结果如下

> gradle -q hello
I'm water
I'm bluewhale
I'm krill

而使用subproject

subprojects {
    task hello {
        doLast { task ->
            println "I'm $task.project.name"
        }
    }
}

结果会是

 > gradle -q hello
I'm bluewhale
I'm krill

可以看到使用subproject时,Gradle会把root project剔除在外,只针对所有subproject进行配置

Gradle task依赖图的管理

上面说道,我们在执行一个Gradle的task时,Gradle会分析整个project,把所有相关联的任务全部添加到一个taskGraph中,当到了excution时期,就会安装taskGraph的顺序来执行。那么我们如何才能动态的根据情况去插入或改变部分task来达到我们的要求。
就比如我们想在java的编译打包的任务中,添加一个生产log文件的需求应该如何实现呢?Gradle为我们提供了很多生命周期相关的方法:

  • project.afterEvaluate 在指定的project对象被配置之后,即build.gradle文件已经解析完成
  • gradle.afterProject 在所有project对象被配置之后
  • tasks.whenTaskAdded 当Task添加到taskGraph中时执行
  • gradle.taskGraph.beforeTask 在Task执行之前taskGraph已经构建好,下同
  • gradle.taskGraph.afterTask

这时候如果要实现上面的需求我们就可以这样

tasks.whenTaskAdded{task->
   if (task.name.equals('jar')) {
      task.dependsOn logTask
    }
}  

Gradle依赖管理

在gradle中依赖管理分为两种,一种是我们的项目使用别人提供的依赖,Gradle可以帮我们快速的找到 这些libraries,无论他们是在本地还是在远程仓库里,另一种是我们的项目本身就是一个library,我们需要生成一个本地的jar,或则上传到远程仓库。
我们先看一段在AndroidProject中声明依赖的代码,首先是在整个工程的build.gradle中

allprojects { 
    repositories { 
        jcenter() 
    }
}

前面我们就已经提到过,allprojects可以为所有项目添加配置,这里就是声明使用jcenter的远程仓库
app module中的build.gradle

apply plugin: 'com.android.application'
dependencies { 
    testCompile 'junit:junit:4.12'
}

这种类似的代码我们经常看到,我们知道testCompile是测试时编译,类似的还有compile,debugCompile,然而这并不是由Gradle提供的,而是由上面com.android.application插件提供的,如果没有使用这个插件,运行会报错。
dependencies在Gradle被归类为configurations,而上面的testCompile其实本质上就是一个配置,并没有做什么,android在运行测试时会去获取配置,然后编译相关联的library,这才是testCompile真正的用处。
为了大家能更深入的理解dependency就是配置,我在在这里举一个例子

repositories{ 
    jcenter()
}
configurations{ 
    myCompile { 
        description = 'compile 
        classpath' transitive = true
   }
}
dependencies{
    myCompile 'junit:junit:4.12'
}
task copyAllDependencies(type: Copy) { 
    from configurations.myCompile 
    into 'allLibs'
}

然后执行

>gradle cAD
:copyAllDependencies

可以看到当前目录下生成了一个allLibs文件夹

allLibs 的目录

2016/11/24 14:09 <DIR>    .
2016/11/24 14:09 <DIR>    ..
2016/11/24 14:09   45,024 hamcrest-core-1.3.jar
2016/11/24 14:09  314,932 junit-4.12.jar

junit库已经被拷贝进去了,同时还有它依赖的hamcrest库,这里例子中,myComplie是我们自己定义的,然后我们用它添加依赖,最后拷贝这个依赖,最后成功从远程仓库得到了jar包,这里只是给大家一个直观的印象,dependencies就是配置。
Gradle中依赖主要又两种写法,String记法和Map记法

dependencies {
    compile 'junit:junit:4.12'
    compile group: 'junit', name: 'junit', version: '4.12'
}

使用String记法在添加远程依赖时,我们只能设置一部分的属性,而使用Map记法我们可以定义所以属性

compile group: 'junit', name: 'junit', version: '4.12', transitive: true

但是两种记法我们都可以用一个闭包来指定所有属性

compile('junit:junit:4.12') { 
    transitive = true
}
compile(group: 'junit', name: 'junit', version: '4.12') { 
    transitive = true
}

上面的例子中我们使用了很多次transitive这条属性,Gradle依赖有个特点,TransitiveDependency,字面上的意思,依赖可以传递,我们依赖的library可能还依赖着其他的libraries,transitive = true就是开启依赖传递,这样gradle就会自动下载library中依赖的其他library。类似的属性还有一些,这里我们也会介绍一些,具体的大家可以去查询官方文档,我们看下面这个官方给出的例子

apply plugin: 'java' //so that I can declare 'compile' dependencies
dependencies { 
    compile('org.hibernate:hibernate:3.1') { 
    //in case of versions conflict '3.1' version of hibernate wins: 
    force = true 

    //excluding a particular transitive dependency: 
    exclude module: 'cglib' //by artifact name 
    exclude group: 'org.jmock' //by group 
    exclude group: 'org.unwanted', module: 'iAmBuggy' //by both name and group 

    //disabling all transitive dependencies of this dependency             
    transitive = false 
    }
}

依赖冲突

Gradle对于依赖版本的冲突一般有两种解决方法

  • 当版本冲突时自动使用最新的一个版本,这是gradle的默认方法,一般是也不会出现问题,除非版本不是向下兼容的
  • 当我们显式选择了多个版本的依赖,这时会导致build failure(如何显示的选择依赖的版本后面详细讲解)

除了上面两种gradle默认提供的冲突解决,gradle也允许我们自己来主动解决冲突,我们知道依赖本质上就是configurations,我们可以在通过改变全局的配置来解决冲突

apply plugin: 'java' //so that there are some configurations

configurations.all { 
    resolutionStrategy { 
        // 如果出现版本冲突就是构建失败 
        failOnVersionConflict() 

        // 优先使用编译体系内的moudule而非外部module   
        preferProjectModules() 

        // 强行使用对应版本的依赖 
        force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4' 
        // 替换指定过的依赖版本 
        forcedModules = ['asm:asm-all:3.3.1'] 

        // 添加依赖替换规则 
        dependencySubstitution { 
            substitute module('org.gradle:api') with project(':api') 
            substitute project(':util') with module('org.gradle:util:3.0') 
        }

         // cache dynamic versions for 10 minutes     
        cacheDynamicVersionsFor 10*60, 'seconds' 
        // don't cache changing modules at all 
        cacheChangingModulesFor 0, 'seconds' 
    }
}

除了使用上面,通过改变全局配置来解决冲突,我们还可以在添加依赖的时候来改变出现冲突时的决策

apply plugin: 'java'
dependencies { 
    compile('org.hibernate:hibernate:3.1') { 
        //如果出现冲突,gradle会选择目前版本作为最终版本 
        force = true 
    }
}

动态版本和可变模块

在我们开发的过程中,我们可能想要让某个依赖能够一直保持一个为最终版本,或者我们开发的library需要一个特定版本范围的依赖,而可变模块是Gradle使用我们给个那个版本,但是module的内容是可以改变的,可变版本的缓存时间默认为12小时,但是也是可以配置的

apply plugin: 'java' //so that there are some configurations

configurations.all { 
    resolutionStrategy { 
        // 动态版本的缓存时间为10分钟 
        cacheDynamicVersionsFor 10*60, 'seconds' 
        // 可变模块的缓存时间为0,也就是不缓存 
        cacheChangingModulesFor 0, 'seconds' 
    }
}

Plugin 插件

事实上Gradle内核基本没有提供出真正意义上的构建功能,Gradle所提供的丰富的功能都是通过插件的形式来提供的,从形式上Gradle的插件分为两种

  • Script Plugin 本质是就是一个build脚本,和我们的build.gradle是一样的,只是进行了更深入的配置
  • Binary Plugin 实现类Plugin接口的类,真正意义上的扩展了Gradle,添加了新的功能和DSL

Plugin在使用时也分两步,解析和应用,解析过程是找到正确版本的pulgin,添加它的脚本classpath,script plugin在应用时,会自解析,而对于一些常用binaryPlugin,Gradle也会自动提供解析,应用的过程就是执行Plugin.apply(T),这样我们就可以直接在project中使用插件中的配置,然而由于使用过程中,解析和应用都是一体的,我们并不会感觉到这个过程

Script Plugin

script plugin很简单,它会自解析,同时支持本地文件和远程地址,如果本地project文件系统内的,可以直接在build.gradle中添加

apply from: 'other.gradle'

就直接应用到我们的脚本中,如果是远程地址,则需要一个http Url

Binary Plugin

要应用一个Binary Plugin,我们需要知道plugin id,对于一些核心的插件,Gradle提供了short name ,比如java插件直接使用’java’就可以了,而对于其他的我们需要知道全名
Apply Plugin
要应用一个Binary插件,我们常用的方式就是

apply plugin: 'java'

但是对于很多非Gradle核心插件,比如android插件,我们没法直接使用apply plugin
来获得,对于这类插件我们需要先把插件地址添加到buildscript的classpath中

buildscript { 
    repositories { 
        jcenter() 
    } 
    dependencies { 
        classpath 'com.android.tools.build:gradle:2.2.2' 
    }
}
apply plugin: 'com.android.application'

通过上面这个方法,我们就可以把远程仓库中plugin的jar包添加到我们project中,前面在依赖中我们已经提过,classpath就是一种configruations,gradle会在需要的时候获取它

总结

如果用脚本去理解gradle,gradle就是脚本,但是如果用类java的思想去看待gradle,gradle其实也是一种编码,本文也只是算是一个gradle的简单介绍,gradle博大精深,很多精髓我还没有领会到,见谅了 !

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

推荐阅读更多精彩内容