ButterKnife蛮艰,不簡單的Annotation

1潮峦、ButterKnife簡介

ButterKnife是Android大神JakeWharton出品的又一神器或舞,它通過注解的方式來替代findViewById來綁定view和一系列匿名的view的事件兴喂。最常見的用法如下:

 @BindView(R.id.title) TextView title;
 @BindView(R.id.subtitle) TextView subtitle;
 @BindView(R.id.footer) TextView footer;
 
 @OnClick(R.id.submit)
 public void sayHi(Button button) {
     button.setText("Hello!");
}

但是ButterKnife除了綁定view和添加事件蔼囊,還能綁定Resource,有@BindBool, @BindColor, @BindDimen, @BindDrawable, @BindInt, @BindString等標簽焚志。

2、ButterKnife總體架構

以截止到2017.6.29的最新版本8.6.0源碼分析畏鼓。它的各個模塊的依賴關系如下:


未命名文件.png-11.1kB
未命名文件.png-11.1kB

1酱酬、butterknife是對外提供接口的模塊;
2、butterknife-compile負責編譯期對注解進行解析云矫,然后生成綁定view的java文件膳沽,以xxx_ViewBinding命名,放在對應工程/build/generated/source目錄下;
3让禀、butterknife-gradle—plugin是8.2.0之后為了支持library工程而新增的模塊;
4挑社、butterknife-annotations是專門放置注解的包,我們常使用的Bindview、BindString巡揍、OnClick等注解都在這個包里痛阻;
5、butterknife是針對butterknife-gradle—plugin提供的靜態(tài)代碼檢查工具腮敌。

3阱当、注解(Annotation)

閱讀ButterKnife的源碼,必須要對java的注解有一定的了解糜工,比如@Override,@Deprecated等弊添,這種注解大家一定見過。
注解的目的是將一些本來重復性的工作捌木,變成程序自動完成油坝,簡化和自動化該過程。比如用于生成Javadoc刨裆,比如編譯時進行格式檢查澈圈,比如自動生成代碼等,用于提升軟件的質量和提高軟件的生產(chǎn)效率崔拥。
作為Android開發(fā)极舔,日常碰到的注解主要有來自JDK里的,也有Android SDK里的链瓦,也有自定義的拆魏。推薦閱讀

ButterKnife其實就是基于自定義注解的方式實現(xiàn)的。

3.1 andorid-apt(Annotation Processing Tool)

android-apt 是一個Gradle插件慈俯,協(xié)助Android Studio 處理annotation processors, 它有兩個目的:

1渤刃、允許配置只在編譯時作為注解處理器的依賴,而不添加到最后的APK或library
2贴膘、設置源路徑卖子,使注解處理器生成的代碼能被Android Studio正確的引用

使用該插件,添加如下到你的構建腳本中:

//配置在Project下的build.gradle中 buildscript {
repositories {
mavenCentral()
}
dependencies {
//替換成最新的 gradle版本
classpath 'com.android.tools.build:gradle:1.3.0'
//替換成最新android-apt版本
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
} } //配置到Module下的build.gradle中 apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt'

伴隨著 Android Gradle 插件 2.2 版本的發(fā)布刑峡,洋闽,Android Gradle 插件提供了名為 annotationProcessor 的功能來完全代替 android-apt玄柠。
修改前配置如下:

compile 'com.jakewharton:butterknife:8.0.1'
apt 'com.jakewharton:butterknife-compiler:8.0.1'

修改后配置如下:

compile 'com.jakewharton:butterknife:8.6.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'

整個注解處理過程包括三部分:1、注解處理器(Processor)诫舅;2羽利、注冊注解處理器(AutoService);2、代碼生成器(JavaPoet).

示例:

package com.example;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
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.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)
public class MyProcessor extends AbstractProcessor {

    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //提供處理注解和生成代碼的工具類
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //1刊懈、處理注解这弧;
        //2、生成代碼虚汛。
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

定義完注解處理器后匾浪,還需要告訴編譯器該注解處理器的信息,需在 src/main/resource/META-INF/service 目錄下增加 javax.annotation.processing.Processor 文件卷哩,并將注解處理器的類名配置在該文件中蛋辈。
![image.png-33kB][2]

![image.png-34.9kB][3]

javapoet也是square開源的生成.java文件的API。

4殉疼、ButterKnife整體流程

image.png-37.5kB
image.png-37.5kB

4.1 ButterKnifeProcessor

ButterKnifeProcessor主要做了兩件事:

1梯浪、解析所有包含了ButterKnife注解的類;
2瓢娜、根據(jù)解析結果,使用JavaPoet生成對應的類礼预。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //解析所有包含了ButterKnife注解的類
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
        //根據(jù)解析結果眠砾,使用JavaPoet生成對應的類
      JavaFile javaFile = binding.brewJava(sdk);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

ButterKnife支持的所有注解在getSupportedAnnotations方法里面定義。

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }
  private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
      OnCheckedChanged.class, //
      OnClick.class, //
      OnEditorAction.class, //
      OnFocusChange.class, //
      OnItemClick.class, //
      OnItemLongClick.class, //
      OnItemSelected.class, //
      OnLongClick.class, //
      OnPageChange.class, //
      OnTextChanged.class, //
      OnTouch.class //
  );

4.2 @BindView托酸、@BindString褒颈、@OnClick

下面以@BindView、@BindString励堡、@OnClick三個比較常用的注解為例谷丸,分析下具體的綁定過程。
在findAndParseTargets方法中解析所有注解应结,每個類對應一個BindingSet刨疼,解析過程中會把每個注解的解析結果放到BindingSet中,比如@BindString對應FieldResourceBinding類型鹅龄,@BindView對應FieldViewBinding類型揩慕,解析完成后調(diào)用BindingSet的brewJava生成對應的JavaFile,即可通過JavaPoet生成.java文件扮休。

4.2.1@BindView:

 private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    
    //1迎卤、TypeElement就是該注解Element所在的類
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
    // Start by verifying common generated code restrictions.
    //2、驗證生成代碼時是否可見和是否在錯誤的包里面
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    //3玷坠、 驗證目標類型是否繼承自View
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      if (elementType.getKind() == TypeKind.ERROR) {
        note(element, "@%s field with unresolved type (%s) "
                + "must elsewhere be generated as a View or interface. (%s.%s)",
            BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
      } else {
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
            BindView.class.getSimpleName(), qualifiedName, simpleName);
        hasError = true;
      }
    }

    //4蜗搔、上述兩步只要有錯誤劲藐,直接返回
    if (hasError) {
      return;
    }

    // Assemble information on the field.
    //5、解析注解的value
    int id = element.getAnnotation(BindView.class).value();

    //6樟凄、查找所在類對應的BindingSet構造類瘩燥。如果有還要判斷這個view的id是否已經(jīng)綁定過,如果沒有則新建不同。
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      if (existingBindingName != null) {
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBindingName,
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

    //7厉膀、解析注解的view。比如@BindView(R.id.tv_title) TextView tvTitle二拐,那么name就是tvTitle,type就是TextView服鹅,required只要沒有@Nullable的注解就是true。
    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    //8百新、將上述信息封裝成FieldViewBinding企软,加到builder中。
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

4.2.2@BindString:

  private void parseResourceString(Element element,
      Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify that the target type is String.
    if (!STRING_TYPE.equals(element.asType().toString())) {
      error(element, "@%s field type must be 'String'. (%s.%s)",
          BindString.class.getSimpleName(), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    // Verify common generated code restrictions.
    hasError |= isInaccessibleViaGeneratedCode(BindString.class, "fields", element);
    hasError |= isBindingInWrongPackage(BindString.class, element);

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    String name = element.getSimpleName().toString();
    int id = element.getAnnotation(BindString.class).value();
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    builder.addResource(
        new FieldResourceBinding(getId(qualifiedId), name, FieldResourceBinding.Type.STRING));

    erasedTargetNames.add(enclosingElement);
  }

看代碼饭望,跟綁定View的過程基本一致仗哨,唯一的區(qū)別是它最后封裝成了
FieldResourceBinding類,然后通過addResource加到builder中铅辞。

4.2.3@OnClick:

所有Listener的解析都在parseListenerAnnotation方法中厌漂,代碼總共200多行,這里不一一分析了斟珊,下面截取關鍵代碼做解析苇倡。
以如下注解代碼為例:

package test;
import butterknife.OnClick;
    public class Test {
      @OnClick(1) void doStuff() {}
   }

 ExecutableElement executableElement = (ExecutableElement) element;//(doStuff())
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();//(test.Test)

    Annotation annotation = element.getAnnotation(annotationClass);//(interface butterknife.OnClick)
    Method annotationValue = annotationClass.getDeclaredMethod("value");//(public abstract int[] butterknife.OnClick.value())
    
int[] ids = (int[])annotationValue.invoke(annotation);//(1)
String name = executableElement.getSimpleName().toString();//(doStuff)
    boolean required = isListenerRequired(executableElement);//(true)

    //

上面是注解及其節(jié)點的一些基本屬性。后面的值是調(diào)試過程中看到的值囤踩。

下面看下OnClick注解的定義:

@Target(METHOD)
@Retention(CLASS)
@ListenerClass(
    targetType = "android.view.View",
    setter = "setOnClickListener",
    type = "butterknife.internal.DebouncingOnClickListener",
    method = @ListenerMethod(
        name = "doClick",
        parameters = "android.view.View"
    )
)
public @interface OnClick {
  /** View IDs to which the method will be bound. */
  @IdRes int[] value() default { View.NO_ID };
}

它有一個ListenerClass的注解旨椒,用來定義目標類型、設置方法堵漱、listener類型综慎、method(listener里面只有一個回調(diào)時使用)、callbacks(listener里面有多個回調(diào)時使用)勤庐。

比如OnTextChanged里面的ListenerClass注解是這樣的示惊,它只實現(xiàn)OnTextChanged方法的回調(diào):

@ListenerClass(
    targetType = "android.widget.TextView",
    setter = "addTextChangedListener",
    remover = "removeTextChangedListener",
    type = "android.text.TextWatcher",
    callbacks = OnTextChanged.Callback.class
)

繼續(xù)看parseListenerAnnotation方法里面的代碼。

ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class);

ListenerMethod method;
ListenerMethod[] methods = listener.method();

通過解析ListenerClass埃元,獲取一個method實例涝涤。然后再根據(jù)methodParameters及其其他一些邏輯生成一個參數(shù)列表。

MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);

builder.addMethod(getId(qualifiedId), listener, method, binding)

最后把注解listener岛杀、method和binding加入到builder中阔拳,用戶后續(xù)生成代碼。

4.2.4 上面大致分析了三種不同注解的解析方式,下面看下到底是如何通過BuildSet生成java類的糊肠。

4.3 生成.java文件

JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);

ButterKnifeProcessor中process方法中上面的代碼觸發(fā)了生成代碼的邏輯辨宠,最后寫到文件中。

JavaFile brewJava(int sdk) {
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

brewJava調(diào)用createType構造具體的類货裹。其中最重要的代碼是:
result.addMethod(createBindingConstructor(sdk));
它負責在構造方法中綁定view嗤形,生成的代碼類似(源碼中SimpleActivity)如下:

@UiThread
  public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
    this.target = target;

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
    view2130968578 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.sayHello();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
      }
    });
    view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
    target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
    view2130968579 = view;
    ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
      }
    });
    target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
    target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));
  }

下面結合createBindingConstructor方法做具體分析,直接在代碼中進行注釋說明。

private MethodSpec createBindingConstructor(int sdk) {
========
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
========1弧圆、上面代碼在方法SimpleActivity_ViewBinding增加UI_THREAD注解和public修飾符赋兵。
    if (hasMethodBindings()) {
      constructor.addParameter(targetTypeName, "target", FINAL);
    } else {
      constructor.addParameter(targetTypeName, "target");
    }
========2、上面代碼增加參數(shù)final SimpleActivity target
    if (constructorNeedsView()) {
      constructor.addParameter(VIEW, "source");
    } else {
      constructor.addParameter(CONTEXT, "context");
    }
========3搔预、上面代碼增加參數(shù)View source

    if (hasUnqualifiedResourceBindings()) {
      // Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
      constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
          .addMember("value", "$S", "ResourceType")
          .build());
    }

    if (hasOnTouchMethodBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
          .addMember("value", "$S", "ClickableViewAccessibility")
          .build());
    }

===========
    if (parentBinding != null) {
      if (parentBinding.constructorNeedsView()) {
        constructor.addStatement("super(target, source)");
      } else if (constructorNeedsView()) {
        constructor.addStatement("super(target, source.getContext())");
      } else {
        constructor.addStatement("super(target, context)");
      }
      constructor.addCode("\n");
    }
    ========上面代碼是判斷有繼承的情況
    
    if (hasTargetField()) {
      constructor.addStatement("this.target = target");
      constructor.addCode("\n");
    }
========上面代碼增加this.target = target;
    if (hasViewBindings()) {
      if (hasViewLocal()) {
        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
      }
      for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render());
      }

      if (!resourceBindings.isEmpty()) {
        constructor.addCode("\n");
      }
    }
========上面代碼實現(xiàn)view的綁定,具體代碼在addViewBinding霹期,它會根據(jù)不同的情況生成不同的代碼;
    if (!resourceBindings.isEmpty()) {
      if (constructorNeedsView()) {
        constructor.addStatement("$T context = source.getContext()", CONTEXT);
      }
      if (hasResourceBindingsNeedingResource(sdk)) {
        constructor.addStatement("$T res = context.getResources()", RESOURCES);
      }
      for (ResourceBinding binding : resourceBindings) {
        constructor.addStatement("$L", binding.render(sdk));
      }
    }
========上面代碼實現(xiàn)resource的綁定;
    return constructor.build();
  }

addViewBinding方法里面的大致步驟是先findView,然后調(diào)用addFieldBinding(result, binding)和addMethodBindings(result, binding)實現(xiàn)view的綁定和listener的設置拯田。

5 運行期間

運行期間,ButterKnife.bind(this)通過反射去獲取對應的xxx__ViewBinding類的實例历造,在該類的構造方法中,完成view或者resource的查找和綁定船庇,以及l(fā)istener的設置吭产。

public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }
 private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } 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);
    }
  }

參考鏈接:
1、https://github.com/ShowJoy-com/showjoy-blog/issues/30?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
2鸭轮、http://www.reibang.com/p/b8b59fb80554
3臣淤、http://blog.csdn.net/crazy1235/article/details/51876192
4、http://blog.csdn.net/asce1885/article/details/52878076

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末张弛,一起剝皮案震驚了整個濱河市荒典,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吞鸭,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件覆糟,死亡現(xiàn)場離奇詭異刻剥,居然都是意外死亡,警方通過查閱死者的電腦和手機滩字,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門造虏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人麦箍,你說我怎么就攤上這事漓藕。” “怎么了挟裂?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵享钞,是天一觀的道長。 經(jīng)常有香客問我诀蓉,道長栗竖,這世上最難降的妖魔是什么暑脆? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮狐肢,結果婚禮上添吗,老公的妹妹穿的比我還像新娘。我一直安慰自己份名,他們只是感情好碟联,可當我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著僵腺,像睡著了一般鲤孵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上想邦,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天裤纹,我揣著相機與錄音,去河邊找鬼丧没。 笑死鹰椒,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的呕童。 我是一名探鬼主播漆际,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼夺饲!你這毒婦竟也來了奸汇?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤往声,失蹤者是張志新(化名)和其女友劉穎擂找,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浩销,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡贯涎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了慢洋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塘雳。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖普筹,靈堂內(nèi)的尸體忽然破棺而出败明,到底是詐尸還是另有隱情,我是刑警寧澤太防,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布妻顶,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏盈包。R本人自食惡果不足惜沸呐,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呢燥。 院中可真熱鬧崭添,春花似錦、人聲如沸叛氨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寞埠。三九已至屁置,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仁连,已是汗流浹背蓝角。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饭冬,地道東北人使鹅。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像昌抠,于是被迫代替她去往敵國和親患朱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,876評論 2 361

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