ButterKnife源碼剖析

logo.png

0X0 前言

做過Android開發的猿類很多都知道ButterKnife這么個東西。這個庫可以大大的簡化我們的代碼,讓我們少寫很多findViewByIdsetOnClickListener這種代碼。網上有很多關于怎么使用這個庫的文章,并且很多人也知道這個庫的工作原理,但是關于這個庫的系統性的源碼分析的文章,似乎并不多。我在研究了一些這個庫的源碼之后,覺得還是有點意思的,在這里簡單的做一下分享。

ButterKnife工作分為三部分,

  1. 搜索代碼中被@Bind注解標記的元素(feild或method),將注解中的id號與元素建立對應關系
  2. 動態生成綁定操作依賴的代碼
  3. 在用戶調用的bing方法中執行最后的綁定

前兩步在預處理過程中執行,最后一步在app運行過程中由app代碼調用執行。

本文以項目源碼附帶的Sample工程的構建過程為例,依次分析各個步驟的執行過程。但在此之前我有義務先給出butterknife的官方源碼倉庫的地址:

https://github.com/JakeWharton/butterknife

由于這個開源項目的版本迭代速度很快,隨著時間的推移,本文中展示的代碼可能會因為過時而和官方倉庫中的產生較大出入,所以我再另外給出我寫這篇文章時從官方倉庫中fork出來的分支的地址:

https://github.com/knightingal/butterknife

以便讀者發現和官方最新的代碼有出入時做參考。

0X1 第一步:解析注解,生成對應關系

和很多其他Android第三方類庫不同,ButterKnife的大部分代碼執行在注解處理器中。注解處理器工作在整個Android工程的構建階段。我們可以把注解處理器理解為類似c語言構建過程中的預處理器的角色,它在編譯器被輸入源碼之前預先對代碼做了一些處理。

注解處理器需要在META-INF中進行注冊才能工作。我們可以在butterknife-x.x.x.jar的/META-INF/services/javax.annotation.processing.Processor中看到注冊的注解處理器butterknife.compiler.ButterKnifeProcessor。該類繼承了jdk中的javax.annotation.processing.AbstractProcessor

ButterKnifeProcessor這個類覆蓋了AbstractProcessor中的4個方法,其中首先需要注意的是getSupportedAnnotationTypes這個方法。它返回了一系列本抽象處理器需要處理的注解,我們可以看到這些注解主要是ButterKnife中和綁定有關的。那么怎么理解這個函數返回的這些注解的作用呢?

通過反復的修改代碼驗證我發現,只有工程代碼中出現了此列表中注冊的注解,才會調用后續的process(Set<? extends TypeElement> elements, RoundEnvironment env)方法。而process(Set<? extends TypeElement> elements, RoundEnvironment env)方法中處理的注解和Set<String> ButterKnifeProcessor.getSupportedAnnotationTypes()返回的注解列表并沒有直接的包含和被包含關系。

比如,我們可以讓getSupportedAnnotationTypes()返回的Set中只包含Unbinder,只要Sample工程中有使用到@Unbinder注解的元素,之后調用的process(Set<? extends TypeElement> elements, RoundEnvironment env)依然可以掃描出@Bind注解的元素,并進行綁定。

相反,如果getSupportedAnnotationTypes()返回的Set中的注解在Sample項目中并沒有被使用到,比如我們只返回BindArrayBindBitmapBindBoolBindColorBindDimenBindDrawableBindIntBindString這幾個,但是實際上Sample工程中并沒有使用到這幾個注解,那么javac就不會執行process(Set<? extends TypeElement> elements, RoundEnvironment env)這個方法,綁定操作就會失效,應用啟動立即core dump。

如果getSupportedAnnotationTypes方法的返回值檢查無誤,接下來編譯器會開始調用process(Set<? extends TypeElement> elements, RoundEnvironment env)方法由此開始正式解析Sample工程中的注解。解析注解的任務主要由Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env)這條語句來完成。

而在findAndParseTargets(env)中,會多次調用
RoundEnvironment.getElementsAnnotatedWith(Class<? extends Annotation> a)方法返回各類被注解的元素集合。比如,調用env.getElementsAnnotatedWith(Bind.class)可以立刻獲得Sample工程中的所有被@Bind注解的元素。

以Sample工程為例,以下是env.getElementsAnnotatedWith(Bind.class)返回的結果。我刪除了一些無需關注的信息。

elementsWithBind = {LinkedHashSet@9711}  size = 9
 0 = {Symbol$VarSymbol@9714} "title"
  name = {UnsharedNameTable$NameImpl@9732} "title"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 1 = {Symbol$VarSymbol@9715} "subtitle"
  name = {UnsharedNameTable$NameImpl@9739} "subtitle"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 2 = {Symbol$VarSymbol@9716} "hello"
  name = {UnsharedNameTable$NameImpl@9744} "hello"
  type = {Type$ClassType@9745} "android.widget.Button"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 3 = {Symbol$VarSymbol@9717} "listOfThings"
  name = {UnsharedNameTable$NameImpl@9750} "listOfThings"
  type = {Type$ClassType@9751} "android.widget.ListView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 4 = {Symbol$VarSymbol@9718} "footer"
  name = {UnsharedNameTable$NameImpl@9756} "footer"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 5 = {Symbol$VarSymbol@9719} "headerViews"
  name = {UnsharedNameTable$NameImpl@9761} "headerViews"
  type = {Type$ClassType@9762} "java.util.List<android.view.View>"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 6 = {Symbol$VarSymbol@9720} "word"
  name = {UnsharedNameTable$NameImpl@9767} "word"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9768} "com.example.butterknife.SimpleAdapter.ViewHolder"
 7 = {Symbol$VarSymbol@9721} "length"
  name = {UnsharedNameTable$NameImpl@9773} "length"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9768} "com.example.butterknife.SimpleAdapter.ViewHolder"
 8 = {Symbol$VarSymbol@9722} "position"
  name = {UnsharedNameTable$NameImpl@9778} "position"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9768} "com.example.butterknife.SimpleAdapter.ViewHolder"

該集合中的元素屬性主要包括元素的名字,類型,所屬的類。

之后在一個foreach循環中對env.getElementsAnnotatedWith(Bind.class)返回集合中的每一個element執行parseBind(element, targetClassMap, erasedTargetNames)

先劇透一下,當parseBind返回時,targetClassMap里面將會保存@Bind注解中的value(即布局文件中的view id)和被注解的元素的對應關系。下面讓我們按住Ctrl鍵單擊parseBind,看看里面都做了什么。

parseBind當中,首先經過一系列的驗證排除掉對集合類元素的注解這種異常場景之后,最終會調用parseBindOne(element, targetClassMap, erasedTargetNames)

而在parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames)中,又要對被注解的元素element是否是安卓View的子類實例以及注解的value是否只有一個值進行校驗。也就是是說,通過@Bind綁定的只能是View,每一個View只能綁定一個id。

校驗結束后,通過element所屬的類enclosingElement(通過element.getEnclosingElement()獲取)為key到targetClassMap中查找bindingClass,對第一個element進行解析的時候肯定是查找不到的,于是就會在getOrCreateTargetClass(targetClassMap, enclosingElement)中創建一個BindingClass類型的bindingClass變量,并且以enclosingElement為key添加到targetClassMap中,后續的element解析過程中會復用這個bindingClass變量。

BindingClass的精髓在于它的viewIdMap成員變量,它是個Map<Integer, ViewBindings>類型的變量,這個Map變量保存了布局中的view id和Activity中被@Bind注解的View類型及其子類型成員變量的對應關系。
成員變量信息保存在ViewBindings的子類FieldViewBinding中。FieldViewBinding的實例通過parseBindOne方法中的最后幾行代碼

String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);

FieldViewBinding binding = new FieldViewBinding(name, type, required);

進行構建,最后通過bindingClass.addField(id, binding);添加到viewIdMap當中。

以下為@Bind注解解析完成后targetClassMap的快照(篇幅有限,刪除了一些無關信息)

targetClassMap = {LinkedHashMap@9687}  size = 2
+-0 = {LinkedHashMap$Entry@9751}
| +-key = {Symbol$ClassSymbol@9753} "com.example.butterknife.SimpleActivity"
| +-value = {BindingClass@9754}
|   +-viewIdMap = {LinkedHashMap@9760}  size = 5
|   | +-0 = {LinkedHashMap$Entry@9770} "2130968576" ->
|   | |  key = {Integer@9775} "2130968576"
|   | |  value = {ViewBindings@9776}
|   | |   id = 2130968576
|   | |   fieldBindings = {LinkedHashSet@9791}  size = 1
|   | |    0 = {FieldViewBinding@9794}
|   | |     name = {String@9795} "title"
|   | |     type = {ClassName@9796} "android.widget.TextView"
|   | +-1 = {LinkedHashMap$Entry@9771} "2130968577" ->
|   | |  key = {Integer@9777} "2130968577"
|   | |  value = {ViewBindings@9778}
|   | |   id = 2130968577
|   | |   fieldBindings = {LinkedHashSet@9800}  size = 1
|   | |    0 = {FieldViewBinding@9803}
|   | |     name = {String@9804} "subtitle"
|   | |     type = {ClassName@9805} "android.widget.TextView"
|   | +-2 = {LinkedHashMap$Entry@9772} "2130968578" ->
|   | |  key = {Integer@9779} "2130968578"
|   | |  value = {ViewBindings@9780}
|   | |   id = 2130968578
|   | |   fieldBindings = {LinkedHashSet@9808}  size = 1
|   | |    0 = {FieldViewBinding@9811}
|   | |     name = {String@9812} "hello"
|   | |     type = {ClassName@9813} "android.widget.Button"
|   | +-3 = {LinkedHashMap$Entry@9773} "2130968579" ->
|   | |  key = {Integer@9781} "2130968579"
|   | |  value = {ViewBindings@9782}
|   | |   id = 2130968579
|   | |   fieldBindings = {LinkedHashSet@9816}  size = 1
|   | |    0 = {FieldViewBinding@9819}
|   | |     name = {String@9820} "listOfThings"
|   | |     type = {ClassName@9821} "android.widget.ListView"
|   | +-4 = {LinkedHashMap$Entry@9774} "2130968580" ->
|   |    key = {Integer@9783} "2130968580"
|   |    value = {ViewBindings@9784}
|   |     id = 2130968580
|   |     fieldBindings = {LinkedHashSet@9824}  size = 1
|   |      0 = {FieldViewBinding@9827}
|   |       name = {String@9828} "footer"
|   |       type = {ClassName@9829} "android.widget.TextView"
|   +-classPackage = {String@9765} "com.example.butterknife"
|   +-className = {String@9766} "SimpleActivity$$ViewBinder"   
+-1 = {LinkedHashMap$Entry@9752}
  +-key = {Symbol$ClassSymbol@9755} "com.example.butterknife.SimpleAdapter.ViewHolder"
  +-value = {BindingClass@9756}
    +-viewIdMap = {LinkedHashMap@9832}  size = 3
    | +-0 = {LinkedHashMap$Entry@9842} "2130968581" ->
    | |  key = {Integer@9845} "2130968581"
    | |  value = {ViewBindings@9846}
    | |   id = 2130968581
    | |   fieldBindings = {LinkedHashSet@9855}  size = 1
    | |    0 = {FieldViewBinding@9858}
    | |     name = {String@9859} "word"
    | |     type = {ClassName@9860} "android.widget.TextView"
    | +-1 = {LinkedHashMap$Entry@9843} "2130968582" ->
    | |  key = {Integer@9847} "2130968582"
    | |  value = {ViewBindings@9848}
    | |   id = 2130968582
    | |   fieldBindings = {LinkedHashSet@9863}  size = 1
    | |    0 = {FieldViewBinding@9866}
    | |     name = {String@9867} "length"
    | |     type = {ClassName@9868} "android.widget.TextView"
    | +-2 = {LinkedHashMap$Entry@9844} "2130968583" ->
    |    key = {Integer@9849} "2130968583"
    |    value = {ViewBindings@9850}
    |     id = 2130968583
    |     fieldBindings = {LinkedHashSet@9871}  size = 1
    |      0 = {FieldViewBinding@9874}
    |       name = {String@9875} "position"
    |       type = {ClassName@9876} "android.widget.TextView"
    +-classPackage = {String@9837} "com.example.butterknife"
    +-className = {String@9838} "SimpleAdapter$ViewHolder$$ViewBinder"   

我們可以從targetClassMap中讀出諸如此類的以下信息:

  • 有兩個類中存在@Bind注解
  • 第一個類為com.example.butterknife.SimpleActivity
  • 該類中有5個被@Bind注解的成員
    
  • 第一個成員名字是title,類型是android.widget.TextView,綁定至id號為2130968576
    
  • 。。。依此類推

至此,對ButterKnife的@Bind注解解析完成,并在targetClassMap中建立起了view id和view實例的對應關系。接下來的任務就是動態的生成綁定依賴的代碼。

0X2 第二步:動態生成綁定依賴的代碼

在第一步的Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env)中,我們得到了有很多個bindingClass的map,其中每一個bindingClass對應一個Sample工程中的涉及ButterKnife注解的類,其中包含了一系列需要ButterKnife處理的信息。這一步就根據這些信息調用bindingClass.brewJava()動態生成綁定依賴的代碼。這里使用到了第三方類庫JavaPoet

還是以SimpleActivity為例,以下代碼首先根據bindingClass.className生成相關的類:

TypeSpec.Builder result = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC)
        .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

這里className=SimpleActivity$$ViewBindertargetClass=SimpleActivity

SimpleActivity沒有父類,于是通過以下代碼給SimpleActivity$$ViewBinder設置父類ViewBinder

  result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));

于是我們到目前為止有了一個動態生成的類SimpleActivity$$ViewBinder,等價如下的代碼:

public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {

}

之后以下語句為這個動態生成的SimpleActivity$$ViewBinder類新增bind方法:

  result.addMethod(createBindMethod());

打開createBindMethod()的實現可以看到,首先構造了一個名為bind的方法,該方法有一個@Override注解,訪問級別為pulbic,三個參數分別為final Finder finder, final T target, Object source

MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(FINDER, "finder", FINAL)
        .addParameter(TypeVariableName.get("T"), "target", FINAL)
        .addParameter(Object.class, "source");

接下來用一個for循環遍歷viewIdMap,這個map中的每一個value就是一個在bindingClass對應的Activity中被@Binde注解的View類型成員變量。在這個for循環中,對每一個View類型變量調用addViewBindings方法,代碼如下:

for (ViewBindings bindings : viewIdMap.values()) {
        addViewBindings(result, bindings);
}

addViewBindings方法中最終調用以下語句針對每一個View類型變量生成具體的執行代碼(以下不是最終生成的代碼,而是控制生成代碼的代碼):

result.addStatement("view = finder.findRequiredView(source, $L, $S)", bindings.getId(),
            asHumanDescription(requiredViewBindings));
result.addStatement("target.$L = finder.castView(view, $L, $S)", fieldBinding.getName(),
            bindings.getId(), asHumanDescription(fieldBindings));

比如對于SimpleActivity中的成員變量title,其最終生成的執行代碼為

view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");

我們暫時可以先不用關心findRequiredViewcastView這兩個方法的實現分別是什么,在這里,即抽象處理器中,它們暫時只是一段自動生成的,不會被編譯運行的文本而已。

等這些代碼都構建完成后,調用writeTo(filer);將構建的類寫入文件,動態生成代碼的工作就完成了。

在這一步中,最終動態生成的源碼可在Sample工程構建完成后的build\generated\source\apt\debug\com\example\butterknife目錄下找到。

0X3 第三步:運行時綁定

綁定操作位于app運行時。通常由在ActivityonCreate方法內調用ButterKnife.bind(this)觸發執行。

打開ButterKnife類的定義,可以看到有多個bind方法的重載,Activity中調用的重載版本是

public static void bind(@NonNull Activity target) {
  bind(target, target, Finder.ACTIVITY);
}

注意這里的第三個參數Finder.ACTIVITY,它是枚舉Finder下的一個枚舉值,而Finder中聲明了兩個抽象方法

protected abstract View findView(Object source, int id);

public abstract Context getContext(Object source);

又分別在包括Finder.ACTIVITY在內的一系列枚舉值當中做了實現。所以我們實際上可以認為Finder是一個抽象類,而Finder.ACTIVITYFinder.VIEWFinder.DIALOG是這個抽象類的實例,他們各自對以上兩個抽象方法做了自己的實現。

后續的綁定流程中,Finder.ACTIVITY會以Finder類型的身份出現,當看到類似finder.findView(source, id)這樣的語句時,我們就可以知道去哪里查看其內部實現。

bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder)中,首先根據target的類型targetClass,在這里即SimpleActivity找到其對應的ViewBinder,該操作位于findViewBinderForClass(targetClass)中。

findViewBinderForClass方法中,針對每個targetClass,如果是初次運行該方法,會通過Class.forName(String className)方法動態加載其對應的ViewBinder類。這里classNametargetClass的名字和$$ViewBinder拼接。以SimpleActivity為例,取到的類就是com.example.butterknife.SimpleActivity$$ViewBinder,即我們之前在build\generated\source\apt\debug\com\example\butterknife下動態生成的類。

如果ViewBinder類獲取成功,newInstance方法獲取其實例,以targetClass為key放入Map BINDERS中,下次再找targetClass對應的ViewBinder類實例時可直接在BINDERS中查找。最后返回這個ViewBinder類的實例。

取到了對應的ViewBinder實例之后,立即執行viewBinder.bind(finder, target, source)這里的finder是剛才的Finder.ACTIVITYtargetsource都是調用ButterKnife.bindSimpleActivity實例。

這里的viewBinder.bind(finder, target, source);執行的就是之前第二步中動態構造出來的方法,里面執行了一系列具體的view綁定操作,就是我們在第二步中暫時不用關心的那兩行代碼:

view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");

現在我們需要了解這兩個代碼具體是怎么執行的綁定操作。

先看finder.findRequiredView(source, 2130968576, "field 'title'")這個方法,它首先會調用Finder.ACTIVITY中的findView(Object source, int id)實現版本,可以看到該版本的findView(Object source, int id)

@Override protected View findView(Object source, int id) {
    return ((Activity) source).findViewById(id);
}

source強轉為Activity類型后調用了它的 findViewById(id)方法,就是我們寫到吐的那個方法。該方法返回了一個View類型變量。

如果我們手寫findViewById(id)的話,通常會對其返回值進行一次類型轉換,轉換為這個View的實際類型,比如最常見的TextViewListView這種。而在這里,這種類型轉換在接下來的target.title = finder.castView(view, 2130968576, "field 'title'")中進行(findRequiredView方法內雖然自帶了一次castView調用,但是findRequiredView的返回值是View類型,所以這里的類型轉換并沒有起作用)。

public <T> T castView(View view, int id, String who) {
    try {
      return (T) view;
    } catch (ClassCastException e) {
      // 處理一些幾乎不可能發生的異常情況
  }

castView方法會根據它的模板類型T,即返回值的類型(此處為target.title的類型TextView)自行推斷需要將view轉換的目標類型。最后將返回值賦值給target.titleSimpleActivitytitle成員變量,至此整個綁定操作完成。

到此為止,butterknife的工作就結束了。

0X4 結束前再寫點廢話

隨著SimpleActivitytitle變量找到了它在布局文件中對應的TextView,本文對butterknife的工作原理的源碼分析也告一段落。寫這篇文章的時間跨度實在有點長,3月份開始看butterknife的源碼并自行調試,4月份開始打草稿,懶癌發作拖拖拉拉搞到今天已經是5月底,再到github上一看發現本文中最為核心的@Bind注解居然不見了,看的我一臉懵逼(后來仔細一看改名為@BindView了),不得不佩服自己當初手快fork了一個分支出來的先見之明。現在還不知道距離兩個月前代碼官方又做了哪些修改,但是不管怎么改,工作原理應該還是沒有太大變化的。本文暫且還是基于7兩個月前的代碼,地址我已經在開頭貼出來了。

希望我的這篇文章對大家學習和使用ButterKnife,甚至根據自己的需要進行魔改有所幫助。

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

推薦閱讀更多精彩內容