简单粗暴的Dagger2使用介绍

安卓基础开发库,让开发简单点。
Demo地址https://github.com/LJYcoder/MvpDagger

学习/参考地址:
https://www.jianshu.com/p/cd2c1c9f68d4
https://blog.csdn.net/lisdye2/article/details/51942511
https://www.jianshu.com/p/24af4c102f62

前言

Dagger2已经出来挺久了,网上相关的教程也很多,普遍都说它比较难上手。
相比起其他框架确实如此,但只要学习后加以实践,还是比较好明白的,说不定你还会从此对它爱不释手。
我会尽量把它讲得容易理解些,如有不当之处,还请大家指出纠正。

什么是Dagger2

Dagger2是一个依赖注入(Dependency Injection)框架。

什么又是依赖注入呢?

借别人的话来说,就是“目标类中所依赖的其他类的初始化过程,不是通过在目标类中编码的方式完成,而是通过其他手段把已经初始化好的实例自动注入到目标类中”。

再换种方式来说,就是把类实例的初始化过程都挪至一个地方统一管理,具体需要哪个实例时,就从这个地方取出(注入到目标类中)。

使用Dagger2有什么好处

知其然,然后要知其所以然。

1. 解耦

假设有一个A类,项目中很多地方都使用到它(在很多地方通过new A()对A实例进行了初始化)。然后由于需求变动,A的构造函数增加了一个参数。
好了,牵一发而动全身,你需要把各个new A()的地方都进行修改。
但如果是使用Dagger2进行管理,你只需在类实例的供应端进行修改即可。

2. 让功能实现更专注于功能实现

假设现在你需要调用A类的x()方法来实现某功能,但是A类的构造过程相当的复杂(这样的例子可以参考GreenDao中获取XXXDao、Retrofit中获取Observable请求)

public void xxx(){
    E e = new E();
    D d = new D(e);
    C c = new C();
    B b = new B(c,d);
    A a = new A(b);
    a.x();
}

结果6行代码中,构造实例a占了5行,调用x()方法实现功能却只占了1行。
但如果使用Dagger2进行管理,将a实例的构造过程移至实例供应端,则功能实现模块的代码会变成这样

@Injcet
A a;
public void xxx(){
    a.x();
}

这就是所说的让功能实现更专注于功能实现,而不必去管a实例的构造过程。

3. 更好地管理类实例

通常我们开发中会有两种类实例:
一种是全局实例(单例),它们的生命周期与app保持一致。
一种是页面实例,它们的生命周期与页面保持一致。
通过Dagger2,我们可以使用一个组件专门管理全局类实例(也免去了单例的写法,不用考虑饿汉懒汉什么的);然后各个页面也有各自组件去管理它们的页面实例。
这样不管是对于实例的管理,还是项目的结构,都会变得更加的清晰明了。

4. 逼格高

(这点可以略过...)
当你不认识Dagger2却看着使用Dagger2的项目代码,很可能会感觉到一种茫然与高大上:
各种@Inject,@Provides,@Singleton,Lazy<>,Provider<>等是什么鬼?
为什么没有实例化的代码?
为什么明明是null却不会报空指针?
等你学会使用Dagger2之后,你也可以来一波"高逼格"的代码。

当然,这里也不得不提一下,使用Dagger2会增加代码量。所以如果是小项目/独立开发,你也可以考虑不用,因为你可能有种失大于得的感觉。如果是大项目/团队开发,使用后就得大于失了。


角色介绍

在讲用法前,先对几个重要角色进行了解。

实例需求端

一个类中,它包含了实例成员,在使用这些实例前,需要对它们进行初始化,但前面已经说了初始化的过程挪至其他地方。所以这个类的需求是,已经完成初始化的实例对象。
暂且把这样的类称为“实例需求端”。

实例供应端

进行实例初始化的地方,并对外供应所需的实例。

Dagger2中,供应端有两种方式可以供应实例:(后面会介绍)
第一种:使用Module
第二种:使用@Inject标注构造函数

桥梁

实例供应端并不知道它要供应的实例交给谁。
这个时候就需要通过桥梁,将实例需求端与实例供应端联系在一起。

Dagger2中,Component就负责扮演桥梁的角色。

使用介绍

1. 初步使用

1.1 添加依赖

compile 'com.google.dagger:dagger:2.14.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.14.1'

1.2 处理实例需求端

在实例需求端中,使用@Inject标注需要注入的实例变量。

public class UploadActivity extends AppCompatActivity{
    @Inject
    UploadPresenter mPresenter;
}

1.3 处理实例供应端

前面说了,供应端有两种方式可以供应实例。

方式一 使用Module

  • 使用@Module标注类,表示它为供应端
  • 在类中使用@Provides标注方法,表示它为提供实例的方法。
    在该方法中对实例进行初始化并返回该实例。
@Module
public class UploadActivityModule {
    @Provides
    UploadPresenter uploadPresenter() {
        return new UploadPresenter();
    }
}

方式二 使用@Inject标注构造函数

public class UploadPresenter{
    @Inject
    public UploadPresenter() {
    }
}

注意

方式一的优先级高于方式二,意思就是:

  • 在供应某实例时,会先通过方式一查找是否存在返回该实例的的方法
  • 如果存在,则获取实例并对外供应
  • 如果不存在,再通过方式二查找是否存在@Inject标注的构造函数
  • 如果存在,则将通过该构造函数构造实例并对外供应
  • 如果不存在,那将报错,因为无法供应所需的实例

1.4 搭建桥梁

  • 使用@Component标注接口,表示它为桥梁。
    (如果使用1.3.1的方式供应实例,则需在@Component(modules=xxx.class)中指明module。)
    一个Component可以没有module,也可以同时有多个module。
  • 添加 void inject(实例需求端) 方法,表明实例供应端中的实例将交给该实例需求端。
    通过这个方法,查找实例需求端中需要注入的实例有哪些(使用@Inject标注的那些实例),然后在实例供应端中获取所需的实例,最后注入到实例需求端中
  • 用来注入的方法,它的方法名不一定要是inject,可以随便取,一般都取inject。但该方法的返回类型必须为void
@Component(modules = UploadActivityModule.class)
public interface UploadActivityComponent {
    void inject(UploadActivity uploadActivity);
}

1.5 编译,注入

  • 完成以上几步后,ReBuild一下项目以生成DaggerUploadActivityComponent类。
  • 在实例需求端中调用inject完成实例的注入
public class UploadActivity extends AppCompatActivity{
    @Inject
    UploadPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入实例
        DaggerUploadActivityComponent.builder()
            .build()
            .inject(this);

        //注入后即可调用mPresenter中的方法了
        mPresenter.xxx();
    }
}

到此,Dagger2的初步使用就结束了,下面开始讲解些进阶用法。


2. 接收外部参数

有些时候,实例的初始化需要接收外部参数才能完成,比如MVP中的Presenter往往需要传入IView接口以便完成数据回调。

现在UploadPresenter的构造函数发生了变动,需传入IUploadView。

public class UploadPresenter{
    IUploadView mIView;
    public UploadPresenter(IUploadView iview) {
          mIView = iview;
    }
}

那在供应端中,如何接收外部参数IUploadView呢?
下面介绍两种方式:

2.1 方式一 通过Module的构造函数传入

对实例供应端的module进行改造

@Module
public class UploadActivityModule {
    IUploadView mIView;
    public UploadActivityModule(IUploadView iview) {
        mIView = iview;
    }

    @Provides
    IUploadView iUploadView(){
         return mIView;
    }

    @Provides
    UploadPresenter uploadPresenter(IUploadView iview) {
        return new UploadPresenter(iview);
    }
}
  • 添加了构造函数以便获取外部参数IUploadView
  • uploadPresenter()方法添加了IUploadView参数
    • 到时构建UploadPresenter实例时,会在这个Module或者同个Component下的其他Module中查找是否存在返回IUploadView的的方法
    • 如果存在,则通过该方法获取IUploadView以构造UploadPresenter
    • 如果不存在,则通过1.3.2方式查找是否存在@Inject标注的构造函数(这个例子中当然不会存在,因为IUploadView是接口)
  • 不直接使用mIView来构造UploadPresenter是为了降低耦合度。uploadPresenter()方法只管获取构造Presenter所需的参数,而该参数从哪来、以后会有哪些变动,交给该参数的供应方法(iUploadView())处理即可。

利用Module构造函数传入外部参数

public class UploadActivity implements IUploadView{
    @Inject
    UploadPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入实例
        DaggerUploadActivityComponent.builder()
            .uploadActivityModule(new UploadActivityModule(this))//通过构造函数传入外部参数IUploadView
            .build()
            .inject(this);

        //注入后即可调用mPresenter中的方法了
        mPresenter.xxx();
    }

    //实现IUploadView接口的方法
    @Override
    public void onUploadSuccess() {
        //上传成功
    }
    @Override
    public void onUploadFail() {
        //上传失败
    }
}

2.2 方式二 通过Component传入

对桥梁Component进行改造

@Component(modules = UploadActivityModule.class)
public interface UploadActivityComponent {
    void inject(UploadActivity uploadActivity);

    IUploadView iUploadView();

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder iUploadView(IUploadView iUploadView);

        UploadActivityComponent build();
    }
}
  • 加入iUploadView()方法返回IUploadView
  • 加入了Builder来接收IUploadView

构建Component时传入外部参数

public class UploadActivity extends AppCompatActivity implements IUploadView{
    @Inject
    UploadPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入实例
        DaggerUploadActivityComponent.builder()
            .iUploadView(this)
            .build()
            .inject(this);

        //注入后即可调用mPresenter中的方法了
        mPresenter.xxx();
    }

    //实现IUploadView接口的方法
    @Override
    public void onUploadSuccess() {
        //上传成功
    }
    @Override
    public void onUploadFail() {
        //上传失败
    }
}


3. 限定符注解 @Qualifier

@Qualifier主要是用于解决,因实例供应端存在多个返回类型相同的供应方法而引起歧义的问题。
下面举个例子,现在页面需有两个Dialog,一个用于登录,一个用于注册。

3.1 使用@Named注解区分

Dagger2默认提供了一个@Named注解,从代码可以看出属于@Qualifier的一种实现。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
    String value() default "";
}

在供应端对两个Dialog进行构造。

@Module
public class TestActivityModule {
    private Context mContext;

    public TestActivityModule(Context context){
          mContext = context;
    }

    @Provides
    Context context(){
          return mContext;
    }
    
    @Named("login")
    @Provides
    Dialog loginDialog(Context context){
         Dialog dialog = new Dialog(context);
         dialog.setTitle("登录提示");
         ....
         return dialog;
    }

    @Named("register")
    @Provides
    Dialog registerDialog(Context context){
         Dialog dialog = new Dialog(context);
         dialog.setTitle("注册提示");
         ....
         return dialog;
    }
}
  • 可以看到,在实例供应端中,需在提供Dialog实例的方法上面加@Named注解以区分。如果不加则会报错,因为Dagger2不知道要使用哪个方法来获取Dialog实例。
@Component(modules = TestActivityModule.class)
public interface TestActivityComponent {
    void inject(TestActivity testActivity);
}
public class TestActivity extends AppCompatActivity{
    @Named("login")
    @Inject
    Dialog mDialogLogin;
    
    @Named("register")
    @Inject
    Dialog mDialogRegister;      

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入实例
        DaggerTestActivityComponent.builder()
            .testActivityModule(new TestActivityModule(this))
            .build()
            .inject(this);
    }    
}
  • 在实例需求端中,同样要使用@Named注解标注要注入的实例。让Dagger2知道,
    @Named("login")标注的mDialogLogin,需要通过@Named("login")标注的供应方法来获取。
    @Named("register")标注的mDialogRegister,需要通过@Named("register")标注的供应方法来获取。

3.2 使用自定义@Qualifier区分

使用@Named注解的话,需要加入字符串来区分,这样比较麻烦也容易出错。所以我们可以使用自定义的限定符注解。

@Qualifier
public @interface DialogLogin {
}
@Qualifier
public @interface DialogRegister {
}

然后把前面涉及的@Named("login")换成@DialogLogin,@Named("register")换成@DialogRegister即可~


4. 作用域注解 @Scope

@Scope的作用是使同一个Component中供应的实例保持唯一。

4.1 使用作用域注解实现局部单例

举例说明:

public class UploadActivity extends AppCompatActivity{
    @Inject
    UploadPresenter mPresenter1;
    @Inject
    UploadPresenter mPresenter2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入实例
        DaggerUploadActivityComponent.builder()
            .build()
            .inject(this);
    }
}
  • 如果不使用作用域注解,则代码中的mPresenter1,mPresenter2将会是两个不一样的实例,可通过打印内存地址查看。
  • 而如下使用作用域注解后,则两者将会是同一个实例,可通过打印内存地址查看。

步骤1 自定义@Scope注解

@Scope
@Documented
@Retention(RUNTIME)
public @interface ActivityScope {}

步骤2 桥梁Component添加作用域注解

@ActivityScope 
@Component(modules = UploadActivityModule.class)
public interface UploadActivityComponent {
    void inject(UploadActivity uploadActivity);
}

步骤3 供应端中提供实例的方法添加作用域注解

@Module
public class UploadActivityModule {
    @ActivityScope
    @Provides
    UploadPresenter uploadPresenter() {
        return new UploadPresenter();
    }
}

如果是使用1.3.2方式提供实例,则在类上方添加作用域注解

@ActivityScope
public class UploadPresenter{
    @Inject
    public UploadPresenter() {
    }
}

经过@Scope处理后,UploadActivity中的UploadPresenter实例将保持唯一。

4.2 使用作用域注解实现全局单例

全局单例,相信大家就很熟悉了,就是平时用饿汉懒汉等等写的那种单例模式。
前面已经说过@Scope作用是使同一个Component中供应的实例保持唯一。
也就是说,如果我在另一个Activity中再创建了一个新的Component,那么它所提供的UploadPresenter实例也将是新的。这和我们理解的全局单例并不一样。
所以,要想实现全局单例,那就要确保获取实例的Component一直都是同一个
如何实现呢?
答案是创建一个Component用于提供全局单例的实例(创建过程和4.1基本一样),然后在Application中对该Component进行初始化,以后要获取单例时,都统一通过它来获取

全局性的实例供应端

@Module
public class AppModule {
    private Application mApplication;

    public AppModule (Application application){
          mApplication = application;
    }

    @Singleton
    @Provides
    Application application(){
          return mApplication;
    }

    @Singleton
    @Provides
    ActivityStackManager activityStackManager() {
        return new ActivityStackManager();
    }
}

全局性的桥梁

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
    ActivityStackManager activityStackManager();
    Application application();
}

Application中初始化

public class MyApplication extends Application {

    public static AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();
       
        mAppComponent= DaggerAppComponent.builder()
              .appModule(new AppModule(this))
              .build();
    }
}

每次都通过Application中的AppComponent获取某实例,即可保证全局单例

public class UploadActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
   
        MyApplication.mAppComponent.activityStackManager().pushOneActivity(this);     
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        MyApplication.mAppComponent.activityStackManager().popOneActivity(this);   
    }
}
  • 使用了Dagger2默认提供的作用域注解@Singleton,通过源码可以发现它的实现其实和前面的@ActivityScope是一样的。
  • 所以真正实现全局单例的不是并@Singleton,而是使用每次获取实例都通过同一个Component。
  • 但由于它的字面意思为单例,所以我们通常把它应用在全局性的桥梁和实例供应端中。
  • 全局性的Component中没有加入inject方法来自动注入(当然你也可以这么做,但全局性的比较少这么做),而是加入了activityStackManager()方法,供外部调用来获取实例。


5. Lazy<T> 和 Provider<T>

假如你并不希望在调用inject()方法后,就对@Inject标注的实例进行初始化注入,而是希望在用到该实例的时候再进行初始化,那么我们就可以使用Lazy<T>和Provider<T>来实现。

举例说明(省略实例供应端和桥梁的代码)

public class UploadActivity extends AppCompatActivity{
    @Inject
    Lazy<UploadPresenter> mPresenter1;
    @Inject
    Provider<UploadPresenter> mPresenter2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //注入实例
        DaggerUploadActivityComponent.builder()
            .build()
            .inject(this);
    }

    public void xxx(){
         //调用Lazy的get()方法后才开始初始化Presenter并得到该实例
         //并且后面每次调用get()方法得到的实例都将是同一个。
         mPresenter1.get().xxx();

         //调用Provider的get()方法后才开始初始化Presenter并得到该实例
         //并且后面每次调用get()方法,都会重新调用供应端的供应方法来获取新实例。
         mPresenter2.get().xxx();
    }
}

注意: 如果使用了前面介绍的作用域注解@Scope控制了实例的唯一性,那么即使多次调用Provider的get()方法,得到的依然是同一个实例。


6. 依赖 和 包含

假设A供应端某个实例的初始化过程需要用到X实例,而X实例在其他桥梁连接的B供应端中有提供,那么我们可以我们可以通过桥梁间的依赖或包含,从B供应端获取需要的实例,这样就不用再在A供应端中写X实例的提供方法了。
比如下面代码中,DbHelper的初始化需要用到全局的Context(即Application),那么我们可以通过桥梁间的依赖或包含,从前面提及的全局AppComponent中获取。

@Module
public class TestActivityModule {
    //需要使用到Application参数
    @Provides
    DbHelper dbHelper(Application application){
          return new DbHelper(application);
    }
}

6.1 通过依赖实现(TestActivityComponent依赖AppComponent)

通过dependencies = xxx.classs指定要依赖的Component :

@Component(modules = TestActivityModule.class, dependencies = AppComponent.class)
public interface TestActivityComponent {
   void inject(TestActivity testActivity);
}

被依赖的Component中需定义相关实例的获取方法 :

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {

    Application application();
    ...
}

初始化TestActivityComponent时需传入依赖的Component

public class TestActivity extends AppCompatActivity{

    @Inject
    DbHelper mDbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        DaggerTestActivityComponent.builder()
            .appComponent(MyApplication.mAppComponent) //传入依赖的Component
            .build()
            .inject(this);
    }    
}

6.2 通过包含实现(AppComponent包含TestActivityComponent)

TestActivityComponent使用@Subcomponent注解而不是@Component

@Subcomponent(modules = TestActivityModule.class)
public interface TestActivityComponent {
    void inject(TestActivity testActivity);
}

AppComponent中定义相关方法,用来包含和获取SubComponent

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {

    TestActivityComponent addSub(TestActivityModule testActivityModule);
    ...
}

通过AppComponent来获取SubComponent,然后注入

public class TestActivity extends AppCompatActivity{

    @Inject
    DbHelper mDbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TestActivityComponent testActivityComponent = MyApplication.mAppComponent.addSub(new TestActivityModule());
        testActivityComponent .inject(this);
            
    }    
}

6.3 依赖和包含使用小结

  • 通过桥梁Component之间的依赖或包含,可以获取到其他桥梁所连接的供应端提供的实例。
  • 使用依赖实现的话(假设A依赖B),A需通过dependencies指定依赖B,B中需定义A所需的相关实例的获取方法,A构造时需传入B。
  • 使用包含实现的话(假设B包含A),A需使用@Subcomponent标注,B中需定义方法来包含/获取A,A是通过调用B的方法来获取的。
  • 具有依赖关系的两个Component,它们的作用域注解@Scope必须不同,否则会引起歧义。


7. 一些Tips

Tips1:
在1.3.1和1.3.2介绍了两种实例供应方式:
方式一:使用Module。
方式二:使用@Inject标注构造函数。
那什么时候应该使用哪种方式呢?
假设现在供应端需要提供A类的实例

  • 当无法在A类的构造函数上加入@Inject时(比如一些第三方库里的类),则使用方式一提供A实例。
  • 当你希望在A类实例初始化时,A类中被@Inject标注的变量也被自动注入,则使用方式二提供A实例。

Tips2:
Module类可以声明为abstract抽象,但相关的供应方法需声明为static静态方法。

Tips3:
@inject标注的实例变量不能声明为private,也不能为static,否则会编译报错。

Tips4:
如果module中的供应方法声明了@Scope,那么它所属的component必须也声明相同的@Scope。
但如果component声明了@Scope,它的module的供应方法并不一定全都要声明@Scope。


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

推荐阅读更多精彩内容