Android性能優(yōu)化系列-騰訊matrix-卡頓監(jiān)控-gradle插件- 字節(jié)碼插樁代碼分析

稀土掘金地址:https://juejin.cn/post/7276412839585415187

前言

對(duì)matrix框架的分析衬衬,第一篇文章 Android性能優(yōu)化系列-騰訊matrix-IO監(jiān)控-IOCanaryPlugin 用來(lái)分析io監(jiān)控與優(yōu)化的方向奄喂。接下來(lái)準(zhǔn)備從卡頓優(yōu)化入手,卡頓是項(xiàng)目中最容易影響用戶體驗(yàn)的一個(gè)問(wèn)題剂府,所以也是至關(guān)重要的一個(gè)優(yōu)化點(diǎn)√甓埽卡頓優(yōu)化功能對(duì)應(yīng)于matrix中的matrix-trace-canary模塊腺占,包含了多方面的卡頓監(jiān)控,如啟動(dòng)監(jiān)控痒谴、慢方法監(jiān)控衰伯、Anr監(jiān)控等等,而這些都依賴于matrix底層的一個(gè)基礎(chǔ)能力-字節(jié)碼插樁积蔚,所以在進(jìn)行卡頓優(yōu)化的代碼分析前意鲸,有必要對(duì)這個(gè)基礎(chǔ)能力的實(shí)現(xiàn)有一個(gè)直觀的了解。

插件入口

在源碼中找到matrix-gradle-plugin這個(gè)模塊库倘,找到插件的入口临扮。
resources/META-INF/gradle-plugins/com.tencent.matrix-plugin.properties

implementation-class=com.tencent.matrix.plugin.MatrixPlugin

搜索MatrixPlugin,開始分析源碼教翩,今天的分析著重于matrix插樁原理杆勇,而不關(guān)注gradle插件的實(shí)現(xiàn),所以有些gradle插件相關(guān)的內(nèi)容會(huì)一筆帶過(guò)饱亿,讀者可以自行搜索相關(guān)內(nèi)容蚜退。

MatrixPlugin-apply

apply是插件執(zhí)行的入口,在這里會(huì)讀取到build.gradle文件中的配置彪笼,配置內(nèi)容包含兩個(gè)方面钻注,一是trace,一是removeUnusedResources, 本篇只分析trace任務(wù)配猫,removeUnusedResources會(huì)在后邊的文章中進(jìn)行分析幅恋。

override fun apply(project: Project) {
    ...
    //進(jìn)入MatrixTasksManager
    MatrixTasksManager().createMatrixTasks(
            project.extensions.getByName("android") as AppExtension,
            project,
            traceExtension,
            removeUnusedResourcesExtension
    )
}

traceExtension和removeUnusedResourcesExtension對(duì)應(yīng)的正是build.gradle中的配置。

matrix {
    trace {
    }
    removeUnusedResources {
    }
}

createMatrixTasks

createMatrixTraceTask和createRemoveUnusedResourcesTask是插件的兩個(gè)核心點(diǎn)泵肄。

fun createMatrixTasks(android: AppExtension,
                      project: Project,
                      traceExtension: MatrixTraceExtension,
                      removeUnusedResourcesExtension: MatrixRemoveUnusedResExtension) {
    createMatrixTraceTask(android, project, traceExtension)
    createRemoveUnusedResourcesTask(android, project, traceExtension)
}

createMatrixTraceTask

方法中針對(duì)不同gradle版本創(chuàng)建了兩個(gè)不同的transform:

  • MatrixTraceTransform
  • MatrixTraceLegacyTransform

最終這兩個(gè)transform會(huì)匯集到一個(gè)入口捆交,那就是MatrixTrace.

MatrixTrace(
        ignoreMethodMapFilePath = config.ignoreMethodMapFilePath,
        methodMapFilePath = config.methodMapFilePath,
        baseMethodMapPath = config.baseMethodMapPath,
        blockListFilePath = config.blockListFilePath,
        mappingDir = config.mappingDir,
        project = project
).doTransform(
        classInputs = inputFiles,
        changedFiles = changedFiles,
        isIncremental = isIncremental,
        skipCheckClass = config.skipCheckClass,
        traceClassDirectoryOutput = outputDirectory,
        inputToOutput = inputToOutput,
        legacyReplaceChangedFile = null,
        legacyReplaceFile = null,
        uniqueOutputName = true
)

來(lái)看doTransform方法淑翼,官方注釋很清楚,分為關(guān)鍵的三步品追,接下來(lái)我們一步一步來(lái)讀一下代碼玄括。

fun doTransform() {
    ...
    /**
     * step 1
     */
    futures.add(executor.submit(ParseMappingTask(
            mappingCollector, collectedMethodMap, methodId, config)))
    for (file in classInputs) {
        if (file.isDirectory) {
            futures.add(executor.submit(CollectDirectoryInputTask()))
        } else {
            futures.add(executor.submit(CollectJarInputTask()))
        }
    }

    /**
     * step 2
     */
    val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
    methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)

    /**
     * step 3
     */
    val methodTracer = MethodTracer(executor, mappingCollector, config, methodCollector.collectedMethodMap, methodCollector.collectedClassExtendMap)
    methodTracer.trace(dirInputOutMap, jarInputOutMap, traceClassLoader, skipCheckClass)
}

第一步

包含三項(xiàng)任務(wù)

ParseMappingTask

這個(gè)任務(wù)是用來(lái)解析mapping.txt文件的,通過(guò)調(diào)用一個(gè)名為MappingReader的類去解析文件肉瓦,解析的內(nèi)容又可以分為類解析和類成員解析遭京。mapping文件解析之后,我們就獲得了混淆前和混淆后類的映射關(guān)系以及混淆前和混淆后方法的映射關(guān)系泞莉。

val mappingFile = File(config.mappingDir, "mapping.txt")
if (mappingFile.isFile) {
    val mappingReader = MappingReader(mappingFile)
    mappingReader.read(mappingCollector)
}

mappingReader.read()

if (!line.startsWith("#")) {
    // a class mapping
    if (line.endsWith(":")) {
        className = parseClassMapping(line, mappingProcessor);
    } else if (className != null) { 
        // a class member mapping
        parseClassMemberMapping(className, line, mappingProcessor);
    }
} 
parseClassMapping

解析出混淆前的類名和混淆后的類名哪雕,將映射關(guān)系保存在MappingProcessor(實(shí)現(xiàn)類MappingCollector)映射表中,對(duì)應(yīng)于下邊的三個(gè)map戒财。

private String parseClassMapping(String line, MappingProcessor mappingProcessor) {
    ...
    boolean ret = mappingProcessor.processClassMapping(className, newClassName);
}

這兩個(gè)集合中的className可能包含包名

HashMap<String, String> mObfuscatedRawClassMap

key value
混淆后的類名 原類名

HashMap<String, String> mRawObfuscatedClassMap

key value
原類名 混淆后的類名

HashMap<String, String> mRawObfuscatedPackageMap

key value
包名 混淆后的包名
parseClassMemberMapping

邏輯也是比較直接的热监,解析出每個(gè)類下的方法方法信息, 最終還是保存在了MappingProcessor中的映射表中,對(duì)應(yīng)于下邊的兩個(gè)map饮寞。

private void parseClassMemberMapping(String className, String line, MappingProcessor mappingProcessor) {
    ...
    mappingProcessor.processMethodMapping(className, type, name, arguments, newClassName, newName);
}

Map<String, Map<String, Set<MethodInfo>>> mObfuscatedClassMethodMap

這個(gè)map用來(lái)記錄一個(gè)類中所有的方法信息

key value
混淆后的類名為key 一個(gè)以混淆后的方法名為key, 以MethodInfo集合為value的map(注意:MethodInfo中的類名方法名都是未混淆的)

Map<String, Map<String, Set<MethodInfo>>> mOriginalClassMethodMap

key value
未混淆的類名為key 一個(gè)未混淆的方法名為key, 以MethodInfo集合為value的map(注意:MethodInfo中的類名方法名都是混淆后的)

下面兩個(gè)任務(wù)針對(duì)directory和jar類型的文件分別處理

CollectDirectoryInputTask

此任務(wù)的輸入是一個(gè)map映射表, 記錄輸入到數(shù)據(jù)的映射關(guān)系孝扛,對(duì)于支持增量編譯的情況下,記錄的是所有發(fā)生改變的文件的映射幽崩,未改變的文件不做記錄苦始。

resultOfDirInputToOut: MutableMap<File, File>

CollectJarInputTask

這個(gè)類的作用也類似,只不過(guò)操作對(duì)象是一個(gè)jar包慌申,同樣輸入一個(gè)map集合記錄映射關(guān)系陌选。

第二步

MethodCollector

MethodCollector是用來(lái)收集所有需要被trace的方法的,它會(huì)過(guò)濾掉一些沒(méi)有trace價(jià)值的方法蹄溉,如構(gòu)造方法咨油、空方法,get柒爵、set方法等役电,還有一些指定不需要trace的類或者指定包下的類也會(huì)被過(guò)濾掉。

注意棉胀,methodId是一個(gè)比較關(guān)鍵的點(diǎn)法瑟,是一個(gè)從0開始遞增的值,每一個(gè)值表示一個(gè)方法名唁奢,用數(shù)字表示方法名霎挟,并記錄數(shù)字和方法名映射關(guān)系,用于后期分析時(shí)解析麻掸,是matrix內(nèi)很巧妙的一個(gè)做法酥夭,感興趣可以深入研究一下,這里不過(guò)多的解釋了。

val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)

collect方法

  • 從文件夾中遍歷得到所有文件采郎,針對(duì)每個(gè)文件執(zhí)行CollectSrcTask任務(wù)
  • 遍歷所有jar,針對(duì)每個(gè)jar執(zhí)行CollectJarTask
  • 上邊兩個(gè)任務(wù)執(zhí)行完成后千所,再執(zhí)行saveIgnoreCollectedMethod和saveCollectedMethod,等待全部完成后返回蒜埋。
public void collect(Set<File> srcFolderList, Set<File> dependencyJarList) throws ExecutionException, InterruptedException {
    ...
    futures.add(executor.submit(new CollectSrcTask(classFile)));
    ...
    futures.add(executor.submit(new CollectJarTask(jarFile)));
    ...
    saveIgnoreCollectedMethod(mappingCollector);
    ...
    saveCollectedMethod(mappingCollector);
}
CollectSrcTask该贾、CollectJarTask

相同的邏輯别凤,只不過(guò)一個(gè)針對(duì)class杏节,一個(gè)針對(duì)jar包须鼎。

這里使用了Asm(一個(gè)字節(jié)碼插樁的庫(kù)低剔,自行百度了解其用法亲善,這里默認(rèn)讀者具備了相關(guān)知識(shí))向臀,所以關(guān)鍵操作在TraceClassAdapter中工三。

InputStream is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new TraceClassAdapter(AgpCompat.getAsmApi(), classWriter);
classReader.accept(visitor, 0);
TraceClassAdapter
private class TraceClassAdapter extends ClassVisitor {

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        //如果是接口或者抽象類犯建,則isABSClass為true
        if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
            this.isABSClass = true;
        }
        //又一個(gè)map記錄類和父類
        collectedClassExtendMap.put(className, superName);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        //抽象類和接口被繞過(guò)讲冠,不做處理
        if (isABSClass) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        } else {
            //這里是記錄類中是否含有onWindowFocusChanged,這是Activity的一個(gè)回調(diào)适瓦,matrix將此用于頁(yè)面可見的記錄時(shí)機(jī)竿开。
            if (!hasWindowFocusMethod) {
                hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);
            }
            //進(jìn)入CollectMethodNode中
            return new CollectMethodNode(className, access, name, desc, signature, exceptions);
        }
    }
}
CollectMethodNode

只看它的visitEnd方法

@Override
public void visitEnd() {
    super.visitEnd();
    TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);

    if ("<init>".equals(name)) {
        isConstructor = true;
    }
    //判斷方法是否需要插樁,哪些方法可以插樁呢玻熙,看下邊
    boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
    // 空方法否彩,get、set方法嗦随,single method都會(huì)被過(guò)濾掉列荔,并記錄數(shù)量,加入map中存儲(chǔ)
    if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
            && isNeedTrace) {
        ignoreCount.incrementAndGet();
        collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
        return;
    }

    if (isNeedTrace && !collectedMethodMap.containsKey(traceMethod.getMethodName())) {
        //需要插樁的方法記錄到collectedMethodMap中
        traceMethod.id = methodId.incrementAndGet();
        collectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
        incrementCount.incrementAndGet();
    } else if (!isNeedTrace && !collectedIgnoreMethodMap.containsKey(traceMethod.className)) {
        ignoreCount.incrementAndGet();
        //不需要插樁的方法記錄到collectedIgnoreMethodMap中
        collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
    }

}

isNeedTrace

public static boolean isNeedTrace(Configuration configuration, String clsName, MappingCollector mappingCollector) {
    boolean isNeed = true;
    //指定不需要插樁的方法枚尼,過(guò)濾掉
    if (configuration.blockSet.contains(clsName)) {
        isNeed = false;
    } else {
        if (null != mappingCollector) {
            //從上邊的分析贴浙,我們知道,這是從mObfuscatedRawClassMap中獲取未混淆的方法名
            clsName = mappingCollector.originalClassName(clsName, clsName);
        }
        clsName = clsName.replaceAll("/", ".");
        for (String packageName : configuration.blockSet) {
            //指定包名下的類也被過(guò)濾掉
            if (clsName.startsWith(packageName.replaceAll("/", "."))) {
                isNeed = false;
                break;
            }
        }
    }
    return isNeed;
}

最終還是產(chǎn)出了兩個(gè)map映射表:collectedMethodMap署恍、collectedIgnoreMethodMap崎溃,結(jié)構(gòu)相同,但是代表的含義不同锭汛,collectedMethodMap存儲(chǔ)需要被插樁的方法笨奠,collectedIgnoreMethodMap存儲(chǔ)不需要插樁的方法。

key value
方法名 TraceMethod

此時(shí)再回頭看看MethodCollector這個(gè)類的確像它的命名一樣唤殴,只是方法的收集者般婆,并不做插樁,而真正執(zhí)行插樁的朵逝,是MethodTracer蔚袍。

saveIgnoreCollectedMethod

邏輯很簡(jiǎn)單,就是將上邊收集到的collectedIgnoreMethodMap中記錄的不需要插樁的方法信息寫入到指定的ignoreMethodMapFilePath文件中,方便查找啤咽,不屬于本次分析的核心晋辆,不做過(guò)多解釋。

private void saveIgnoreCollectedMethod(MappingCollector mappingCollector) {
    ...
}
saveCollectedMethod

和saveIgnoreCollectedMethod方法類似宇整,只不過(guò)保存的是collectedMethodMap中的方法信息瓶佳,保存在配置的methodMapFilePath文件路徑中。有些特殊的是鳞青,它主動(dòng)將Handler的dispatchMessage也加進(jìn)去了霸饲,目的是什么,暫不看了臂拓。

private void saveCollectedMethod(MappingCollector mappingCollector) {
    ...
    TraceMethod extra = TraceMethod.create(TraceBuildConstants.METHOD_ID_DISPATCH, Opcodes.ACC_PUBLIC, "android.os.Handler",
        "dispatchMessage", "(Landroid.os.Message;)V");
    collectedMethodMap.put(extra.getMethodName(), extra);
    ...
}

第三步

接下來(lái)才是整個(gè)插件的重中之重厚脉,真正要開始插樁了

MethodTracer

trace

public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList, ClassLoader classLoader, boolean ignoreCheckClass) throws ExecutionException, InterruptedException {
    ...
    traceMethodFromSrc(srcFolderList, futures, classLoader, ignoreCheckClass);
    traceMethodFromJar(dependencyJarList, futures, classLoader, ignoreCheckClass);
    ...
}

traceMethodFromSrc

只保留核心代碼

private void innerTraceMethodFromSrc(File input, File output, ClassLoader classLoader, boolean ignoreCheckClass) {
    ...
    is = new FileInputStream(classFile);
    ClassReader classReader = new ClassReader(is);
    ClassWriter classWriter = new TraceClassWriter(ClassWriter.COMPUTE_FRAMES, classLoader);
    ClassVisitor classVisitor = new TraceClassAdapter(AgpCompat.getAsmApi(), classWriter);
    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
    is.close();
    
    ...

    if (!ignoreCheckClass) {
        try {
             ClassReader cr = new ClassReader(data);
             ClassWriter cw = new ClassWriter(0);
             ClassVisitor check = new CheckClassAdapter(cw);
             cr.accept(check, ClassReader.EXPAND_FRAMES);
        } catch (Throwable e) {

        }
    }
    ...
}

TraceClassAdapter

private class TraceClassAdapter extends ClassVisitor {

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
        this.isActivityOrSubClass = isActivityOrSubClass(className, collectedClassExtendMap);
        this.isNeedTrace = MethodCollector.isNeedTrace(configuration, className, mappingCollector);
        if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
            this.isABSClass = true;
        }

    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        //類中是否包含onWindowFocusChanged方法,Activity中的方法胶惰,matrix將它作為頁(yè)面可見的時(shí)機(jī)傻工。
        if (!hasWindowFocusMethod) {
            hasWindowFocusMethod = MethodCollector.isWindowFocusChangeMethod(name, desc);
        }
        //是否是抽象類或接口,是則直接跳過(guò)孵滞,不插樁
        if (isABSClass) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        } else {
            MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
            return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
                    hasWindowFocusMethod, isActivityOrSubClass, isNeedTrace);
        }
    }
}

TraceMethodAdapter

private class TraceMethodAdapter extends AdviceAdapter {
    ...
    @Override
    protected void onMethodEnter() {
        //在方法入口插入AppMethodBeat.i(timestamp)方法

        TraceMethod traceMethod = collectedMethodMap.get(methodName);
        if (traceMethod != null) {
            traceMethodCount.incrementAndGet();
            mv.visitLdcInsn(traceMethod.id);
            mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);

            if (checkNeedTraceWindowFocusChangeMethod(traceMethod)) {
                //在onWindowFocusChanged方法插入AppMethodBeat.at(timestamp)方法中捆,
                //用于記錄onWindowFocusChanged的執(zhí)行時(shí)間,分析啟動(dòng)耗時(shí)剃斧。
                traceWindowFocusChangeMethod(mv, className);
            }
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        //在方法出口插入AppMethodBeat.o(timestamp)方法
        TraceMethod traceMethod = collectedMethodMap.get(methodName);
        if (traceMethod != null) {
            traceMethodCount.incrementAndGet();
            mv.visitLdcInsn(traceMethod.id);
            mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
        }
    }
}

總結(jié)

插件的核心內(nèi)容在于TraceMethodAdapter中的三項(xiàng)操作:

  • 在符合條件的方法入口插入AppMethodBeat.i()
  • 在符合條件的方法出口插入AppMethodBeat.o()
  • 在Activity的onWindowFocusChanged方法中插入AppMethodBeat.at()

在編譯期間轨香,除被排除掉的方法外,大量方法的入口和出口處被插入了AppMethodBeat的方法幼东,意在能通過(guò)這兩個(gè)方法計(jì)算出方法執(zhí)行的耗時(shí)臂容,于是,每一個(gè)方法執(zhí)行的耗時(shí)情況就清晰的展現(xiàn)在我們開發(fā)者眼前根蟹,借助這些數(shù)據(jù)才能更好的發(fā)現(xiàn)卡頓問(wèn)題的原因脓杉。

有了這些基礎(chǔ),后邊進(jìn)行啟動(dòng)優(yōu)化或者anr分析的時(shí)候就有跡可循简逮,matrix會(huì)幫我們將方法按耗時(shí)時(shí)長(zhǎng)排列出來(lái)球散,方便我們有的放矢的去解決問(wèn)題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末散庶,一起剝皮案震驚了整個(gè)濱河市蕉堰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悲龟,老刑警劉巖屋讶,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異须教,居然都是意外死亡皿渗,警方通過(guò)查閱死者的電腦和手機(jī)斩芭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)乐疆,“玉大人划乖,你說(shuō)我怎么就攤上這事〖吠粒” “怎么了琴庵?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)仰美。 經(jīng)常有香客問(wèn)我细卧,道長(zhǎng),這世上最難降的妖魔是什么筒占? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮蜘犁,結(jié)果婚禮上翰苫,老公的妹妹穿的比我還像新娘。我一直安慰自己这橙,他們只是感情好奏窑,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著屈扎,像睡著了一般埃唯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹰晨,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天墨叛,我揣著相機(jī)與錄音,去河邊找鬼模蜡。 笑死漠趁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的忍疾。 我是一名探鬼主播闯传,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼卤妒!你這毒婦竟也來(lái)了甥绿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤则披,失蹤者是張志新(化名)和其女友劉穎共缕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體收叶,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骄呼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜓萄。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隅茎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嫉沽,到底是詐尸還是另有隱情辟犀,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布绸硕,位于F島的核電站堂竟,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏玻佩。R本人自食惡果不足惜出嘹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咬崔。 院中可真熱鬧税稼,春花似錦、人聲如沸垮斯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)兜蠕。三九已至扰肌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間熊杨,已是汗流浹背曙旭。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留猴凹,地道東北人夷狰。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像郊霎,于是被迫代替她去往敵國(guó)和親沼头。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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