javassist 动态修改字节码

一、gradle Transform 接收一个输入input,同时需要有一组输出,作为下一个Transform的输入。

(1)最简单的一个Transform实现,需要实现
将输入数据input,原样不动输出到output
(2)Transform处理的结果,会位于工程目录⁩/build⁩/⁨intermediates⁩/transform文件夹下。
如下图XXX目录即为自定义的一个Transfrom。
由图可知除XXX外,还经过了dexBuilder、dexMerger、mergeJavaRes、mergeJniLibs、StripDebugSymbol等多个Transform处理

image.png

二、自定义gradle插件实例

1、自定义gradle插件的build.gradle

apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'java'
apply plugin: 'maven-publish'

dependencies {
//    implementation fileTree(dir: 'libs', include: ['*.jar'])

    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk


    //build tools
    compile 'com.android.tools.build:gradle:3.1.2'
    //transform
    compile 'com.android.tools.build:transform-api:1.5.0'
    //javassit
    compile 'javassist:javassist:3.12.1.GA'
    //commons-io
    compile 'commons-io:commons-io:2.5'
}
repositories {
    jcenter()
    google()//加在这里
}


SecondPlugin.groovy 自定义插件,内部为android注册了一个ReClassTransform 接口。

package com.feifei.second

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.feifei.second.transform.ReClassTransform
import org.gradle.api.Plugin
import org.gradle.api.Project

public class SecondPlugin implements Plugin<Project>{

    void apply(Project project){
        System.out.println("==========")
        System.out.println("feifei  第二个内部用插件")
        System.out.println("==========")

        project.extensions.create("pluginExt",PluginExtension)
        project.pluginExt.extensions.create("nestExt", PluginNestExtension)
        project.task('customTask',type:CustomTask)

        def isApp = project.plugins.getPlugin(AppPlugin)

        if(isApp){
            def android =  project.extensions.getByType(AppExtension)
            android.registerTransform(new ReClassTransform(project))
        }
    }
}

最原始的Transform实现。
ReClassTransfrom.groovy

package com.feifei.second.transform
import com.android.build.api.transform.*
import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.utils.FileUtils
import org.apache.commons.codec.digest.DigestUtils
import org.gradle.api.Project
import org.gradle.internal.impldep.org.apache.ivy.util.FileUtil
import org.gradle.jvm.tasks.Jar
import com.android.build.gradle.internal.pipeline.TransformManager

import javax.xml.crypto.dsig.TransformException

public class ReClassTransform extends Transform{

    private Project mProject;

    public ReClassTransform(Project p){
        this.mProject = p;
    }

    //transform的名称
    /**
     * 最终运行的名字为 transformClassWith+getName()+For+{BuildType}+{ProductFlavor}
     * 如 transformClassWithXXXForDebug
     * @return
     */
    @Override
    String getName() {
        return "XXX"
    }

    /**
     * 需要处理的数据类型,有两种枚举类型
     * CLASSES和RESOURCES,CLASSES代表处理的java的class文件;RESOURCES代表要处理java的资源.
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 指Transform要操作内容的范围,官方文档Scope有7种类型:
     * EXTERNAL_LIBRARIES   只有外部库
     * PROJECT              只有项目内容
     * PROJECT_LOCAL_DEPS   只有项目的本地依赖(本地jar)
     * PROVIDED_ONLY        只提供本地或远程依赖项
     * SUB_PROJECTS         只有子项目
     * SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
     * TESTED_CODE          由当前变量(包括依赖项)测试的代码
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }
//指明当前Transform是否支持增量编译
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * Transform中的核心方法,
     *
     * @param context 。
     * @param inputs  传过来的输入流, 其中有两种格式,一种是jar包格式一种是目录格式
     * @param referencedInputs
     * @param outputProvider  获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
     * @param isInCremental
     * @throws IOException
     * @throws TransformException
     */
    @Override
    public void transform(Context context,
                          Collection<TransformInput> inputs,
            Collection<TransformInput> referencedInputs,
            TransformOutputProvider outputProvider,
            boolean isInCremental
    ) throws IOException, TransformException{

        welecome()

        inputs.each { TransformInput input->

            //遍历目录
            input.directoryInputs.each { DirectoryInput directoryInput ->

                println "direction = "+directoryInput.file.getAbsolutePath()
                //获取输出目录
                def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)

                //对于目录中的class文件原样输出
                FileUtils.copyDirectory(directoryInput.file,dest)
            }

            //遍历jar文件,对jar不操作,但是要输出到out目录
            input.jarInputs.each { JarInput jarInput->

                // 将jar文件 重命名输出文件(同目录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 = outputProvider.getContentLocation(jarName+md5Name,jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }

        }

        end()

    }


    def welecome(){
        println "----welcome to ReClassTransform"
    }

    def end(){
        println "----ReClassTransform end"
    }
}

执行./gradlew :test_gradle_use_plugin:assembleDebug
时的输出内容。

> Task :test_gradle_use_plugin:transformClassesWithXXXForDebug 
----welcome to ReClassTransform
jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/ad39ea76672d18218cf29f42ea94a4d7/jars/classes.jar
jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
----ReClassTransform end

2、利用向文件中写入字符串的形式直接生成类文件

Hostconfig.groovy
增加HostConfig的调用

package com.feifei.second.hostconfig


public class HostConfig {
    static def void createHostConfig(variant,config){

def content = """
package com.sogou.teemo.test_use_gradle_plugin;
public class TheHostConfig{
    public static final String ip = "${config.param1}";
    public static final String port = "5050"; 

}

"""

        File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
        println "feifei createHostConfig outputDir:"+outputDir.getAbsolutePath()
        def javaFile = new File(outputDir, "TheHostConfig.java")
        javaFile.write(content,'UTF-8')

    }
}

SecondPlugin.groovy

package com.feifei.second

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.api.ApplicationVariant
import com.android.repository.impl.meta.Archive
import com.feifei.second.hostconfig.HostConfig
import com.feifei.second.transform.ReClassTransform
import org.gradle.api.Plugin
import org.gradle.api.Project

public class SecondPlugin implements Plugin<Project>{

    void apply(Project project){
        System.out.println("==========")
        System.out.println("feifei  第二个内部用插件")
        System.out.println("==========")

        project.extensions.create("pluginExt",PluginExtension)
        project.pluginExt.extensions.create("nestExt", PluginNestExtension)
        project.task('customTask',type:CustomTask)

        def isApp = project.plugins.getPlugin(AppPlugin)

        if(isApp){
            def android =  project.extensions.getByType(AppExtension)
            android.registerTransform(new ReClassTransform(project))



            android.applicationVariants.all { variants->

                def variantData =  variants.variantData
                def scope = variantData.scope

                println "feifei current scope:"+scope

                //scope.getTaskName 的作用 就是结合当前scope 拼接人物名
                def taskName = scope.getTaskName("CreateHostConfig")
                def createTask = project.task(taskName)

                println "feifei CreateHostConfigTaskName:"+taskName

                //自定义task 增加action
                createTask.doLast {
                    HostConfig.createHostConfig(variants,project.pluginExt)
                }

                String generateBuildConfigTaskName = scope.getGenerateBuildConfigTask().name
                def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                println "feifei  generateBuildConfigTaskName:"+generateBuildConfigTaskName

                if(generateBuildConfigTask){
                    createTask.dependsOn generateBuildConfigTask
                    generateBuildConfigTask.finalizedBy(createTask)//执行完generateBuildConfigTask之后,执行createTask任务
                }
            }
        }
    }
}

执行 ./gradlew clean :test_gradle_use_plugin:assembleDebug
输出如下:

 Configure project :test_gradle_use_plugin 
==========
feifei  第二个内部用插件
==========
feifei current scope:VariantScopeImpl{debug}
feifei CreateHostConfigTaskName:CreateHostConfigDebug
feifei  generateBuildConfigTaskName:generateDebugBuildConfig
feifei current scope:VariantScopeImpl{release}
feifei CreateHostConfigTaskName:CreateHostConfigRelease
feifei  generateBuildConfigTaskName:generateReleaseBuildConfig

> Task :test_gradle_use_plugin:transformClassesWithXXXForDebug 
----welcome to ReClassTransform
jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/5ae74cdeff58ee396218df991052866b/jars/classes.jar
jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
----ReClassTransform end




生成类文件的位置:

image.png

3、利用javassist 向现有类中动态插入代码

Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类。其功能与jdk自带的反射功能类似,但比反射功能更强大
ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似,
CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
CtField:用来访问域
CtMethod :用来访问方法
CtConstructor:用来访问构造器

新建
CodeInjects.groovy 用于想MainActivity中动态插入代码

package com.feifei.second.codeinject

import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project

public class CodeInjects {
    private final static ClassPool pool =  ClassPool.getDefault();

    public static void inject(String path, Project project){

        //当前路径加入类池,不然找不到这个类
        pool.appendClassPath(path)

        //project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        pool.appendClassPath(project.android.bootClasspath[0].toString())

        pool.importPackage("android.os.Bundle");
        pool.importPackage(" android.app.Activity")

        File dir = new File(path)
        if(dir.isDirectory()){
            //遍历目录
            dir.eachFileRecurse {File file->
                String filePath = file.absolutePath
                println("CodeInjects filePath:"+filePath)
                if(file.getName().equals("MainActivity.class")){

                    //获取MainActivity.class
                    CtClass ctClass = pool.getCtClass("com.sogou.teemo.test_use_gradle_plugin.MainActivity");
                    println("CodeInjects ctClass = "+ctClass)

                    if(ctClass.isFrozen()){
                        ctClass.defrost()
                    }

                    //获取到onCreate方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate");
                    println("CodeInjects 方法名 = " + ctMethod)

                    String insetBeforeStr = """ android.widget.Toast.makeText(this,"插件中自动生成的代码",android.widget.Toast.LENGTH_SHORT).show();
                                            """

                    ctMethod.insertAfter(insetBeforeStr)

                    ctClass.writeFile(path)

                    ctClass.detach()//释放

                }
            }
        }


    }


}

ReClassTransform.groovy中,遍历class文件时,调用CodeInjects.inject(directoryInput.file.absolutePath,mProject)。过滤出MainActivity.class并动态修改onCreate()方法

  /**
     * Transform中的核心方法,
     *
     * @param context 。
     * @param inputs  传过来的输入流, 其中有两种格式,一种是jar包格式一种是目录格式
     * @param referencedInputs
     * @param outputProvider  获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
     * @param isInCremental
     * @throws IOException
     * @throws TransformException
     */
    @Override
    public void transform(Context context,
                          Collection<TransformInput> inputs,
            Collection<TransformInput> referencedInputs,
            TransformOutputProvider outputProvider,
            boolean isInCremental
    ) throws IOException, TransformException{

        welecome()

        inputs.each { TransformInput input->

            //遍历目录
            input.directoryInputs.each { DirectoryInput directoryInput ->

                println "direction = "+directoryInput.file.getAbsolutePath()

                CodeInjects.inject(directoryInput.file.absolutePath,mProject)
                //获取输出目录
                def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)

                //对于目录中的class文件原样输出
                FileUtils.copyDirectory(directoryInput.file,dest)
            }

            //遍历jar文件,对jar不操作,但是要输出到out目录
            input.jarInputs.each { JarInput jarInput->

                // 将jar文件 重命名输出文件(同目录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 = outputProvider.getContentLocation(jarName+md5Name,jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }

        }

        end()

    }

将test_gradle_use_plugin-debug.apk 反编译后,如下图所示:


image.png

Github: 查看buildSrc 和test_gradle_use_plugin 两个module

四、相关知识背景

1、Transfrom API

基于Gradle的Transform API,在编译期的构建任务流中,class转为dex之前,插入一个Transform,并在此Transform流中,基于Javassist实现对字节码文件的注入。
[图片上传失败...(image-317838-1563938953106)]
http://google.github.io/android-gradle-dsl/javadoc/current/

2、javassist

Javassist是一个动态类库,可以用来检查、”动态”修改以及创建 Java类.其功能与jdk自带的反射功能类似,但比反射功能更强大.

  • ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似。
  • CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
  • CtField:用来访问域
  • CtMethod :用来访问方法
  • CtConstructor:用来访问构造器
  • insertClassPath:为ClassPool添加搜索路径,否则ClassPool 无法找打对应的类
     classPool.insertClassPath(new ClassClassPath(String.class));
            classPool.insertClassPath(new ClassClassPath(Person.class));
            classPool.insertClassPath("/Users/feifei/Desktop/1");
  • classPool.get(className);加载一个类

  • classPool.makeClass(className);//创建一个类

  • CtClass.addField();CtClass.addMethod(); 添加方法和属性

  CtField ageField = new CtField(CtClass.intType,"age",stuClass);
            stuClass.addField(ageField);
    CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);

            stuClass.addMethod(getMethod);
  • Class<?>clazz = stuClass.toClass();将CtCLass对象转化为JVM对象

创建一个类,并写入到本地文件

 public static void testCreateClass(){

        System.out.println("testCreateClass");
        //创建ClassPool
        ClassPool classPool = ClassPool.getDefault();

        //添加类路径
//        classPool.insertClassPath(new ClassClassPath(this.getClass()));
        classPool.insertClassPath(new ClassClassPath(String.class));
        //创建类
        CtClass stuClass = classPool.makeClass("com.feifei.Student");

        //加载类
        //classPool.get(className)
        try {
            //添加属性
            CtField idField = new CtField(CtClass.longType,"id",stuClass);
            stuClass.addField(idField);

            CtField nameField = new CtField(classPool.get("java.lang.String"),"name",stuClass);
            stuClass.addField(nameField);

            CtField ageField = new CtField(CtClass.intType,"age",stuClass);
            stuClass.addField(ageField);


            //添加方法
            CtMethod getMethod = CtMethod.make("public int getAge(){return this.age;}",stuClass);
            CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);

            stuClass.addMethod(getMethod);
            stuClass.addMethod(setMethod);

            //toClass 将CtClass 转换为java.lang.class
            Class<?>clazz = stuClass.toClass();
            System.out.println("testCreateClass clazz:"+clazz);

            System.out.println("testCreateClas ------ 属性列表 -----");
            Field[] fields = clazz.getDeclaredFields();
            for(Field field:fields){
                System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
            }

            System.out.println("testCreateClass ------ 方法列表 -----");

            Method[] methods = clazz.getDeclaredMethods();
            for(Method method:methods){
                System.out.println("feifei  "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
            }

            stuClass.writeFile("/Users/feifei/Desktop/1");
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }finally {

            //将stuClass 从ClassPool 移除
            if(stuClass != null){
                stuClass.detach();
            }
        }

    }

修改一个类的父类


package com.example.myjavassist;

public class Person {
}

public static void testSetSuperClass(){

        System.out.println("testSetSuperClass");
        //创建ClassPool
        ClassPool classPool = ClassPool.getDefault();


        try {
            //添加类路径
            classPool.insertClassPath(new ClassClassPath(String.class));
            classPool.insertClassPath(new ClassClassPath(Person.class));
            classPool.insertClassPath("/Users/feifei/Desktop/1");

            // 加载类
            //创建类
            CtClass stuClass = classPool.get("com.feifei.Student");
            CtClass personClass = classPool.get("com.example.myjavassist.Person");

            if(stuClass.isFrozen()){
                stuClass.freeze();
            }
            stuClass.setSuperclass(personClass);

            //toClass 将CtClass 转换为java.lang.class
            Class<?>clazz = stuClass.toClass();
            System.out.println("testSetSuperClass ------ 属性列表 -----");
            Field[] fields = clazz.getDeclaredFields();
            for(Field field:fields){
                System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
            }

            System.out.println("testSetSuperClass ------ 方法列表 -----");

            Method[] methods = clazz.getDeclaredMethods();
            for(Method method:methods){
                System.out.println("testSetSuperClass  "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
            }

            stuClass.writeFile("/Users/feifei/Desktop/1");
            personClass.writeFile("/Users/feifei/Desktop/1");

        } catch (NotFoundException | CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

        }
    }
image.png

方法重命名、复制方法、新建方法,添加方法体。


package com.example.myjavassist;

public class Calculator {

    public void getSum(long n) {
        long sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        System.out.println("n="+n+",sum="+sum);
    }

}


 public static void testInsertMethod(){

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = null;
        try {
            ctClass = pool.get("com.example.myjavassist.Calculator");

            //获取类中现有的方法
            String getSumName = "getSum";
            CtMethod methodOld = ctClass.getDeclaredMethod(getSumName);


            String methodNewName = getSumName+"$impl";
            //修改原有方法的方法名
            methodOld.setName(methodNewName);


            //创建一个新的方法getSumName,并将旧方法 复制成新方法中.
            CtMethod newMethod = CtNewMethod.copy(methodOld,getSumName,ctClass,null);

            //设置新newMethod的方法体
            StringBuffer body = new StringBuffer();
            body.append("{\nlong start = System.currentTimeMillis();\n");
            // 调用原有代码,类似于method();($$)表示所有的参数
            body.append(methodNewName + "($$);\n");
            body.append("System.out.println(\"Call to method " + methodNewName
                    + " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
            body.append("}");

            newMethod.setBody(body.toString());

            //为类新添加方法
            ctClass.addMethod(newMethod);

            Calculator calculator =(Calculator)ctClass.toClass().newInstance();
            calculator.getSum(10000);

            //将类输出到文件
            ctClass.writeFile("/Users/feifei/Desktop/1");

        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            if(ctClass!=null){
                ctClass.detach();
            }
        }
    }
image.png

Github: 选择 myjavaassit module

五、参考文章

https://www.jianshu.com/p/a6be7cdcfc65

https://www.jianshu.com/p/a9b3aaba8e45

https://blog.csdn.net/top_code/article/details/51708043

http://www.javassist.org/tutorial/tutorial2.html

javassit github:
https://github.com/jboss-javassist/javassist

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

推荐阅读更多精彩内容