AOP : APT 和 ASM 紡織代碼

AOP 中, 我們以處理階段為劃分產(chǎn)生了很多可選的技術(shù)手段:

  • java 源代碼階段 (apt 、 ksp、 java)
  • class 字節(jié)碼階段 (asm javaassist)
  • dex 階段 (tinker)

apt 處理的是 java 源代碼文件,項(xiàng)目中若有很多類具有相似的樣板代碼, 可以考慮將這些樣板代碼在編譯期間進(jìn)行處理。 常常會(huì)搭配 javapoet 來編譯期間生成一些樣板類, 解放手工

asm 處理的是 class 文件, 比如做一些代碼插樁, 映射采集 等字節(jié)碼增強(qiáng)和生成

apt

apt 簡(jiǎn)單來說做的工作: 通過輸入(java文件), 找到帶有需要處理的注解的元素, 讀取這些注解的信息, 為后續(xù)的 代碼植入做準(zhǔn)備。

apt 是 gradle build 階段一個(gè) task 觸發(fā)的

正常執(zhí)行下 app:assembleDebug 觸發(fā)的 gradle task 如下:

Starting Gradle Daemon...
Gradle Daemon started in 1 s 286 ms
> Task :annotation:compileKotlin UP-TO-DATE
> Task :annotation:compileJava UP-TO-DATE
> Task :annotation:compileGroovy NO-SOURCE
> Task :annotation:processResources UP-TO-DATE
> Task :annotation:classes UP-TO-DATE
> Task :annotation:inspectClassesForKotlinIC UP-TO-DATE
> Task :annotation:jar UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
> Task :app:kaptDebugKotlin UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:compileDebugSources UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:mergeProjectDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE

Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
Task :app:kaptDebugKotlin UP-TO-DATE

就是 apt 的位置, apt 后才會(huì)生成 class 文件, 進(jìn)一步dex , 最后 package

具體 apt 的代碼都寫在 AbstractProcessor 的實(shí)現(xiàn)類中

該類中主要常用的幾個(gè)元素
    override fun init(processingEnvironment: ProcessingEnvironment?) {
        super.init(processingEnvironment)
        mTypeUtil = processingEnvironment?.getTypeUtils()
        mElementUtil = processingEnvironment?.getElementUtils()
        mFiler = processingEnvironment?.getFiler()
        mMessager = processingEnvironment?.getMessager()
    }
    override fun process(
        set: MutableSet<out TypeElement>,
        processingEnvironment: RoundEnvironment
    ): Boolean {
        // 具體 apt 代碼
}

process 中, 可以根據(jù) RoundEnvironment 可以取到所有帶有某個(gè)注釋的 類、接口、方法

TypeElement 是 類/接口
Elements 是一個(gè) 工具類, 常用來獲取所有帶某個(gè)注解的元素如: 所有方法

一般流程:


整個(gè)過程來說:

    1. 解析注解
    1. 構(gòu)造一個(gè)數(shù)據(jù)結(jié)構(gòu)保存注解中的有效信息

javapoet

習(xí)慣語法即可

首先寫一個(gè) javaFile涉及到的核心步驟:

  • 類相關(guān) TypeSpec
  • 構(gòu)造函數(shù) MethodSpec
  • 成員變量 FieldSpec
  • 方法 MethodSpec
  • 注解 AnnotationSpec

具體如何使用可以直接參考:
https://blog.csdn.net/qq_17766199/article/details/112429217

不再贅述

         val genClass =
                TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
                    .addSuperinterface(ClassName.get(element))
                    .addModifiers(Modifier.PUBLIC)

            for (field in fields) {
                genClass.addField(field)
            }
            for (method in methods) {
                genClass.addMethod(method)
            }

            JavaFile.builder(
                mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
                genClass.build()
            )
                .addFileComment("Generated code")
                .build()
                .writeTo(mFiler)

實(shí)踐

  • app 模塊
  • annotation模塊

具體build.gradle 可以參考 github:

注解代碼:

package com.example.perla

import com.example.annotation.*

@Man(name = "jackie", age = 1, coutry = JackCountry::class)
interface Jackie : IFigher {

    @Body(weight = 200, height = 200)
    fun body()

    @GetCE(algorithm = Algorithm::class)
    fun ce(): Int

    @GetInstance
    fun instance(): IFigher
}

class Algorithm : IAlgorithm {
    override fun ce(figher: IFigher): Int {
        return -1
    }
}

class JackCountry : ICountry {
    override fun name(): String {
        return "China"
    }

}

注解生成代碼:

// Generated code
package com.example.perla;

import com.example.annotation.IAlgorithm;
import com.example.annotation.IFigher;
import java.lang.Override;
import java.lang.String;
import java.lang.System;

public class Jackie$$Impl implements Jackie {
  private String mKey;

  private String name;

  private int age;

  private String country;

  private int weight;

  private int height;

  private IAlgorithm algorithm;

  public Jackie$$Impl(String key) {
    mKey = key;
    name = "jackie";
    age = 1;
    country = new JackCountry().name();
    algorithm = new Algorithm();
  }

  @Override
  public void body() {
    weight = 200;
    height = 200;
  }

  @Override
  public int ce() {
    if (algorithm != null) {
      return algorithm.ce(instance());
    }
    return weight  + height;
  }

  @Override
  public IFigher instance() {
    return new Jackie$$Impl(String.valueOf(System.currentTimeMillis()));
  }
}

核心代碼:

PerlaProcessor.kt

package com.example.annotation


import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
import com.google.auto.service.AutoService
import com.squareup.javapoet.*
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types


@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@SupportedOptions()
@SupportedAnnotationTypes("*")
class PerlaProcessor : AbstractProcessor() {

    private var mTypeUtil: Types? = null
    private var mElementUtil: Elements? = null
    private var mFiler: Filer? = null
    private var mMessager: Messager? = null
    private val aptSourceBook = HashMap<TypeElement, AptManInfo>()


    override fun init(processingEnvironment: ProcessingEnvironment?) {
        super.init(processingEnvironment)
        mTypeUtil = processingEnvironment?.getTypeUtils()
        mElementUtil = processingEnvironment?.getElementUtils()
        mFiler = processingEnvironment?.getFiler()
        mMessager = processingEnvironment?.getMessager()

    }

    override fun process(
        set: MutableSet<out TypeElement>,
        processingEnvironment: RoundEnvironment
    ): Boolean {


        try {

            for (element in processingEnvironment.getElementsAnnotatedWith(Man::class.java)) {
                parseAnnotation(aptSourceBook, element as TypeElement)
            }


            write()


        } catch (ex: Exception) {

        }
        return true
    }

    private fun write() {


        for ((element, info) in aptSourceBook) {

            val fields = ArrayList<FieldSpec>()
            val methods = ArrayList<MethodSpec>()

            val keyField = FieldSpec.builder(ClassName.get(String::class.java), "mKey")
                .addModifiers(Modifier.PRIVATE).build()

            val nameField = FieldSpec.builder(String::class.java, "name")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val ageField = FieldSpec.builder(Int::class.java, "age")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val countryField = FieldSpec.builder(String::class.java, "country")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val weightField = FieldSpec.builder(Int::class.java, "weight")
                .addModifiers(Modifier.PRIVATE)
                .build()

            val heightField = FieldSpec.builder(Int::class.java, "height")
                .addModifiers(Modifier.PRIVATE)
                .build()


            val algorithmField = FieldSpec.builder(IAlgorithm::class.java, "algorithm")
                .addModifiers(Modifier.PRIVATE)
                .build()


            fields.add(keyField)
            fields.add(nameField)
            fields.add(ageField)
            fields.add(countryField)
            fields.add(weightField)
            fields.add(heightField)
            fields.add(algorithmField)

            val constructor =
                MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(String::class.java), "key")
                    .addStatement("mKey = key")
                    .addStatement("name = \$S", info.name)
                    .addStatement("age = \$L", info.age)
                    .addStatement("country = new \$T().name()", info.country)
                    .addStatement("algorithm = new \$T()", info.algorithm)


            val body =
                MethodSpec.methodBuilder("body")
                    .addAnnotation(Override::class.java)
                    .addModifiers(Modifier.PUBLIC)

            info.bodyInfo?.let {
                body.addStatement("weight = \$L", it.weight)
                body.addStatement("height = \$L", it.height)
            }

            val ce =
                MethodSpec.methodBuilder("ce")
                    .addAnnotation(Override::class.java)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.INT)
                    .beginControlFlow("if (algorithm != null)")
                    .addStatement("return algorithm.ce(instance())")
                    .endControlFlow()
                    .addStatement("return weight  + height")

            val getInstance =
                MethodSpec.methodBuilder("instance")
                    .addAnnotation(Override::class.java)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(ClassName.get(IFigher::class.java))
                    .addStatement(
                        "return new \$T(String.valueOf(\$T.currentTimeMillis()))",
                        ClassName.bestGuess(element.simpleName.toString() + "$\$Impl"),
                        System::class.java
                    )




            methods.add(constructor.build())
            methods.add(body.build())
            methods.add(ce.build())
            methods.add(getInstance.build())


            val genClass =
                TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
                    .addSuperinterface(ClassName.get(element))
                    .addModifiers(Modifier.PUBLIC)

            for (field in fields) {
                genClass.addField(field)
            }
            for (method in methods) {
                genClass.addMethod(method)
            }

            JavaFile.builder(
                mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
                genClass.build()
            )
                .addFileComment("Generated code")
                .build()
                .writeTo(mFiler)
        }
    }

    private fun parseAnnotation(
        aptSourceBook: java.util.HashMap<TypeElement, AptManInfo>,
        element: TypeElement
    ) {

        val aptManInfo = AptManInfo()
        val annotationInfo = element.getAnnotation(Man::class.java)
        aptManInfo.apply {
            name = annotationInfo.name
            age = annotationInfo.age
            country = getAnnotationClassName(element, Man::class.java, "coutry")?.toString()
                ?.let { ClassName.bestGuess(it) }
        }
        aptSourceBook[element] = aptManInfo

        val methods = mElementUtil!!.getAllMembers(element)
            .filter {
                it.kind == ElementKind.METHOD &&
                        MoreElements.isAnnotationPresent(it, GetInstance::class.java) ||
                        MoreElements.isAnnotationPresent(it, GetCE::class.java) ||
                        MoreElements.isAnnotationPresent(
                            it,
                            Body::class.java
                        )

            }.map { MoreElements.asExecutable(it) }.groupBy {
                when {
                    MoreElements.isAnnotationPresent(it, Body::class.java) -> Body::class.java
                    MoreElements.isAnnotationPresent(
                        it,
                        GetInstance::class.java
                    ) -> GetInstance::class.java
                    MoreElements.isAnnotationPresent(it, GetCE::class.java) -> GetCE::class.java
                    else -> Any::class.java
                }
            }

        methods[Body::class.java]?.forEach {
            val body = it.getAnnotation(Body::class.java)
            aptManInfo.bodyInfo = BodyInfo().apply {
                weight = body.weight
                height = body.height
            }
        }

        methods[GetInstance::class.java]?.forEach {
            val instance = it.getAnnotation(GetInstance::class.java)
            aptManInfo.getInstance = instance
        }


        methods[GetCE::class.java]?.forEach {
            aptManInfo.algorithm =
                getAnnotationClassName(it, GetCE::class.java, "algorithm").toString()
                    .let { ClassName.bestGuess(it) }
        }


    }

    private fun getAnnotationClassName(
        element: Element,
        key1: Class<out Annotation>,
        key: String
    ): Any? {
        return MoreElements.getAnnotationMirror(element, key1)
            .orNull()?.let {
                AnnotationMirrors.getAnnotationValue(it, key)?.value
            }
    }
}

asm

apt 主要處理 java 文件, asm 處理 class 文件。
asm 也會(huì)搭配 gradle plugin 來進(jìn)行一些代碼增強(qiáng),代碼生成。

asm 主要是解決如何拿到 class 文件然后進(jìn)行代碼增強(qiáng)
參考:
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

舉兩個(gè)實(shí)例來看 asm 的使用:

  • 字節(jié)碼插樁
  • 映射收集

插樁

假設(shè)我們需要寫一個(gè) trace 插樁

在函數(shù)出入口調(diào)用 Trace.beginSection 和 end 就可采集 Trace 數(shù)據(jù)事后使用 perfetto進(jìn)行分析

下面是 Recyclerview 中的一個(gè) trace 方法:

   TraceCompat.beginSection(TRACE_SCROLL_TAG);
        fillRemainingScrollValues(mState);

        int consumedX = 0;
        int consumedY = 0;
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }

        TraceCompat.endSection();

插樁后結(jié)合 systrace 統(tǒng)計(jì)的圖,perfetto工具查看到的效果

如果不會(huì)使用 systrace 可以查看文章https://mp.weixin.qq.com/s/9dexhnWuWIopdhdU_aKkZw

這里避免 代碼中手動(dòng)每個(gè)函數(shù)調(diào)用 Trace.beginSection 采用字節(jié)碼插樁來在 gradle plugin 中批處理添加插樁代碼

下面是method-trace 插件的具體開發(fā)過程:

目錄架構(gòu):

MethodTracePlugin

package com.ss.android.ugc.bytex.method_trace

import com.android.build.gradle.AppExtension
import com.ss.android.ugc.bytex.common.CommonPlugin
import com.ss.android.ugc.bytex.common.flow.main.Process
import com.ss.android.ugc.bytex.common.visitor.ClassVisitorChain
import com.ss.android.ugc.bytex.pluginconfig.anno.PluginConfig
import org.gradle.api.Project
import org.objectweb.asm.ClassReader


@PluginConfig("bytex.method-trace")
class MethodTracePlugin : CommonPlugin<MethodTraceExtension, MethodTraceContext>() {
    override fun getContext(
            project: Project,
            android: AppExtension,
            extension: MethodTraceExtension
    ): MethodTraceContext {
        return MethodTraceContext(project, android, extension)
    }

    override fun transform(relativePath: String, chain: ClassVisitorChain): Boolean {
        chain.connect(MethodTraceClassVisitor(context, extension))
        return super.transform(relativePath, chain)
    }

    override fun flagForClassReader(process: Process?): Int {
        return ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES or ClassReader.EXPAND_FRAMES
    }
}

MethodTraceExtension可讀取如下build.gradle中配置

// apply ByteX宿主
apply plugin: 'bytex'
ByteX {
    enable pluginEnable
    enableInDebug pluginEnableInDebug
    logLevel pluginLogLevel
}

apply plugin: 'bytex.method-trace'

MethodTracePlugin {
    enable pluginEnable
    enableInDebug pluginEnableInDebug
    whiteList = ['com/gongshijie']
}
package com.ss.android.ugc.bytex.method_trace;

import com.ss.android.ugc.bytex.common.BaseExtension;

import java.util.ArrayList;
import java.util.List;

public class MethodTraceExtension extends BaseExtension {

    private List<String> whiteList = new ArrayList<>();


    @Override
    public String getName() {
        return "MethodTracePlugin";
    }

    public List<String> getWhiteList() {
        return whiteList;
    }

    public void setWhiteList(List<String> whiteList) {
        this.whiteList = whiteList;
    }
}

TraceMethodVisitor

package com.ss.android.ugc.bytex.method_trace

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.commons.AdviceAdapter

class TraceMethodVisitor(private var context: MethodTraceContext,
                         private var className: String, api: Int, mv: MethodVisitor?,
                         access: Int, var methodName: String?, desc: String?
) : AdviceAdapter(api, mv, access, methodName, desc) {


    override fun onMethodEnter() {
        super.onMethodEnter()

        context.logger.i("TraceMethodVisitor", "----插樁----className: $className  methodName: ${methodName}------")

        if (methodName != null) {
            mv.visitLdcInsn("$className#$methodName");
            mv.visitMethodInsn(INVOKESTATIC, "com/ss/android/ugc/bytex/method_trace_lib/MyTrace", "beginSection", "(Ljava/lang/String;)V", false);
        }
    }

    override fun onMethodExit(opcode: Int) {
        super.onMethodExit(opcode)
        mv.visitMethodInsn(INVOKESTATIC, "com/ss/android/ugc/bytex/method_trace_lib/MyTrace", "endSection", "()V", false);

    }
}

映射采集

apt 和 asm 往往會(huì)搭配起來使用

比如我們各個(gè)模塊內(nèi)部,可以根據(jù)注解生成一些映射關(guān)系(apt), 后面再通過 asm 來跨模塊收集這些映射關(guān)系

目錄架構(gòu)

熟悉的環(huán)境工作不再贅述, 核心部分就是 采集各模塊 apt 生成的文件映射關(guān)系, 然后 asm 增強(qiáng)到 一個(gè) class 文件內(nèi)。

這樣的處理在很多框架中都可以找到。

class ManCollectTransform(val project: Project, val appPlugin: AppPlugin?) : Transform() {
    override fun getName(): String {
        return "ManCollectTransform"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun isIncremental(): Boolean {
        return false
    }


    override fun transform(transformInvocation: TransformInvocation?) {
        val map = HashMap<String, String>()
        transformInvocation?.inputs?.forEach { it ->

            it.jarInputs.forEach { jarInput ->
                val jarFile = JarFile(jarInput.file)
                val entries = jarFile.entries()
                for (entry in entries) {
                    if (entry.name.endsWith("$\$Impl.class")) {
                        val inputStream = jarFile.getInputStream(entry)
                        val reader = ClassReader(inputStream)
                        val classNode = ClassNode(ASM5)
                        reader.accept(classNode, ClassReader.SKIP_DEBUG)
                        map.put(classNode.interfaces.first(), classNode.name)
                        inputStream.close()
                    }
                }
            }

            it.directoryInputs.forEach { dirInput ->
                project.fileTree(dirInput.file).forEach {
                    if (it.absolutePath.endsWith("$\$Impl.class")) {
                        val inputStream = FileInputStream(it)
                        val reader = ClassReader(inputStream)
                        val classNode = ClassNode(Opcodes.ASM5)
                        reader.accept(classNode, ClassReader.SKIP_DEBUG)
                        if (classNode.interfaces.isNotEmpty()) {
                            map.put(classNode.interfaces.first(), classNode.name)
                        }
                        inputStream.close()
                    }
                }
            }
        }

        println("輸出映射關(guān)系")
        for((k, v) in map) {
            println("""映射關(guān)系采集: $k : $v""")
        }

        transformInvocation?.inputs?.forEach { it ->
            it.jarInputs.forEach { jarInput ->
                val jarFile = JarFile(jarInput.file)
                val manFinderEntry = jarFile.getJarEntry("com/example/mancollect_api/ManFinder.class")
                val dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                if (manFinderEntry != null) {
                    val inputStream = jarFile.getInputStream(manFinderEntry)
                    val reader = ClassReader(inputStream)
                    val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)
                    val vis = ManFinderClassAdapter(writer, map)
                    reader.accept(vis, ClassReader.SKIP_DEBUG)
                    inputStream.close()
                }
            }
        }
    }
}

總之 , apt 和 asm 可以幫助我們處理大量的樣板代碼, 可以幫助我們自動(dòng)化一些配置化的代碼。

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

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