Android AOP-ASM字節(jié)碼插樁+自定義gradle插件

簡介

AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程勿负,通過預(yù)編譯方式和運(yùn)行期動態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開發(fā)中的一個熱點(diǎn)额湘,也是Spring框架中的一個重要內(nèi)容,是函數(shù)式編程的一種衍生范型把夸。利用AOP可以對業(yè)務(wù)邏輯的各個部分進(jìn)行隔離而线,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率膀篮。
常見的AOP工具按照生效時機(jī)區(qū)分主要分為兩大類:預(yù)編譯期及運(yùn)行期嘹狞,以下列舉出市面上常用的AOP工具及對應(yīng)開源框架:

1.APT工具

代表開源框架:ButterKnife、Dagger2誓竿、DBFlow磅网、AndroidAnnotation 注解處理器 Java5 中叫APT(Annotation Processing Tool),在Java6開始筷屡,規(guī)范化為 Pluggable Annotation Processing涧偷。Apt應(yīng)該是這其中我們最常見到的了,難度也最低毙死。定義編譯期的注解燎潮,再通過繼承Proccesor實(shí)現(xiàn)代碼生成邏輯,實(shí)現(xiàn)了編譯期生成代碼的邏輯扼倘。

2.AspectJ工具

AspectJ是一種嚴(yán)格意義上的AOP技術(shù)确封,因?yàn)樗峁┝送暾拿嫦蚯忻婢幊痰淖⒔猓@樣讓使用者可以在不關(guān)心字節(jié)碼原理的情況下完成代碼的織入再菊,因?yàn)榫帉懙那忻娲a就是要織入的實(shí)際代碼爪喘。

AspectJ實(shí)現(xiàn)代碼織入有兩種方式,一是自行編寫.ajc文件纠拔,二是使用AspectJ提供的@Aspect秉剑、@Pointcut等注解,二者最終都是通過ajc編譯器完成代碼的織入绿语。

舉個簡單的例子秃症,假設(shè)我們想統(tǒng)計(jì)所有view的點(diǎn)擊事件,使用AspectJ只需要寫一個類即可吕粹。

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect5";

    //切面表達(dá)式种柑,聲明需要過濾的類和方法 
    @Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
    public void callMethod() {
    }

    //before表示在方法調(diào)用前織入
    @before("callMethod()")
    public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
        //編寫業(yè)務(wù)代碼
    }
}
復(fù)制代碼

注解簡明直觀,上手難度近乎為0匹耕。

常用的函數(shù)耗時統(tǒng)計(jì)工具Hugo聚请,就是AspectJ的一個實(shí)際應(yīng)用,Android平臺Hujiang開源的AspectJX插件靈感也來自于Hugo稳其,詳情見舊文Android 函數(shù)耗時統(tǒng)計(jì)工具之Hugo驶赏。

AspectJ雖然好用,但也存在一些嚴(yán)重的問題既鞠。

  • 重復(fù)織入煤傍、不織入

AspectJ切面表達(dá)式支持繼承語法,雖然方便了開發(fā)嘱蛋,但存在致命的問題蚯姆,就是在繼承樹上的類可能都會織入代碼五续,這在多數(shù)業(yè)務(wù)場景下是不適用的,比如無埋點(diǎn)龄恋。

另外Java8語法在aspectjx 2.0.0版本開始支持疙驾。

3.ASM

ASM是非常底層的面向字節(jié)碼編程的AOP框架,理論上可以實(shí)現(xiàn)任何關(guān)于字節(jié)碼的修改郭毕,非常硬核它碎。許多字節(jié)碼生成API底層都是用ASM實(shí)現(xiàn),常見比如Groovy显押、cglib扳肛,因此在Android平臺下使用ASM無需添加額外的依賴。完整的學(xué)習(xí)ASM必須了解字節(jié)碼和JVM相關(guān)知識煮落。
比如要織入一句簡單的日志輸出

Log.d("tag", " onCreate");

復(fù)制代碼使用ASM編寫是下面這個樣子敞峭,沒錯因?yàn)镴VM是基于棧的,函數(shù)的調(diào)用需要參數(shù)先入棧蝉仇,然后執(zhí)行函數(shù)入棧旋讹,最后出棧,總共四條JVM指令轿衔。

mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);

復(fù)制代碼可以看出ASM與AspectJ有很大的不同沉迹,AspectJ織入的代碼就是實(shí)際編寫的代碼,但ASM必須使用其提供的API編寫指令害驹。一行java代碼可能對應(yīng)多行ASM API代碼鞭呕,因?yàn)橐恍衘ava代碼背后可能隱藏這多個JVM指令。
你不必?fù)?dān)心不會編寫ASM代碼宛官,官方提供了ASM Bytecode Outline插件可以直接將java代碼生成ASM代碼葫松。

4.Javassist

javassit是一個開源的字節(jié)碼創(chuàng)建、編輯類庫底洗,現(xiàn)屬于Jboss web容器的一個子模塊,特點(diǎn)是簡單亥揖、快速珊擂,與AspectJ一樣,使用它不需要了解字節(jié)碼和虛擬機(jī)指令费变,這里是官方文檔摧扇。

javassit核心的類庫包含ClassPool,CtClass 挚歧,CtMethod和CtField扛稽。

  • ClassPool:一個基于HashMap實(shí)現(xiàn)的CtClass對象容器。
  • CtClass:表示一個類滑负,可從ClassPool中通過完整類名獲取庇绽。
  • CtMethods:表示類中的方法锡搜。
  • CtFields :表示類中的字段。

javassit API簡潔直觀瞧掺,比如我們想動態(tài)創(chuàng)建一個類,并添加一個helloWorld方法凡傅。

ClassPool pool = ClassPool.getDefault();
//通過makeClass創(chuàng)建類
CtClass ct = pool.makeClass("test.helloworld.Test");//創(chuàng)建類
//為ct添加一個方法
CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);
ct.addMethod(helloMethod);
//寫入文件
ct.writeFile();
//加載進(jìn)內(nèi)存
// ct.toClass();
復(fù)制代碼

然后辟狈,我們想在helloWorld方法前后織入代碼。

ClassPool pool = ClassPool.getDefault();
//獲取class
CtClass ct = pool.getCtClass("test.helloworld.Test");
//獲取helloWorld方法
CtMethod m = ct.getDeclaredMethod("helloWorld");
//在方法開頭織入
m.insertBefore("{ System.out.print(\"before insert\");");
//在方法末尾織入 可使用this關(guān)鍵字
m.insertAfter("{System.out.println(this.x); }");
//寫入文件
ct.writeFile();
復(fù)制代碼

javassit的語法直觀簡潔的特點(diǎn)夏跷,使得在很多開源項(xiàng)目中都有它的身影。

5.動態(tài)代理

動態(tài)代理是代理模式的一種實(shí)現(xiàn),用于在運(yùn)行時動態(tài)增強(qiáng)原始類的行為轩触,實(shí)現(xiàn)方式是運(yùn)行時直接生成class字節(jié)碼并將其加載進(jìn)虛擬機(jī)杭攻。

各類框架總結(jié)

image.png

下面我們就以ASM這個框架給大家舉例講解

一、最終實(shí)現(xiàn)的效果

這次我們的目標(biāo)是在Demo App啟動后在MainActivity的onCreate()方法之前自動輸出一段簡單的日志信息“Log.e("TAG", "===== This is just a test message =====");”也就是最終我們需要將這個 代碼插入到MainActivity的onCreate()方法之前猫态。**

要達(dá)到這樣的目的我們就需要使用ASM佣蓉,ASM 是一個 Java 字節(jié)碼操控的框架,也就是說我們可以直接操作.class文件亲雪。這樣我們就可以在不侵入MainActivity類的情況下勇凭,直接達(dá)到目的。
為了實(shí)現(xiàn)目標(biāo)我們首先需要知道幾個簡單的類:

1.1义辕、ClassVisitor

首先我們是要處理單個.class文件虾标,那肯定需要訪問到這個.class文件的內(nèi)容,ClassVisitor就是處理這些的灌砖,他可以拿到class文件的類名璧函,父類名,接口基显,包含的方法蘸吓,等等信息。

1.2续镇、MethodVisitor

因?yàn)槲覀冃枰诜椒▓?zhí)行前插入一些字節(jié)碼美澳,所以我們需要MethodVisitor來幫我們處理并插入字節(jié)碼。真正進(jìn)行方法插樁的地方摸航。

1.3制跟、Transform

Transform是gradle構(gòu)建的時候從class文件轉(zhuǎn)換到dex文件期間處理class文件的一套方案,也就是說處理class的吧酱虎。上文的ClassVisitor可以是看做處理單個class文件雨膨,那這里的話Transform可以處理一系列的class文件:從查找到所有class文件,到交給ClassVisitor和MethodVisitor處理后读串,再到重新覆蓋原來的class文件這么一個流程聊记。

二撒妈、開始編程

根據(jù)上文的步驟我們順序在gradleAOP工程的plugin模塊中編寫ClassVisitor、MethodVisitor排监、以及Transform狰右。

這里選用kotlin來編寫所有腳本。所以plugin插件的module看起來是這樣的:main文件夾下kotlin來分別存儲對應(yīng)的代碼


image.png

另外要想實(shí)現(xiàn)這樣根據(jù)語言分文件夾的效果需要在插件module的build.gradle中配置一下sourceSets 舆床,如下代碼所示棋蚌。除了這些,還添加了kotlin插件以及kotlin和gradle的依賴挨队,因?yàn)殚_發(fā)Transform的需要谷暮。最后是插件倉庫地址的配置信息.

apply plugin: 'kotlin'
apply plugin: 'maven'

sourceSets {
    main {
        kotlin {
            srcDir "src/main/kotlin"
        }

        resources {
            srcDir 'src/main/resources'
        }
    }
}

dependencies {
    implementation gradleApi()
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'com.android.tools.build:gradle:4.0.2'
}

uploadArchives {
    repositories {
        mavenDeployer {
            pom.groupId = 'com.cjh.plugin'
            pom.artifactId = 'plugin'
            pom.version = '1.0'
            //生成的文件地址
            repository(url: uri('E:/Repo'))
        }
    }
}

2.1、ClassVisitor

在ClassVisitor中我們拿到相應(yīng)class的類名盛垦,比如這時候是MainActivity.class湿弦,那么類名就是““com/example/mygradleaop/MainActivity””,你可以自行打印嘗試【注意這里的包名是app工程的包名腾夯,而不是gradleAOP工程的包名颊埃,因?yàn)槲覀兪且幚淼氖莂pp對吧】。匹配到類名后覆寫visitMethod()方法俯在,根據(jù)當(dāng)前方法名是否匹配onCreate方法來將具體的插樁操作交給DemoMethodVisitor處理竟秫。

DemoClassVisitor類源碼如下

class DemoClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, classVisitor) {

    private var className: String? = null

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name
    }


    //關(guān)鍵方法重寫visitMethod方法
    //匹配MainActivity的onCreate方法
    //匹配到之后進(jìn)去DemoMethodVisitor方法進(jìn)行插樁
    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        //com.example.mygradleaop.MainActivity
        if (className.equals("com/example/mygradleaop/MainActivity")) {
            if (name.equals("onCreate")) {
                return DemoMethodVisitor(methodVisitor)
            }
        }

        return methodVisitor
    }
}
2.2、MethodVisitor

經(jīng)過上一步ClassVisitor的處理我們已經(jīng)匹配到onCreate方法了跷乐,此時我們需要在DemoMethodVisitor類中進(jìn)行插入字節(jié)碼操作肥败。如下所示,直接繼承自MethodVisitor愕提,并覆寫visitCode()方法馒稍。其中的代碼就是我們要插入的代碼了,乍一看完全不是我們平常那種Log.e("TAG", "===== This is just a test message =====");的寫法浅侨,而是復(fù)雜了很多纽谒。是的,這時候你就知道visitCode中的代碼和我們上邊的Log信息等價(jià)就好了如输,等這篇文章閱讀完鼓黔,咱們就可以去深入學(xué)習(xí)JVM字節(jié)碼的相關(guān)信息了,現(xiàn)在不要想那么多不见,直接拿去用澳化。

DemoMethodVisitor類源碼如下:

class DemoMethodVisitor(methodVisitor: MethodVisitor) : MethodVisitor(Opcodes.ASM5, methodVisitor) {

    //插入:Log.e("TAG", "===== This is just a test message =====");
    override fun visitCode() {
        super.visitCode()

        mv.visitLdcInsn("TAG")
        mv.visitLdcInsn("===== This is just a test message cjh=====")
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "android/util/Log",
            "e",
            "(Ljava/lang/String;Ljava/lang/String;)I",
            false
        )
        mv.visitInsn(Opcodes.POP)
    }
}
2.3、Transform

經(jīng)過前兩步的處理我們已經(jīng)可以將字節(jié)碼插入到MainActivity.class的onCreate方法前了稳吮,但是此時我們怎么去找到想要的.class文件呢缎谷,字節(jié)碼插入完后我們又要怎么寫回到.class文件呢?Transform就可以登場了灶似,如下所示列林,DemoTransform繼承自Transform瑞你,同時實(shí)現(xiàn)Plugin接口,這個plugin接口還熟悉吧希痴,應(yīng)用到resources/META-INF/gradle-plugins/xxx.properties的時候需要者甲。然后依次實(shí)現(xiàn)所有必須的方法,除了transform()方法其他都是一些比較固定的寫法了润梯,直接搬過去即可:

package com.cooloongwu.plugin1

import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream


class DemoTransform : Transform(), Plugin<Project> {

    override fun apply(project: Project) {
        println(">>>>>> 1.1.1 this is a log just from DemoTransform")
        val appExtension = project.extensions.getByType(AppExtension::class.java)
        appExtension.registerTransform(this)
    }

    override fun getName(): String {
        return "KotlinDemoTransform"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun isIncremental(): Boolean {
        return false
    }

    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
    }

}

接下來是transform()方法里的內(nèi)容过牙,大致流程就是查找到所有的.class文件【代碼中還添加了一些條件,過濾掉了一些class文件】纺铭,然后通過ClassReader讀取并解析class文件,然后又經(jīng)由我們編寫的ClassVisitor和MethodVisitor處理后交給ClassWriter刀疙,最后通過FileOutputStream將新的字節(jié)碼內(nèi)容寫回到class文件舶赔。

 /*
    * 接下來是transform()方法里的內(nèi)容,大致流程就是查找到所有的.class文件
    * 【代碼中還添加了一些條件谦秧,過濾掉了一些class文件】竟纳,
    * 然后通過ClassReader讀取并解析class文件,然后又經(jīng)由
    * 我們編寫的ClassVisitor和MethodVisitor處理后交給ClassWriter疚鲤,
    * 最后通過FileOutputStream將新的字節(jié)碼內(nèi)容寫回到class文件锥累。
    *
    *
    * */
    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
        val inputs = transformInvocation?.inputs
        val outputProvider = transformInvocation?.outputProvider

        if (!isIncremental) {
            outputProvider?.deleteAll()
        }

        inputs?.forEach { it ->
            it.directoryInputs.forEach {
                if (it.file.isDirectory) {
                    FileUtils.getAllFiles(it.file).forEach {
                        val file = it
                        val name = file.name
                        //1.過濾其他不合符條件的class文件
                        if (name.endsWith(".class") && name != ("R.class")
                            && !name.startsWith("R\$") && name != ("BuildConfig.class")
                        ) {

                            val classPath = file.absolutePath
                            println(">>>>>> classPath :$classPath")
                            //2.ClassReader讀取并解析class文件
                            val cr = ClassReader(file.readBytes())
                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            //3.經(jīng)由我們編寫的ClassVisitor和MethodVisitor處理
                            val visitor = DemoClassVisitor(cw)
                            cr.accept(visitor, ClassReader.EXPAND_FRAMES)

                            //4.通過FileOutputStream將新的字節(jié)碼內(nèi)容寫回到class文件
                            val bytes = cw.toByteArray()
                            val fos = FileOutputStream(classPath)
                            fos.write(bytes)
                            fos.close()
                        }
                    }
                }

                val dest = outputProvider?.getContentLocation(
                    it.name,
                    it.contentTypes,
                    it.scopes,
                    Format.DIRECTORY
                )
                FileUtils.copyDirectoryToDirectory(it.file, dest)
            }

            //  !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!!
            //使用androidx的項(xiàng)目一定也注意jar也需要處理,否則所有的jar都不會最終編譯到apk中集歇,千萬注意
            //導(dǎo)致出現(xiàn)ClassNotFoundException的崩潰信息桶略,當(dāng)然主要是因?yàn)檎也坏礁割悾驗(yàn)楦割怉ppCompatActivity在jar中
            it.jarInputs.forEach {
                val dest = outputProvider?.getContentLocation(
                    it.name,
                    it.contentTypes,
                    it.scopes,
                    Format.JAR
                )
                FileUtils.copyFile(it.file, dest)
            }
        }

至此诲宇,所有的插件內(nèi)容基本完成了际歼,最后就是在resources/META-INF/gradle-plugins/myplugin.properties文件中寫入我們新的Plugin類:

implementation-class=com.example.gradleaop.DemoTransform

然后右側(cè)gradle任務(wù)中執(zhí)行uploadArchives,發(fā)布我們的插件到本地倉庫中姑蓝。
發(fā)布完成后在Demo的根build.gradle中添加依賴信息如下:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.3.72"
    repositories {
        google()
        jcenter()
        maven{
            url 'E:/Repo'
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //implementation-class=com.example.gradleaop.DemoTransform
        //這里的路徑就是gradle插件里面發(fā)布本地插件時寫的
        //classpath 'groupId:artifactId:version'
        /*
           mavenDeployer {
            pom.groupId = 'com.cjh.plugin'
            pom.artifactId = 'plugin'
            pom.version = '1.0'
            //生成的文件地址
            repository(url: uri('E:/Repo'))
        }
        */
        classpath 'com.cjh.plugin:plugin:1.0'

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

allprojects {
    repositories {
        google()
        jcenter()
        maven{
            url 'E:/Repo'
        }
    }
}

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

最后在app model下面build.gradle添加插件.這里的名稱就是我們gradle插件里面清單文件
resources/META-INF/gradle-plugins/com.geo.plugin.properties的名稱com.geo.plugin

apply plugin: 'com.geo.plugin'

此時直接運(yùn)行Demo工程鹅心,app運(yùn)行起來后在控制臺是不是就看到了相應(yīng)的信息呢:

2020-04-08 21:50:17.750 3804-3804/com.cooloongwu.asmdemo E/TAG: ===== This is just a test message =====

此時我們最終在MainActivity的onCreate方法前面插入了這行日志代碼

三、總結(jié)

1)先明白自己想要干什么纺荧,像這個例子我們是需要在某個類的某個方法前面插入一行代碼旭愧,那我們其實(shí)就是對方法進(jìn)行插樁
2)先通過DemoClassVisitor匹配到需要插樁的類宙暇,這里就是MainActivity.class.匹配到onCreate方法后输枯,就對方法進(jìn)行插樁,實(shí)現(xiàn)類是DemoMethodVisitor
3)DemoMethodVisitor里面重寫visitCode方法客给,把需要插入的代碼轉(zhuǎn)換成字節(jié)碼的形式就是插入即可用押。這里就是最關(guān)鍵的地方,我們 可以利用ASM插件把對應(yīng)的java代碼轉(zhuǎn)換成這種字節(jié)碼靶剑,然后照著寫入即可蜻拨。
4)最后一步也就是使用Transform進(jìn)行關(guān)聯(lián)池充。需要用Transform拿到所有的類,然后中途交給前面我們編寫的DemoClassVisitor和DemoMethodVisitor處理進(jìn)行插樁缎讼,最后還是通過Transform寫回去收夸,這樣就實(shí)現(xiàn)中途插入字節(jié)碼的功能了,這就是字節(jié)碼插樁血崭。

項(xiàng)目源碼:https://gitee.com/canjunhao/MyGradleAOP
引用:https://blog.csdn.net/u010976213/article/details/105395590

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卧惜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子夹纫,更是在濱河造成了極大的恐慌咽瓷,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舰讹,死亡現(xiàn)場離奇詭異茅姜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)月匣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門钻洒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锄开,你說我怎么就攤上這事素标。” “怎么了萍悴?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵头遭,是天一觀的道長。 經(jīng)常有香客問我退腥,道長任岸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任狡刘,我火速辦了婚禮享潜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗅蔬。我一直安慰自己剑按,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布澜术。 她就那樣靜靜地躺著艺蝴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸟废。 梳的紋絲不亂的頭發(fā)上猜敢,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼缩擂。 笑死鼠冕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胯盯。 我是一名探鬼主播懈费,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼博脑!你這毒婦竟也來了憎乙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤叉趣,失蹤者是張志新(化名)和其女友劉穎泞边,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疗杉,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡繁堡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了乡数。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡闻牡,死狀恐怖净赴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罩润,我是刑警寧澤玖翅,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站割以,受9級特大地震影響金度,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜严沥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一猜极、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧消玄,春花似錦跟伏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兔跌,卻和暖如春勘高,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工华望, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蕊蝗,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓立美,卻偏偏與公主長得像匿又,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子建蹄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容