Android ASM自动埋点方案实践

这段时间想到一个有趣的功能,就是在Android的代码编译期间进行一些骚操作,来达到一些日常情境下难以实现的功能,比如监听应用中的所有onClick点击时间,或者监听某些方法的运行耗时,如果在代码中一个方法一个方法修改会很蛋疼,所以想通过Gradle插件来实现在应用的编译期间进行代码插入的功能。

1、AOP的概念

其实这已经涉及到AOP(Aspect Oriented Programming),即面向切面编程,在编译期间对代码进行动态管理,以达到统一维护的目的。


AOP切面

举个栗子,Android开发我们都知道,在项目越来越大的时候,应用可能被分解为多个模块,如果你要往所有模块的方法里头加一句‘我是大傻叼’的Toast,那是不是得跪。所以最好的方式是想办法在编译的时候拿到所有方法,往方法里头怼一个Toast,这样还不会影响到运行期间性能。

2、Transform

Android打包流程

如图所示是Android打包流程,.java文件->.class文件->.dex文件,只要在红圈处拦截住,拿到所有方法进行修改完再放生就可以了,而做到这一步也不难,Google官方在Android Gradle的1.5.0 版本以后提供了 Transfrom API, 允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件,我们做的就是实现Transform进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。

/**
 * 自动埋点追踪,遍历所有文件更换字节码
 */
public class AutoTransform extends Transform {

    @Override
    String getName() {
        return "AutoTrack"
    }
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }
    @Override
    public void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental) throws IOException, TransformException,   InterruptedException {
           //此处会遍历所有文件
           /**遍历输入文件*/
          inputs.each { TransformInput input ->
             /**
             * 遍历jar
             */
             input.jarInputs.each { JarInput jarInput ->
                 ...
             }
             /**
             * 遍历目录
             */
             input.directoryInputs.each { DirectoryInput directoryInput ->
                ...
             }

      }

}

3、Gradle插件实现

通过Transform提供的api可以遍历所有文件,但是要实现Transform的遍历操作,得通过Gradle插件来实现,关于Gradle插件的知识可以看相关博客,也可以直接看博主的项目Luffy。编写Gradle插件可能需要一点Goovy知识,具体编写直接用java语言写也可以,Goovy是完全兼容java的,只截取插件入口部分实现PluginEntry.groovy

class PluginEntry implements Plugin<Project> {

    @Override
    void apply(Project project) {
        ...
        //使用Transform实行遍历
        def android = project.extensions.getByType(AppExtension)
        registerTransform(android)
        ...
    }

def static registerTransform(BaseExtension android) {
    AutoTransform transform = new AutoTransform()
    android.registerTransform(transform)
}

4、字节码编写

完成上面的操作以后就剩下一件事了,那就是拿到.class文件了,大家都知道.class文件是字节码格式的,操作起来难度是相当于大的,所以需要一个字节码操作库来减轻难度,那就是ASM了。

4.1、ASM简介

ASM 可以直接产生二进制的class 文件,也可以在增强既有类的功能。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。

4.2、具体使用ASM

ASM框架中的核心类有以下几个:

  • ClassReader:该类用来解析编译过的class字节码文件。
  • ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
  • ClassVisitor:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。
  • AdviceAdapter:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。

ClassVisitor的全部方法如下,按一定的次序来遍历类中的成员。


ClassVisitor全部api

在ClassVisitor中根据你的条件进行判断,满足条件的类才会修改其中方法,比如要统计点击事件的话,需要实现View$OnClickListener接口的类才会遍历其中的方法进行操作。

class AutoClassVisitor extends ClassVisitor {

    AutoClassVisitor(final ClassVisitor cv) {
        super(Opcodes.ASM4, cv)
    }

    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

        //进行需要满足类的条件过滤
        ...
        super.visit(version, access, name, signature, superName, interfaces)
    }

    @Override
    void visitInnerClass(String name, String outerName, String innerName, int access) {
        // 内部类信息
        ...
        super.visitInnerClass(name, outerName, innerName, access)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        // 拿到需要修改的方法,执行修改操作
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions)
        MethodVisitor adapter = null
        ...
        adapter = new AutoMethodVisitor(methodVisitor, access, name, desc)
        ...
        return methodVisitor
    }

    @Override
    void visitEnd() {
        //类中成员信息遍历介绍
        ...
        super.visitEnd()
    }
}

在MethodVisitor中根据对已经拿到的方法进行修改了。

MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
        boolean isAnnotation = false
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            //进入方法时可以插入字节码
           ...
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode)
            //退出方法前可以插入字节码
           ...
        }

        /**
         * 需要通过注解的方式加字节码才会重写这个方法来进行条件过滤
         */
        @Override
        AnnotationVisitor visitAnnotation(String des, boolean visible) {
            ...
            return super.visitAnnotation(des, visible)
        }
    }

5、实战演练

以上就是总体的思路了,现在就通过Luffy根据具体需求实战一下,比如说在onClick方法点击的耗时(自动埋点也是一样的道理,只不过换了插桩的方法)。

5.1、插件配置

先打包一下插件到本地仓库进行引用,在项目的根build.gradle加入插件的依赖

dependencies {
    classpath 'com.xixi.plugin:plugin:1.0.1-SNAPSHOT'
}

在app的build.gradle中

apply plugin: 'apk.move.plugin'

xiaoqingwa{
    name = "小傻逼"
    isDebug = true
    //具体配置
    matchData = [
            //是否使用注解来找对应方法
            'isAnotation': false,
            //方法的匹配,可以通过类名或者实现的接口名匹配
            'ClassFilter': [
                    ['ClassName': null, 'InterfaceName':null,
                     'MethodName':null, 'MethodDes':null]
            ],
            //插入的字节码,方法的执行顺序visitAnnotation->onMethodEnter->onMethodExit
            'MethodVisitor':{
                MethodVisitor methodVisitor, int access, String name, String desc ->
                    MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
                        boolean isAnnotation = false
                        @Override
                        protected void onMethodEnter() {
                            super.onMethodEnter()
                            //使用注解找对应方法的时候得加这个判断
                        }

                        @Override
                        protected void onMethodExit(int opcode) {
                            super.onMethodExit(opcode)
                            //使用注解找对应方法的时候得加这个判断
                        }

                        /**
                         * 需要通过注解的方式加字节码才会重写这个方法来进行条件过滤
                         */
                        @Override
                        AnnotationVisitor visitAnnotation(String des, boolean visible) {
                            return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}

要是使用演示的话,因为还没上传到jcenter库,所以只能本地仓库打包插件,记得要先把依赖都注释掉,插件打包完成后再启用,不然会编译不过去的。
xiaoqingwa{}里头的配置信息先不用管,等会会讲到,主要是为了能够不修改插件进行动态更换插桩的方法。

5.2、应用测试

插件配置好了之后就可以测试一下效果了,先写一个耗时统计的工具类
TimeCache.java

/**
 * Author:xishuang
 * Date:2018.01.10
 * Des:计时类,编译器加入指定方法中
 */
public class TimeCache {
    public static Map<String, Long> sStartTime = new HashMap<>();
    public static Map<String, Long> sEndTime = new HashMap<>();

    public static void setStartTime(String methodName, long time) {
        sStartTime.put(methodName, time);
    }

    public static void setEndTime(String methodName, long time) {
        sEndTime.put(methodName, time);
    }

    public static String getCostTime(String methodName) {
        long start = sStartTime.get(methodName);
        long end = sEndTime.get(methodName);
        long dex = end - start;
        return "method: " + methodName + " cost " + dex + " ns";
    }
}

大概思路就是使用HashMap来临时保存对应方法的时间,退出方法时获取时间差。
在一个方法的前后插入时间统计的方法,这个具体的过程要怎么操作呢,因为class文件是字节码格式的,ASM也是进行字节码操作,所以必须先把插入的代码转换成字节码先。这里推荐一个字节码查看工具Java Bytecode Editor,导入.class文件就可以看到对应字节码了。
比如我们要插入的代码如下:

private void countTime() {
    TimeCache.setStartTime("newFunc", System.currentTimeMillis());
    
    TimeCache.setEndTime("newFunc", System.currentTimeMillis());
    Log.d("耗时", TimeCache.getCostTime("newFunc"));
}

先把.java文件编译成.class文件,用Java Bytecode Editor打开

插入代码的字节码

然后根据其用ASM提供的Api一一对应的把代码填进来加到onMethodEnter和onMethodExit中。

//方法前加入
methodVisitor.visitMethodInsn
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false)

//方法后加入
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false)
methodVisitor.visitLdcInsn("耗时")
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)

在app的build.gradle中配置得到的字节码,最后设置一下过滤条件,最终的代码如下:
build.gradle

xiaoqingwa{
    name = "小傻逼"
    isDebug = true
    //具体配置
    matchData = [
            //是否使用注解来找对应方法
            'isAnotation': false,
            //方法的匹配,可以通过类名或者实现的接口名匹配
            'ClassFilter': [
                    ['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener',
                     'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V']
            ],
            //插入的字节码,方法的执行顺序visitAnnotation->onMethodEnter->onMethodExit
            'MethodVisitor':{
                MethodVisitor methodVisitor, int access, String name, String desc ->
                    MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
                        boolean isAnnotation = false
                        @Override
                        protected void onMethodEnter() {
                            super.onMethodEnter()
                            //使用注解找对应方法的时候得加这个判断
//                            if (!isAnnotation){
//                                return
//                            }

                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity", "notifyInsert", "()V", false)
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false)
                        }

                        @Override
                        protected void onMethodExit(int opcode) {
                            super.onMethodExit(opcode)
                            //使用注解找对应方法的时候得加这个判断
//                            if (!isAnnotation){
//                                return
//                            }

                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false)
                            methodVisitor.visitLdcInsn("耗时")
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
                        }

                        /**
                         * 需要通过注解的方式加字节码才会重写这个方法来进行条件过滤
                         */
                        @Override
                        AnnotationVisitor visitAnnotation(String des, boolean visible) {
//                            if (des.equals("Lcom/xishuang/annotation/AutoCount;")) {
//                                println "注解匹配:" + des
//                                isAnnotation = true
//                            }
                            return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}

'isAnotation'表示是否使用注解的方式找到对应方法,这里false,因为我们现在是通过具体类信息来判断的。
'ClassFilter'表示过滤条件,其中'ClassName''InterfaceName'用于判断哪些类中的方法可以遍历其中的方法进行匹配修改,不满足的话就不会进行方法名匹配了,这些感兴趣的童鞋都可以改插件自定义扩展。
'MethodName''MethodDes'是方法名和方法描述符,可以唯一确定一个方法名,满足类过滤条件的就会进行方法匹配,例如我们要统计的点击事件onClick(View v)
意思就是继承自android/view/View$OnClickListener的类或者类名是'com.xishuang.plugintest.MainActivity'就可以进行方法的遍历,然后方法满足onClick(View v)就会进行代码插入操作。

设置完之后rebuild一下就可以了,可以通过日志看下具体信息,isDebug = true可以开启日志打印。

日志

通过日志可以看到我们设置的字节码确实插桩成功,现在再看一下编译后的文件验证一下,具体位置是:app\build\intermediates\transforms\AutoTrack\debug\folders


编译后的.class文件

其中的notifyInsert()是我用来弹Toast额外调试用的,请忽略。在手机上点击一下按钮测试一下,发现确实记录下点击的耗时时间,完成。

5.3、注解匹配

除了以上的方式来查找修改的方法之外,还可以通过注解来查找,切换很简单,只需要改一下app的build.gradle文件就可以了,项目中也有栗子,添加了一个注解类。

/**
 * Author:xishuang
 * Date:2018.1.9
 * Des:时间统计注解
 */
@Target(ElementType.METHOD)
public @interface AutoCount {
}

然后在对应的方法上添加你自定义的注解

@AutoCount
    private void onClick() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    @AutoCount
    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.button) {
            Toast.makeText(this, "我是按钮", Toast.LENGTH_SHORT).show();
        }
    }

修改一下build.gradle中的配置文件

xiaoqingwa{
    name = "小傻逼"
    isDebug = true
    //具体配置
    matchData = [
            //是否使用注解来找对应方法
            'isAnotation': true,
            //方法的匹配,可以通过类名或者实现的接口名匹配
            'ClassFilter': [
                    ['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener',
                     'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V']
            ],
            //插入的字节码,方法的执行顺序visitAnnotation->onMethodEnter->onMethodExit
            'MethodVisitor':{
                MethodVisitor methodVisitor, int access, String name, String desc ->
                    MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
                        boolean isAnnotation = false
                        @Override
                        protected void onMethodEnter() {
                            super.onMethodEnter()
                            //使用注解找对应方法的时候得加这个判断
                            if (!isAnnotation){
                                return
                            }

                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity", "notifyInsert", "()V", false)
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false)
                        }

                        @Override
                        protected void onMethodExit(int opcode) {
                            super.onMethodExit(opcode)
                            //使用注解找对应方法的时候得加这个判断
                            if (!isAnnotation){
                                return
                            }

                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false)
                            methodVisitor.visitLdcInsn("耗时")
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
                        }

                        /**
                         * 需要通过注解的方式加字节码才会重写这个方法来进行条件过滤
                         */
                        @Override
                        AnnotationVisitor visitAnnotation(String des, boolean visible) {
                            if (des.equals("Lcom/xishuang/annotation/AutoCount;")) {
                                println "注解匹配:" + des
                                isAnnotation = true
                            }
                            return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}

关键代码在于把'isAnotation'设为true,然后在visitAnnotation方法中添加你的注解类匹配,也就是这句des.equals("Lcom/xishuang/annotation/AutoCount;")代码,注解类的描述符,运行效果和上面差不多,但是不会打印日志,因为通过注解来查找方法会遍历每个方法,打印信息太多电脑会爆炸。

6、插件扩展自动埋点功能

针对以下的方法进行埋点监听,并实现了View的唯一区别链

  • 1、View的onClick(View v)方法
  • 2、Fragment的onResume()方法
  • 3、Fragment的onPause()方法
  • 4、Fragment的setUserVisibleHint(boolean b)方法
  • 5、Fragment的onHiddenChanged(boolean b)方法
  • 6、在app的module中手动设置的监听条件:指定方法或注解方法

插件的增加自动埋点处理类主要是ChoiceUtil

App中对监听的方法处理的类是AutoHelper.java

/**
 * Author:xishuang
 * Date:2018.03.01
 * Des:自动埋点帮助类
 */
public class AutoHelper {
    private static final String TAG = AutoHelper.class.getSimpleName();
    private static Context context = AutoApplication.getInstance().getApplicationContext();


    /**
     * 实现onClick点击时间的自动注入处理
     */
    public static void onClick(View view) {
        String path = AutoUtil.getPath(context, view);
        String activityName = AutoUtil.getActivityName(view);
        path = activityName + ":onClick:" + path;
        Log.d(TAG, path);
    }

    /**
     * 实现onClick点击时间的自动注入处理
     */
    public static void onClick() {
        Log.d(TAG, "onClick()");
    }

    public static void onFragmentResume(Fragment fragment) {
        Log.d(TAG, "onFragmentResume" + fragment.getClass().getSimpleName());
    }

    public static void onFragmentPause(Fragment fragment) {
        Log.d(TAG, "onFragmentPause"  + fragment.getClass().getSimpleName());
    }

    public static void setFragmentUserVisibleHint(Fragment fragment, boolean isVisibleToUser) {
        Log.d(TAG, "setFragmentUserVisibleHint->" + isVisibleToUser + "->" + fragment.getClass().getSimpleName());
    }

    public static void onFragmentHiddenChanged(Fragment fragment, boolean hidden) {
        Log.d(TAG, "onFragmentHiddenChanged->" + hidden + "->" + fragment.getClass().getSimpleName());
    }
View监听以及Fragment捕获

具体的信息可以看下源码,已共享到github上,在这里讲了下大概的思路和代码框架,博主已经初步扩展完成自动埋点的基础功能,更有趣的玩法大家可以自己修改一下插件来实现。
github地址:Luffy

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