Android路由方案ARouter分析

一、路由方案

原生的路由方案缺点:

  • 显式:直接的类依赖,耦合严重

  • 隐式:规则集中式管理,协作困难

  • Manifest扩展性较差

  • 跳转过程无法控制

  • 失败无法降级

ARouter的优势:

  • 使用注解,实现了映射关系自动注册 与 分布式路由管理

  • 编译期间处理注解,并生成映射文件,没有使用反射,不影响运行时性能

  • 映射关系按组分类、多级管理,按需初始化

  • 灵活的降级策略,每次跳转都会回调跳转结果,避免StartActivity()一旦失败将会抛出运营级异常

  • 自定义拦截器,自定义拦截顺序,可以对路由进行拦截,比如登录判断和埋点处理

  • 支持依赖注入,可单独作为依赖注入框架使用,从而实现 跨模块API调用

  • 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中

  • 支持获取Fragment

  • 支持多模块使用,支持组件化开发

…….

这么多好处,是时候来了解一下ARouter了。

二、ARouter框架

上图是根据ARouter一次基本的路由导航过程,整理的基本框架图,涉及到主要流程,下面进行详细介绍。

三、路由管理

1.注册

通过注解,在编译时收集使用了注解的类或变量并经过Android Process Tool处理进行统一管理。

包含三种注解@Autowired,@Interceptor,@Route。

@Route

注解定义

String path();//路径URL字符串
String group() default "";//组名,默认为一级路径名;一旦被设置,跳转时必须赋值
String name() default "undefined";//该路径的名称,用于产生JavaDoc
int extras() default Integer.MIN_VALUE;//额外配置的开关信息;譬如某些页面是否需要网络校验、登录校验等
int priority() default -1;//该路径的优先级

实现 @Route 注解

BlankFragment               @Route(path = "/test/fragment") 
Test1Activity               @Route(path = "/test/activity1")

该注解主要用于描述路由中的路径URL信息,使用该注解标注的类将被自动添加至路由表中。

@Autowired

注解定义

boolean required() default false;
String desc() default "No desc.";

实现 @Autowired 注解

@Autowired
int age = 10;
@Autowired
HelloService helloService;

该注解是在页面跳转时参数传递用的。目标Class中使用该注解标志的变量,会在页面被路由打开的时候,在调用inject()后自动赋予传递的参数值。

@Interceptor

注解定义

int priority();//该拦截器的优先级
String name() default "Default";//该拦截器的名称,用于产生JavaDoc

实现 @Interceptor 注解

一般应用于IInterceptor的实现类,是路由跳转过程中的拦截器,不分module,应用全局。

@Interceptor(priority = 7)
public class Test1Interceptor implements IInterceptor {
    @Override
    public void process(final Postcard postcard, final InterceptorCallback callback) {
    ............
    }
}

2.收集

在编译期间自动生成映射文件,arouter-compiler实现了一些注解处理器,目标在于生成映射文件与辅助文件。


三种类型的注解处理器,都实现了AbstractProcessor,主要功能如下:

  • 首先通过注解处理器扫出被标注的类文件

  • 按照不同种类的源文件进行分类

  • 按照固定的命名格式生成映射文件

这样就可以在运行期初始化的时候通过固定的包名来加载映射文件。

关于注解处理的源码详解见阿里路由框架--ARouter 源码解析之Compiler


以官方demo为例,通过注解处理器,按照固定的命名格式生成映射文件。

具体以ARouter$$Root$$app为例,看下注解处理器生成的类文件的内容:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", ARouter$$Group$$service.class);
    routes.put("test", ARouter$$Group$$test.class);
  }
}

通过调用loadInto()方法将其管理的group类文件加载到集合中,方便后续路由查找。

3.加载

前面的收集都是在编译器处理获得的,那么加载就是到了运行期。ARouter为了避免内存和性能损耗,提出了“分组管理,按需加载”的方式。在前面的编译处理的过程中,已经按照不同种类生成对应的映射文件。

以官方demo为示例,一个app模块有一个Root结点,管理各个Group分组,每个Group分组下有着多个界面;此外app模块下还有着Interceptor结点,以及provider结点。

其中Interceptor结点对应于自定义的拦截器,provider结点对应于IOC,以实现跨模块API调用。

ARouter在初始化的时候只会一次性地加载所有的root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去。

初始加载

ARouter 其实是一个代理类,它的所有函数实现都交给_ARouter去实现,两个都是单例模式。

public static void init(Application application) {//静态函数进行初始化,不依赖对象
    if (!hasInit) {
        logger = _ARouter.logger; //持有 日志打印的 全局静态标量
        _ARouter.logger.info(Consts.TAG, "ARouter init start.");//打印 ARouter初始化日志
        hasInit = _ARouter.init(application);//移交 _ARouter去 初始化

        if (hasInit) {
            _ARouter.afterInit();
        }

        _ARouter.logger.info(Consts.TAG, "ARouter init over.");//打印 ARouter初始化日志
    }
}

继续看一下_ARouter的初始化方法

protected static synchronized boolean init(Application application) {
        mContext = application;// Application的上下文
        LogisticsCenter.init(mContext, executor);//移交逻辑中心进行初始化,并传入线城池对象
        logger.info(Consts.TAG, "ARouter init success!");//打印日志
        hasInit = true;//标示是否初始化完成

        // It's not a good idea.
        // if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        //     application.registerActivityLifecycleCallbacks(new AutowiredLifecycleCallback());
        // }
        return true;
    }

继续往下走,看LogisticsCenter的初始化方法

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context; //静态持有Application的上下文
        executor = tpe;//静态持有 线城池

        try {
            // These class was generate by arouter-compiler.
            // 通过指定包名com.alibaba.android.arouter.routes,找到所有 编译期产生的routes目录下的类名(不包含装载类)
            List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

            for (String className : classFileNames) {//组别列表com.alibaba.android.arouter.routes.ARouter\$\$Root
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {//模块内的拦截器列表com.alibaba.android.arouter.routes.ARouter\$\$Interceptors
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {//IOC的动作路由列表com.alibaba.android.arouter.routes.ARouter\$\$Providers
                    // Load providerIndex
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }

            if (Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG, "No mapping files were found, check your configuration please!");
            }

            if (ARouter.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
            }
        } catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        }
    }

通过上述代码,实现了“分组管理,按需加载”的方式,加载了对应的三个注解处理器生成的类中管理的结点到路由集合中。


其中内存仓库Warehouse缓存了全局应用的组别的清单列表、IOC的动作路由清单列表、模块内的拦截器清单列表,3个map对象。

class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();//组别的列表 包含了组名与对应组内的路由清单列表Class的映射关系
    static Map<String, RouteMeta> routes = new HashMap<>();//组内的路由列表 包含了对应分组下的,路由URL与目标对象Class的映射关系

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>(); //缓存IOC  目标class与已经创建了的对象 
    
    static Map<String, RouteMeta> providersIndex = new HashMap<>();//IOC 的动作路由列表包含了使用依赖注入方式的某class的  路由URL 与class映射关系

    // Cache interceptor
    //模块内的拦截器列表 包含了某个模块下的拦截器 与 优先级的映射关系
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();//已排序的拦截器实例对象
 
}

四、路由查找

ARouter.getInstance().build("/test/activity2").navigation();</pre>

以上述例子为例,看一下ARouter路由查找的过程。首先看一下build过程

1.build()

public Postcard build(String path) {
    return _ARouter.getInstance().build(path);
}

protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return build(path, extractGroup(path));
        }
    }

其使用了代理类_ARouter的build()并构建和返回PostCard对象。 一个Postcard对象就对应了一次路由请求,作用于本次路由全过程。

这部分代码主要包含两个部分:

  • 使用 IOC byType()方式寻找PathReplaceService.class接口的实现类,该实现类的作用就是实现 “运行期动态修改路由”。
  • 继续进行本次路由导航

首先来看一下PathReplaceService.class接口:

public interface PathReplaceService extends IProvider {

    /**
     * For normal path.
     *
     * @param path raw path
     */
    String forString(String path);

    /**
     * For uri type.
     *
     * @param uri raw uri
     */
    Uri forUri(Uri uri);
}

主要包含forString()和forUri两个方法,针对路径进行预处理,实现 “运行期动态修改路由”。

接下下,继续通过build(path, extractGroup(path))进行路由导航,其中extractGroup()是从路径中获取默认的分组信息。

然后build()方法会返回一个Postcard对象,并把对应的路径和分组信息传入该对象。

分析完上面的过程,下面来详细看下PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);中的navigation()方法,该方法实际调用了代理类_ARouter的navigation(Class<? extends T> service)方法。

2.navigation(Class<? extends T> service)

protected <T> T navigation(Class<? extends T> service) {
    try {
        Postcard postcard = LogisticsCenter.buildProvider(service.getName());

        // Compatible 1.0.5 compiler sdk.
        if (null == postcard) { // No service, or this service in old version.
            postcard = LogisticsCenter.buildProvider(service.getSimpleName());
        }

        LogisticsCenter.completion(postcard);
        return (T) postcard.getProvider();
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());
        return null;
    }
}
  • 首先LogisticsCenter.buildProvider(service.getName())根据Warehouse保存的providersIndex的信息查找并构建返回一个PostCard对象

  • 然后执行LogisticsCenter.completion(postcard),该方法会根据Warehouse保存的routes的路由信息完善postcard对象,该方法在下面还会出现,到时候具体介绍

再回到上文介绍ARouter.getInstance().build("/test/activity2").navigation(),返回PostCard对象后,开始调用对应的navigation()方法。

3.navigation()

观察PostCard中的该方法

public Object navigation() {
        return navigation(null);
    }
    
    public Object navigation(Context context) {
        return navigation(context, null);
    }

    public Object navigation(Context context, NavigationCallback callback) {
        return ARouter.getInstance().navigation(context, this, -1, callback);
    }

    public void navigation(Activity mContext, int requestCode) {
        navigation(mContext, requestCode, null);
    }

    public void navigation(Activity mContext, int requestCode, NavigationCallback callback) {
        ARouter.getInstance().navigation(mContext, this, requestCode, callback);
    }

最终调用了ARouter中的navigation()方法,在其中其实是调用了_ARouter中的navigation()方法。

该方法包含查找回调的调用、降级处理、拦截器处理具体路由操作。

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());

            if (debuggable()) { // Show friendly tips for user.
                Toast.makeText(mContext, "There's no route matched!\n" +
                        " Path = [" + postcard.getPath() + "]\n" +
                        " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
            }
    
            if (null != callback) {
                callback.onLost(postcard);//触发路由查找失败
            } else {    // No callback for this invoke, then we use the global degrade service.
                DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
                if (null != degradeService) {
                    degradeService.onLost(context, postcard);
                }
            }

            return null;
        }
        //找到了路由元信息,触发路由查找的回调
        if (null != callback) {
            callback.onFound(postcard);
        }
        //绿色通道校验 需要拦截处理
        if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            //调用拦截器截面控制器,遍历内存仓库的自定义拦截器,并在异步线程中执行拦截函数
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }

                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        } else {
            return _navigation(context, postcard, requestCode, callback);
        }

        return null;
    }

其中最重要的两个方法就是LogisticsCenter.completion()_navigation(),下面详细介绍。

public synchronized static void completion(Postcard postcard) {
    if (null == postcard) {
        throw new NoRouteFoundException(TAG + "No postcard!");
    }
    //根据路径URL获取到路径元信息
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
        //可能没加载组内清单路径,从组别的清单列表拿到对应组
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
        if (null == groupMeta) {
            throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
        } else {
            //将该组的组内清单列表加入到内存仓库中,并把组别移除
            try {
                if (ARouter.debuggable()) {
                    logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                }

                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                iGroupInstance.loadInto(Warehouse.routes);
                Warehouse.groupsIndex.remove(postcard.getGroup());

                if (ARouter.debuggable()) {
                    logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                }
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }

            completion(postcard);   // 再次触发完善逻辑
        }
    } else {
        postcard.setDestination(routeMeta.getDestination());//目标 class
        postcard.setType(routeMeta.getType());//路由类
        postcard.setPriority(routeMeta.getPriority());//路由优先级
        postcard.setExtra(routeMeta.getExtra());//额外的配置开关信息

        Uri rawUri = postcard.getUri();
        if (null != rawUri) {   // Try to set params into bundle.
            Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
            Map<String, Integer> paramsType = routeMeta.getParamsType();

            if (MapUtils.isNotEmpty(paramsType)) {
                // Set value by its type, just for params which annotation by @Param
                for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                    setValue(postcard,
                            params.getValue(),
                            params.getKey(),
                            resultMap.get(params.getKey()));
                }

                // Save params name which need auto inject.
                postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
            }

            // Save raw uri
            postcard.withString(ARouter.RAW_URI, rawUri.toString());
        }

        switch (routeMeta.getType()) {
            case PROVIDER:  // if the route is provider, should find its instance
                // Its provider, so it must implement IProvider
                Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                IProvider instance = Warehouse.providers.get(providerMeta);
                if (null == instance) { // There's no instance of this provider
                    IProvider provider;
                    try {
                        provider = providerMeta.getConstructor().newInstance();
                        provider.init(mContext);
                        Warehouse.providers.put(providerMeta, provider);
                        instance = provider;
                    } catch (Exception e) {
                        throw new HandlerException("Init provider failed! " + e.getMessage());
                    }
                }
                postcard.setProvider(instance);
                postcard.greenChannel();    // Provider should skip all of interceptors
                break;
            case FRAGMENT:
                postcard.greenChannel();    // Fragment needn't interceptors
            default:
                break;
        }
    }
}

该方法就是完善PostCard,来实现一次路由导航。

接下来介绍另一个方法_navigation()

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
            case ACTIVITY://如果是Acitvity,则实现Intent跳转
                // Build intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Navigation in main looper.
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }

                        if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }

                        if (null != callback) { // Navigation over.
                            callback.onArrival(postcard);
                        }
                    }
                });

                break;
            case PROVIDER://如果是IOC,则返回目标对象实例
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT://如果是Fragment,则返回实例,并填充bundle
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }

至此我们就完成了一次路由跳转。

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