Gradle In Action

前言

从2013年Google推出Android Studio(后面以AS简称)开始,到现在已经历经3年,版本也发展到了2.x版本,目前Android开发者基本上已经没有再用Eclipse开发的了。从Eclipse切换到AS,项目组织结构和环境变动很大,不过上手AS还是很简单的,新建一个项目,选择API版本,选择一个默认的空Activity,然后点击确定一个项目就创建好了,直接点击运行就可以把项目运行到你手机上或者模拟器上面。

上面很容易创建了一个Android项目,然后就可以愉快的进行开发了。很快遇到了一个需要显示网络图片的需求,先看看网上有没有现成的轮子,随便一搜发现Facebook出品的一个叫Fresco的库,网上一片好评,那就用它了。安装官方说明,找到项目build.gradle文件,在dependencies段加入如下一行

 compile 'com.facebook.fresco:fresco:0.14.1'  

同步一下项目,然后在代码中发现已经可以引用到fresco库了,虽然不懂是怎么解决的,但是既然已经能用了 ,就继续开发。很快项目开发好了,准备发版测试了,突然QA提出要记录每次打包的时间,显示在应用内的调试页面,这样以免以后撕逼时乌龙,果然是老司机!怎么实现呢?官方文档翻了几遍,终于看到一篇讲怎么注入变量到Manifest里面的:

android {    
        defaultConfig {        
                manifestPlaceholders = [hostName:"www.example.com"]    
        }    ...
}  

思路有了,我在AndroidManifest里面配置一个变量表示打包时间BUILD_TIME,然后打包时利用上面的方法把当前时间赋值给BUILD_TIME,程序从AndroidManifest中读取BUILD_TIME显示到调试面板中,经历各种Google细节完成如下:

<!--AndroidManifest中配置打包时间->
<meta-data android:name="BUILD_TIME"    android:value="${BUILD_TIME}" />

def releaseTime() { 
   return new Date().format("yyyyMMdd hh:mm:ss", TimeZone.getTimeZone("GMT+8"))
}
defaultConfig {    
    multiDexEnabled true    
    applicationId "xxxxx"     
    minSdkVersion 16   
    targetSdkVersion 23    
    versionCode 3    
    versionName "1.1.1"    
    manifestPlaceholders = [BUILD_TIME:"${releaseTime()}"]
}

程序运行下看看,竟然好使!等等,defaultConfig里面那堆东西看起来那么面熟啊,这不是AndroidManifest里面配置的东西么,这里面到底还能塞进来什么东西?我咋知道minSdkVersion有没有拼写错误啊?不管了,反正程序现在能跑,赶紧继续发版,今天还要早点下班约会,虽然不知道跟谁约。
刚打好包运营妹子就跑了过来,说哥哥我要发四十个渠道,每个渠道都要统计用户量,你就扔给我一个包,真的好吗?额,这个等一下,马上处理。统计渠道好搞,应用中都集成了友盟,只需要在下面的配置里写上渠道名,打个包,再修改渠道号打包,重复四十次就能搞定了。

<meta-data    android:name="UMENG_CHANNEL"    android:value="360" />

当然我不会这么傻手动打包四十次,好歹也是计算机科班出身的码农,虽然我确实不知道该怎么办,但是我可以问百度啊(这事可以问百度,渠道和友盟都是国情嘛)。很快找到解决方案:

productFlavors 
{        
    360{}
    yingyongbao{}
    wandoujia{}
    xiaomi{}
    ......      
}
productFlavors.all { flavor ->    
     flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE:name]
}
buildTypes {        
    debug {            
        ......
    }        
    release {            
        buildConfigField "boolean", 
        minifyEnabled true            
        zipAlignEnabled true            
        shrinkResources true            
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'            
        signingConfig signingConfigs.release            
        applicationVariants.all { variant ->                
                 variant.outputs.each { output ->                    
                         def outputFile = output.outputFile                    
                         if (outputFile != null && outputFile.name.endsWith('.apk')) { 
                              //MyApp_xiaomi_2016-10-18_v6.2.0.apk                       
                                def fileName = "MyApp_${variant.productFlavors[0].name}_${releaseTime()}_v${defaultConfig.versionName}.apk"                       
                             output.outputFile = new File(outputFile.parent, fileName) 
                      }                
               } 
        } 
     }    
}

果不其然,问题完美解决,但是现在真的没办法淡定的继续写开发了,这里一坨一坨的东西不把它弄清楚,相信后面的日子会很难过,那么我们就开始研究一下这些东西吧。

1. Gradle For Android

Google在推出AS的时候就说了采用Gradle替代Ant来构建项目,不难猜出,上面我们写的就是Gradle“代码”了,那我们再来看一下Gradle到底是什么东西。很快我们找到了它的官方用户手册---https://docs.gradle.org/current/userguide/userguide.html, 里面它是这样自我介绍的:“a build system that we think is a quantum leap for build technology in the Java (JVM) world”,它谦虚的表示自己是Java世界中编译技术的一项巨大突破的一个编译系统,使用了Groovy作为编译脚本,定义了大量的领域模型(domain model)。嗯,还不是很懂它在讲什么,那么我们先把文档看一遍吧。。。纳尼,竟然72节,有点看不下去,并且看标题很多跟Android没关系,还是先学会怎么写点简单的Android配置吧。还好Android官方文档里面有一章介绍怎么用Gradle配置工程(https://developer.android.com/studio/build/index.html), Gradle官网上也有也有一个模块专门介绍Gradle在Android中的应用(https://gradle.org/getting-started-android-build/#build-master), 并且提供了一本电子书和一个讲解视频,那就赶紧先上车吧。

1.1 Gradle Wrapper

当构建一个项目时,往往要需要对应的工具支持,如果本地没有安装或者本地安装的版本和项目要求的不一致时,就会比较麻烦。Gradle引入了Gradle Wrapper很好的解决了这个问题。我们先看一下Gradle Wrapper的组成部分:

  • gradlew (Unix Shell script)
  • gradlew.bat (Windows batch file)
  • gradle/wrapper/gradle-wrapper.jar(Wrapper JAR)
  • gradle/wrapper/gradle-wrapper.properties(Wrapper properties)

看起来是不是很眼熟,去看一下上面创建的Android项目,可以看到项目根目录就有这几个文件,gradlew和gradlew.bat分别是Unix系和Windows系操作系统下的命令,打开gradle-wrapper.properties,可以看到有下面的一行配置:

distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip

这个即本项目需要的gradle版本号。平时执行Gradle命令任务时,使用的是

gradle <task>

但是以后你可以使用(Mac上,windows上类似) :

./gradlew <task>

它会先看当前是否已经安装gradle并且当前指定的gradle版本本地已经存在,如果不是的话,它就先自动下载对应的gradle版本然后在执行task。如果想改变当前项目对应的gradle版本,可以直接编辑gradle-wrapper.properties文件或者在AS中打开module setting,选择"Project"选项,在右边可以直接编辑。也可以使用下面两种方式:

gradle wrapper --gradle-version 2.14

task wrapper(type: Wrapper) { 
    gradleVersion = ' 2.14  '
}

1.2 Gradle 项目结构

Gradle所有的工作就是基于projects和tasks,我们可以先暂时不用理解什么是projects和tasks。每一个Gradle构建都包含一个或多个project,而每一个project就是一个待编译的工程,它通常包含一系列的task。Gradle要求每一个project在它根目录下都要有一个build.gradle,官方称一个build.gradle文件为"编译脚本",它定义了一个project和它的tasks。在继续之前,我们可以先写个小例子熟悉一下,创建一个文件build.gradle,打开并且输入一下代码:

task taskX(dependsOn: 'taskY') << { 
    println 'taskX'
}
task taskY << { 
    println 'taskY'
}

在同级目录输入如下命令看下输出

gradle taskX

上面就是一个简易的project。如果一个项目里面有多个project怎么办?平时开发时经常会把一个项目拆分成多个子项目。这种情况Gradle中称作Multi-Projects Build,一个Multi-Projects Build由一个根project和一个或多个子project组成,这些子project也 可以包含自己的子project,它的组织结构如下:

root/
   build.gradle
   settings.gradle
   subpro1/
       build.gradle   
  subpro2/
      build.gradle

我们可以看到每个project都要对应一个build.gradle,这个我们上面已经讲过了,但是这里在根目录多出来一个settings.gradle。这个就是Multi-Projects Build的奥妙所在,这个settings.gradle主要用来配置这个Multi-Projects Build包含哪些子project。如果你有多子工程的Android项目,打开看看gradle的结构是不是跟上面一样。当然,如果你不想把subpro1和subpro2放到root目录下也没关系,只需要在setting.gradle中指定子project的目录即可,参考目录结构和setting配置:

project/
    root/
        build.gradle
        settings.gradle
        app/
            build.gradle    
    subpro1/
        build.gradle
    subpro2
        build.gradle


//setting.gradle
include ':app', ':subpro1', ':subpro2'
def projectTreeRootDir = new File("../");
project(":subpro1").projectDir = new File(projectTreeRootDir, "subpro1");
project(":subpro2").projectDir = new File(projectTreeRootDir, "subpro2");

如果你重来没有使用过Multi-Projects Build项目,建议你尝试一下怎么为当前项目添加一个子Module,对的,在Android Studio中每个子项目叫做Module,以后你项目大了,肯定有必要把它拆成多个Module。创建过程很简单,在Project视图下,在当前项目根目录上面右键,"new"->"Module"->"Android Library" (或者"Phone & Tablat Module"),这里就不贴图了。

1.3 Gradle Recipes For Android

前戏了这么久,该进入正文了,我们打开自己的Android项目,挨个研究一下里面的Gradle配置吧。

settings.gradle:

它位于根目录下,用来配置构建一个应用时都需要编译那些module,所以你项目中的所以子project都要在这里配置,通常如下:

include ':app', ':submodule1', ':submodule2'
Top-level build.gradle:

它位于根目录下,用来配置此项目下所有module都应用的设置。下面是常见的一个例子以及说明。

/**
buildscript{}块配置Gradle自己的仓库(repositories)和依赖(dependencies),
如下面所示,Gradle如果想要能编译Android项目,需要依赖谷歌提供的一个gradle插件 
(Gradle Plugin),它里面包含了一些编译需要的指令。
*/  
buildscript {   

  /**
   repositories{}配置了Gradle查找和下载依赖的仓库,通常它默认配置了JCenter、
   Maven Central 和 Ivy等远程仓库,你也可以配置自己的本地仓库和私人远程仓库。
  */
  repositories {        
       jcenter()    
  }
  
  /**
   dependencies {} 配置了Gradle编译项目需要的依赖,这里制定了依赖2.0.0版本
   的Gradle Plugin
  */
  dependencies {        
       classpath 'com.android.tools.build:gradle:2.0.0'    
   }
}

/**
allprojects {}配置你项目中所有module都使用的仓库(repositories)和依赖
(dependencies)。*/
allprojects {   
   repositories {       
       jcenter()   
   }
}
Module-level build.gradle

也就是每个子工程的build.gradle,通常位于<project>/<module>/目录下面,主要设定当前module的配置信息。同样,我们也根据下面的例子解释:

/** 
 在当前工程中为Gradle使用Android插件,可以支持使用android{}配置Android专有的
 编译选项,插件为 com.android.application 表示可运行Android项目,如果为一个aar库
 时应该应用com.android.library */

apply plugin: 'com.android.application'

/**android{} 配置所有Android编译相关的选项*/
android {
    compileSdkVersion 23  
    buildToolsVersion "23.0.3"
    
    /**
     默认的编译变量和设置,可以动态的覆盖AndroidManifest.xml里面的属性
    */
    defaultConfig {    
       applicationId 'com.example.myapp'    
       minSdkVersion 14    
       targetSdkVersion 23    
       versionCode 1    
       versionName "1.0"  
    }
   
    /**
      编译类型,默认有debug和release,主要设置打包、混淆相关参数。
    */
    buildTypes {         
        release {        
           minifyEnabled true 
           proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }  
    }

    /**
     产品配置,可以设置多个产品选项,覆盖defaultConfig{}里面的值。
     直观点讲就是可以一下打多个包,每个包有不同的设置。
    */
    productFlavors {    
        free {      
           applicationId 'com.example.myapp.free'    
        }    
       paid {      
           applicationId 'com.example.myapp.paid'    
       }  
    }
 
  /**
   splits{}可以配置编译不同的APK,每个包只包含针对特定屏幕密度或ABI的代码和资源
  */
  splits {    
     density {      
        enable false       
        exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"    
     }  
  }
}

上面写了貌似挺详细了,但有感觉啥用没有,很多东西还是一知半解,前面提到的问题----这里面到底都支持什么参数?还是没有解决。还好我找到了Android Gradle DSL 我们选择左边的“AppExtension”,右边的“ Properties”可以看到android{}块支持的所以配置字段和解释,我们可以沿着这个根找到所以其他相关字段的配置,例如buildTypes里面的每种type支持哪些字段,查找到的截图如下:

Paste_Image.png

另外,我们发现左边还有一个“LibraryExtension”,估计不说你也能猜到了,它和前面提到的“AppExtension”分别对应com.android.application和com.android.library。属性值太多了,这里就不挨个解(翻)释(译)了。

Gradle Properties Files

这里主要指根目录下gradle.properties和local.properties两个文件。前者主要是指项目范围内的Gradle配置,例如Gradle进程堆栈大小。后者主要配置一些本地SDK路径,一般IDE会自动生成,所以最好不要编辑和提交到版本控制系统里(svn, git)。

2. In-Depth Study Of Gradle

看完上面的东西,应该已经可以翻着文档配置下Android构建脚本了,但是想要在构建时配置写高级的东东,发现前面的知识还是不够用,那我们接下来就深入点看看Gradle的知识吧。

2.1 Task

Task是指编译脚本中的一个独立的编译单元,代表了一些要执行的动作和行为,它是project的组成元素,可以包含多个action(block), 在上面的例子中我们已经演示过怎么写一个task, task有两个函数doFirst和doLast,表示最先执行和最后执行的action。下面我们写一个例子

task mytask 

task task2 {
    println "test2"
}

task task3 << {
    println "task3"
}
mytask.doFirst {
    println "do first"
}

mytask.doLast {
    println "do last"
}

task task2 {...}这种写法是表示创建task2后返回前,先执行block的内容。而 task task3 << {...}是doLast的一种简写形式。通过命令 "gradle taskName"可以执行一个task,如下命令可以看到当前所以的task:

gradle tasks
gradle tasks --all

而我们之前的例子中可以看到tasks之前是可以存在依赖关系的,所以整个项目构建就可以通过这些tasks来完成了。

2.2 Build Lifecycle

构建主要分三个阶段:初始化、配置和执行。在初始化阶段,主要确定哪些project参与构建,并且创建一个Project对象。配置阶段,主要是解析每一个project的脚本文件,确定所有的task并且根据他们的依赖关系创建一个有向图,最后一个阶段就是执行这些tasks。而我们在这些阶段之间,可以插入一些hook来完成一些特殊的需求。下面是几个例子:

preBuild.dependsOn 'dfqin'
task dfqin << {    
    println "do after dfqin task"
}
tasks.getByName("preBuild"){    
    it.doLast {        
        println "$project.name:  after preBuild"    
    }    
    it.doFirst {        
        println "$project.name: before preBuild"    
    }
}
project.afterEvaluate {    
    println "after evaluate"
}
2.3 Gradle Build Language(DSL)

Gradle 是一种可配置脚本,主要体现在它可以定义一些SB( script blocks),SB类似于一个函数调用,block作为参数传递给调用者来完成特定的配置工作,即它可以称作“领域描述语言”。我们可以看下官方文档

Paste_Image.png
Paste_Image.png

上面的截图中的解释看懂了吧?我就不解释了———你如果看懂了,给我解释一下,因为我没看懂:( 好了,不懂也得死磕,它讲到,Gradle是可配置脚本,当脚本执行后,它会配置(生成)一个特定类型的对象。脚本也分类型,有"Build script"类型的脚本,有"Init script"类型的脚本,有"Settings script"脚本,执行不同类型的脚本会生成不同的对象,如上三种类型脚本分别生成Project、Gradle和Settings对象。下面讲到常用的一些Build script可以由一些语句(方法调用、属性赋值和变量定义)和SB(script block)构成,我们在上面的截图中看到了一些熟悉的东西,就是上面所说的SB,以buildscript{}为例,我们去项目中看一下,发现竟然可以按command进入到源码,截图如下:

Paste_Image.png

我们可以看到,它是Project接口的一个方法,现在我可以这样理解,我们要创建一个Project类型的对象,这个对象的方法实现就是依靠我们的SB,系统后面调用这个对象的方法时,就是执行了我们的SB,通过这种方式,我们实现了对构建项目的配置。嗯,现在不管你懂不懂,反正我是懂了,以后再看到如下的脚本,我就知道它是一个SB,是可以执行的一个方法,大括号中间的东西是一个block,作为参数传递给方法的。关于这个block,我们可以叫做闭包,js、swift中都有,我们这里是groovy的闭包,三言两语解释不清,我也没能力解释清楚:`( 就理解成它是一段可以执行的代码好了。这样看,它的确比那些靠配置xml构建的工具强大一些(也难学一些)。

buildscript {    
    repositories {        
        maven {            
             url uri('./repo')        
        }        
        jcenter()    
    }    
    dependencies {        
        classpath 'com.android.tools.build:gradle:2.2.1'        
    }
}

那如何知道buildscript这个SB里面会有repositories和dependencies这两个SB呢?我们可以看下文档,buildscript这个SB是回调给ScriptHandler对象的,而这个对象中我们可以找到对应的两个方法。以后再“写”脚本时,网上没有可以参考(抄)的案例时,我们就可以这样翻着文档来实现了。

Paste_Image.png
2.4 Gradle Plugin

这两年热更新比较火爆,各厂和一些大牛纷纷开源了自己的热更新方案,去研究这些开源方案,发现大家几乎都定制了自己的Gradle插件,项目在使用它们时一句“apply xxx”风轻云淡的就搞定了。那到底什么是Gradle插件呢?我们在官网看他们讲到,Gradle的核心功能提供的东西其实很少,各种有用的特征例如编译Java代码都是由插件完成的。插件可以添加新的task、配置等,也能扩展其他的插件,下面我们尝试使用Android Studio写一个插件:

  • 1、 新建项目。新建一个Android项目,里面默认会有一个名称为app的module,我们会在次项目里面创建一个插件,并在app里面引用我们创建的插件。
  • 2、创建插件module。在工程中新建一个子module, 类型为Android Library,这里的module名称就是插件项目名,这里我们命名为myplugin。
  • 3、初始化插件module项目结构。因为AS默认没有groovy项目模板的,我们需要手动构建项目结构,把myplugin项目内的东西全删了,只留build.gradle,此文件内容也清空。然后创建目录src/main/groovy,groovy会被识别成groovy源码目录,然后我们在创建src/main/resources/META-INF/gradle-plugins目录。目前myplugin已经被我们改造成gradle插件项目结构了。
  • 4、创建插件实现文件,这里我们创建一个名字为MyPlugin.groovy的文件并且放到对应的包名目录中。下面代码中我们创建了一个名为"printTask"的task,项目中如果引用了这个插件,就能调用此task,这里是groovy语言实现的 ,这样的话能做的事就比较多了。
package com.dfqin.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

public class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
       project.task('printTask') << {
            println "this is a plugin task"
        }
    }
} 
  • 5、创建属性文件。我们在src/main/resources/META-INF/gradle-plugins目录下创建dfqin.plugin.properties文件,文件名 就是对外发布的插件名,在其他项目中使用此插件时需要声明如下:
apply plugin: 'dfqin.plugin'

打开dfqin.plugin.properties文件,输入如下配置:

implementation-class=com.dfqin.plugin.MyPlugin

这里我们把插件指向了我们上面groovy实现的类,使用插件时就能找到这个类了。

  • 6、配置插件module的gradle文件。打开myplugin下面的build.gradle,输入以下代码:
apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

repositories {
    mavenCentral()
}

///********** 分割线 ************///

group='com.mygroup'
version='1.0.1'

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri('../repo'))
        }
    }
}

这里用斜线做了一个分割,上面的部分是插件的gradle配置文件,用来编译生成插件时使用。而分割线下面的是配置发布插件到maven仓库的。这里我们发布到当前工程的目录里。这里的group和version对应maven坐标的groupId和version,而modulename即我们这里的myplugin对于坐标的artifactId。按照上面的配置,我们可以通过com.mygroup:myplugin:1.0.1来找到我们的插件。下面我们看一下现在的项目结构图:

Paste_Image.png
  • 7、发布插件到本地仓库。我们只需要运行我们前面写的task uploadArchives即可。在命令行下面运行下面命令或者直接在AS的gradle视图里面找到此task双击运行。等执行结束,在项目根目录应该可以看到一个repo的目录,我们在里面可以找到生成的plugin插件。
./gradlew task uploadArchives  
上传插件的task
生成插件到本地仓库
  • 8、使用插件。这个就跟平时使用插件一样了,首先配置maven仓库地址,我们默认都配置了jcenter,这里因为我们的插件在本地仓库,所以要加上一个本地仓库,然后在项目的gradle文件中使用插件。项目根目录的build.gradle文件中配置如下:

buildscript {
    repositories {
        maven {
            url uri('./repo')
        }
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.1'
        classpath 'com.mygroup:myplugin:1.0.1'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

再在app module的build.gradle中添加

apply plugin: 'dfqin.plugin'

这时候我们就可以运行我们再插件中定义的printTask了,一个最简单的插件即完成了。运行插件中的task结果如下

Paste_Image.png

注意:因为目前的插件和使用插件的module app在一个项目中,所以当你编辑脚本执行uploadArchives有时会报错,但真正的错误并不一定是插件的错误,有可能是app module中的错误,而生成插件时所有的gradle脚本都会执行。demo已放到github上面,地址:https://github.com/dfqin/GradleDemo

3. 后记

最早是因为要在小组里面分享gradle,想写篇文章整理下知识点吧,后面发现需要讲的内容比较多,加上有时项目忙,所以文章写得断断续续,整片文章连贯性也并不好。目前gradle相关的知识点基本覆盖掉了,只是比较粗略,从宏观上介绍了整体轮廓,没有多少能直接拿到项目中使用的东西,但是根据上面内容应该可以找到解决问题的方法。
部分参考文章:
http://blog.csdn.net/sbsujjbcy/article/details/50782830
http://blog.csdn.net/innost/article/details/48228651#comments
https://segmentfault.com/a/1190000004229002

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

推荐阅读更多精彩内容