手寫B(tài)utterKnife注解框架

[TOC]

zero bind library是一個仿ButterKnife的編譯期注解框架的練習(xí),旨在熟悉編譯期注解和注解處理器的工作原理以及相關(guān)的API鹦付。當(dāng)前基本都使用Android Studio進行android開發(fā)变骡,因此這個練習(xí)也基于AS開發(fā)環(huán)境(AS3.0, gradle-4.1-all, com.android.tools.build:gradle:3.0.0)。練習(xí)中大量參考了ButterKnife的源碼,這些代碼基本都源于ButterKnife倍啥,甚至目錄結(jié)構(gòu)和gradle的一些配置和編寫風(fēng)格,注釋未及之處參考JakeWharton/butterknife 澎埠。筆者水平有限虽缕,錯誤在所難免,歡迎批評指正蒲稳。

關(guān)于Processor

為了能更好的了解注解處理器在處理注解時進行了那些操作氮趋,代碼調(diào)試的功能似乎是必不可少的,然而注解處理器是在javac之前執(zhí)行江耀,所以直接在處理器中打斷點然后運行是調(diào)試不到注解處理器的剩胁。可以搜索相關(guān)的文章了解祥国,比如這個如何調(diào)試編譯時注解處理器AnnotationProcessor 昵观,鑒于調(diào)試的麻煩,剛開始了解Processor可以使用類似于打印日志的方式舌稀,這里需要注意的是System.out.println()無法在控制臺打印日志啊犬,因此首先搭建一個具有日志輸出功能的Processor。以下給出一個LoggerProcessor

package zero.annotation.processor;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;

public abstract class LoggerProcessor extends AbstractProcessor {

  private Messager messager;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    messager = processingEnv.getMessager();
  }

  protected void error(Element element, String message, Object... args) {
    printMessage(Diagnostic.Kind.ERROR, element, message, args);
  }

  protected void note(Element element, String message, Object... args) {
    printMessage(Diagnostic.Kind.NOTE, element, message, args);
  }

  private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
    if (args.length > 0) {
      message = String.format(message, args);
    }
    messager.printMessage(kind, message, element);
  }
}

Processor#init顧名思義對注解處理器進行一些配置壁查,如這里獲取Message對象觉至。注解處理器框架涉及到大量的接口,這些接口用于幫助我們對注解進行處理睡腿,比如Processor康谆、MessagerElement等等都是接口嫉到。

Messager#printMessage(Diagnostic.Kind, CharSequence, Element)

    /**
     * Prints a message of the specified kind at the location of the
     * element.
     *
     * @param kind the kind of message
     * @param msg  the message, or an empty string if none
     * @param e    the element to use as a position hint
     */
    void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e);

這里傳入的參數(shù)Element用于源碼的定位沃暗,比如處理注解時警告或者錯誤信息。上面的note()方法使用后note(element, "bind with layout id = %#x", id)的效果如:

/home/jmu/AndroidStudioProjects/zero/sample/src/main/java/com/example/annotationtest/MainActivity.java:9: 注: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
       ^

error()將使得注解處理器在調(diào)用處打印錯誤信息何恶,并導(dǎo)致最終編譯失斈踝丁:

...MainActivity.java:9: 錯誤: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
       ^
2 個錯誤

:sample:compileDebugJavaWithJavac FAILED

FAILURE: Build failed with an exception.

有了這兩個日志方法,就可以在適當(dāng)?shù)臅r候在控制臺打印想要了解的信息细层。

第一個注解@ContentView

ContentView.java

package zero.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ContentView {
  int value();
}

這個注解使用在Activity類上惜辑,為Activity指定布局。類似于ButterKnife(ButterKnife不提供類似的注解)疫赎,@ContentView的作用使得我們將來要在

package com.example.annotationtest;

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Zero.bind(this);
  }
}

Zero.bind(this)之后調(diào)用注解處理器生成的java代碼Activity.setContentView(id)盛撑,注意不是使用反射來調(diào)用Activity.setContentView

ContentViewProcessor

package zero.annotation.processor;

@SupportedAnnotationTypes({"zero.annotation.ContentView"})
public class ContentViewProcessor extends LoggerProcessor {
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
    for (Element element : elements) {
      Element enclosingElement = element.getEnclosingElement();
      System.out.println(enclosingElement.getClass());
      int id = element.getAnnotation(ContentView.class).value();
      note(element, "bind with layout id = %#x", id);
    }
    return true;
  }

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

再議Processor(詳見api)

  1. Set<String> getSupportedAnnotationTypes();

    指定該注解處理器可以處理那些注解捧搞,重寫該方法返回一個Set<String>或者在處理器上使用注解@SupportedAnnotationTypes

  2. SourceVersion getSupportedSourceVersion();

    支持的java編譯器版本抵卫,重寫或者使用@SupportedSourceVersion注解

  3. void init(ProcessingEnvironment processingEnv)狮荔;

    Initializes the processor with the processing environment.

  4. boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);

    處理注解的方法,待處理的注解通過參數(shù)annotations傳遞介粘,返回值表示注解是否已被處理殖氏,roundEnv表示當(dāng)前和之前的處理環(huán)境。

上面的代碼簡單的遍歷了使用@ContentView的類姻采,并將其中的布局文件id打印在控制臺(驗證System.out.println是否生效)雅采。我們循序漸進旨在能在探索中了解Processor 。為了在AS上使用該處理器慨亲,需要進行一些配置婚瓜,這些配置相比eclipse相對簡單。

//1.結(jié)構(gòu)
sample
├── build.gradle
├── proguard-rules.pro
└── src
    └── main
        ├── AndroidManifest.xml
        └── java/android/com/example/annotationtest
                                    └── MainActivity.java
zero-annotation
├── build.gradle
└── src/main/java/zero/annotation
                       └── ContentView.java

zero-annotation-processor/
├── build.gradle
└── src/main
        ├── java/zero/annotation/processor
        │                        ├── ContentViewProcessor.java
        │                        └── LoggerProcessor.java
        └── resources/META-INF/services
                               └── javax.annotation.processing.Processor
                               
//2.1 javax.annotation.processing.Processor內(nèi)容
zero.annotation.processor.ContentViewProcessor

//2.2 sample/build.gradle依賴部分
dependencies {
    //其他依賴...
    annotationProcessor project(path: ':zero-annotation-processor')
    api project(path: ':zero-annotation')
}

對比eclipse下的配置刑棵,as中只需要上面的2.1,2.2即可使用自定義的注解處理器闰渔。

Processor生成java代碼

建立Android library :zero, 依賴

zero
├── build.gradle
├── proguard-rules.pro
└── src/main
        ├── AndroidManifest.xml
        └── java/zero
                ├── IContent.java
                └── Zero.java

//IContent.java
public interface IContent {
  void setContentView(Activity activity);
}

//build.gradle.dependencies
dependencies {
    ...
    annotationProcessor project(path: ':zero-annotation-processor')
    compile project(path: ':zero-annotation-processor')
}

提供IContent接口,希望使用了@ContentView后的Activity可以在同目錄下生成一個形如Activity$$ZeroBind的類铐望,并且實現(xiàn)IContent接口冈涧,如MainActivity$$ZeroBind

// Generated code from Zero library. Do not modify!
package com.example.annotationtest;

public class MainActivity$$ZeroBind implements zero.IContent {

  @Override
  public void setContentView(android.app.Activity activity) {
    activity.setContentView(2131296283);
  }
}

當(dāng)使用Zero.bind(this)時,反射創(chuàng)建MainActivity$$ZeroBind對象正蛙,調(diào)用IContent.setContentView來為MainActivity設(shè)置布局督弓。因此下面的小目標(biāo)就是通過Processor生成MainActivity$$ZeroBind.java文件:

@SupportedAnnotationTypes({"zero.annotation.ContentView"})
public class ContentViewProcessor extends LoggerProcessor {

  public static final String SUFFIX = "$$ZeroBind";

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

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    filer = processingEnv.getFiler();
    elementUtils = processingEnv.getElementUtils();
    typeUtils = processingEnv.getTypeUtils();
  }

  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
    for (Element element : elements) {
//      Element enclosingElement = element.getEnclosingElement();
//      note(enclosingElement, "%s", enclosingElement.getClass().getSuperclass());
      int id = element.getAnnotation(ContentView.class).value();
//      note(element, "bind with layout id = %#x", id);
      TypeMirror typeMirror = element.asType();
//      note(element, "%s\n%s", typeMirror.toString(), typeMirror.getKind());

      try {
        String classFullName = typeMirror.toString() + SUFFIX;
        JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
        Writer writer = sourceFile.openWriter();
        TypeElement typeElement = elementUtils.getTypeElement(typeMirror.toString());
        PackageElement packageOf = elementUtils.getPackageOf(element);
        writer.append("http:// Generated code from Zero library. Do not modify!\n")
          .append("package ").append(packageOf.getQualifiedName()).append(";\n\n")
          .append("public class ").append(typeElement.getSimpleName()).append(SUFFIX).append(" implements zero.IContent {\n\n")
          .append("  @Override\n")
          .append("  public void setContentView(android.app.Activity activity) {\n")
          .append("    activity.setContentView(").append(String.valueOf(id)).append(");\n")
          .append("  }\n")
          .append("}")
          .flush();
        writer.close();
      } catch (IOException e) {
        error(element, "不能寫入java文件!");
      }
    }
    return true;
  }

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

通過上面的處理器將產(chǎn)生MainActivity$$ZeroBind.java文件在:

sample/
├── build
    └── generated
        └── source
            └── apt
                └── debug
                    └── com
                        └── example
                            └── annotationtest
                                └── MainActivity$$ZeroBind.java
 //注解處理器生成的源代碼都在 build/generated/source/apt目錄下

這個源碼與MainActivity在同一個包中乒验,因此可以訪問到MainActivity中的包級成員愚隧。

為了說明上面的代碼以及理解,需要一些準(zhǔn)備知識锻全。

javax.lang.model包

描述
javax.lang.model Classes and hierarchies of packages used to model the Java programming language.
javax.lang.model.element Interfaces used to model elements of the Java programming language.
javax.lang.model.type Interfaces used to model Java programming language types.
javax.lang.model.util Utilities to assist in the processing of program elements and types.

主要介紹:ElementTypeMirror

Element

參看https://docs.oracle.com/javase/7/docs/api/javax/lang/model/element/Element.html

All Known Subinterfaces:

ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement

繼承關(guān)系
Element
    PackageElement (javax.lang.model.element)
    ExecutableElement (javax.lang.model.element)
    VariableElement (javax.lang.model.element)
    TypeElement (javax.lang.model.element)
    QualifiedNameable (javax.lang.model.element)
        PackageElement (javax.lang.model.element)
        TypeElement (javax.lang.model.element)
    Parameterizable (javax.lang.model.element)
        ExecutableElement (javax.lang.model.element)
        TypeElement (javax.lang.model.element)
    TypeParameterElement (javax.lang.model.element)

public interface Element

代表程序中的元素狂塘,如包、類或方法鳄厌。每個元素表示一個靜態(tài)的荞胡、語言級的構(gòu)造(不是運行時虛擬機構(gòu)造的)。

元素的比較應(yīng)該使用 equals(Object) 方法. 不能保證任何特定元素總是由同一對象表示了嚎。

實現(xiàn)基于一個 Element 對象的類的操作, 使用 visitor 或者 getKind() 方法. 由于一個實現(xiàn)類可以選擇多個 Element 的子接口泪漂,使用 instanceof 來決定在這種繼承關(guān)系中的一個對象的實際類型未必是可靠的。

package com.example.demo;//[PackageElement, ElementKind.PACKAGE]
public class Main {//[TypeElement,ElementKind.CLASS]
  int a;//[VariableElement, ElementKind.FIELD]
  
  static {//[ExecutableElement, ElementKind.STATIC_INIT]
    System.loadLibrary("c");
  }
  {//[ExecutableElement, ElementKind.INSTANCE_INIT]
    a = 100;
  }
  public Main(){//[ExecutableElement,ElementKind.CONSTRUCTOR]
    int b = 10;//[VariableElement, ElementKind.LOCAL_VARIABLE]
  }
  
  public String toString(){//[ExecutableElement, ElementKind.METHOD]
    return super.toString();
  }
}

public @interface OnClick{//[TypeElement, ElementKind.ANNOTATION_TYPE]
  
}

public interface Stack<T>{//[TypeElement,ElementKind.INTERFACE]
  T top;//[VariableElement, ElementKind.FIELD, TypeKind.TYPEVAR]
  TypeNotExists wtf;//[VariableElement, ElementKind.FIELD, TypeKind.ERROR]
}

Method Detail

  1. TypeMirror asType() 返回元素定義的類型

    一個泛型元素定義一族類型歪泳,而不是一個萝勤。泛型元素返回其原始類型. 這是元素在類型變量相應(yīng)于其形式類型參數(shù)上的調(diào)用. 例如, 對于泛型元素 C<N extends Number>, 返回參數(shù)化類型 C<N> . Types 實用接口有更通用的方法來獲取元素定義的所有類型的范圍。

  2. ElementKind getKind() 返回元素的類型

  3. List<? extends AnnotationMirror> getAnnotationMirrors() 返回直接呈現(xiàn)在元素上的注解

    使用getAllAnnotationMirrors可以獲得繼承來的注解

  4. <A extends Annotation> A getAnnotation(Class<A> annotationType)

    返回呈現(xiàn)在元素上的指定注解實例呐伞,不存在返回null 敌卓。注解可以直接直接呈現(xiàn)或者繼承。

  5. Set<Modifier> getModifiers() 返回元素的修飾符

  6. Name getSimpleName() 返回元素的簡單名字

    泛型類的名字不帶任何形式類型參數(shù)伶氢,比如 java.util.Set<E> 的SimpleName是 "Set". 未命名的包返回空名字趟径, 構(gòu)造器返回"<init>"瘪吏,靜態(tài)代碼快返回 "<clinit>" , 匿名內(nèi)部類或者構(gòu)造代碼快返回空名字.

  7. Element getEnclosingElement()

    返回元素所在的最里層元素, 簡言之就是閉包.

    • 如果該元素在邏輯上直接被另一個元素包裹舵抹,返回該包裹的元素
    • 如果是一個頂級類, 返回包元素(PackageElement)
    • 如果是包元素返回null
    • 如果是類型參數(shù)或泛型元素,返回類型參數(shù)(TypeParametrElement)
  8. List<? extends Element> getEnclosedElements()

    返回當(dāng)前元素直接包裹的元素集合劣砍。類和接口視為包裹字段惧蛹、方法、構(gòu)造器和成員類型刑枝。 這包括了任何隱式的默認(rèn)構(gòu)造方法香嗓,枚舉中的valuesvalueOf方法。包元素包裹在其中的頂級類和接口装畅,但不認(rèn)為包裹了子包靠娱。其他類型的元素當(dāng)前默認(rèn)不包裹任何元素,但可能 跟隨API和編程語言而變更掠兄。

    注意某些類型的元素可以通過 ElementFilter中的方法分離出來.

TypeMirror

參考https://docs.oracle.com/javase/7/docs/api/javax/lang/model/type/TypeMirror.html

All Known Subinterfaces:

ArrayType, DeclaredType, ErrorType, ExecutableType, NoType, NullType, PrimitiveType, ReferenceType, TypeVariable, UnionType, WildcardType

public interface TypeMirror

表示java中的一個類型. 類型包含基本類型像云、聲明類型 (類和接口)、數(shù)組蚂夕、類型變量和null 類型. 也表示通配符類型參數(shù)(方法簽名和返回值中的), 以及對應(yīng)包和關(guān)鍵字 void的偽類型.

類型的比較應(yīng)該使用 Types. 不能保證任何特定類型總是由同一對象表示迅诬。

實現(xiàn)基于一個 TypeMirror 對象的類的操作, 使用 visitor 或者 getKind() 方法. 由于一個實現(xiàn)類可以選擇多個 TypeMirror 的子接口,使用 instanceof 來決定在這種繼承關(guān)系中的一個對象的實際類型未必是可靠的婿牍。

Utility

javax.lang.model.util下的接口(主要指Elements侈贷,Types)擁有一些實用的方法。

  1. PackageElement Elements.getPackageOf(Element type)

    Returns the package of an element. The package of a package is itself.

  2. TypeElement Elements.getTypeElement(CharSequence name)

    Returns a type element given its canonical name.

  3. boolean Types.isAssignable(TypeMirror t1, TypeMirror t2)

    Tests whether t1 is assignable to t2.

  4. boolean Types.isSameType(TypeMirror t1, TypeMirror t2)

    Tests whether two TypeMirror objects represent the same type. Return true if and only if the two types are the same

  5. boolean Types.isSubtype(TypeMirror t1, TypeMirror t2)

    Return true if and only if the t1 is a subtype of t2

Process生成java代碼續(xù)

現(xiàn)在我們詳細注釋下ContentViewProcessor#process 等脂,代碼有少許不同

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
    for (Element element : elements) {
      //ContentView定義時指定作用范圍是類俏蛮,所以只能作用于類上,Element一定是類元素
      if(element.getKind() != ElementKind.CLASS){
        error(element, "ContentView注解必須作用在類上!");
        throw new RuntimeException();
      }
      
      TypeElement typeElement = (TypeElement) element;
      //獲取包元素上遥,主要為了方便獲取Element的包名
      //element是類元素搏屑,因此還可以使用:
      //PackageElement packageOf = (PackageElement) element.getEnclosingElement();
      PackageElement packageOf = elementUtils.getPackageOf(element);
      int id = element.getAnnotation(ContentView.class).value();

      try {
        //仿照ButterKnife粉楚,使用自己的后綴
        String classFullName = typeElement.getQualifiedName() + SUFFIX;
        //JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
        //name:完整類名
        //originatingElements:和創(chuàng)建的文件相關(guān)的類元素或包元素睬棚,可省略或為null
        JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
        Writer writer = sourceFile.openWriter();
        //關(guān)于ContentView注解的java 文件模板
        String tmp =
          "http:// Generated code from Zero library. Do not modify!\n" +
            "package %s;\n\n" +
            "public class %s implements zero.IContent {\n\n" +
            "  @Override\n" +
            "  public void setContentView(android.app.Activity activity) {\n" +
            "    activity.setContentView(%d);\n" +
            "  }\n" +
            "}";
        //填充包名,類名解幼,布局文件id
        writer.write(String.format(tmp, packageOf.getQualifiedName(), typeElement.getSimpleName()+SUFFIX, id));
        writer.close();
      } catch (IOException e) {
        error(element, "不能寫入java文件抑党!");
      }
    }
    return true;//ContentView被我處理了
  }

Zero.bind

基于注解處理器生成的java代碼已完成,最后一道工序需要將代碼調(diào)用起來即可撵摆。

public class Zero {
  public static void bind(Activity activity){
    try {
      String fullName = activity.getClass().getCanonicalName()+ ContentViewProcessor.SUFFIX;
      Class<?> zeroBind = Class.forName(fullName);
      IContent content = (IContent) zeroBind.getConstructor().newInstance();
      content.setContentView(activity);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

現(xiàn)在可以向ButterKnife一樣使用Zero.bind 底靠。這里根據(jù)我們定義的規(guī)則使用了少量的運行時反射手段用于動態(tài)調(diào)用適當(dāng)?shù)拇a,另外發(fā)布時需要將相應(yīng)的類不做混淆處理即可特铝。

本文著重介紹注解處理器相關(guān)api及其應(yīng)用暑中,至于代碼的封裝可以參考筆者添加 @BindView@OnClick 后的代碼(zero-bind-library)或者ButterKnife 壹瘟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鳄逾,隨后出現(xiàn)的幾起案子稻轨,更是在濱河造成了極大的恐慌,老刑警劉巖雕凹,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件殴俱,死亡現(xiàn)場離奇詭異,居然都是意外死亡枚抵,警方通過查閱死者的電腦和手機线欲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汽摹,“玉大人李丰,你說我怎么就攤上這事”破” “怎么了趴泌?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拉庶。 經(jīng)常有香客問我踱讨,道長,這世上最難降的妖魔是什么砍的? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任痹筛,我火速辦了婚禮,結(jié)果婚禮上廓鞠,老公的妹妹穿的比我還像新娘帚稠。我一直安慰自己,他們只是感情好床佳,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布滋早。 她就那樣靜靜地躺著,像睡著了一般砌们。 火紅的嫁衣襯著肌膚如雪杆麸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天浪感,我揣著相機與錄音昔头,去河邊找鬼。 笑死影兽,一個胖子當(dāng)著我的面吹牛揭斧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播峻堰,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼讹开,長吁一口氣:“原來是場噩夢啊……” “哼盅视!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起旦万,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤闹击,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后成艘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赏半,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年狰腌,在試婚紗的時候發(fā)現(xiàn)自己被綠了除破。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牧氮。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡琼腔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出踱葛,到底是詐尸還是另有隱情丹莲,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布尸诽,位于F島的核電站甥材,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏性含。R本人自食惡果不足惜洲赵,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望商蕴。 院中可真熱鬧叠萍,春花似錦、人聲如沸绪商。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽格郁。三九已至腹殿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間例书,已是汗流浹背锣尉。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留决采,地道東北人悟耘。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像织狐,于是被迫代替她去往敵國和親暂幼。 傳聞我的和親對象是個殘疾皇子筏勒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351