Gradle 基础知识与原理(2)

了解了 Gradle 的前世,现在咱们来看 Gradle 的今生,本文主要介绍 Gradle 的一些基础知识与原理,包括 Gradle 各个文件的作用,以及生命周期,构建总体流程,以及生命周期 Hook 方法等。
了解 Gradle 的这些基础原理,可以帮助我们更好的了解 Android 构建打包的过程,也方便我们利用 Gradle 生命周期做一些 Hook 工作,提升开发效率。废话不多说,见下方内容:

1. Gradle 到底是什么?

上篇,咱们了解了 Gradle 是一个依赖管理和构建自动化工具。深入点说呢,Gradle 是一个 运行在 JVM 的通用构建工具,其核心模型是一个由 Task 组成的有向无环图(Directed Acyclic Graphs)

2. Gradle Wrapper 是什么?

说起来我们一直在使用 Gradle,但仔细想想我们在项目中其实没有用 gradle 命令,而一般是使用 gradlew 命令,同时如下图所示,找遍整个项目,与 gradle 有关的就这两个文件夹,和文件: gradle-wrapper.jar、gradle-wrapper.properties、gradlew、gradle.properties、settings.gradle 以及 build.gradle,如下图:

那么问题来了,gradlew 是什么,gradle-wrapper.jar 又是什么?
wrapper 的意思:包装。
那么可想而已,就是 gradle 的包装。其实是这样的,因为 gradle 处于快速迭代阶段,经常发布新版本,如果我们的项目直接去引用,那么更改版本等会变得无比麻烦。而且每个项目又有可能用不一样的 gradle 版本,这样去手动配置每一个项目对应的 gradle 版本就会变得麻烦,gradle 的引入本来就是想让大家构建项目变得轻松,如果这样的话,岂不是又增加了新的麻烦?
所以 android 想到了包装,引入 gradle-wrapper,通过读取配置文件中 gradle 的版本,为每个项目自动的下载和配置 gradle,就是这么简单。我们便不用关心如何去下载 gradle,如何配置到项目中。
再来看下面一张图:

上面我们看到的图其实是 Gradle 提供内置的 Wrapper task 帮助我们自动生成 Wrapper 所需的目录文件。再看看我们 Android 项目里面自动生成的文件

这里 gradlew 也是一样的道理,它共有两个文件,gradlew 是在 linuxmac 下使用的,gradlew.bat 是在 window 下使用的,提供在命令行下执行 gradle 命令的功能
至于为什么不直接执行 gradle ,而是执行 gradlew 命令呢?
因为就像 wrapper 本身的意义,gradle 命令行也是善变的,所以 wrapper 对命令行也进行了一层封装,使用同一的 gradlew 命令,wrapper 会自动去执行具体版本对应的 gradle 命令。
同时如果我们配置了全局的 gradle 命令,在项目中如果也用 gradle 容易造成混淆,而 gradlew 明确就是项目中指定的 gradle 版本,更加清晰与明确

3. gradle-wrapper.properties 的作用

首先来看,它的配置字段:

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

下面再看看每个属性的作用

字段名 说明
distributionBase 下载的 Gradle 压缩包解压后存储的目录
distributionPath 相对于 distributionBase 的解压后的 Gradle 压缩包的路径
distributionUrl Gradle 发行版压缩包的下载路径
zipStoreBase 同 distributionBase 只不过是存放 zip 压缩包的
zipStorePath 同 distributionPath ,只不过是存放 zip 压缩包的

其实我们最关心的应该是 distributionUrl 这个属性,他是下载 Gradle 的路径,它下载的东西会出现在以下的文件夹中

这个文件夹包含了各个版本你下载的 Gradle
问个问题:是不是大家和我一样,在“全球最大同性交友网站(github)”上下载东西,之后运行会遇到下图中的问题:

导入项目的时候一直会停留在这个界面,这是为什么?其实原因很简单,就是你常用项目的 Gradle 版本跟你新导入项目的 Gradle 版本不一致造成的,那怎么解决?我本人自己是这么做的:

  • 在能访问的情况下 ,由它自己去下载,不过下载时间有长有短,不能保证。
  • 当你在公司被限网速的时候,当然也是我最常用的,就是把你最近常用项目的 gradle-wrapper.properties 文件替换掉你要导入项目的该文件,基本上我是这样解决的,当然有时候也会遇到替换掉报错的情况,不过比较少。

4. AGP到底是什么?

Gradle 本身是一个通用的构建系统, 它并不知道你要编译的是 Java 还是 C。如果是在 Java 中需要调用 javac.java 文件编译为 .class 文件, 而 C 则需要调用 gcc.c 文件编译为 .o 文件. 那么这些构建流程如果让每个开发者自己去管理就太麻烦了. 所谓插件, 就是将某种类型编译的模板。

AGP 也就是是一系列适合 Android 开发的 Gradle 插件的集合,比如 com.android.application 等。
AGP 插件提供了 compileKotlin,compileJava,processResource等一系列 Task,并设置了 Task 之间的依赖关系. 同时还提供了很多可配置属性。而使用者只需要在 build script 中通过 plugins {...}引入插件, 根据项目情况配置几个属性, 即可实现自定义的 Android 构建。 通过 AGP 插件可以快速实现 Android 项目的构建,这就是 AGP 插件的意义,其执行过程中的 task 列表如下所示

5. gradle.properties 是什么?

除了 gradlewAGP ,我们也经常会用到 gradle.properties,我们经常在 gradle.peoperties 中定义一些统一的版本号,如 minSdkVersion,targetSdkVersion等,然后再在各个 module 中通过 rootProject.minSdkVersion 获取以实现复用

那么问题来了:rootProject 是如何获取 gradle.properties 中定义的值的呢?
答案其实很简单,Gradle 启动时会默认读取 gradle.properties, 并加载其中的参数。这跟我们在运行 Gradle 的时候通过命令行向其传递参数,效果是一样的。
当然不同的方式有不同的优先级,指定参数的优先级:命令行参数 > GRADLE_USER_HOME gradle.properties 文件 > 项目根目录 gradle.properties 文件。

Gradle 使用的两个目录:
Gradle 在执行过程中会涉及到两个目录, 一个是 Gradle User Home 另一个是 Project Root Directory.
Gradle User Home
User Home 中主要保存全局配置, 全局初始化脚本以及依赖的缓存和日志等文件. 如果开启 build cache 的话, 构建缓存也会存在这里共所有项目共享.
默认为: $USER_HOME/.gradle.
Project Root Directory
Project 目录则存储与当前项目构建相关的内容. 例如用于增量编译缓存.

总得来说,gradle.properties 其实就是一个参数的配置文件,与在命令行传递参数是一样的效果,因此在 Project 中可以读取到。

6. settings.gradle是什么?

当我们在某个目录执行 gradle 命令时, 约定的会从当前目录查找以下两个文件:

  • settings.gradle(.kts)
  • build.gradle(.kts)

我们常常会在 settings.gradle 中配置 module ,那么 settings.gradle 究竟是什么?起什么作用?
所有需要被构建的模块都需要在 settings.gradle 中注册, 因此它的作用是描述 "当前构建所参与的模块"。

settings.gradle 查找顺序为: 从前目录开始, 如果找到 settings.gradle(.kts) 则停止, 否则向父目录递归查找。
setting script 承担了统筹所有模块的重任,因此 api 主要是在操作所参与构建的模块以及管理构建过程需要的插件。
可以通过如下方式注册需要参与构建的模块,项目名称中:代表项目的分隔符,类似路径中的 /. 如果以 : 开头则表示相对于 root project

include(":app", ":libs:someLibrary")

include(":anotherLibrary")
project(":anotherLibrary").projectDir = File(rootDir, "../another-library")

好了,我们说完settings.gradle文件之后就慢慢进入其他文件了,但是首先我们要解释一下什么是Groovy:

7. Groovy?

Groovy 是基于 JVM 虚拟机的一种动态语言,它的语法和 Java 非常相似,由 Java 入门学习 Groovy 基本没有障碍。Groovy 完全兼容 Java ,又在此基础上增加了很多动态类型和灵活的特性,比如支持密保,支持 DSL ,可以说它就是一门非常灵活的动态脚本语言。

一开始我总把 GradleGroovy 搞混了,现在我总把他们的关系弄清楚了。Gradle 像是一个软件,而 Groovy 就是写这个软件的语言,这就很简单明了吧。那下面我们说到的内容都是用 Groovy 语法写的,但是这个知识点就有请下一位阐释啦。

8. build.gradle 是什么?

到了我们最熟悉也是最常用的 build.gradle 了,每个模块都会有一个 build.gradle 来配置当前模块的构建信息, 根目录模块的 build.gradle 叫做 root build script,其他子模块的 build script 叫做 module build script

项目构建的流程大致如下所示,其中的 init script$GRADLE_USER_HOME 目录下的 init.gradle 文件,主要做一些初始化配置。
单模块构建的执行流程大致为:init script -> setting script -> build script
而多模块的构建流程, 比单模块多了一步:init script -> setting script -> root build script -> build script

一般而言, root build script 并不是一个实际的模块, 而是用于对子模块进行统一的配置, 所以 root build script 一般不会有太多的内容。

GradleInitialization 阶段还没有执行 build.gradle(.kts) 文件, 真正解析 build script 是在 Configuration 阶段。但是 build script 的执行比较特殊,它并不是简单执行所有代码,其本质是 用代码描述和配置构建规则, 然后按规则执行任务。Build script 作为整个 Gradle 中配置最复杂的脚本, 实际上仅仅做了两件事:

  • 引入插件
  • 配置属性

所谓引入插件如下所示,plugins 闭包中还可以通过 version 指定插件的版本, 以及 apply 来决定是否立刻应用插件:

plugins {
    id("com.android.application")
    id("com.dorongold.task-tree")   version "1.4"
    id("com.dorongold.task-tree")   version "1.4"   apply false
}

而所谓配置属性,实际上是对引入的插件进行配置. 原本 build script 中并没有 android {...} 这个 dsl 属性, 这是 plugin 提供的。 一旦应用了某个插件,就可以使用插件提供的 dsl 对其进行配置,从而影响该模块的构建过程。换个角度看,这些插件提供的属性配置 dsl 就相当于插件 init 函数的参数, 最终传入到插件中,当构建执行的时候就会根据配置对当前模块进行编译。

plugins {
    id("com.android.application")
}

android {
    compileSdkVersion(28)

    defaultConfig {
        ....
    }
}
....

9. Gradle生命周期是怎样的?

在了解了上面这些知识后,我们可以开始了解一下 Gradle 的生命周期。在了解了 Gradle 的生命周期后,我们可以对 Gradle 执行的总体流程有一个了解,也可以利用这些生命周期做一些 Hook 的操作。

不同于传统脚本的自上而下执行, 一次 Gradle 构建涉及到多个文件, 主体流程如下:

总体来说,Gradle 的执行分为三大阶段:Initialization -> Configuration -> Execution,每个阶段都有自己的职责。

9.1 Initialization 阶段

Initialization 阶段主要目的是初始化构建,它又分为两个子过程,一个是执行 Init Script,另一个是执行 Setting Script
Init script 会读取全局脚本,主要作用是初始化一些全局通用的属性,例如获取 Gradle User Home 目录,Gradle version 等。
Setting Script 就是我们上面提到的 settings.gradle
主要步骤:
1、执行 Init 脚本:Initialization Scripts[5] 会在构建最开始执行,一般用于设置全局属性、声明周期监听、日志打印等。
Gradle 支持多种配置 Init 脚本的方法,以下方式配置的所有 Init 脚本都会被执行:

• gradle 命令行指定的文件:gradle —init-script <file>
• USER_HOME/.gradle/init.gradle 文件
• USER_HOME/.gradle/init.d/ 文件夹下的 .gradle 文件
• GRADLE_HOME/init.d/ 文件夹下的 .gradle 文件

2、实例化 Settings[6] 接口实例:解析根目录下的 settings.gradle 文件,并实例化一个 Settings 接口实例;
3、执行 settings.gradle 脚本:在 settings.gradle 文件中的代码会在初始化阶段执行;
4、实例化 Project 接口实例:Gradle 会解析 include 声明的模块,并为每个模块 build.gradle 文件实例化 Project 接口实例。Gradle 默认会在工程根目录下寻找 include 包含的项目,如果你想包含其他工程目录下的项目,可以这样配置:

  // 引用当前工程目录下的模块
  include ':app'

  // 引用其他工程目录下的模块
  include 'video' // 易错点:不要加’冒号 :‘
  project(:video).projectDir = new File("..\\libs\\video")
9.2 Configuration 阶段

当构建完成 Initialization 阶段后,将进入 Configuration 阶段。 这个阶段开始加载项目中所有模块的 Build Script。所谓 "加载" 就是执行 build.gradle(.kts) 中的语句,根据脚本代码创建对应的 task,最终根据所有 task 生成对应的依赖图。我们上面说过"Gradle 核心模型是一个 Task 组成的有向无环图(Directed Acyclic Graphs)" 吗? 这个任务依赖图就是在这个阶段生成的。

需要注意的是,Configuration 阶段各个模块的加载顺序是无序的,跟依赖关系与加入顺序都没有关系。
主要包含 3 步:

1、下载插件和依赖:Project 通常需要依赖其他插件或 Project 来完成工作,如果有需要先下载;
2、执行脚本代码:在 build.gradle 文件中的代码会在配置阶段执行;
3、构造 Task DAG:根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task。
注:执行任何 Gradle 构建命令,都会先执行初始化阶段和配置阶段。
9.3 Execution 阶段

当完成任务依赖图后, Gradle 就做好了一切准备, 然后进入 Execution 阶段. 这个阶段才真正进行编译和打包动作。对于 Java 而言是调用 javac 编译源码, 然后打包成 jar。对于 Android 而言则更加复杂些,这些差异来源于我们应用的插件。总得来说,就是开始执行 task 了。
这里有两个容易理解错误的地方:

  • Task 配置代码在配置阶段执行,而 Task 动作在执行阶段执行;
  • 即使执行一个 Task,整个工程的初始化阶段和所有 Project 的配置阶段也都会执行,这是为了支持执行过程中访问构建模型的任何部分。

介绍完三个生命周期阶段后,你可以通过以下 Demo 体会各个代码单元所处的执行阶段:

USER_HOME/.gradle/init.gradle
  println 'init.gradle:This is executed during the initialization phase.'

settings.gradle
  rootProject.name = 'basic'
  println 'settings.gradle:This is executed during the initialization phase.'

build.gradle
  println 'build.gradle:This is executed during the configuration phase.'

  tasks.register('test') {
      doFirst {
          println 'build.gradle:This is executed first during the execution phase.'
      }
      doLast {
          println 'build.gradle:This is executed last during the execution phase.'
      }
      // 易错点:这里在配置阶段执行
      println 'build.gradle:This is executed during the configuration phase as well.'

输出:
Executing tasks: [test] in project /Users/pengxurui/workspace/public/EasyUpload

init.gradle:This is executed during the initialization phase.
settings.gradle:This is executed during the initialization phase.

> Configure project :
build.gradle:This is executed during the configuration phase.
build.gradle:This is executed during the configuration phase as well.

> Task :test
build.gradle:This is executed first during the execution phase.
build.gradle:This is executed last during the execution phase.
...
}

9.4 生命周期 Hook

Gradle 提供了丰富的生命周期 Hook,我们可以根据我们的需要添加各种 HooK,见下图:

根据图中生命周期的位置,可以清楚地了解到 "生命周期的最晚注册时机"。比如,settingsEvaluated 是在 setting scriptevaluated 完毕后回调,那么在 init scriptsetting script 中注册都是没问题的。但是如果注册在 build script 中,则无法发挥作用。

同时关于生命周期 Hook ,还有下面几点需要注意:

  1. projectsLoaded 之前 Project 还没有创建,因此只能使用 gradlesettings 对象。
settings.gradle

  // Settings 配置完毕
  gradle.settingsEvaluated {
      ...
  }

  // 所有 Project 对象创建(注意:此时 build.gradle 中的配置代码还未执行)
  gradle.projectsLoaded {
      ...
  }
  1. projectsLoaded 回调时已经根据 setting script 创建了各个模块的 Project 对象, 我们可以引用 project 对象从而设置一些 hook,便是 build script 还没有被配置,因此拿不到配置信息。
  2. 每当一个 build.gradle(.kts) 被执行完毕, 都会产生 afterEvaluate 回调, 代表着 projectevaluate 完成。从此,project 对象内容完整了, 即:当前 build.gradle(.kts) 中所有的配置项都能够被访问。
// 执行 build.gradle 前
project.beforeEvaluate { 
    ...
}

// 执行 build.gradle 后
project.afterEvaluate { 
    ...
}

除此之外,Gradle 接口也提供了配置阶段的监听:

// 执行 build.gradle 前
gradle.beforeProject { project ->
    ...
}

// 执行 build.gradle 后
gradle.afterProject { project ->
    // 配置后,无论成功或失败
    if (project.state.failure) {
        println "Evaluation of $project FAILED"
    } else {
        println "Evaluation of $project succeeded"
    }
}

// 与 project.beforeEvaluate 和 project.afterEvaluate 等价
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
    @Override
    void beforeEvaluate(Project project) {
        ...
    }

    @Override
    void afterEvaluate(Project project, ProjectState projectState) {
        ...
    }
})

// 依赖关系解析完毕
gradle.addListener(new DependencyResolutionListener() {
    @Override
    void beforeResolve(ResolvableDependencies dependencies) {
        ....
    }

    @Override
    void afterResolve(ResolvableDependencies dependencies) {
        ....
    }
})

// Task DAG 构造完毕
gradle.taskGraph.whenReady {   
}

// 与 gradle.taskGraph.whenReady 等价
gradle.addListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
        ...
    }
})

// 所有 Project 的 build.gradle 执行完毕
gradle.projectsEvaluated {
    ...
}
  1. 所有的 Project 配置结束,会回调 projectsEvaluated
  2. Gradle 的核心逻辑就是根据 task 的依赖关系生成有向无环图,然后依次执行图中的 task,task graph 生成后会回调 graphPopulated
  3. 当所有 task 都执行完毕, 整个构建也宣告结束,这个时候会回调 buildFinished

10. Project 核心 API

Project 可以理解为模块的构建管理器,在初始化阶段,Gradle 会为每个模块的 build.gradle 文件实例化一个接口对象。在 .gradle 脚本中编写的代码,本质上可以理解为是在一个 Project 子类中编写的。

10.1 Project API

Project 提供了一系列操作 Project 对象的 API

  • getProject():返回当前 Project;
  • getParent():返回父 Project,如果在工程 RootProject 中调用,则会返回 null;
  • getRootProject():返回工程 RootProject;
  • getAllprojects():返回一个 Project Set 集合,包含当前 Project 与所有子 Project;
  • getSubprojects():返回一个 Project Set 集合,包含所有子 Project;
  • project(String):返回指定 Project,不存在时抛出 UnKnownProjectException;
  • findProject(String):返回指定 Project,不存在时返回 null;
  • allprojects(Closure):为当前 Project 以及所有子 Project 增加配置;
  • subprojects(Closure):为所有子 Project 增加配置。
10.2 Project 属性 API

Project 提供了一系列操作属性的 API,通过属性 API 可以实现在Project 之间共享配置参数:

  • hasProperty(String):判断是否存在指定属性名;
  • property(Stirng):获取属性值,如果属性不存在则抛出 MissingPropertyException;
  • findProperty(String):获取属性值,如果属性不存在则返回 null;
  • setProperty(String, Object):设置属性值,如果属性不存在则抛出 MissingPropertyException。

实际上,你不一定需要显示调用这些 API,当我们直接使用属性名时,Gradle 会帮我们隐式调用 property()setProperty()。例如:
build.gradle

name => 相当于 project.getProperty("name")
project.name = "Peng" => 相当于 project.setProperty("name", "Peng")

10.2.1 属性匹配优先级

Project 属性的概念比我们理解的字段概念要复杂些,不仅仅是一个简单的键值对。Project 定义了 4 种命名空间(scopes)的属性 —— 自有属性、Extension 属性、ext 属性、Task。当我们通过访问属性时,会按照这个优先级顺序搜索。

getProperty() 的搜索过程:

1、自有属性:Project 对象自身持有的属性,例如 rootProject 属性;
2、Extension 属性;
3、ext 属性;
4、Task:添加到 Project 上的 Task 也支持通过属性 API 访问;
5、父 Project 的 ext 属性:会被子 Project 继承,因此当 1 ~ 5 未命中时,会继续从父 Project 搜索。需要注意:从父 Project 继承的属性是只读的;
6、以上未命中,抛出 MissingPropertyException 或返回 null。

setProperty() 的搜索路径(由于部分属性是只读的,搜索路径较短):

1、自有属性。
2、ext 额外属性。
提示:其实还有 Convention 命名空间,不过已经过时了,我们不考虑。

10.2.2 Extension 扩展

Extension 扩展是插件为外部构建脚本提供的配置项,用于支持外部自定义插件的工作方式,其实就是一个对外开放的 Java Bean 或 Groovy Bean。例如,我们熟悉的 android{} 就是 Android Gradle Plugin 提供的扩展。

关于插件 Extension 扩展的更多内容,下次再讲。

10.2.3 ext 属性

Gradle 为 Project 和 Task 提供了 ext 命名空间,用于定义额外属性。如前所述,子 Project 会继承 父 Project 定义的 ext 属性,但是只读的。我们经常会在 Root Project 中定义 ext 属性,而在子 Project 中可以直接复用属性值,例如:

项目 build.gradle

ext {
    kotlin_version = '1.4.31'
}

模块 build.gradle

// 如果子 Project 也定义了 kotlin_version 属性,则不会引用父 Project
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
10.3 Project 文件 API

10.3.1 文件路径

  • getRootDir():Project 的根目录(不是工程根目录)
  • getProjectDir():包含 build 文件夹的项目目录
  • getBuildDir():build 文件夹目录

10.3.2 文件获取

  • File file(Object path):获取单个文件,相对位置从当前 Project 目录开始。
  • ConfigurableFileCollection files(Object... paths):获取多个文件,相对位置从当前 Project 目录开始。
def destFile = file('releases.xml')
if (destFile != null && !destFile.exists()) {
    destFile.createNewFile()
}

10.3.3 文件拷贝

  • copy(Closure):文件拷贝,参数闭包用于配置 CodeSpec[8] 对象。
copy {
    // 来源文件
    from file("build/outputs/apk")
    // 目标文件
    into getRootProject().getBuildDir().path + "/apk/"
    exclude {
        // 排除不需要拷贝的文件
    }
    rename {
        // 对拷贝过来的文件进行重命名
    }
}

10.3.4 文件遍历

  • fileTree(Object baseDir):将指定目录转化为文件树,再进行遍历操作。
fileTree("build/outputs/apk") { FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        // 文件操作
    }
}

最后再来看一下,Gradle 配置详解。

三.、Gradle 配置详解

Gradle 配置详解

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

推荐阅读更多精彩内容