Gradle+ASM實(shí)戰(zhàn)——進(jìn)階篇

前言

  • 上篇文章我寫了入門篇:Gradle 插件 + ASM 實(shí)戰(zhàn)——入門篇,對gradle+ASM不熟的大家可以去上篇文章查看

  • github地址:https://github.com/Peakmain/AsmActualCombat

  • Gradle Transform
    Gradle Transform是Android官方提供給開發(fā)者在項(xiàng)目構(gòu)建階段(.class -> .dex轉(zhuǎn)換期間)用來修改.class文件的一套標(biāo)準(zhǔn)API,即把輸入的.class文件轉(zhuǎn)變成目標(biāo)字節(jié)碼文件


    image.png
  • ClassVisitor
    訪問類的成員信息

模板搭建

  • 修改訪問入口BuryPointPlugin
package com.peakmain.analytics.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class BuryPointPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        BuryPointExtension extension = project.extensions.create("peakmainPlugin", BuryPointExtension)
        boolean disableBuryPointPlugin = false
        Properties properties = new Properties()
        //gradle.properties是否存在
        if(project.rootProject.file('gradle.properties').exists()){
             //gradle.properties文件->輸入流
            properties.load(project.rootProject.file('gradle.properties').newDataInputStream())
            disableBuryPointPlugin=Boolean.parseBoolean(properties.getProperty("peakmainPlugin.disableAppClick","false"))
        }
        //如果disableBuryPointPlugin可用
        if(!disableBuryPointPlugin){
            AppExtension appExtension = project.extensions.findByType(AppExtension.class)
            appExtension.registerTransform(new BuryPointTransform(project,extension))
        }else{
            println("------------您已關(guān)閉了埋點(diǎn)插件--------------")
        }
    }
}
  • BuryPointTransform
package com.peakmain.analytics.plugin

import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
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.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import org.objectweb.asm.ClassVisitor
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.gradle.api.Project
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

class BuryPointTransform extends Transform {
    private static Project project
    private BuryPointExtension buryPointExtension

    BuryPointTransform(Project project, BuryPointExtension buryPointExtension) {
        this.project = project
        this.buryPointExtension = buryPointExtension
    }

    @Override
    String getName() {
        return "BuryPoint"
    }
    /**
     * 需要處理的數(shù)據(jù)類型网严,有兩種枚舉類型
     * CLASS->處理的java的class文件
     * RESOURCES->處理java的資源
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    /**
     * 指 Transform 要操作內(nèi)容的范圍婉陷,官方文檔 Scope 有 7 種類型:
     * 1. EXTERNAL_LIBRARIES        只有外部庫
     * 2. PROJECT                   只有項(xiàng)目內(nèi)容
     * 3. PROJECT_LOCAL_DEPS        只有項(xiàng)目的本地依賴(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或遠(yuǎn)程依賴項(xiàng)
     * 5. SUB_PROJECTS              只有子項(xiàng)目肛冶。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子項(xiàng)目的本地依賴項(xiàng)(本地jar)敷扫。
     * 7. TESTED_CODE               由當(dāng)前變量(包括依賴項(xiàng))測試的代碼
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
    /**
     * 是否增量編譯
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        _transform(transformInvocation.context, transformInvocation.inputs, transformInvocation.outputProvider)
    }
    /**
     *
     * @param context
     * @param inputs 有兩種類型秕衙,一種是目錄,一種是 jar 包掷匠,要分開遍歷
     * @param outputProvider 輸出路徑
     */
    void _transform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider) throws IOException, TransformException, InterruptedException {
        if (!incremental) {
            //不是增量更新刪除所有的outputProvider
            outputProvider.deleteAll()
        }
        inputs.each { TransformInput input ->
            //遍歷目錄
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            // 遍歷jar 第三方引入的 class
            input.jarInputs.each { JarInput jarInput ->
                handleJarInput(jarInput, outputProvider)
            }
        }
    }

    void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        if (directoryInput.file.isDirectory()) {
            directoryInput.file.eachFileRecurse { File file ->
                String name = file.name
                if (filterClass(name)) {
                    // 用來讀 class 信息
                    ClassReader classReader = new ClassReader(file.bytes)
                    // 用來寫
                    ClassWriter classWriter = new ClassWriter(0 /* flags */)
                    //todo 改這里就可以了
                    ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
                    // 下面還可以包多層
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    // 重新覆蓋寫入文件
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(
                            file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }
        // 把修改好的數(shù)據(jù)滥崩,寫入到 output
        def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
                directoryInput.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.absolutePath.endsWith(".jar")) {
            // 重名名輸出文件,因?yàn)榭赡芡?會覆蓋
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //插樁class
                if (filterClass(entryName)) {
                    //class文件處理
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(0)
                    //todo 改這里就可以了
                    ClassVisitor classVisitor = new BuryPointVisitor(classWriter)
                    // 下面還可以包多層
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //結(jié)束
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

    boolean filterClass(String className) {
        return (className.endsWith(".class") && !className.startsWith("R\$")
                && "R.class" != className && "BuildConfig.class" != className)
    }
}

不同項(xiàng)目只需要TODO位置就可以了

  • BuryPointVisitor
class BuryPointVisitor extends ClassVisitor {

    private ClassVisitor classVisitor
    private String[] mInterfaces
    BuryPointVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM6, classVisitor)
        this.classVisitor = classVisitor
    }
    /**
     * 掃描類的時(shí)候進(jìn)入這里
     * @param version 類版本
     * @param access 修飾符
     * @param name 類名
     * @param signature 泛型信息
     * @param superName 父類
     * @param interfaces 實(shí)現(xiàn)的接口
     */
    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            println("name->"+name+",superName->"+superName)
        super.visit(version, access, name, signature, superName, interfaces)
        this.mInterfaces=interfaces
    }
}
  • 編譯結(jié)果


    image.png

ClassVisitor方法詳解

  • visit方法:掃描類的時(shí)候會進(jìn)入這里,它的作用:可以替換一些類讹语,比如ImageView
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        if(superName.equals("android/widget/ImageView")
                && !name.equals("com/peakmain/PeakmainImageView")){
          superName = "com/peakmain/PeakmainImageView";
        }
        super.visit(version, access, name, signature, superName, interfaces);
    }
  • visitMethod:掃描到方法的時(shí)候調(diào)用
    /**
     * 掃描類的方法進(jìn)行調(diào)用
     * @param access 修飾符
     * @param name 方法名字
     * @param descriptor 方法簽名
     * @param signature 泛型信息
     * @param exceptions 拋出的異常
     * @return
     */
    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        println("name->"+name+"----------------------descriptor->"+descriptor)
        return methodVisitor
    }
  • visitVarInsn
    aload 0 相當(dāng)于字節(jié)碼mv.visitVarInsn(ALOAD, 0);加載局部變量表下標(biāo)0位置對象到操作數(shù)棧

  • visitMethodInsn
    執(zhí)行方法

private final static String SDK_API_CLASS = "com/peakmain/sdk/SensorsDataAutoTrackHelper"
  methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)

等價(jià)于:執(zhí)行SensorsDataAutoTrackHelper的靜態(tài)方法trackViewOnClick參數(shù)是View view

  • onMethodEnter
    方法執(zhí)行之前插入字節(jié)碼代碼
            protected void onMethodEnter() {
                super.onMethodEnter()
                if(name == "sendMessageAtTime"){
                    println("進(jìn)入sendMessageAtTime")
                    methodVisitor.visitLdcInsn("TAG");
                    methodVisitor.visitLdcInsn("PeakmainHandler->sendMessageAtTime")
                    methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);

                }
}

相當(dāng)于在SendMessageAtTime方法之前插入了

 Log.e("TAG", "PeakmainHandler->sendMessageAtTime");
image.png
  • visitInsn:方法return返回之前插入字節(jié)碼,
  • visitCode:方法調(diào)用前插入字節(jié)碼,并在onMethodEnter之后插入字節(jié)碼

實(shí)戰(zhàn)

注解方式獲取方法消耗的時(shí)間

    private void getMessageStartCostTime(MethodVisitor methodVisitor) {
        methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        methodVisitor.visitVarInsn(LSTORE, 1)
        Label label1 = new Label()
        methodVisitor.visitLabel(label1)
    }
    private void getMessageEndCostTime(MethodVisitor methodVisitor, String name) {
        methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        methodVisitor.visitVarInsn(LLOAD, 1)
        methodVisitor.visitInsn(LSUB)
        methodVisitor.visitVarInsn(LSTORE, 2)
        Label label2 = new Label();
        methodVisitor.visitLabel(label2)
        methodVisitor.visitLdcInsn("LogMessageCostTime")
        methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
        methodVisitor.visitInsn(DUP);
        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
        methodVisitor.visitLdcInsn(name + "消耗的時(shí)間:")
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        methodVisitor.visitVarInsn(LLOAD, 2)
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
        methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false)
        methodVisitor.visitInsn(POP);
        Label label3 = new Label()
        methodVisitor.visitLabel(label3)
    }

編譯之后結(jié)果:


image.png

實(shí)現(xiàn)點(diǎn)擊事件的埋點(diǎn)
這里只截取部分代碼钙皮,完整代碼可查看github

    protected void onMethodEnter() {
                super.onMethodEnter()

                /**
                 * 在 android.gradle 的 3.2.1 版本中,針對 view 的 setOnClickListener 方法 的 lambda 表達(dá)式做特殊處理。
                 */
                BuryPointMethodCell lambdaMethodCell = mMethodCells.get(nameDesc)
                if (lambdaMethodCell != null) {
                    Type[] types = Type.getArgumentTypes(lambdaMethodCell.desc)
                    int length = types.length
                    Type[] lambdaTypes = Type.getArgumentTypes(descriptor)
                    int paramStart = lambdaTypes.length - length
                    if (paramStart < 0) {
                        return
                    } else {
                        for (int i = 0; i < length; i++) {
                            if (lambdaTypes[paramStart + i].descriptor != types[i].descriptor) {
                                return
                            }
                        }
                    }
                    boolean isStaticMethod = SensorsAnalyticsUtils.isStatic(access)
                    if (!isStaticMethod) {
                        if (lambdaMethodCell.desc == '(Landroid/view/MenuItem;)Z') {
                            methodVisitor.visitVarInsn(ALOAD, 0)
                            methodVisitor.visitVarInsn(ALOAD, getVisitPosition(lambdaTypes, paramStart, isStaticMethod))
                            methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, lambdaMethodCell.agentName, '(Ljava/lang/Object;Landroid/view/MenuItem;)V', false)
                            return
                        }
                    }

                    for (int i = paramStart; i < paramStart + lambdaMethodCell.paramsCount; i++) {
                        methodVisitor.visitVarInsn(lambdaMethodCell.opcodes.get(i - paramStart), getVisitPosition(lambdaTypes, i, isStaticMethod))
                    }
                    methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, lambdaMethodCell.agentName, lambdaMethodCell.agentDesc, false)
                    return
                }

                if (nameDesc == 'onContextItemSelected(Landroid/view/MenuItem;)Z' ||
                        nameDesc == 'onOptionsItemSelected(Landroid/view/MenuItem;)Z') {
                    methodVisitor.visitVarInsn(ALOAD, 0)
                    methodVisitor.visitVarInsn(ALOAD, 1)
                    methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Ljava/lang/Object;Landroid/view/MenuItem;)V", false)
                }

                if (isSensorsDataTrackViewOnClickAnnotation) {
                    if (desc == '(Landroid/view/View;)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                        return
                    }
                }

                if ((mInterfaces != null && mInterfaces.length > 0)) {
                    if ((mInterfaces.contains('android/view/View$OnClickListener') && nameDesc == 'onClick(Landroid/view/View;)V')) {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                    } else if (mInterfaces.contains('android/content/DialogInterface$OnClickListener') && nameDesc == 'onClick(Landroid/content/DialogInterface;I)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ILOAD, 2)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/content/DialogInterface;I)V", false)
                    } else if (mInterfaces.contains('android/content/DialogInterface$OnMultiChoiceClickListener') && nameDesc == 'onClick(Landroid/content/DialogInterface;IZ)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ILOAD, 2)
                        methodVisitor.visitVarInsn(ILOAD, 3)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/content/DialogInterface;IZ)V", false)
                    } else if (mInterfaces.contains('android/widget/CompoundButton$OnCheckedChangeListener') && nameDesc == 'onCheckedChanged(Landroid/widget/CompoundButton;Z)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ILOAD, 2)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/CompoundButton;Z)V", false)
                    } else if (mInterfaces.contains('android/widget/RatingBar$OnRatingBarChangeListener') && nameDesc == 'onRatingChanged(Landroid/widget/RatingBar;FZ)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                    } else if (mInterfaces.contains('android/widget/SeekBar$OnSeekBarChangeListener') && nameDesc == 'onStopTrackingTouch(Landroid/widget/SeekBar;)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
                    } else if (mInterfaces.contains('android/widget/AdapterView$OnItemSelectedListener') && nameDesc == 'onItemSelected(Landroid/widget/AdapterView;Landroid/view/View;IJ)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ALOAD, 2)
                        methodVisitor.visitVarInsn(ILOAD, 3)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/AdapterView;Landroid/view/View;I)V", false)
                    } else if (mInterfaces.contains('android/widget/TabHost$OnTabChangeListener') && nameDesc == 'onTabChanged(Ljava/lang/String;)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackTabHost", "(Ljava/lang/String;)V", false)
                    } else if (mInterfaces.contains('android/widget/AdapterView$OnItemClickListener') && nameDesc == 'onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ALOAD, 2)
                        methodVisitor.visitVarInsn(ILOAD, 3)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/widget/AdapterView;Landroid/view/View;I)V", false)
                    } else if (mInterfaces.contains('android/widget/ExpandableListView$OnGroupClickListener') && nameDesc == 'onGroupClick(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ALOAD, 2)
                        methodVisitor.visitVarInsn(ILOAD, 3)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackExpandableListViewGroupOnClick", "(Landroid/widget/ExpandableListView;Landroid/view/View;I)V", false)
                    } else if (mInterfaces.contains('android/widget/ExpandableListView$OnChildClickListener') && nameDesc == 'onChildClick(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z') {
                        methodVisitor.visitVarInsn(ALOAD, 1)
                        methodVisitor.visitVarInsn(ALOAD, 2)
                        methodVisitor.visitVarInsn(ILOAD, 3)
                        methodVisitor.visitVarInsn(ILOAD, 4)
                        methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackExpandableListViewChildOnClick", "(Landroid/widget/ExpandableListView;Landroid/view/View;II)V", false)
                    }
                }

            }
  • 參考書籍:《Android全埋點(diǎn)解決方案》
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末短条,一起剝皮案震驚了整個(gè)濱河市导匣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茸时,老刑警劉巖贡定,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異可都,居然都是意外死亡缓待,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門渠牲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旋炒,“玉大人,你說我怎么就攤上這事签杈√闭颍” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵答姥,是天一觀的道長铣除。 經(jīng)常有香客問我,道長鹦付,這世上最難降的妖魔是什么尚粘? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮睁壁,結(jié)果婚禮上背苦,老公的妹妹穿的比我還像新娘。我一直安慰自己潘明,他們只是感情好行剂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著钳降,像睡著了一般厚宰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上遂填,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天铲觉,我揣著相機(jī)與錄音,去河邊找鬼吓坚。 笑死撵幽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的礁击。 我是一名探鬼主播盐杂,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼逗载,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了链烈?” 一聲冷哼從身側(cè)響起厉斟,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎强衡,沒想到半個(gè)月后擦秽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡漩勤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年感挥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锯七。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡链快,死狀恐怖誉己,靈堂內(nèi)的尸體忽然破棺而出眉尸,到底是詐尸還是另有隱情,我是刑警寧澤巨双,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布噪猾,位于F島的核電站,受9級特大地震影響筑累,放射性物質(zhì)發(fā)生泄漏袱蜡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一慢宗、第九天 我趴在偏房一處隱蔽的房頂上張望坪蚁。 院中可真熱鬧,春花似錦镜沽、人聲如沸敏晤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嘴脾。三九已至,卻和暖如春蔬墩,著一層夾襖步出監(jiān)牢的瞬間译打,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工拇颅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奏司,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓樟插,卻偏偏與公主長得像韵洋,于是被迫代替她去往敵國和親哥谷。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

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