Android注解的使用

说明:本文主要用于记录学习笔记
原文参见:https://mp.weixin.qq.com/s/fJKAwpqn_hXRXRS4oh1dEw
注解使用范围:类、方法、变量、参数都可以被注解

可以干啥

  • 利用注解可以标记源码以便编译器为源码生成文档和检查代码
  • 也可以让编译器和注解处理器在编译时根据注解自动生成代码,甚至可以保留到运行时以便改变运行时的行为

Java 内置了一些注解,如 @Override 注解用来表明该方法是重写父类方法,编译器会负责检查该方法与父类方法的声明是否一致。@Deprecated 注解用来表明该元素已经被废弃不建议使用了。@SuppressWarnings 注解用来表示编译器可以忽略特定警告。

注解类型的声明和接口的声明类似,不过需要使用 @interface 和元注解(用来定义注解的注解)描述,每个方法声明定义了注解类型的一个元素,且方法声明不能包含任何参数或 throws,方法的返回类型必须是原语类型、String、Class、枚举、注解和这些类型的数组,方法可以有默认值,如:

public @interface RequestForEnhancement {
    int    id();
    String synopsis();
    String engineer() default "[unassigned]"; 
    String date();    default "[unimplemented]"; 
}

定义完注解类型后,就可以用它去注解一些声明了。注解是一种特殊的修饰符,可以像 public、static 或 final 修饰符一样使用,不过通常注解要写在这些修饰符之前。使用时为 @ 符号加注解类型加元素值对列表并用括号括起来,如:

@RequestForEnhancement(
    id       = 2868724,
    synopsis = "Enable time-travel",
    engineer = "Mr. Peabody",
    date     = "4/1/3007"
)
public static void travelThroughTime(Date destination) { ... }

注解类型也可以没有方法/元素,被称为标记注解类型,如:

public @interface Preliminary { }

@Preliminary public class TimeTravel { ... }

如果注解类型只有一个元素,那么元素应该命名为 value,使用时也就可以忽略元素名和等号了,如:

public @interface Copyright {
    String value();
}

@Copyright("2002 Yoyodyne Propulsion Systems")
public class OscillationOverthruster { ... }

除了这些,很多注解还需要元注解(用来定义注解的注解)去描述,如:

@Documented//标明改注解类型可以被javadoc等工具文档化
@Retention(RetentionPolicy.RUNTIME)//注解类型保留时长(枚举)
@Target(ElementType.METHOD)//注解使用范围
@Inherited//表示注解类型被自动继承
public @interface MethodInfo {
    String author() default "Peabody";
    String date();
    int version() default 1;
}
  • @Documented 表明该注解类型可以被 javadoc 等工具文档化

  • @Retention 表明该注解类型可以保留多长时间,值为枚举值 RetentionPolicy:

RetentionPolicy.SOURCE(只保留在源码中,会被编译器丢弃)

RetentionPolicy.CLASS(注解会被编译器记录在class文件中,但不需要被VM保留到运行时,这也是默认的行为)

RetentionPolicy.RUNTIME(注解会被编译器记录在class文件中并被VM保留到运行时,所以可以通过反射获取)
  • @Target 表明该注解类型可以注解哪些程序元素,如果注解类型不使用 @Target 描述那么表明默认可以注解所有程序元素,值是枚举数组 ElementType[]:
ElementType.TYPE(类、接口(包括注解类型)、枚举的声明)

ElementType.FIELD(字段(包括枚举常量)的声明)

ElementType.METHOD(方法的声明)

ElementType.PARAMETER(形参的声明)

ElementType.CONSTRUCTOR(构造器的声明)

ElementType.LOCAL_VARIABLE(本地变量的声明)

ElementType.ANNOTATION_TYPE(注解类型的声明)

ElementType.PACKAGE(包的声明)

ElementType.TYPE_PARAMETER(泛型参数的声明)

ElementType.TYPE_USE(泛型的使用)
  • @Inherited 表明该注解类型将被自动继承。也就是说,如果注解类型被 @Inherited 注解,此时用户查询一个类声明的注解,而类声明没被该注解类型注解,那么将自动查询该类父类的注解类型,以此类推直到找到该注解类型或达到顶层 Object 对象。

Android Support Library 提供了很多实用注解,如可以使用 @NonNull 注解进行空检查,使用 @UiThread、@WorkerThread 注解进行线程检查,使用 @IdRes 表明这个整数代表资源引用,还可以通过 @IntDef、@StringDef 注解自定义注解来代替枚举,如描述应用中使用的字体文件:

public final class TypefaceManager {

    //真正的变量值,以前我们可以用枚举(内存占用大),或者直接定义字段(填错了不会报错)
    public static final int FONT_TYPE_ICONIC = 0;
    public static final int FONT_TYPE_IMPACT = 1;
    public static final int FONT_TYPE_HELVETICA = 2;
    public static final int FONT_TYPE_DIN = 3;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({FONT_TYPE_ICONIC, FONT_TYPE_IMPACT, FONT_TYPE_HELVETICA, FONT_TYPE_DIN})
    @interface FontType {
    }

    private Context mContext;
    private SparseArray<Typeface> mTypefaceSparseArray;

    public TypefaceManager(Context context) {
        this.mContext = context;
        this.mTypefaceSparseArray = new SparseArray<>();
    }

    public static void setTypeface(TextView textView, @FontType int fontType) {
        Typeface localTypeface = MyApplication.getInstance().getTypefaceManager().getTypeface(fontType);
        if (localTypeface != null && localTypeface != textView.getTypeface()) {
            textView.setTypeface(localTypeface);
        }
    }

    public static void setTypeface(Paint paint, @FontType int fontType) {
        Typeface localTypeface = MyApplication.getInstance().getTypefaceManager().getTypeface(fontType);
        if (localTypeface != null && localTypeface != paint.getTypeface()) {
            paint.setTypeface(localTypeface);
        }
    }

    public Typeface getTypeface(@FontType int fontType) {
        Typeface typeface = mTypefaceSparseArray.get(fontType);
        if (typeface == null) {
            try {
                String path = null;
                if (fontType == FONT_TYPE_ICONIC) {
                    path = "fonts/fontawesome-webfont.ttf";
                } else if (fontType == FONT_TYPE_IMPACT) {
                    path = "fonts/impact.ttf";
                } else if (fontType == FONT_TYPE_HELVETICA) {
                    path = "fonts/Helvetica.ttf";
                } else if (fontType == FONT_TYPE_DIN) {
                    path = "fonts/ptdin.ttf";
                }
                typeface = Typeface.createFromAsset(mContext.getAssets(), path);
                this.mTypefaceSparseArray.put(fontType, typeface);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return typeface;
    }

}

下面来看看IntDef注解的定义

@Retention(RetentionPolicy.SOURCE)//只保留在源码中,会被编译器丢弃
@Target({ElementType.ANNOTATION_TYPE})//表示是用于注解A对直接B的声明,可以从上方用法理解
public @interface IntDef {
    int[] value() default {};

    boolean flag() default false;
}

我们自己写个示例,对上面的用法做一个全面理解:

 public static final int CHECK_METHOD_COMPLETE = 1;//校验方法完整性
    public static final int TEST_METHOD = 2;//调试模式(测试方法调用)
    public static final int RELEASE = 0;//正式环境使用
    //调试模式
    public static @ModeType int mModeType = RELEASE;
    /**
     * 调试模式 0、正式使用(默认模式) 1、校验方法是否完整实现 2、调试方法
     */
    @IntDef({CHECK_METHOD_COMPLETE, TEST_METHOD, RELEASE})
    @Retention(RetentionPolicy.SOURCE)//定义仅在源码中保留,编译后自动移除
    public @interface ModeType {}
    public static void setModeType(@ModeType int type) {
        mModeType = type;
    }

然后我们接着去调用setModeType方法会发现:同样传递的值都是1,第一种写法就会报错,而第二种则正常,这种写法既简捷又不容易出错。

示例

自定义注解

RUNTIME 注解&应用示例

对于 @Retention(RetentionPolicy.RUNTIME) 的注解,注解会被编译器记录在 class 文件中并被 VM 保留到运行时,所以可以通过反射获取,如:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {
    String author() default "Peabody";
    String date();
    int version() default 1;
}

使用

public class App {
    @MethodInfo(
        author = “frank”,
        date = "2018/02/27",
        version = 2)
    public String getDescription() {
        return "no description";
    }
}

可以写个工具在运行时利用反射获取注解

public static void main(String[] args) {
    try {
        Class cls = Class.forName("com.frank.App");
        for (Method method : cls.getMethods()) {
            MethodInfo methodInfo = method.getAnnotation(
MethodInfo.class);
            if (methodInfo != null) {
                System.out.println("method author:" + methodInfo.author());
                System.out.println("method version:" + methodInfo.version());
                System.out.println("method date:" + methodInfo.date());
            }
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

这里需要注意,注解也是可以被混淆的,如果不想被混淆,也需要额外keep。

CLASS 注解&应用示例

对于 @Retention(RetentionPolicy.CLASS) 的注解,注解会被编译器记录在 class 文件中,但不需要被 VM 保留到运行时,如:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}

也就是说,这种编译时注解适合用来在编译时自动生成代码,这就需要 apt(Annotation Processing Tool)工具查找并执行注解处理器(Annotation Processor)以生成源码和文件,最终 javac 会编译这些原始源文件和自动生成的文件。Android Gradle 插件的 2.2 版本开始支持注解处理器,你只需要使用 annotationProcessor 依赖注解处理器或者使用 javaCompileOptions.annotationProcessorOptions {} DSL指定注解处理器即可。定义注解处理器最简单的方式就是继承 AbstractProcessor,在其 process 实现方法中实现注解元素的分析和源码文件的生成。

以简化一系列 findViewById 为例:

public class MainActivity extends AppCompatActivity {

    TextView mTitleTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTitleTextView = (TextView) findViewById(R.id.titleTextView);
        mTitleTextView.setText("Hello World!");
    }
}

使用注解处理器后可以这么写

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.titleTextView)
    TextView mTitleTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SimpleButterKnife.bind(this);//一行代码搞定所有findviewbyid
        mTitleTextView.setText("Hello World!");
    }
}

实现方式就是利用注解和注解编译器在编译时自动生成一个这样的文件:

public class MainActivity_ViewBinding {
  public MainActivity target;

  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTitleTextView = (TextView) source.findViewById(2131165300);
  }
}

在 SimpleButterKnife.bind(this); 的实现中加载这个类并执行构造器就可以了。 实现起来也很简单,先新建一个 java-library (这里注意是java-library)的module:simplebutterknife-annotations,用来声明注解

@Retention(RetentionPolicy.CLASS)//存在字节码中,但是不会保留到运行时
@Target(ElementType.FIELD)//表示用于修饰字段
public @interface BindView {
    @IdRes int value();//入参是一个int值 IdRes表示资源类型的int值
}

simplebutterknife-annotations module依赖:

//编译用的android api
compileOnly 'com.google.android:android:4.1.1.4'
//注解
api 'com.android.support:support-annotations:27.0.2'

再新建一个注解处理器 java-library的module:simplebutterknife-compiler,用来对注解的元素进行分析和生成源码文件:
simplebutterknife-compiler module依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':simplebutterknife-annotations')//自定义注解
    implementation 'com.google.auto:auto-common:0.10'
    api 'com.squareup:javapoet:1.9.0'//用于方便的生成java类
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'//避免手动创建meta文件 参见 https://www.jianshu.com/p/deeb39ccdc53
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'//加上这个,避免高版本gradle生成失败
}

注解处理器代码:

package com.example.simplebutterknife_compiler;

import com.example.simplebutterknife_annotations.BindView;
import com.google.auto.common.MoreElements;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;

/**
 * 注解处理器
 * <p>
 * 利用了 Google 的 AutoService 为注解处理器自动生成 metadata 文件并将注解处理器jar文件加入构建路径,
 * 这样也就不需要再手动创建并更新 META-INF/services/javax.annotation.processing.Processor 文件了
 *参考 java sip:https://www.jianshu.com/p/deeb39ccdc53
 *
 */
@AutoService(Processor.class)
public class SimpleButterKnifeProcessor extends AbstractProcessor {

    /**
     * 方法指定可以支持最新的 Java 版本(直接指定最新的就可以了)
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 方法指定该注解处理器用于处理哪些注解(我们这里只处理 @BindView 注解)
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        //本例子 就是 处理BINDView
        types.add(BindView.class.getCanonicalName());
        return types;
    }

    /**
     * 检索注解元素并生成代码的是 process 方法的实现:
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        /**
         * 需要为每个包含注解的 Activity 都生成一个对应的 _ViewBinding 文件,
         * 所以使用 Map 来存储。BindingSet 存储 Activity 信息和它的 View 绑定信息,
         * View 绑定信息(ViewBinding)包括绑定 View 的类型、View 的 ID 以及 View 的变量名。
         */
        Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
        /**
         * 查找所有被 @BindView 注解的程序元素(Element),为了简化,
         * 这里只认为被注解的元素是 View 字段且它的外层元素(EnclosingElement)为 Activity 类:
         */
        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            // 注解元素的外侧元素,即 View 的所在 Activity 类
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            // 注解的 value 值,即 View 的 id
            int id = element.getAnnotation(BindView.class).value();
            // 注解元素的名字,即 View 变量名
            Name simpleName = element.getSimpleName();
            String name = simpleName.toString();
            // 注解元素的类型,即 View 的类型
            TypeMirror elementType = element.asType();
            TypeName type = TypeName.get(elementType);

            //然后把这些信息存到 Activity 对应的 View 绑定中:
            BindingSet bindingSet = bindingMap.get(enclosingElement);
            if (bindingSet == null) {
                bindingSet = new BindingSet();
                TypeMirror typeMirror = enclosingElement.asType();
                TypeName targetType = TypeName.get(typeMirror);
                String packageName = MoreElements.getPackage(enclosingElement).getQualifiedName().toString();
                String className = enclosingElement.getQualifiedName().toString().substring(
                        packageName.length() + 1).replace('.', '$');
                ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
                bindingSet.targetTypeName = targetType;
                bindingSet.bindingClassName = bindingClassName;
                bindingMap.put(enclosingElement, bindingSet);
            }
            if (bindingSet.viewBindings == null) {
                bindingSet.viewBindings = new ArrayList<>();
            }
            ViewBinding viewBinding = new ViewBinding();
            viewBinding.type = type;
            viewBinding.id = id;
            viewBinding.name = name;
            bindingSet.viewBindings.add(viewBinding);
            /**
             * 确定完 Activity 信息和它对应的 View 绑定信息后,为每个 Activity 生成对应的 XXX_ViewBinding.java 文件,
             * 文件内容就是前面所说类绑定类
             *
             * 虽然通过字符串拼接可以拼出这样的文件内容,但我们还得考虑 import,还得考虑大括号和换行,
             * 甚至还得考虑注释和代码美观,所以利用 JavaPoet 来生成 .java 文件是个不错的选择:
             */

            for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
                TypeElement typeElement = entry.getKey();
                BindingSet binding = entry.getValue();

                TypeName targetTypeName = binding.targetTypeName;
                ClassName bindingClassName = binding.bindingClassName;
                List<ViewBinding> viewBindings = binding.viewBindings;
                /**
                 * 生成binding类 public class MainActivity_ViewBinding {}
                 */
                TypeSpec.Builder viewBindingBuilder = TypeSpec.classBuilder(bindingClassName.simpleName())
                        .addModifiers(Modifier.PUBLIC);
                /**
                 * 生成binding类中的成员变量 public MainActivity target;
                 */
                // public的target字段用来保存 Activity 引用
                viewBindingBuilder.addField(targetTypeName, "target", Modifier.PUBLIC);
                /**
                 * 生成构造函数
                 *
                 *   public MainActivity_ViewBinding(MainActivity target) {
                 *     this(target, target.getWindow().getDecorView());
                 *   }
                 */
                MethodSpec.Builder activityViewBuilder = MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(targetTypeName, "target");
                activityViewBuilder.addStatement("this(target, target.getWindow().getDecorView())");
                viewBindingBuilder.addMethod(activityViewBuilder.build());

                /**
                 * 生成构造函数2
                 *
                 * public MainActivity_ViewBinding(MainActivity target, View source) {
                 *     this.target = target;
                 *
                 *     target.mTitleTextView = (TextView) source.findViewById(2131165300);
                 *   }
                 */
                MethodSpec.Builder viewBuilder = MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(targetTypeName, "target")
                        .addParameter(ClassName.get("android.view", "View"), "source");
                viewBuilder.addStatement("this.target = target");
                viewBuilder.addCode("\n");
                for (ViewBinding temp : viewBindings) {//这里就是遍历所有要绑定的元素,进行findviewbyId
                    CodeBlock.Builder builder = CodeBlock.builder()
                            .add("target.$L = ", temp.name);
                    builder.add("($T) ", temp.type);
                    builder.add("source.findViewById($L)", CodeBlock.of("$L", temp.id));
                    viewBuilder.addStatement("$L", builder.build());
                }
                viewBindingBuilder.addMethod(viewBuilder.build());
                // 输出 Java 文件
                JavaFile javaFile = JavaFile.builder(bindingClassName.packageName(), viewBindingBuilder.build())
                        .build();
                try {
                    javaFile.writeTo(processingEnv.getFiler());
                } catch (IOException e) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
                }
            }

        }

        return false;
    }

    class BindingSet {
        TypeName targetTypeName;
        ClassName bindingClassName;
        List<ViewBinding> viewBindings;
    }

    class ViewBinding {
        TypeName type;
        int id;
        String name;
    }
}

在 app module 中需要依赖注解 module 并注册注解处理器 module:

dependencies {
    ...
    api project(':simplebutterknife-annotations')
    annotationProcessor project(':simplebutterknife-compiler')
}

这里有个注意点,如果我们需要在kotlin代码使用对用注解,需要做一些引入调整
首先在主工程引入kapt插件

apply plugin: 'kotlin-kapt'//引入kapt插件

然后把 annotationProcessor 变更为 kapt

dependencies {
    ...
    api project(':simplebutterknife-annotations')
    kapt project(':simplebutterknife-compiler')
}

app module 中的工具类 SimpleButterKnife 的 bind 方法只需要加载这个自动生成的类并执行它的构造器就行了:

public final class SimpleButterKnife {

    public static void bind(Activity target) {
        View sourceView = target.getWindow().getDecorView();
        Class<?> targetClass = target.getClass();
        String targetClassName = targetClass.getName();
        Constructor constructor;
        try {
            Class<?> bindingClass = targetClass.getClassLoader().loadClass(targetClassName + "_ViewBinding");
            constructor = bindingClass.getConstructor(targetClass, View.class);
        } catch (ClassNotFoundException e) {
            // TODO Not found. should try search its superclass
            throw new RuntimeException("Not found. should try search its superclass of " + targetClassName, e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + targetClassName, e);
        }
        try {
            constructor.newInstance(target, sourceView);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }
}

重新构建下工程,就可以在 build\generated\source\apt\debug 目录中查看自动生成的文件了:

package com.frank.simplebutterknife;

import android.view.View;
import android.widget.TextView;

public class MainActivity_ViewBinding {
  public MainActivity target;

  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTitleTextView = (TextView) source.findViewById(2131165300);
  }
}

重新构建下工程,就可以在 build\generated\source\apt\debug 目录中查看自动生成的文件了:


image.png

自测demo练习部分

demoall-sample工程


image.png

除了上面的butterknife,还有简单的注解处理并生成map集合的应用处理,具体参见笔记代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342