EventBus简介及源码分析

EventBus

介绍

EventBus是一款针对Android应用发布/订阅事件的开源框架。它的出现是为了降低组件之间的耦合,只需要几行代码,就可以实现目前android开发中常用的Intent,Handler,BroadCast等类的作用,消除了依赖,提高了开发效率.

本文的代码基于Event3.X,请务必注意

优点

  1. 简化了组件之间的通信
  2. 对事件的发送者和接收者进行解耦
  3. 可以根据业务需求,在Activity,Fragment(UI线程),或者后台线程中执行
  4. 避免了复杂且容易出错的依赖关系及生命周期
  5. 速度快,专为高性能优化
  6. 依赖的jar包体积小(<50K)
  7. 对于分发线程的控制,及事件优先级的支持也很好
  8. 使用方便,只需要在需要订阅的方法加入@Subscribe 注解即可,由于建立了注释的时间索引,EventBus不需要再运行期执行注解反射
  9. 事件订阅者继承,面向对象的规则同样适用于事件和订阅者类,比如,A是B的父类,分发B类型的事件,也会同样分发给关注A事件的订阅者。

按照惯例首先说明下使用步骤

使用步骤

Step 0: 准备工作

引入EventBus,在Gradle文件中添加如下依赖

 compile 'org.greenrobot:EventBus:3.0.0'

Step 1: 定义事件

所谓事件,其实就是定义一个普通的java类,用来承载我们需要传输的数据,3.X版本的一个特点是,如果要发布一个事件,就需要自定义定义一个新的JavaBean来承载消息。一个JavaBean只能用来传递一类事件,因为EventBus3.X是通过该JavaBean的全类名来区分需要哪个方法来处理该事件的。下面详细说明。

public class MessageEvent {   
    public final String message;    
    public MessageEvent(String message) {        
      this.message = message;    
    }
}

Step 2: 定义订阅者(接受事件所触发的方法)

订阅者需要实现事件处理方法,当事件发送出的时候会被调用,每一个事件处理方法必须添加@Subscribe来声明该方法是一个Event需要关注的事件处理方法,请注意EventBus3.X可以自由定义方法的名字,与2.X不同。

// 当发送的Object是EventMsg时会调用该方法
@Subscribe
public void onMessageEvent(EventMsg msg){    
  Toast.makeText(this , "onMessageEvent", Toast.LENGTH_SHORT).show();    
}
// 当发送的Object是SomeOtherEvent时会调用该方法
@Subscribe
public void onMessageEventElse(SomeOtherEvent msg){    
  Toast.makeText(this , "onMessageEventElse", Toast.LENGTH_SHORT).show();    
}

Step 3: 注册订阅者

只有注册后,才会接收事件,在Activity,Fragment通常与生命周期绑定在一起

@Override
public void onStart() {    
  super.onStart();     
  EventBus.getDefault().register(this);
}
@Override
public void onStop() {   
  super.onStop();
  EventBus.getDefault().unregister(this);    
}

Step 4: 发布事件

EventBus.getDefault().post(new MessageEvent("Hello everyone!"));

在适当的时机和位置,我们可以分发一个MessageEvent类型的事件,而接收方就是以MessageEvent为参数的,声明了@Subscribe注解的方法

由此,我们可以看出,EventBus3.x的事件发布与接收的区分手段是不同的JavaBean,至于原理在下面讲解

黏性事件(Sticky Events)

有些时候,我们需要在指定的事件发生后,才能获取到指定的感兴趣的信息,比如说一个事件信号,初始化任务完成等。相较于实现自己的缓存,这种可以使用sticky events来实现,EventBus会在内存中保存该类型的最新一个sticky event对象,sticky event会被分发到订阅者或者被明确查询,你不需要任何特殊的逻辑来考虑已有数据。

来看一个栗子吧

Step 1: 发布事件

@Override
protected void onCreate(Bundle savedInstanceState) {    
  super.onCreate(savedInstanceState);     
  setContentView(R.layout.activity_main);    
  EventBus.getDefault().postSticky(new EventMsg("Hello everyone1!"));    
  EventBus.getDefault().postSticky(new EventMsg("Hello everyone2!"));
}

Step 2: 注册

@Override
public void onStart() {    
  super.onStart();     
  EventBus.getDefault().register(this);
}
@Override
public void onStop() {   
  super.onStop();
  EventBus.getDefault().unregister(this);    
}

Step 3: 订阅者逻辑

@Subscribe(sticky = true , threadMode = ThreadMode.MAIN)
public void onEvent(EventMsg msg){    
  Toast.makeText(this, msg.message, Toast.LENGTH_SHORT).show();
}

此时,注册的逻辑在发布EventBus的注册之后,但是,当注册完成时,依然可以执行订阅者内部的逻辑,这就是黏性事件,但是,黏性事件只会保存一个,即,发送多个黏性事件,Eventbus会按照发送顺序,只执行最后一个。

手动移除sticky event

有时候,我们可能会有手动获取或者移除黏性事件的要求,比如当达到某个指定条件时,我们要取消掉之前已经发布的sticky event,此时我们可以这么做

@Override
protected void onResume() {    
  super.onResume();        
  //移除并返回指定类型的黏性事件
  EventMsg message = EventBus.getDefault().removeStickyEvent(EventMsg.class);    
  if (message != null){        
    //Now do something with it    
  }    
  //移除所有的黏性事件    
  EventBus.getDefault().removeAllStickyEvents();
}

EventBus中的继承体系

EventBus是支持事件的继承的,这在上面的优点中提到过,具体分为:

事件的继承体系

订阅方法的执行顺序为首先按照代码顺序执行订阅者中声明为该事件的方法,其次,如果该事件类有父类的话,则会再去按照顺序执行父类的方法,如果该父类还有父类,顺序也和之前是一样的。

订阅者(类)的继承体系:

当订阅者(例如activity)和订阅者的父类(BaseActivity)同时订阅了某一事件类型时,执行顺序为首先执行当前类的订阅方法(执行顺序为代码顺序),其次执行订阅者父类中订阅方法(代码顺序)以此类推。

下面以代码为例,会让读者清晰的明白EventBus的继承体系:

基本事件类型(子类)
public class EventMsg extends ParentEventMsg{    
  public final String message;    
  public EventMsg(String message){        
    super(message);        
    this.message = message;    
  }  
}
父事件类型(父类)
public class ParentEventMsg {    
  private String tag;    
  public ParentEventMsg(String tag) {        
    this.tag = tag;    
  }    
  public ParentEventMsg() {}    
 }
订阅者
public class MainActivity extends BaseActivity {

  @Override    
  protected void onResume() {        
    super.onResume();    
    //在子Activity中分发一个基本事件类型    
    EventBus.getDefault().post(new EventMsg("send test"));
  }

  @Subscribe
  public void onMessageEvent1(EventMsg msg) {    
      log("MainActivity", "EventMsg1");
  }  
  @Subscribe
  public void onMessageEvent2(EventMsg msg) {    
      log("MainActivity", "EventMsg2");
  }

  @Subscribe
  public void onParentMessageEvent1(ParentEventMsg msg) {   
     log("MainActivity", "ParentEventMsg1");
  }
  @Subscribe
  public void onParentMessageEvent2(ParentEventMsg msg) {    
      log("MainActivity", "ParentEventMsg2");
  }

}
订阅者的父类
public class BaseActivity extends Activity{
  @Subscribe
  public void onMessageEvent3(EventMsg msg){    
    log("BaseActivity" , "EventMsg3");
  }
  @Subscribe
  public void onMessageEvent4(EventMsg msg){    
    log("BaseActivity" , "EventMsg4");
  }
  @Subscribe
  public void onParentMessageEvent3(ParentEventMsg msg) { 
    log("BaseActivity", "ParentEventMsg3");
  }
  @Subscribe
  public void onParentMessageEvent4(ParentEventMsg msg) {
    log("BaseActivity", "ParentEventMsg4");
  }
}

简单说明下,EventMsg与ParentEventMsg两个事件类型为集成关系,MainActivity和BaseActivity两个订阅者为继承关系,当我们在分发事件的时候,订阅者方法的执行顺序是这样的

测试结果:

//优先按照代码顺序执行订阅者本类中订阅该事件类型的方法
08-03 05:34:37.773 2488-2488/? W/EventBusTest: MainActivity:EventMsg1
08-03 05:34:37.773 2488-2488/? W/EventBusTest: MainActivity:EventMsg2
//订阅者本类中的方法执行完了,执行订阅者父类中订阅该事件类型的方法
08-03 05:34:37.773 2488-2488/? W/EventBusTest: BaseActivity:EventMsg3
08-03 05:34:37.773 2488-2488/? W/EventBusTest: BaseActivity:EventMsg4
//EventMsg事件类型的订阅方法已经全部执行完,这时候该执行订阅了EventMsg父类的方法,
//首先在订阅者本类中查找
08-03 05:34:37.773 2488-2488/? W/EventBusTest: MainActivity:ParentEventMsg1
08-03 05:34:37.773 2488-2488/? W/EventBusTest: MainActivity:ParentEventMsg2
//然后从订阅者父类中查找
08-03 05:34:37.773 2488-2488/? W/EventBusTest: BaseActivity:ParentEventMsg3
08-03 05:34:37.773 2488-2488/? W/EventBusTest: BaseActivity:ParentEventMsg4

总结:

  1. 首先会根据事件类型,从订阅者中查找适当的方法执行,查找的顺序为代码的顺序
  2. 当订阅者包含的订阅方法全部执行完之后,再去父类中查找指定事件类型的方法,查找顺序依然为代码顺序。当订阅者继承链上的所有类全部查找完之后,该事件类型就查找完成。
  3. 接下来,EventBus又会从订阅者中查找到事件类型的父类,然后再次重复1~2的步骤,直至事件类型的继承链也查找完毕。至此Eventbus的事件分发才算完结。

关于执行顺序的问题,在后面的源码分析中会介绍到。

事件优先级及取消事件

事件的优先级

在EventBus中,可以通过priority来修改事件分发的默认顺序,默认的优先级是0,在同一分发线程中,高优先级的订阅者会优先接收到事件。

@Subscribe(priority = 3)
public void onMessageEvent(EventMsg msg){    
  Toast.makeText(this, "onMessageEvent", Toast.LENGTH_SHORT).show();
}
@Subscribe(priority = 4)
public void onMessageEventElse(EventMsg msg){    
  Toast.makeText(this , "onMessageEventElse", Toast.LENGTH_SHORT).show();
}

此时,优先级为4的方法会先于3的方法执行

但是:优先级对不同线程的分发顺序不会产生影响

我们可以通过cancelEventDelivery(Object)方法来取消事件的分发,订阅者不会受到事件

Eventbus的实现源码解析

本次的源码解析包括如下三个步骤,注册,事件的分发,及订阅方法执行

Step1. 注册

//作用:让EventBus扫描当前类,把所有onEvent开头的方法记录下来,
//使用Map,Key为方法的参数类型,Value中包含我们的方法
public void onCreate(Bundle savedInstanceState){      
    super.onCreate(savedInstanceState);       
    EventBus.getDefault().register(this);
}

当注册时,EventBus会去当前的类中遍历全部的方法,从中找出符合条件的订阅方法,并存储到一个map集合中,当收到事件时就从该集合查找并执行指定的方法

//用来存储订阅类和方法的集合
Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>()

public void register(Object subscriber) {
    Class<?> subscriberClass = subscriber.getClass();
    //查找当前类的订阅方法
    List<SubscriberMethod> subscriberMethods =
    subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
           for (SubscriberMethod subscriberMethod : subscriberMethods) {

               subscribe(subscriber, subscriberMethod);
         }
      }
  }

查找订阅方法

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    //尝试从缓存中查找对应Class的方法集
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    //ignoreGeneratedIndex默认是false,如果是true表示强制使用反射查找方法
    if (ignoreGeneratedIndex) {
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    if (subscriberMethods.isEmpty()) {
      throw new EventBusException("Subscriber " + subscriberClass+ " and its super classes have no public methods with the @Subscribe annotation");
   } else {
      METHOD_CACHE.put(subscriberClass, subscriberMethods);
      return subscriberMethods;
    }
}

尝试从自身及父类中查找@Subscribe修饰的方法

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
 //从FindState的缓存数组中获取一个已经实例化好的对象(FindState)如果没有则new一个,FindState是...
  FindState findState = prepareFindState();
  findState.initForSubscriber(subscriberClass);
  while (findState.clazz != null) {
   //有兴趣的同学可以去看下getSubscriberInfo方法,该方法包含了两个if的判断模块,但是首次的话这两个判断语句都为false ,所以这里getSubscriberInfo方法的返回值为null        findState.subscriberInfo = getSubscriberInfo(findState);
    if (findState.subscriberInfo != null) {
      SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
      for (SubscriberMethod subscriberMethod : array) {
        if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
             findState.subscriberMethods.add(subscriberMethod);
          }
        }
      } else {
        findUsingReflectionInSingleClass(findState);
      }
      //跳转到订阅者的父类中然后继续轮询(除去系统类,比如activity等)
      findState.moveToSuperclass();
    }
  //获取并返回方法列表
  return getMethodsAndRelease(findState);
}

接下来就是通过反射查找方法

//通过反射查找方法
private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
   try {
    methods = findState.clazz.getDeclaredMethods();
  } catch (Throwable th) {
    methods = findState.clazz.getMethods();
    findState.skipSuperClasses = true;
  }
    //遍历该class的方法集合
    for (Method method : methods) {
       int modifiers = method.getModifiers();
       //必须是public的
       if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
           Class<?>[] parameterTypes = method.getParameterTypes();
          //方法过滤检查,必须有Subscribe注解并且参数个数必须是1
         if (parameterTypes.length == 1) {
           Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
           if (subscribeAnnotation != null) {
               Class<?> eventType = parameterTypes[0];
             //执行两层检查,一层只检查事件类型,一层进行完整的签名检查
             //缓存中没有就返回true
             if (findState.checkAdd(method, eventType)) {
                ThreadMode threadMode = subscribeAnnotation.threadMode();
                findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                   subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
          }
        }
      } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
      //@Subscribe修饰的方法必须有且仅有一个参数
    }
  } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
     //订阅方法必须是public 非静态,非抽象的方法
    }
 }
}

2. 发布事件

//根据post中实参的类型,去Map中查找对于的方法,
//如果有两个注册方法的参数相同,则全部执行。
EventBus.getDefault().post(XXX);

发送消息

public void post(Object event) {    
    //通过threadlocal来维护各个线程之间(UI线程或者子线程)的PostingThreadState        
    //PostingThreadState是一个静态内部类用来保存当前线程的事件队列以及各种标识(是否是主线程等)
    PostingThreadState postingState = currentPostingThreadState.get();    
    List<Object> eventQueue = postingState.eventQueue;  
    //将该事件加入到事件队列
    eventQueue.add(event);    
     //判断是否正在发送该消息
    if (!postingState.isPosting) {       
        //判断是否是主线程 
        postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();        
        postingState.isPosting = true;        
        if (postingState.canceled) {           
           throw new EventBusException("Internal error. Abort state was not reset");        }        
          try {            
              //如果事件队列不为空则从队列中取出并发送该消息
              while (!eventQueue.isEmpty()) {     
                   postSingleEvent(eventQueue.remove(0), postingState);      
                  }        
              } finally {            
                  postingState.isPosting = false;  
                  postingState.isMainThread = false;        
        }    
    }
}

发送单个事件

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {   
   Class<?> eventClass = event.getClass();    
   boolean subscriptionFound = false;  
    //是否支持事件继承,默认支持  TODO:
    if (eventInheritance) {   
        //查找该事件的父类继承链以及接口,优先从事件的缓存中找     
        List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);  
        int countTypes = eventTypes.size();    
        //支持继承查找,则查找事件类型的父类及实现的接口    
        for (int h = 0; h < countTypes; h++) {            
            Class<?> clazz = eventTypes.get(h);            
            subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);        
         }    
     } else {    
            //不支持继承查找则只查找当前事件类型    
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);    
     }    
        if (!subscriptionFound) {        
          if (logNoSubscriberMessages) {            
                Log.d(TAG, "No subscribers registered for event " + eventClass);        
            }        
          //如果是发送无订阅者事件并且事件类型不是NoSubscriberEvent或者SubscriberExceptionEvent则发送一个无订阅者事件
         if (sendNoSubscriberEvent && 
                eventClass != NoSubscriberEvent.class &&
                eventClass != SubscriberExceptionEvent.class) {       
                   post(new NoSubscriberEvent(this, event));        
          }    
      }
  }

接收事件,本质上分发和接收事件的逻辑是在一起的,但是这里为了方法区分两个过程就分开来讲(其实postSingleEventForEventType方法追进去就是下面的逻辑)

调用反射去执行我们的方法

@Subscribe
public void onEventXXXX(Event.ItemListEvent event){    
  Log.w("eventbus", "onEventXXXX");    
}

根据事件类型查找并调用方法

private boolean postSingleEventForEventType(Object event, 
    PostingThreadState postingState, Class<?> eventClass) { 
    //写时复制的容器,下面介绍
    CopyOnWriteArrayList<Subscription> subscriptions;    
    synchronized (this) {   
          //从缓存中查找该事件类型的订阅者     
          subscriptions = subscriptionsByEventType.get(eventClass);    
        }    
    if (subscriptions != null && !subscriptions.isEmpty()) {        
    for (Subscription subscription : subscriptions) {         
        postingState.event = event;            
        postingState.subscription = subscription;            
        boolean aborted = false;            
        try {    
            //重点:查找到订阅者(方法)后通过反射执行该方法            
            postToSubscription(subscription, event, postingState.isMainThread);                
            aborted = postingState.canceled;            
        } finally {                
            postingState.event = null;                
            postingState.subscription = null;         
            postingState.canceled = false;            
          }            
    if (aborted) {                
          break;            
        }        
      }        
    return true;    
  }    
  return false;
}

发送事件给订阅者。判断线程类型,通过反射执行对应方法

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {    
    switch (subscription.subscriberMethod.threadMode) {        
        case POSTING:            
            invokeSubscriber(subscription, event);            
            break;        
        case MAIN:            
            if (isMainThread) {                
                invokeSubscriber(subscription, event);            
              } else {                
                mainThreadPoster.enqueue(subscription, event);            
              }            
            break;        
        case BACKGROUND:            
            if (isMainThread) {   
               backgroundPoster.enqueue(subscription, event);            
              } else {                
               invokeSubscriber(subscription, event);            
              }            
            break;        
        case ASYNC:            
            asyncPoster.enqueue(subscription, event);            
            break;        
        default:            
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);    
    }
}

反射执行方法

void invokeSubscriber(Subscription subscription, Object event) {    
    try {     
         subscription.subscriberMethod.method.invoke(subscription.subscriber, event);      
      } catch (InvocationTargetException e) {     
       handleSubscriberException(subscription, event, e.getCause());   
     } catch (IllegalAccessException e) {      
      throw new IllegalStateException("Unexpected exception", e);   
     }
}

至此,EventBus的源码分析就结束了。关于取消注册的代码,比较简单,就不在这里分析了。因公司发展需要,老衲需要去做一段时间的python及Java服务端开发,后续关于Android文章,可能会更新较慢并且夹杂一部分java服务端的文章。敬请期待。

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

推荐阅读更多精彩内容