Android ASM自動埋點方案實踐

這段時間想到一個有趣的功能,就是在Android的代碼編譯期間進行一些騷操作腿时,來達到一些日常情境下難以實現(xiàn)的功能俭令,比如監(jiān)聽應(yīng)用中的所有onClick點擊時間那先,或者監(jiān)聽某些方法的運行耗時,如果在代碼中一個方法一個方法修改會很蛋疼却音,所以想通過Gradle插件來實現(xiàn)在應(yīng)用的編譯期間進行代碼插入的功能改抡。

1、AOP的概念

其實這已經(jīng)涉及到AOP(Aspect Oriented Programming)系瓢,即面向切面編程阿纤,在編譯期間對代碼進行動態(tài)管理,以達到統(tǒng)一維護的目的夷陋。


AOP切面

舉個栗子欠拾,Android開發(fā)我們都知道,在項目越來越大的時候骗绕,應(yīng)用可能被分解為多個模塊藐窄,如果你要往所有模塊的方法里頭加一句‘我是大傻叼’的Toast,那是不是得跪酬土。所以最好的方式是想辦法在編譯的時候拿到所有方法荆忍,往方法里頭懟一個Toast,這樣還不會影響到運行期間性能撤缴。

2东揣、Transform

Android打包流程

如圖所示是Android打包流程,.java文件->.class文件->.dex文件腹泌,只要在紅圈處攔截住嘶卧,拿到所有方法進行修改完再放生就可以了,而做到這一步也不難凉袱,Google官方在Android Gradle的1.5.0 版本以后提供了 Transfrom API, 允許第三方 Plugin 在打包 dex 文件之前的編譯過程中操作 .class 文件芥吟,我們做的就是實現(xiàn)Transform進行.class文件遍歷拿到所有方法,修改完成對原文件進行替換专甩。

/**
 * 自動埋點追蹤钟鸵,遍歷所有文件更換字節(jié)碼
 */
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插件實現(xiàn)

通過Transform提供的api可以遍歷所有文件涤躲,但是要實現(xiàn)Transform的遍歷操作棺耍,得通過Gradle插件來實現(xiàn),關(guān)于Gradle插件的知識可以看相關(guān)博客种樱,也可以直接看博主的項目Luffy蒙袍。編寫Gradle插件可能需要一點Goovy知識俊卤,具體編寫直接用java語言寫也可以,Goovy是完全兼容java的害幅,只截取插件入口部分實現(xiàn)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消恍、字節(jié)碼編寫

完成上面的操作以后就剩下一件事了,那就是拿到.class文件了以现,大家都知道.class文件是字節(jié)碼格式的狠怨,操作起來難度是相當于大的,所以需要一個字節(jié)碼操作庫來減輕難度邑遏,那就是ASM了佣赖。

4.1、ASM簡介

ASM 可以直接產(chǎn)生二進制的class 文件记盒,也可以在增強既有類的功能茵汰。Java class 被存儲在嚴格格式定義的 .class文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱孽鸡、方法蹂午、屬性以及 Java 字節(jié)碼(指令)。

4.2彬碱、具體使用ASM

ASM框架中的核心類有以下幾個:

  • ClassReader:該類用來解析編譯過的class字節(jié)碼文件豆胸。
  • ClassWriter:該類用來重新構(gòu)建編譯后的類,比如說修改類名巷疼、屬性以及方法晚胡,甚至可以生成新的類的字節(jié)碼文件。
  • ClassVisitor:主要負責 “拜訪” 類成員信息嚼沿。其中包括標記在類上的注解估盘,類的構(gòu)造方法,類的字段骡尽,類的方法遣妥,靜態(tài)代碼塊。
  • AdviceAdapter:實現(xiàn)了MethodVisitor接口攀细,主要負責 “拜訪” 方法的信息箫踩,用來進行具體的方法字節(jié)碼操作。

ClassVisitor的全部方法如下谭贪,按一定的次序來遍歷類中的成員境钟。


ClassVisitor全部api

在ClassVisitor中根據(jù)你的條件進行判斷,滿足條件的類才會修改其中方法俭识,比如要統(tǒng)計點擊事件的話慨削,需要實現(xiàn)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) {
        // 內(nèi)部類信息
        ...
        super.visitInnerClass(name, outerName, innerName, access)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        // 拿到需要修改的方法,執(zhí)行修改操作
        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中根據(jù)對已經(jīng)拿到的方法進行修改了缚态。

MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
        boolean isAnnotation = false
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            //進入方法時可以插入字節(jié)碼
           ...
        }

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

        /**
         * 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
         */
        @Override
        AnnotationVisitor visitAnnotation(String des, boolean visible) {
            ...
            return super.visitAnnotation(des, visible)
        }
    }

5磁椒、實戰(zhàn)演練

以上就是總體的思路了,現(xiàn)在就通過Luffy根據(jù)具體需求實戰(zhàn)一下猿规,比如說在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 = [
            //是否使用注解來找對應(yīng)方法
            'isAnotation': false,
            //方法的匹配师郑,可以通過類名或者實現(xiàn)的接口名匹配
            'ClassFilter': [
                    ['ClassName': null, 'InterfaceName':null,
                     'MethodName':null, 'MethodDes':null]
            ],
            //插入的字節(jié)碼环葵,方法的執(zhí)行順序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()
                            //使用注解找對應(yīng)方法的時候得加這個判斷
                        }

                        @Override
                        protected void onMethodExit(int opcode) {
                            super.onMethodExit(opcode)
                            //使用注解找對應(yīng)方法的時候得加這個判斷
                        }

                        /**
                         * 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
                         */
                        @Override
                        AnnotationVisitor visitAnnotation(String des, boolean visible) {
                            return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}

要是使用演示的話,因為還沒上傳到j(luò)center庫宝冕,所以只能本地倉庫打包插件张遭,記得要先把依賴都注釋掉,插件打包完成后再啟用地梨,不然會編譯不過去的菊卷。
xiaoqingwa{}里頭的配置信息先不用管,等會會講到宝剖,主要是為了能夠不修改插件進行動態(tài)更換插樁的方法洁闰。

5.2、應(yīng)用測試

插件配置好了之后就可以測試一下效果了万细,先寫一個耗時統(tǒng)計的工具類
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來臨時保存對應(yīng)方法的時間,退出方法時獲取時間差赖钞。
在一個方法的前后插入時間統(tǒng)計的方法腰素,這個具體的過程要怎么操作呢,因為class文件是字節(jié)碼格式的雪营,ASM也是進行字節(jié)碼操作弓千,所以必須先把插入的代碼轉(zhuǎn)換成字節(jié)碼先。這里推薦一個字節(jié)碼查看工具Java Bytecode Editor献起,導(dǎo)入.class文件就可以看到對應(yīng)字節(jié)碼了计呈。
比如我們要插入的代碼如下:

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

先把.java文件編譯成.class文件,用Java Bytecode Editor打開

插入代碼的字節(jié)碼

然后根據(jù)其用ASM提供的Api一一對應(yīng)的把代碼填進來加到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中配置得到的字節(jié)碼捌显,最后設(shè)置一下過濾條件,最終的代碼如下:
build.gradle

xiaoqingwa{
    name = "小傻逼"
    isDebug = true
    //具體配置
    matchData = [
            //是否使用注解來找對應(yīng)方法
            'isAnotation': false,
            //方法的匹配总寒,可以通過類名或者實現(xiàn)的接口名匹配
            'ClassFilter': [
                    ['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener',
                     'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V']
            ],
            //插入的字節(jié)碼扶歪,方法的執(zhí)行順序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()
                            //使用注解找對應(yīng)方法的時候得加這個判斷
//                            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)
                            //使用注解找對應(yīng)方法的時候得加這個判斷
//                            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)
                        }

                        /**
                         * 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
                         */
                        @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'表示是否使用注解的方式找到對應(yīng)方法,這里false,因為我們現(xiàn)在是通過具體類信息來判斷的善镰。
'ClassFilter'表示過濾條件妹萨,其中'ClassName''InterfaceName'用于判斷哪些類中的方法可以遍歷其中的方法進行匹配修改,不滿足的話就不會進行方法名匹配了炫欺,這些感興趣的童鞋都可以改插件自定義擴展乎完。
'MethodName''MethodDes'是方法名和方法描述符,可以唯一確定一個方法名品洛,滿足類過濾條件的就會進行方法匹配树姨,例如我們要統(tǒng)計的點擊事件onClick(View v)
意思就是繼承自android/view/View$OnClickListener的類或者類名是'com.xishuang.plugintest.MainActivity'就可以進行方法的遍歷桥状,然后方法滿足onClick(View v)就會進行代碼插入操作帽揪。

設(shè)置完之后rebuild一下就可以了,可以通過日志看下具體信息辅斟,isDebug = true可以開啟日志打印转晰。

日志

通過日志可以看到我們設(shè)置的字節(jié)碼確實插樁成功,現(xiàn)在再看一下編譯后的文件驗證一下士飒,具體位置是:app\build\intermediates\transforms\AutoTrack\debug\folders


編譯后的.class文件

其中的notifyInsert()是我用來彈Toast額外調(diào)試用的查邢,請忽略。在手機上點擊一下按鈕測試一下酵幕,發(fā)現(xiàn)確實記錄下點擊的耗時時間侠坎,完成。

5.3裙盾、注解匹配

除了以上的方式來查找修改的方法之外实胸,還可以通過注解來查找,切換很簡單番官,只需要改一下app的build.gradle文件就可以了庐完,項目中也有栗子,添加了一個注解類徘熔。

/**
 * Author:xishuang
 * Date:2018.1.9
 * Des:時間統(tǒng)計注解
 */
@Target(ElementType.METHOD)
public @interface AutoCount {
}

然后在對應(yīng)的方法上添加你自定義的注解

@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 = [
            //是否使用注解來找對應(yīng)方法
            'isAnotation': true,
            //方法的匹配门躯,可以通過類名或者實現(xiàn)的接口名匹配
            'ClassFilter': [
                    ['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener',
                     'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V']
            ],
            //插入的字節(jié)碼,方法的執(zhí)行順序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()
                            //使用注解找對應(yīng)方法的時候得加這個判斷
                            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)
                            //使用注解找對應(yīng)方法的時候得加這個判斷
                            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)
                        }

                        /**
                         * 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
                         */
                        @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
            }
    ]
}

關(guān)鍵代碼在于把'isAnotation'設(shè)為true酷师,然后在visitAnnotation方法中添加你的注解類匹配讶凉,也就是這句des.equals("Lcom/xishuang/annotation/AutoCount;")代碼,注解類的描述符山孔,運行效果和上面差不多懂讯,但是不會打印日志,因為通過注解來查找方法會遍歷每個方法台颠,打印信息太多電腦會爆炸褐望。

6、插件擴展自動埋點功能

針對以下的方法進行埋點監(jiān)聽,并實現(xiàn)了View的唯一區(qū)別鏈

  • 1瘫里、View的onClick(View v)方法
  • 2实蔽、Fragment的onResume()方法
  • 3、Fragment的onPause()方法
  • 4谨读、Fragment的setUserVisibleHint(boolean b)方法
  • 5局装、Fragment的onHiddenChanged(boolean b)方法
  • 6、在app的module中手動設(shè)置的監(jiān)聽條件:指定方法或注解方法

插件的增加自動埋點處理類主要是ChoiceUtil

App中對監(jiān)聽的方法處理的類是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();


    /**
     * 實現(xiàn)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);
    }

    /**
     * 實現(xiàn)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監(jiān)聽以及Fragment捕獲

具體的信息可以看下源碼劳殖,已共享到github上铐尚,在這里講了下大概的思路和代碼框架,博主已經(jīng)初步擴展完成自動埋點的基礎(chǔ)功能闷尿,更有趣的玩法大家可以自己修改一下插件來實現(xiàn)塑径。
github地址:Luffy女坑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末填具,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匆骗,更是在濱河造成了極大的恐慌劳景,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碉就,死亡現(xiàn)場離奇詭異盟广,居然都是意外死亡,警方通過查閱死者的電腦和手機瓮钥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門筋量,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碉熄,你說我怎么就攤上這事桨武。” “怎么了锈津?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵呀酸,是天一觀的道長。 經(jīng)常有香客問我琼梆,道長性誉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任茎杂,我火速辦了婚禮错览,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘煌往。我一直安慰自己蝗砾,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悼粮,像睡著了一般闲勺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扣猫,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天菜循,我揣著相機與錄音,去河邊找鬼申尤。 笑死癌幕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的昧穿。 我是一名探鬼主播勺远,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼时鸵!你這毒婦竟也來了胶逢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤饰潜,失蹤者是張志新(化名)和其女友劉穎初坠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彭雾,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡碟刺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了薯酝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片半沽。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吴菠,靈堂內(nèi)的尸體忽然破棺而出者填,到底是詐尸還是另有隱情,我是刑警寧澤橄务,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布幔托,位于F島的核電站,受9級特大地震影響蜂挪,放射性物質(zhì)發(fā)生泄漏重挑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一棠涮、第九天 我趴在偏房一處隱蔽的房頂上張望谬哀。 院中可真熱鬧,春花似錦严肪、人聲如沸史煎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篇梭。三九已至氢橙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間恬偷,已是汗流浹背悍手。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留袍患,地道東北人坦康。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像诡延,于是被迫代替她去往敵國和親滞欠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345