我所理解的Android组件化之通信机制

<p><img src="http://upload-images.jianshu.io/upload_images/11462765-6b9f04f8eefade48" /></p><section><span style="letter-spacing: 0px;">之前写过一篇关于Android组件化的文章,</span>
</section><section class="output_wrapper"><p>《Android组件化框架设计与实践》</p>,之前没看过的小伙伴可以先点击阅读。那篇文章是从实战中进行总结得来,是公司的一个真实项目进行组件化架构改造,粒度会分的更粗些,是对整体架构实践进行相应的总结,里面说了要打造一个组件化框架的话,需要从以下7个方面入手:<p></p><ul class=" list-paddingleft-2"><li><p><span>代码解耦。如何将一个庞大的工程分成有机的整体?</span></p></li><li><p><span>组件单独运行。因为每个组件都是高度内聚的,是一个完整的整体,如何让其单独运行和调试?</span></p></li><li><p><span>组件间通信。由于每个组件具体实现细节都互相不了解,但每个组件都需要给其他调用方提供服务,那么主项目与组件、组件与组件之间如何通信就变成关键?</span></p></li><li><p><span>UI 跳转。UI 跳转指的是特殊的数据传递,跟组件间通信区别有什么不同?</span></p></li><li><p><span>组件生命周期。这里的生命周期指的是组件在应用中存在的时间,组件是否可以做到按需、动态使用、因此就会涉及到组件加载、卸载等管理问题。</span></p></li><li><p><span>集成调试。在开发阶段如何做到按需编译组件?一次调试中可能有一两个组件参与集成,这样编译时间就会大大降低,提高开发效率。</span></p></li><li><p><span>代码隔离。组件之间的交互如果还是直接引用的话,那么组件之间根本没有做到解耦,如何从根本上避免组件之间的直接引用,也就是如何从根本上杜绝耦合的产生?</span></p></li></ul><p>今天则会从更小细粒度入手,主要讲讲在组件化架构下组件与组件之间通信机制是如何、包括所谓的UI跳转,其实也是组件化通信,只不过它稍微特殊点,单独抽取出来而已。学习知识的过程很常见的一个思路就是从整体概况入手,首先对整体有个粗略的印象,然后再深入细节,抽丝剥茧般去挖掘其中的内在原理,一个点一个不断去突破,这样就能建立起自己整个知识树,所以今天我们就从通信机制这个点入手,看看其中内在玄机有哪些。</p><h4><span>思维导图</span></h4><p>同样,在每写一篇文章之前,放个思维导图,这样做的好处对于想写的内容有很好的梳理,逻辑和结构上显得清晰点。</p><figure><img class="" data-ratio="0.5280172413793104" src="http://upload-images.jianshu.io/upload_images/11462765-3f164cd035457494.png" data-type="png" data-w="1856" title="思维导图"><figcaption>思维导图</figcaption></figure><h4><span>主流方式</span></h4><p>总所周知,Android提供了很多不同的信息的传递方式,比如在四大组件中本地广播、进程间的AIDL、匿名间的内存共享、Intent Bundle传递等等,那么在这么多传递方式,哪种类型是比较适合组件与组件直接的传递呢。</p><ul class=" list-paddingleft-2"><li><p><span>本地广播,也就是LoacalBroadcastRecevier。更多是用在同一个应用内的不同系统规定的组件进行通信,好处在于:发送的广播只会在自己的APP内传播,不会泄漏给其他的APP,其他APP无法向自己的APP发送广播,不用被其他APP干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组件化通信过程中采用比例不高。</span></p></li><li><p><span>进程间的AIDL。这个粒度在于进程,而我们组件化通信过程往往是在线程中,况且AIDL通信也是属于系统级通信,底层以Binder机制,虽说Android提供模板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组件化通信过程中。</span></p></li><li><p><span>匿名的内存共享。比如用Sharedpreferences,在处于多线程场景下,往往会线程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等等。</span></p></li><li><p><span>Intent Bundle传递。包括显性和隐性传递,显性传递需要明确包名路径,组件与组件往往是需要互相依赖,这背离组件化中SOP(关注点分离原则),如果走隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错,排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其他组件打交道则使用场景不多。</span></p></li></ul><p>说了这么多,那组件化通信什么机制比较适合呢?既然组件层中的模块是相互独立的,它们之间并不存在任何依赖。没有依赖就无法产生关系,没有关系,就无法传递消息,那要如何才能完成这种交流?</p><p>目前主流做法之一就是引入第三者,比如图中的Base Module。</p><figure><img class="" data-ratio="0.5897058823529412" src="http://upload-images.jianshu.io/upload_images/11462765-9eed735e6676e6de.png" data-type="png" data-w="680" title="基础组件化架构"><figcaption>基础组件化架构</figcaption></figure><p>组件层的模块都依赖于基础层,从而产生第三者联系,这种第三者联系最终会编译在APP Module中,那时将不会有这种隔阂,那么其中的Base Module就是跨越组件化层级的关键,也是模块间信息交流的基础。比较有代表性的组件化开源框架有得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。</p><p>除了这种以通过引入第三者方式,还有一种解决方式是以事件总线方式,但这种方式目前开源的框架中使用比例不高,如图:</p><figure><img class="" data-ratio="0.3681462140992167" src="http://upload-images.jianshu.io/upload_images/11462765-86e5f08c5e89e373.png" data-type="png" data-w="766" title="事件总线"><figcaption>事件总线</figcaption></figure><p>事件总线通过记录对象,使用监听者模式来通知对象各种事件,比如在现实生活中,我们要去找房子,一般都去看小区的公告栏,因为那边会经常发布一些出租信息,我们去查看的过程中就形成了订阅的关系,只不过这种是被动去订阅,因为只有自己需要找房子了才去看,平时一般不会去看。小区中的公告栏可以想象成一个事件总线发布点,监听者则是哪些想要找房子的人,当有房东在公告栏上贴上出租房信息时,如果公告栏有订阅信息功能,比如引入门卫保安,已经把之前来这个公告栏要查看的找房子人一一进行电话登记,那么一旦有新出租消息产生,则门卫会把这条消息一一进行短信群发,那么找房子人则会收到这条消息进行后续的操作,是马上过来看,还是延迟过来,则根据自己的实际情况进行处理。在目前开源库中,有EventBus、RxBus就是采用这种发布/订阅模式,优点是简化了Android组件之间的通信方式,实现解耦,让业务代码更加简洁,可以动态设置事件处理线程和优先级,缺点则是每个事件需要维护一个事件类,造成事件类太多,无形中加大了维护成本。那么在组件化开源框架中有ModuleBus、CC 等等。</p><p>这两者模式更详细的对比,可以查看这篇文章多个维度对比一些有代表性的开源android组件化开发方案</p><h4><span>实现方案</span></h4><p>事件总线,又可以叫做组件总线,路由+接口,则相对好理解点,今天从阅读它们框架源码,我们来对比这两种实现方案的不同之处。</p><h6><span>组件总线</span></h6><p>这边选取的是ModuleBus框架,这个方案特别之处在于其借鉴了EventBus的思想,组件的注册/注销和组件调用的事件发送都跟EventBus类似,能够传递一些基础类型的数据,而并不需要在Base Moudel中添加额外的类。所以不会影响Base模块的架构,但是无法动态移除信息接收端的代码,而自定义的事件信息类型还是需要添加到Base Module中才能让其他功能模块索引。</p><p>其中的核心代码是在与 <strong>ModuleBus</strong> 类,其内部维护了两个ArrayMap键对值列表,如下:</p><pre><code class="java language-java hljs"><span class="hljs-comment">/
     
 Object methodClass
     
 String methodName;
      MethodInfo method info
     
/</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> ArrayMap<Object,ArrayMap<String,MethodInfo>> moduleEventMethods = <span class="hljs-keyword">new</span> ArrayMap<>();

    <span class="hljs-comment">/
     
 Class IBaseClient.class
     
 String methodName
      Object methodClass
     
/</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> ArrayMap<Class<?>,ArrayMap<String,ArrayList<Object>>> moduleMethodClient = <span class="hljs-keyword">new</span> ArrayMap<>();
</code></pre><p>在使用方法上,在onCreate()和onDestroy()中需要注册和解绑,比如</p><pre><code class="java language-java hljs">ModuleBus.getInstance().register(<span class="hljs-keyword">this</span>);
ModuleBus.getInstance().unregister(<span class="hljs-keyword">this</span>);
</code></pre><p>最终使用类似EventBus 中 post 方法一样,进行两个组件间的通信。这个框架的封装的post 方法如下</p><pre><code class="java language-java hljs"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">post</span><span class="hljs-params">(Class<?> clientClass,String methodName,Object...args)</span></span>{
        <span class="hljs-keyword">if</span>(clientClass == <span class="hljs-keyword">null</span> || methodName == <span class="hljs-keyword">null</span> ||methodName.length() == <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

        ArrayList<Object> clientList = getClient(clientClass,methodName);

        <span class="hljs-keyword">if</span>(clientList == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span>;

        <span class="hljs-keyword">try</span>{
            <span class="hljs-keyword">for</span>(Object c: clientList){
                <span class="hljs-keyword">try</span>{
                    ArrayMap<String,MethodInfo> methods = moduleEventMethods.get(c);
                    Method method = methods.get(methodName).m;
                    <span class="hljs-keyword">if</span>(method == <span class="hljs-keyword">null</span>){
                        Log.e(TAG,<span class="hljs-string">"cannot find client method"</span>+methodName +<span class="hljs-string">"for args["</span>+args.length+<span class="hljs-string">"]"</span> + Arrays.toString(args));
                        <span class="hljs-keyword">return</span>;
                    }<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(method.getParameterTypes() == <span class="hljs-keyword">null</span>){
                        Log.e(TAG,<span class="hljs-string">"cannot find client method param:"</span>+method.getParameterTypes() +<span class="hljs-string">"for args["</span>+args.length+<span class="hljs-string">"]"</span> + Arrays.toString(args));
                        <span class="hljs-keyword">return</span>;
                    }<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(method.getParameterTypes().length != args.length){
                        Log.e(TAG,<span class="hljs-string">"method "</span>+methodName +<span class="hljs-string">" param number not matched:method("</span>+method.getParameterTypes().length+<span class="hljs-string">"), args("</span> + args.length+<span class="hljs-string">")"</span>);
                        <span class="hljs-keyword">return</span>;
                    }
                    method.invoke(c,args);
                }<span class="hljs-keyword">catch</span> (Throwable e){
                    Log.e(TAG,<span class="hljs-string">"Notifiy client method invoke error."</span>,e);
                }
            }

        }<span class="hljs-keyword">catch</span> (Throwable e){
            Log.e(TAG,<span class="hljs-string">"Notify client error"</span>,e);
        }
    }
</code></pre><p>可以看到,它是通过遍历之前内部的ArrayMap,把注册在里面的方法找出,根据传入的参数进行匹配,使用反射调用。</p><h6><span>接口+路由</span></h6><p>接口+路由实现方式则相对容易理解点,我之前实践的一个项目就是通过这种方式实现的。具体地址如下:DemoComponent实现思路是专门抽取一个LibModule作为路由服务,每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。</p><p>比如定义两个路由地址,一个登陆组件,一个设置组件,核心代码:</p><pre><code class="java language-java hljs"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RouterPath</span> </span>{

    <span class="hljs-comment">//注意路由的命名,路径第一个开头需要不一致,保证唯一性</span>
    <span class="hljs-comment">//Login Service</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String ROUTER_PATH_TO_LOGIN_SERVICE = <span class="hljs-string">"/login/service"</span>;

    <span class="hljs-comment">//Setting Service</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String ROUTER_PATH_TO_SETTING_SERVICE = <span class="hljs-string">"/setting/service"</span>;
}
</code></pre><p>那么就相应着就有两个接口API,如下:</p><pre><code class="java language-java hljs"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ILoginProvider</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">IProvider</span> </span>{

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">goToLogin</span><span class="hljs-params">(Activity activity)</span></span>;
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ISettingProvider</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">IProvider</span> </span>{

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">goToSetting</span><span class="hljs-params">(Activity activity)</span></span>;
}
</code></pre><p>这两个接口API对应着是向外暴露这两个组件的能提供的通信能力,然后每个组件对接口进行实现,如下:</p><pre><code class="java language-java hljs"><span class="hljs-meta">@Route</span>(path = RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE, name = <span class="hljs-string">"登陆页面"</span>)
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ILoginProvider</span> </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">(Context context)</span> </span>{}


    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">goToLogin</span><span class="hljs-params">(Activity activity)</span> </span>{
        Intent loginIntent = <span class="hljs-keyword">new</span> Intent(activity, LoginActivity.class);
        activity.startActivity(loginIntent);
    }
}
</code></pre><p>这其中使用的到了阿里的ARouter页面跳转方式,内部本质也是接口+实现方式进行组件间通信。</p><p>调用则很简单了,如下:</p><pre><code class="java language-java hljs"> ILoginProvider loginService = (ILoginProvider) ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).navigation();
<span class="hljs-keyword">if</span>(loginService != <span class="hljs-keyword">null</span>){
    loginService.goToLogin(MainActivity.<span class="hljs-keyword">this</span>);
}
</code></pre><p>还有一个组件化框架,就是ModularizationArchitecture ,它本质实现方式也是接口+实现,但是封装形式稍微不一样点,它是每个功能模块中需要使用注解建立Action事件,每个Action完成一个事件动作。invoke只是方法名为反射,并未用到反射,而是使用接口方式调用,参数是通过HashMap<string,string>传递的,无法传递对象。具体详解可以看这篇文章 Android架构思考(模块化、多进程)。</string,string></p><h4><span>页面跳转</span></h4><p>页面跳转也算是一种组件间的通信,只不过它相对粒度更细化点,之前我们描述的组件间通信粒度会更抽象点,页面跳转则是定位到某个组件的某个页面,可能是某个Activity,或者某个Fragment,要跳转到另外一个组件的Activity或Fragment,是这两者之间的通信。甚至在一般没有进行组件化架构的工程项目中,往往也会封装页面之间的跳转代码类,往往也会有路由中心的概念。不过一般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity。每个组件可以注册自己所能处理的短链的 Scheme 和 Host,并定义传输数据的格式,然后注册到统一的 UIRouter 中,UIRouter 通过 Scheme 和 Host 的匹配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注解,然后通过 APT 形成具体的逻辑代码。</p><p>下面简单介绍目前比较主流的两个框架核心实现思路:</p><h5><span>ARouter</span></h5><p>ARouter 核心实现思路是,我们在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activityClass映射关系的类文件,然后app进程启动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。</p><h5><span>ActivityRouter</span></h5><p>ActivityRouter 核心实现思路是,它是通过路由 + 静态方法来实现,在静态方法上加注解来暴露服务,但不支持返回值,且参数固定位(context, bundle),基于apt技术,通过注解方式来实现URL打开Activity功能,并支持在WebView和外部浏览器使用,支持多级Activity跳转,支持Bundle、Uri参数注入并转换参数类型。它实现相对简单点,也是比较早期比较流行的做法,不过学习它也是很有参考意义的。</p><h4><span>小结</span></h4><p>总的来说,组件间的通信机制在组件化编程和组件化架构中是很重要的一个环节,可能在每个组件独自开发阶段,不需要与其他组件进行通信,只需要在内部通信即可,当处于组件集成阶段,那就需要大量组件进行互相通信,体现在每个业务互相协作,如果组件间设计的不好,打开一个页面或调用一个方法,想当耗时或响应慢,那么体现的则是这个APP使用比较卡顿,仅仅打开一个页面就是需要好几秒才能打开,则严重影响使用者的体验了,甚至一些大型APP,可能组件分化更小,种类更多,那么组件间的通信则至关重要了。所以,要打造一个良好的组件化框架,如何设计一个更适合自己本身的业务类型的通信机制,就需要多多进行思考了。</p></section><p>
</p><p>
</p><p style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; margin-top: 16px; margin-bottom: 16px; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px;">-----点击上方关注持续收听面试干货</p><div class="pgc-img" style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px;"><img src="http://upload-images.jianshu.io/upload_images/11462765-68258e90f4ccc5cb" img_width="411" img_height="150" alt="人工智能竟然会让你变得更穷 最迟25年内应验" inline="0" style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; border-style: none; margin-top: 10px; margin-bottom: 10px;"><p class="pgc-img-caption" style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; text-align: center; font-size: 12px; color: rgb(119, 119, 119); line-height: 16px;"></p></div><p class="ql-align-center" style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; margin-top: 16px; margin-bottom: 16px; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; font-size: 16px;">私信 “666” 索要Android高级视频(Flutter进阶,插件化,热修复技术)</p>

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

推荐阅读更多精彩内容