Gradle系列一 -- Groovy、Gradle和自定義Gradle插件

1. 概述

Android項目的構建過程是由Gradle插件完成的,Gradle 插件是在Gradle框架的基礎上實現的,Gradle框架是使用Groovy語言實現的。因此學習一下Groovy語言的一些常用語法是有必要的。

Gradle插件源碼下載:
gradle_3.0.0

2. Groovy語法

Groovy語言對Java語言的進行了拓展,它提供了更簡單、更靈活的語法,可以在運行時動態地進行類型檢查;因此Java語言語法都適用于Groovy語言。

關于Groovy語法可以參考精通 Groovy,我這里就不再講解了。

3. 配置Gradle

3.1 安裝Gradle

對于安裝gradle,大家可以參考官方文檔:
gradle Installation

3.2 Android 項目中配置Gradle

Android studio中的android 項目具體用什么版本的gradle,可以在android項目的根目錄下的gradle/wraaper/gradle-wrapper.properties文件中進行配置:

#Tue Nov 03 16:49:32 CST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip

上面最后一行配置該android項目中使用的gradle版本為4.1

注意:gradle與com.android.tools.build:gradle插件庫之間版本的對應關系


上圖是官網Android Plugin for Gradle Release Notes給出的。

4. Gradle

4.1 概述

Gradle中的所有內容都基于兩個基本概念:project和task

Project
這個接口是build file與Gradle交互的主要API。 通過Project接口可以訪問所有Gradle的功能。

Project的生命周期

Task
一個Project本質上是Task對象的集合。 每個Task都執行一些基本的工作,比如編譯類,運行單元測試,或者壓縮WAR文件。 可以使用TaskContainer上的某個create()方法(如TaskContainer.create(java.lang.String))將任務添加到Project中,可以使用TaskContainer上的某個查找方法(如TaskCollection.getByName(java.lang.String))查找現有Task。

4.2 build的生命周期

在Gradle中,你可以定義task和task之間的依賴關系。 Gradle保證這些task按照它們的依賴關系執行,并且每個task只執行一次,這些task形成有向無環圖。 Gradle在執行任何任務之前完成了對完整的依賴關系圖的構建,這是Gradle的核心,使許多事情成為可能,否則將是不可能的。

Gradle build包含三個的階段:

  1. Initialization
    Gradle支持單個和多個Project的build。 在初始化階段,Gradle確定哪些Project將參與build,并為每個Project創建一個Project實例。
    除了build script文件外,Gradle還定義了一個settings文件,settings文件在初始化階段執行。 多Project buiid必須在多Project層次結構的根Project中具有settings.gradle文件。 這是必需的,因為settings文件定義了哪些Project正在參與多Project構建。 對于單Project build,settings文件是可選的。
    對于build script,屬性訪問和方法調用被委托給一個Project對象。 同樣,settings文件中的屬性訪問和方法調用被委托給settings對象。
  2. Configuration
    在這個階段,通過Project對應的構建腳本(比如Android項目的build.gradle文件)的執行來配置該Project對象,Task形成的有向無環圖就是在這個階段被創建。
  3. Execution
    首先確定在配置階段創建和配置的Task的子集,以便執行, 該子集由傳遞給gradle命令的Task名稱和參數和當前目錄確定。 Gradle然后執行集合中的Task。

build script執行過程的監聽
build script可以在build script執行過程中收到通知。 這些通知通常采用兩種形式:實現特定的監聽接口,或者在觸發通知時提供一個閉包去執行。 下面以Android項目HotFix(該項目包含app、patchClassPlugin、hackdex 3個module)為例并且使用閉包的方式處理通知:

  1. 在Project執行前后(即Project對應的build script執行前后)立即收到通知。 這可以用來執行一些事情,例如在build script執行后執行額外的配置、打印自定義日志或者分析:
allprojects {
    afterEvaluate { project ->
        println "Adding smile task to $project"
        project.task('smile') {
            doLast {
                println "Running smile task for $project"
            }
        }
    }
}

上面的代碼被添加在Android項目HotFix的根目錄的build.gradle中的。
運行結果:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew smile
> Configure project :
Adding smile task to root project 'HotFix'
> Configure project :app
Adding smile task to project ':app'
> Configure project :hackdex
Adding smile task to project ':hackdex'
> Configure project :patchClassPlugin
Adding smile task to project ':patchClassPlugin'
> Task :smile
Running smile task for root project 'HotFix'
> Task :app:smile
Running smile task for project ':app'
> Task :hackdex:smile
Running smile task for project ':hackdex'
> Task :patchClassPlugin:smile
Running smile task for project ':patchClassPlugin'

此示例使用Project.afterEvaluate方法添加一個閉包,當這個Project的build.gradle已經被執行后該閉包立即會被調用。 如果想在指定的Project上添加smile任務,則可以直接在Project對應的build.gradle中添加如此代碼:

afterEvaluate { project ->
    println "Adding smile task to $project"
    project.task('smile') {
        doLast {
            println "Running smile task for $project"
        }
    }
}

除了上面提供的方式,還可以使用下面這種方式:

gradle.afterProject {project, projectState ->
    if (projectState.failure) {
        println "Evaluation of $project FAILED"
    } else {
        println "Evaluation of $project succeeded"
    }
}

上面的代碼被添加在Android項目HotFix的子項目app根目錄的build.gradle中的,運行結果:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew clean

> Configure project :app
Evaluation of project ':app' succeeded

> Configure project :hackdex
Evaluation of project ':hackdex' succeeded

> Configure project :patchClassPlugin
Evaluation of project ':patchClassPlugin' succeeded
  1. 將Task添加到Project后可以立即收到通知。 這可以用來設置一些默認值或者在build文件中的Task可用之前添加行為。
    以下示例在添加每個Task后設置srcDir屬性。
tasks.whenTaskAdded { task ->
    task.ext.srcDir = 'src/main/java'
}

task a

println "source dir is $a.srcDir"

運行結果:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew a

> Configure project :app
source dir is src/main/java
  1. Task有向無環圖構建完成后可以立即收到通知
    舉例如下:
gradle.taskGraph.whenReady {
    println "task graph build completed"
}
  1. 可以在執行任何Task之前和之后立即收到通知
    以下示例記錄每個Task執行的開始和結束。 請注意,無論Task是成功完成還是失敗并發生異常,都會收到afterTask通知:
task ok

task broken(dependsOn: ok) {
    doLast {
        throw new RuntimeException('broken')
    }
}

gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}

gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    }
    else {
        println "done"
    }
}

運行結果:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew broken

> Task :app:ok
executing task ':app:ok' ...
done

> Task :app:broken
executing task ':app:broken' ...
FAILED
  1. build完成后可以立即接到通知
    監聽方法如下:
gradle.buildFinished {result ->
    println "buildResult = $result.failure"
}

運行結果:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew clean

BUILD SUCCESSFUL in 2s
4 actionable tasks: 3 executed, 1 up-to-date
buildResult = Build

上面講解了Gradle build的生命周期和對生命周期中重要節點的監聽,下面通過一張圖來概括一下:


對于Android項目,在Configuration階段會解析Android項目根目錄下的build.gradle文件:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {

    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

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

上面的classpath 'com.android.tools.build:gradle:3.0.0'就是用來導入用于構建Android項目的Gradle 插件庫的,classpath后面的參數由三部分組成,下面就來看一下該插件庫的構建腳本:


看到上面紅框的內容,相信大家應該明白了classpath后面三部分的由來。

導入了用來構建Android項目的Gradle 插件庫后,下一步就是在Android項目的子module的build.gradle中應用該插件庫中的插件:

apply plugin: 'com.android.application'

上面的這句話大家應該非常熟悉,這句話會執行com.android.application插件的apply方法,從而創建build Android項目的所需要的Gradle Task,具體是怎么創建的,在下面講解Transform API時會詳細說明。

4.3 Android項目的build過程

build Android 項目也會經歷上面的3個階段,而build Android 項目是通過執行一個Task完成的:

// 這個命令會執行assembleFreeWandoujiaRelease Task,
// FreeWandoujiaRelease代表[build variant](https://developer.android.google.cn/studio/build/build-variants.html)
./gradlew app:aFreeWandoujiaR

執行Task也會經歷上面的三個階段,在Android項目構建過程中,通常需要在某個Task執行開始或者結束時hook指定操作,上面的Task是由一個Task集合組成,為了看到所以Task執行的順序,我就在該module的build.gradle的文件中加了如下代碼:

gradle.taskGraph.beforeTask { Task task ->
    println "executing:  $task.name"
}

上面的作用就是在任務執行之前打印任務的名稱,下面就來看看assembleFreeWandoujiaRelease Task和其所依賴的Task的執行順序:

chenyangdeMacBook-Pro:HotFix chenyang$ ./gradlew app:aFreeWandoujiaR | grep executing
executing:  preBuild
executing:  extractProguardFiles
executing:  preFreeWandoujiaReleaseBuild
executing:  compileFreeWandoujiaReleaseAidl
executing:  compileFreeWandoujiaReleaseRenderscript
executing:  checkFreeWandoujiaReleaseManifest
executing:  generateFreeWandoujiaReleaseBuildConfig
executing:  prepareLintJar
executing:  generateFreeWandoujiaReleaseResValues
executing:  generateFreeWandoujiaReleaseResources
executing:  mergeFreeWandoujiaReleaseResources
executing:  createFreeWandoujiaReleaseCompatibleScreenManifests
executing:  processFreeWandoujiaReleaseManifest
executing:  splitsDiscoveryTaskFreeWandoujiaRelease
executing:  processFreeWandoujiaReleaseResources
executing:  generateFreeWandoujiaReleaseSources
executing:  javaPreCompileFreeWandoujiaRelease
executing:  compileFreeWandoujiaReleaseJavaWithJavac
executing:  compileFreeWandoujiaReleaseNdk
executing:  compileFreeWandoujiaReleaseSources
executing:  mergeFreeWandoujiaReleaseShaders
executing:  compileFreeWandoujiaReleaseShaders
executing:  generateFreeWandoujiaReleaseAssets
executing:  mergeFreeWandoujiaReleaseAssets
executing:  processFreeWandoujiaReleaseJavaRes
executing:  transformResourcesWithMergeJavaResForFreeWandoujiaRelease
executing:  transformClassesAndResourcesWithProguardForFreeWandoujiaRelease
executing:  transformClassesWithDexForFreeWandoujiaRelease
executing:  mergeFreeWandoujiaReleaseJniLibFolders
executing:  transformNativeLibsWithMergeJniLibsForFreeWandoujiaRelease
executing:  transformNativeLibsWithStripDebugSymbolForFreeWandoujiaRelease
executing:  packageFreeWandoujiaRelease
executing:  lintVitalFreeWandoujiaRelease
executing:  assembleFreeWandoujiaRelease
chenyangdeMacBook-Pro:HotFix chenyang$ 

可以看到Android 項目的一次構建過程執行了很多Task,下面解釋一些比較關鍵的Task:
? mergeFreeWandoujiaReleaseResources -- 收集所有的resources
? processFreeWandoujiaReleaseManifest -- 生成最終的AndroidManif.xml文件
? compileFreeWandoujiaReleaseJavaWithJavac -- 編譯Java文件
? mergeFreeWandoujiaReleaseAssets -- 收集所有的assets
? transformClassesAndResourcesWithProguardForFreeWandoujiaRelease -- 混淆
? transformClassesWithDexForFreeWandoujiaRelease -- 生成dex
? packageFreeWandoujiaRelease -- 打包生成apk
知道了build Android 項目被執行的Task,接下來的hook按照上一節的講解做就可以了。

大家可以通過Android Studio右側的gradle窗口來查看Android 項目中所有的Gradle Task,如下圖所示:


所有的Gradle Task被分組列舉,看起來更清晰,而且可以通過雙擊某個Gradle Task來運行該Gradle Task,是不是很爽。

5. 自定義Gradle插件

為了講解自定義Gradle插件,那我就提出一個問題,然后通過自定義插件的形式解決它。

問題:如何將指定代碼注入到class文件的構造方法中?
解決方案:
1 > 通過hook compileFreeWandoujiaReleaseJavaWithJavac解決。
2 > 通過Google專門提供了Transform API來解決。
既然Google專門提供Transform API,那么下面就使用Transform API解決問題,首先來看下Google對Transform API的解釋:

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files.
(The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)

The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.
Note: this applies only to the javac/dx code path. Jack does not use this API at the moment.

The API doc is http://google.github.io/android-gradle-dsl/javadoc/.

To insert a transform into a build, you simply create a new class implementing one of the Transform interfaces, and register it with android.registerTransform(theTransform) or android.registerTransform(theTransform, dependencies).

Important notes:
The Dex class is gone. You cannot access it anymore through the variant API (the getter is still there for now but will throw an exception)
Transform can only be registered globally which applies them to all the variants. We'll improve this shortly.
There's no way to control ordering of the transforms.
We're looking for feedback on the API. Please file bugs or email us on our adt-dev mailing list.

大概意思是:
從1.5.0-beta1開始,Gradle插件包含了一個Transform API,該API允許第三方插件在已編譯的class文件被轉換為dex文件之前處理已編譯的class文件。
這個API的目標就是簡化自定義class文件的操作而不用對Task進行處理,并提供更多的操作靈活性。在1.5.0-beta1版本中,內部代碼處理(jacoco,progard,multi-dex)已經全部轉移到這個新機制中。為了將Transform插入到構建過程中,只需創建一個繼承Transform抽象類的新類,然后使用android.registerTransform(theTransform)或android.registerTransform(theTransform,dependencies)進行注冊。

Gradle提供了強大的自定義插件的功能,官方講解文檔
Gradle 一共提供了三種方式創建自定義插件:

  1. 直接在build腳本中包含插件的源代碼
    這樣做的好處是插件可以自動編譯并包含在build腳本的classpath中,而無需執行任何操作。 但是插件在build腳本之外是不可見的,所以就不能在build腳本之外重用插件。
  2. 在buildSrc project中創建自定義插件
    你可以把插件的源代碼放在rootProjectDir / buildSrc / src / main / groovy目錄下。 Gradle將負責編譯和測試插件,并使其在build腳本的classpath中可用。 該插件對build使用的每個build腳本都是可見的。 但是在build之外是不可見的,所以不能在build之外重用該插件。
  3. 在獨立的project(指Android項目中的子module)中創建自定義插件
    你可以為你的插件創建一個單獨的project。 該project生成并發布一個JAR,然后您可以在多個build中使用JAR并與他人共享。 通常該JAR可能包含一些插件,或將幾個相關的Task類捆綁到一個庫中,或兩者的一些組合。

前兩種方式大家可以參考官方講解文檔,我直接使用第三種方式進行舉例說明:

1> 首先創建一個名稱為injectClassPlugin的子module,然后將該module中(除了build.gradle文件)的文件全部刪除,接著按照下圖的目錄創建:



大家按照上面injectClassPlugin的目錄結構(Gradle插件所需要的目錄結構)創建就行。

2> 完成了目錄結構的創建后,接下來來看看該插件的build.gradle的內容(如上圖所示),非常的簡單,沒什么可說的;現在我們繼承Plugin類,實現自定義插件的第一步:

class InjectClassPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {

        //AppExtension對應build.gradle中android{...}
        def android = project.extensions.getByType(AppExtension)
        //注冊一個Transform
        def classTransform = new InjectClassTransform(project)
        android.registerTransform(classTransform)

        // 通過Extension的方式傳遞將要被注入的自定義代碼
        def extension = project.extensions.create("InjectClassCode", InjectClassExtension)
        project.afterEvaluate {
            classTransform.injectCode = extension.injectCode
        }
    }
}

上面的代碼很簡單,主要完成兩個事情:

1 注冊InjectClassTransform
上面Google對Transform API的解釋提到了注冊Transform的方式,即調用
android.registerTransform(theTransform)方法,其中android是AppExtension類型,
并且是通過build.gradle中android{...}配置的,那么上面注冊Transform的代碼就很好理解了。

2 通過Extension的方式傳遞將要被注入的自定義代碼
Gradle腳本中通過Extension傳遞一些配置參數給自定義插件,在這個例子中通過InjectClassExtension對象傳遞要注入的代碼。
首先在extensions容器中添加一個名稱為InjectClassCode 類型為InjectClassExtension的對象,然后在apply該插件
的app module的build.gradle執行(完成了對InjectClassCode對象的賦值,下面第三步會講解)完成后將要
注入的代碼傳遞給classTransform對象。

3> 接下來我們來看看app module的build.gradle 和 InjectClassExtension類:

apply plugin: 'com.android.application'
apply plugin: 'com.cytmxk.injectclassplugin'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.2"

    defaultConfig {
        applicationId "com.cytmxk.hotfix"
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

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

        debug {
            signingConfig android.signingConfigs.debug
        }
    }

    flavorDimensions "tier", "channel"

    productFlavors {
        free {
            dimension "tier"
            applicationIdSuffix ".free"
            versionNameSuffix "-free"
        }
        paid {
            dimension "tier"
            applicationIdSuffix ".paid"
            versionNameSuffix "-paid"
        }

        wandoujia {
            dimension "channel"
        }

        market91 {
            dimension "channel"
        }
    }
}

InjectClassCode {
    injectCode = """ android.widget.Toast.makeText(this,"測試Toast代碼!!",android.widget.Toast.LENGTH_SHORT).show(); """
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

/--------------------------------------------/
package com.cytmxk

class InjectClassExtension {
    String injectCode
}

上面的InjectClassCode閉包完成了第二步中InjectClassCode對象的賦值。

4> 接下來就來看看InjectClassTransform的實現:

/**
 * 用來向每一個calss文件中注入指定代碼
 */
class InjectClassTransform extends Transform{

    Project project
    String injectCode;

    InjectClassTransform(Project project) {
        this.project = project
    }

    /**
     * 設置我們自定義的Transform對應的Task名稱, 類似:TransformClassesWithPreDexInjectCodeForXXX
     * @return
     */
    @Override
    String getName() {
        return "PreDexInjectCode"
    }

    /**
     * 需要處理的數據類型,CONTENT_CLASS代表處理class文件
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指定Transform的作用范圍
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 指明當前Transform是否支持增量編譯
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        transformInvocation.inputs.each { TransformInput input ->
            //遍歷文件夾
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //注入代碼
                InjectClass.inject(directoryInput.file.absolutePath, project, injectCode)
                // 獲取output目錄
                def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                // 將input的目錄復制到output指定目錄
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
            //遍歷jar文件 對jar不操作,但是要輸出到out路徑
            input.jarInputs.each { JarInput jarInput ->
                // 重命名輸出文件(同名文件copyFile會沖突)
                def jarName = jarInput.name
                println("jar = " + jarInput.file.getAbsolutePath())
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

在第一步中注冊InjectClassTransform會生成與之對應的名稱為TransformClassesWithPreDexInjectCodeForXXX的Task,后面的XXX代表app module的build variant的名稱,該Task被執行時InjectClassTransform的transform方法會被調用,上面的注釋很清晰,就不再贅敘了,下面看一下注入代碼的實現:

class InjectClass {
    //初始化類池
    private final static ClassPool pool = ClassPool.getDefault()
    static void inject(String path,Project project, String injectCode) {
        println("filePath = " + path)
        //將當前路徑加入類池,不然找不到這個類
        pool.appendClassPath(path)
        //project.android.bootClasspath 加入android.jar,不然找不到android相關的所有類
        pool.appendClassPath(project.android.bootClasspath[0].toString())

        File dir = new File(path)
        if (dir.isDirectory()) {
            //遍歷文件夾
            dir.eachFileRecurse { File file ->
                if (file.getName().equals("MainActivity.class")) {
                    //獲取MainActivity.class
                    CtClass ctClass = pool.getCtClass("com.cytmxk.hotfix.MainActivity")
                    println("ctClass = " + ctClass)
                    //解凍
                    if (ctClass.isFrozen()) ctClass.defrost()
                    //獲取到OnCreate方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
                    println("方法名 = " + ctMethod)
                    println "injectCode = " + injectCode
                    //在方法開始注入代碼
                    ctMethod.insertBefore(injectCode)
                    ctClass.writeFile(path)
                    ctClass.detach()//釋放
                }
            }
        }
    }
}

上面代碼將代碼注入到了com.cytmxk.hotfix.MainActivity.class的onCreate方法開始,然后通過下面命令build app module:

./gradlew app:assembleFreeWandoujiaRelease

然后我們來看看com.cytmxk.hotfix.MainActivity.class文件:



可以看到代碼被注入了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容