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

推荐阅读更多精彩内容