ASM對匿名內部類、Lambda及方法引用的Hook研究

ASM在安卓開發中的應用十分廣泛,本文重點探討通過ASM對匿名內部類、Lambda表達式及方法引用的Hook。

安卓的編譯流程中Java文件會被編譯成.class.class會被編譯成.dex。而ASM的執行時機就是在.class編譯成.dex的過程中發生的。因此要想通過ASM修改自己碼就需要知道我們的Java文件編譯成的.class是怎樣的。

PS:本文假設你對ASM有一定了解。

一,匿名內部類方式

我們在面試時經常會說起handler的內存泄漏問題,原因是匿名內部類默認會持有外部類的引用,因此巴拉巴拉。。。

那么匿名內部類為什么會持有外部類的引用,編譯后又是什么樣子呢?我們擼代碼看下。

寫一段簡單的啟動線程的代碼:

public class FuncActivity {
    
    private void test1(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.e("FuncActivity", "thread - new Runnable");
            }
        }).start();
    }
}

下面來看下其編譯后的產物。

1,匿名內部類編譯后的.class文件

匿名內部類會生成一個新的.class文件,命名格式為:外部類類名$序號.class

首先查看一下生成的匿名內部類的.class代碼:

class FuncActivity$1 implements Runnable {
    FuncActivity$1(final FuncActivity this$0) {
        this.this$0 = this$0;
    }

    public void run() {
        Log.e("FuncActivity", "thread - new Runnable");
    }
}

自動生成的FuncActivity$1實現了Runnable接口,其構造方法傳入了外部類FuncActivity的對象,因此匿名內部類持有了外部類的引用。

2,外部類ASM代碼

通過Android Studio查看編譯后的.class文件,發現編輯器對其做了反編譯:

public class FuncActivity {

    private void test1() {
        (new Thread(new Runnable() {
            public void run() {
                Log.e("FuncActivity", "thread - new Runnable");
            }
        })).start();
    }
}

通過Byte Code Analyzer插件可以查看對應ASM代碼。

{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "test1", "()V", null, null);
methodVisitor.visitCode();
...
// new Thread
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
// 在操作數棧中復制上面new Thread的對象
methodVisitor.visitInsn(DUP);
// new 自動生成的匿名內部類(FuncActivity$1)
methodVisitor.visitTypeInsn(NEW, "me/wsj/performance/ui/FuncActivity$1");
// 復制上面的匿名內部類對象
methodVisitor.visitInsn(DUP);
// 加載當前class對象(FuncActivity.this)
methodVisitor.visitVarInsn(ALOAD, 0);
// FuncActivity$1初始化(執行init),傳入FuncActivity.this對象
methodVisitor.visitMethodInsn(INVOKESPECIAL, "me/wsj/performance/ui/FuncActivity$1", "<init>", "(Lme/wsj/performance/ui/FuncActivity;)V", false);
// new Thread對象初始化,傳入FuncActivity$1對象
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
...
// 執行線程的start方法
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);
...
methodVisitor.visitInsn(RETURN);
...
methodVisitor.visitEnd();
}

每行代碼的含義已經大概注釋出,關鍵點就是創建了一個FuncActivity$1對象,并將其作為參數創建一個Thread,最后執行了Thread對象的start()方法。

3,植入代碼

明白了代碼編譯后的大概執行邏輯就可以對其進行hook植入代碼。

通過重寫ClassVisitorvisitMethod()方法,在其中根據方法的namedescriptor做過濾即可找到需要hook的Method:

class TrackerClassAdapter(api: Int, cv: ClassVisitor?) : BaseClassVisitor(api, cv) {
    
    override fun visitMethod(
        access: Int,
        name: String,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?,
        mv: MethodVisitor
    ): MethodVisitor {
        // 找到需要hook的Method
        if (isRunMethod(name, descriptor)) {
            // 自定義MethodVisitor
            return TrackerMethodAdapter(descriptor, api, access, mv)
        }
        return mv
    }
    
    // 根據方法的name及descriptor做判斷
    fun isRunMethod(name: String, descriptor: String?): Boolean {
        return name == "run" && descriptor == "()V"
    }
}

通過在自定義的MethodVisitor中重寫visitInSn()即可植入自定義代碼:

class TrackerMethodAdapter(
    private val descriptor: String?,
    api: Int,
    access: Int,
    mv: MethodVisitor?
) : LocalVariablesSorter(api, access, descriptor, mv) {

    override fun visitInsn(opcode: Int) {
        // 植入代碼
        weaveTrackCode()
        super.visitInsn(opcode)
    }

    private fun weaveTrackCode() {
        mv.visitMethodInsn(
            INVOKESTATIC,
            "me/wsj/apm/thread/ThreadTracker",
            "trackOnce",
            "()V",
            false
        )
    }
}

植入代碼后編譯出的.class代碼如下:

private void test1() {
    (new Thread(new Runnable() {
        public void run() {
            Log.e("FuncActivity", "thread - new Runnable");
            ThreadTracker.trackOnce("me.wsj.performance.ui.FuncActivity$1 line num: 33");       
        }
    })).start();
}

詳細代碼可以參考:OutSiderAPM/ThreadTrackerClassAdapter.kt at master · Jinxqq/OutSiderAPM · GitHub

4,小結

1,自定義ClassVisitor重寫visitMethod()方法

2,在其中根據方法的namedescriptor做過濾,找到需要hook的Method

3,自定義MethodVisitor重寫visitInSn()方法

4,在其中插入自定義代碼的ASM代碼

二,Lambda方式

通過Lambda表達式啟動線程:

public class FuncActivity {
    
    private void test2(){
        
        new Thread(() -> {
            Log.e("FuncActivity", "thread - lambda");
        }).start();
        
    }
}
1,編譯后的.class文件

通過Android Studio查看編譯后的.class文件,發現編輯器對其做了反編譯,看到的還是Lambda的形式:

public class FuncActivity {

    private void test2() {
        (new Thread(() -> {
            Log.e("FuncActivity", "thread - lambda");
        })).start();
    }
}

實際上Lambda表達式在編譯時會生成一個方法,此方法默認是隱藏的,如果想查看,可以使用 java 的 javap -p -v xxx.class 命令查看這個方法。也可以通過編譯后的ASM代碼查看生成的方法。

2,編譯后的ASM代碼

通過Byte Code Analyzer插件查看對應ASM代碼如下:

// test2方法部分
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "test2", "()V", null, null);
methodVisitor.visitCode();
...
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
methodVisitor.visitInsn(DUP);

// 通過visitInvokeDynamicInsn訪問lambda方法
methodVisitor.visitInvokeDynamicInsn("run", "()Ljava/lang/Runnable;", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false), new Object[]{Type.getType("()V"), new Handle(Opcodes.H_INVOKESTATIC, "me/wsj/performance/ui/FuncActivity", "lambda$test2$0", "()V", false), Type.getType("()V")});

methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
...
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);
...
methodVisitor.visitInsn(RETURN);
...
methodVisitor.visitEnd();
}

// Lambda方法部分
{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, "lambda$test2$0", "()V", null, null);
methodVisitor.visitCode();
...
methodVisitor.visitLdcInsn("FuncActivity");
methodVisitor.visitLdcInsn("thread - lambda");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
...
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 0);
methodVisitor.visitEnd();
}

代碼包含兩個方法:

第一個方法就是我們寫的test2(),其大致邏輯如下:

  1. 通過NEW指令創建一個Thread對象
  2. 通過INVOKEDYNAMIC指令創建一個Runnable對象
  3. 使用這個Runnable對象初始化Thread對象
  4. 執行了Thread對象的start()方法

第二個方法是自動生成的方法,使用了ACC_SYNTHETIC來修飾,方法名為lambda$test2$0。其中包含了我們寫的Lambda表達式中的代碼邏輯。

第一個方法通過InvokeDynamic指令調用了第二個方法,InvokeDynamic指令是在 JDK 7 引入的,用來實現動態類型語言功能,簡單來說就是能夠在運行時去調用實際的代碼。接著重點看一下methodVisitor.visitInvokeDynamicInsn()

// 這行代碼很長,格式化如下
methodVisitor.visitInvokeDynamicInsn(
    "run", 
    "()Ljava/lang/Runnable;", 
    new Handle(
        Opcodes.H_INVOKESTATIC, 
        "java/lang/invoke/LambdaMetafactory", 
        "metafactory", 
        "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", 
        false
    ), 
    new Object[]{
        Type.getType("()V"), 
        new Handle(
            Opcodes.H_INVOKESTATIC, 
            "me/wsj/performance/ui/FuncActivity", 
            // 第二個方法的name
            "lambda$test2$0", 
            "()V", 
            false
        ), 
        Type.getType("()V")
    }
);

MethodType 描述了方法的參數和返回值,MethodHandle 則是根據類名、方法名并且配合 MethodType 來找到特定方法然后執行它;MethodType 和 MethodHandle 配合起來完整表達了一個方法的構成。

鏈接:http://www.lxweimin.com/p/ec19af9b2f19

其中第四個參數new Object[]的第二個參數是一個MethodHandle對象,MethodHandle對象的第二個參數是Lambda方法所在的類名,三個參數lambda$test2$0是Lambda方法的方法名。

那么我們簡單總結一下Lambda編譯后的邏輯就是:根據Lambda表達式的代碼生成一個方法,在調用Lambda表達式的地方調用這個生成的方法。事實上InvokeDynamic指令會創建一個對象,對象內部調用了生成的方法,這里不做深究。

下面分兩種方式實現對Lambda表達式hook:

3,對Lambda表達式hook(一)

和匿名內部類的方法一樣只需要找到Lambda表達式在編譯時代碼生成方法(如lambda$test2$0),在其中插入代碼即可。

class LambdaNodeAdapter(api: Int, val classVisitor: ClassVisitor) : ClassNode(api) {
    init {
        this.cv = classVisitor
    }

    override fun visitEnd() {
        super.visitEnd()
        if (TypeUtil.isNeedWeaveMethod(this.name, access)) {
            val shouldHookMethodList = mutableSetOf<String>()
            for (methodNode in this.methods) {
                // 判斷方法內部是否有需要處理的 lambda 表達式
                val invokeDynamicInsnNodes = methodNode.findHookPointLambda()
                invokeDynamicInsnNodes.forEach {
                    val handle = it.bsmArgs[1] as? Handle
                    if (handle != null) {
                        shouldHookMethodList.add(handle.name + handle.desc)
                    }
                }
            }
            if (shouldHookMethodList.isNotEmpty()) {
                for (methodNode in methods) {
                    val methodNameWithDesc = methodNode.nameWithDesc
                    if (shouldHookMethodList.contains(methodNameWithDesc)) {
                        // 獲取當前方法的指令集
                        val instructions = methodNode.instructions
                        if (instructions != null && instructions.size() > 0) {
                            // 植入代碼
                            val list = InsnList()
                            list.add(
                                MethodInsnNode(
                                    Opcodes.INVOKESTATIC,
                                    "me/wsj/apm/thread/ThreadTracker",
                                    "trackOnce",
                                    "()V",
                                )
                            )
                            // 將要植入的指令集插入當前方法的指令集
                            instructions.insert(list)
                        }
                    }
                }
            }
        }
        accept(cv)
    }
}

1,繼承ClassNode,可以獲取當前類中的所有方法,遍歷每個方法中的指令集找到符合條件的Lambda表達式。

2,在Lambda表達式生成的方法中的指令集中插入Hook代碼的指令集即可實現hook。

該方法存在一定缺陷:1,難以獲取插入位置的行號(可以實現,遍歷方法中的指令集合找到LineNumberNode即可獲取)。2,只能針對Lambda表達式進行Hook,無法對方法引用進行Hook。

詳細代碼可以參考:OutSiderAPM/LambdaNodeAdapter.kt at master · Jinxqq/OutSiderAPM · GitHub

4,對Lambda表達式hook(二)

方案一存在著兩個缺陷,那么能否克服著兩個缺陷呢?

可以通過新建一個方法來代理原方法,然后在代理方法中調用原方法的同時也可以植入我們Hook的代碼。

具體做法是:生成一個新的方法,新的方法中實現 InvokeDynamic指令中描述的代碼邏輯。然后創建新的 MethodHandle,將這個 MethodHandle 替換原本的 MethodHandle。代碼如下:

class LambdaMethodReferAdapter(api: Int, val classVisitor: ClassVisitor) : ClassNode(api) {
    init {
        this.cv = classVisitor
    }

    private val hookSignature = "run()V"

    private val syntheticMethodList = ArrayList<MethodNode>()

    private val counter = AtomicInteger(0)


    override fun visitEnd() {
        super.visitEnd()
        this.methods.forEach { methodNode ->
            val iterator = methodNode.instructions.iterator()
            while (iterator.hasNext()) {
                val node = iterator.next()
                if (node is InvokeDynamicInsnNode) {
                    val desc = node.desc
                    val descType = Type.getType(desc)
                    val samBaseType = descType.returnType
                    // sam 接口名
                    val samBase = samBaseType.descriptor
                    // sam 方法名
                    val samMethodName: String = node.name
                    val bsmArgs: Array<Any> = node.bsmArgs
                    // sam 方法描述符
                    val samMethodType = bsmArgs[0] as Type
                    // sam 實現方法實際參數描述符
                    val implMethodType = bsmArgs[2] as Type
                    // sam name + desc,可以用來辨別是否是需要 Hook 的 lambda 表達式
                    val bsmMethodNameAndDescriptor = samMethodName + samMethodType.descriptor
                    // 判斷是否需要hook
                    if (hookSignature != bsmMethodNameAndDescriptor) {
                        continue
                    }

                    // 中間方法的名稱
                    val middleMethodName =
                        "lambda$" + samMethodName + "\$wsj" + counter.incrementAndGet()
                    // 中間方法的描述符
                    var middleMethodDesc = ""
                    val descArgTypes: Array<Type> = descType.argumentTypes
                    if (descArgTypes.isEmpty()) {
                        middleMethodDesc = implMethodType.descriptor
                    } else {
                        middleMethodDesc = "("
                        for (tmpType in descArgTypes) {
                            middleMethodDesc += tmpType.descriptor
                        }
                        middleMethodDesc += implMethodType.descriptor.replace("(", "")
                    }

                    // INDY原本的handle,將此handle替換成新的handle
                    val oldHandle = bsmArgs[1] as Handle
                    val newHandle = Handle(
                        Opcodes.H_INVOKESTATIC,
                        name, middleMethodName, middleMethodDesc, false
                    )
                    val newDynamicNode = InvokeDynamicInsnNode(
                        node.name,
                        node.desc,
                        node.bsm,
                        samMethodType,
                        newHandle,
                        implMethodType
                    )
                    iterator.remove()
                    iterator.add(newDynamicNode)
                    generateMiddleMethod(oldHandle, middleMethodName, middleMethodDesc)
                }
            }
        }
        
        methods.addAll(syntheticMethodList)
        accept(cv)
    }

    private fun generateMiddleMethod(
        oldHandle: Handle,
        middleMethodName: String,
        middleMethodDesc: String
    ) {
        val methodNode = LambdaMiddleMethodAdapter(this.name,oldHandle, middleMethodName, middleMethodDesc)
        methodNode.visitCode()
        // 添加到中間方法列表
        syntheticMethodList.add(methodNode)
    }
}

在中間方法中執行hook邏輯:

class LambdaMiddleMethodAdapter(
    private val className: String,
    val oldHandle: Handle,
    methodName: String,
    val methodDesc: String?,
) : MethodNode( /* latest api = */Opcodes.ASM8,Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC /*| Opcodes.ACC_SYNTHETIC*/,
    methodName, methodDesc, null, null) {

    override fun visitCode() {
        super.visitCode()
        // 此處執行hook邏輯
        weaveHookCode(this)

        // 此塊 tag 具體可以參考: [https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic)
        var accResult = oldHandle.tag
        when (accResult) {
            Opcodes.H_INVOKEINTERFACE -> accResult = Opcodes.INVOKEINTERFACE
            Opcodes.H_INVOKESPECIAL -> accResult = Opcodes.INVOKESPECIAL // private, this, super 等會調用
            Opcodes.H_NEWINVOKESPECIAL -> {
                // constructors
                accResult = Opcodes.INVOKESPECIAL
                this.visitTypeInsn(Opcodes.NEW, oldHandle.owner)
                this.visitInsn(Opcodes.DUP)
            }
            Opcodes.H_INVOKESTATIC -> accResult = Opcodes.INVOKESTATIC
            Opcodes.H_INVOKEVIRTUAL -> accResult = Opcodes.INVOKEVIRTUAL
        }
        val middleMethodType = Type.getType(methodDesc)
        val argumentsType = middleMethodType.argumentTypes
        if (argumentsType.isNotEmpty()) {
            var loadIndex = 0
            for (tmpType in argumentsType) {
                val opcode = tmpType.getOpcode(Opcodes.ILOAD)
                this.visitVarInsn(opcode, loadIndex)
                loadIndex += tmpType.size
            }
        }
        this.visitMethodInsn(
            accResult,
            oldHandle.owner,
            oldHandle.name,
            oldHandle.desc,
            false
        )
        val returnType = middleMethodType.returnType
        val returnOpcodes = returnType.getOpcode(Opcodes.IRETURN)
        this.visitInsn(returnOpcodes)
        this.visitEnd()
    }


    private fun weaveHookCode(mv: MethodVisitor) {
        // todo 植入代碼
    }
}

最終編譯出來的.class如下:

public class FuncActivity {

    private void test2() {
        (new Thread(FuncActivity::lambda$run$wsj1)).start();
    }

    private static void lambda$run$wsj1() {
        ThreadTracker.trackOnce("me/wsj/performance/ui/FuncActivity by Lambda:");
        lambda$test2$0();
    }
    
}

詳細代碼可以參考:OutSiderAPM/LambdaMethodReferAdapter.kt at master · Jinxqq/OutSiderAPM · GitHub

三,方法引用

有些時候我們可以通過方法引用來簡化Lambda表達式,如下:

public class FuncActivity {
    private void test3(){
        new Thread(MyThreadPool::getInstance).start();
    }
}

直接看其編譯后的ASM如下:

{
methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "test3", "()V", null, null);
methodVisitor.visitCode();
...
methodVisitor.visitTypeInsn(NEW, "java/lang/Thread");
methodVisitor.visitInsn(DUP);
    
methodVisitor.visitInvokeDynamicInsn("run", "()Ljava/lang/Runnable;", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false), new Object[]{Type.getType("()V"), new Handle(Opcodes.H_INVOKESTATIC, "me/wsj/performance/test/CibThreadPool", "getInstance", "()Lme/wsj/performance/test/CibThreadPool;", false), Type.getType("()V")});
    
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);
...
methodVisitor.visitInsn(RETURN);
...
methodVisitor.visitEnd();
}

可見跟Lambda一樣都是調用了InvokeDynamic指令,methodVisitor.visitInvokeDynamicInsn()如下:

methodVisitor.visitInvokeDynamicInsn(
    "run", 
    "()Ljava/lang/Runnable;", 
    new Handle(
        Opcodes.H_INVOKESTATIC, 
        "java/lang/invoke/LambdaMetafactory", 
        "metafactory", 
        "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", 
        false), 
    new Object[]{
        Type.getType("()V"), 
        new Handle(
            Opcodes.H_INVOKESTATIC, 
            "me/wsj/performance/test/CibThreadPool", 
            "getInstance", 
            "()Lme/wsj/performance/test/CibThreadPool;", 
            false), 
        Type.getType("()V")
    }
);

跟Lambda不同的是,這里沒有生成新的方法,因為方法引用是一個現成的方法,可以直接訪問。因此對方法引用進行hook的思路和對Lambda表達式hook一樣,都是生成一個方法,方法內調用源方法,最后替換調用處。

使用同Lambda同樣的方式(方案二),最終結果如下:

public class FuncActivity {

    private void test3() {
        (new Thread(FuncActivity::lambda$run$wsj2)).start();
    }

    private static void lambda$run$wsj2() {
        ThreadTracker.trackOnce("me/wsj/performance/ui/FuncActivity by MethodReference:");
        CibThreadPool.getInstance();
    }
    
}

四,總結

  1. 匿名內部類的方式只需要根據方法名及方法簽名作為hook點,植入代碼即可。

  2. Lambda表達式方式的Hook有兩種方案:

    2.1,找到Lambda表達式編譯生成的方法,在其指令集中植入Hook的代碼指令集即可實現Hook。

    2.2,生成一個中間方法,在這個方法中調用這個 Lambda 編譯時生成的中間方法,然后將自定義的 MethodHandle 指向生成的方法,最后替換掉Bootstrap Mehtod中的MethodHandle,達到偷梁換柱的效果。

  3. 方法引用的方式的思路是:生成一個中間方法,把方法引用里的內容放到生成的中間方法中,然后將自定義的 MethodHandle 指向生成的方法,最后替換掉 Bootstrap Method 中的 MethodHandle,達到偷梁換柱的效果。

版權聲明:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。 本文鏈接:http://www.lxweimin.com/p/aeaacb5bf367

參考:

http://www.lxweimin.com/p/ec19af9b2f19

https://juejin.cn/post/7042328862872567838#heading-3

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

推薦閱讀更多精彩內容