android-priority-jobqueue源码分析

android-priority-jobqueue是一个后台任务队列框架,可以对任务进行磁盘缓存,当网络恢复连接的时候继续执行任务。

1. 介绍

1.1 优点

  • 便于解耦Application的业务逻辑,让你的代码更加健壮,易于重构和测试。
  • 不处理AsyncTask的生命周期。
  • Job Queue关心优先Jobs,检测网络连接,并行运行等。
  • 可以延迟jobs。
  • 分组jobs来确保串行执行。
  • 默认情况下,Job Queue监控网络连接(所以你不需要担心),当设备处于离线状态,需要网络的jobs不会运行,直到网络重新连接。

1.2 UML

AndroidPriorityJobQueue UML.png

1.3 主类

  • JobManager:Job管理类,负责任务的添加、删除等。
  • MessageFactory:Message工厂类,负责创建相应的Message,包括AddJobMessage、JobConsumerIdleMessage、RunJobMessage等。
  • PriorityMessageQueue:优先级Message队列。
  • MessageQueue:Message队列接口,负责新增、停止、清空消息。
  • SafeMessageQueue:非优先级Message队列。
  • JobManagerThread:Job Runnable对象,轮询消息队列进行处理。
  • Scheduler:调度器,唤醒app或者JobManager。

2. 基本用例

来自AndroidPriorityJobQueue

2.1 创建Job

public class PostTweetJob extends Job {
    public static final int PRIORITY = 1;
    private String text;
    public PostTweetJob(String text) {
        // requireNetwork,需要网络连接
        // persist,需要持久化
        super(new Params(PRIORITY).requireNetwork().persist());
    }
    @Override
    public void onAdded() {
        // Job已经被保存到磁盘里,可以用来更新UI
    }
    @Override
    public void onRun() throws Throwable {
        // 在这里处理Job逻辑,例如网络请求等,所有的工作就是异步完成
        webservice.postTweet(text);
    }
    @Override
    protected RetryConstraint shouldReRunOnThrowable(Throwable throwable, int runCount,
            int maxRunCount) {
        // 在onRun里发生异常处理
        return RetryConstraint.createExponentialBackoff(runCount, 1000);
    }
    @Override
    protected void onCancel(@CancelReason int cancelReason, @Nullable Throwable throwable) {
        // Job被取消是调用
    }
}

2.2 发送Job

//...
public void onSendClick() {
    final String status = editText.getText().toString();
    if(status.trim().length() > 0) {
      jobManager.addJobInBackground(new PostTweetJob(status));
      editText.setText("");
    }
}
//...

3. 源码分析

AndroidPriorityJobQueue细节非常多,就不一一分析,在这里我主要带大家一起看下Job是如何被添加到队列里被执行以及如果再网络连接的时候继续完成Job。其他部分可以自行查看。

3.1 流程图

Job处理流程图.png
新增Job流程图.png

3.2 添加Job

我们先来看下如何添加Job到异步线程处理。

public void addJobInBackground(Job job) {
        AddJobMessage message = messageFactory.obtain(AddJobMessage.class);
        message.setJob(job);
        messageQueue.post(message);
    }

MessageFactory通过obtain创建AddJobMessage,将Job设置到message里面,然后通过messageQueue.post发送。

public <T extends Message> T obtain(Class<T> klass) {
        final Type type = Type.mapping.get(klass);
        //noinspection SynchronizationOnLocalVariableOrMethodParameter
        synchronized (type) {
            Message message = pools[type.ordinal()];
            if (message != null) {
                pools[type.ordinal()] = message.next;
                counts[type.ordinal()] -= 1;
                message.next = null;
                //noinspection unchecked
                return (T) message;
            }
            try {
                return klass.newInstance();
            } catch (InstantiationException e) {
                JqLog.e(e, "Cannot create an instance of " + klass + ". Make sure it has a empty" +
                        " constructor.");
            } catch (IllegalAccessException e) {
                JqLog.e(e, "Cannot create an instance of " + klass + ". Make sure it has a public" +
                        " empty constructor.");
            }
        }
        return null;
    }

创建AddJobMessage,如果pools缓存里面有该Message,则使用,否则通过newInstance创建。

@Override
    public void post(Message message) {
        synchronized (LOCK) {
            postJobTick = true;
            int index = message.type.priority;
            if (queues[index] == null) {
                queues[index] = new UnsafeMessageQueue(factory, "queue_" + message.type.name());
            }
            queues[index].post(message);
            timer.notifyObject(LOCK);
        }
    }

我们可以看到,queues是一个UnsafeMessageQueue数组,根据Message的优先级进行排列,将message保存到UnsafeMessageQueue里面,并且通知监控该对象的线程。

3.4 执行Job

@Override
    public void run() {
        messageQueue.consume(new MessageQueueConsumer() {
            @Override
            public void handleMessage(Message message) {
                switch (message.type) {
                    case ADD_JOB:
                        handleAddJob((AddJobMessage) message);
                        break;
                        \\去除无关代码
                        ...
                }
            }

            \\去除无关代码
            ...
        });
    }
@Override
    public void consume(MessageQueueConsumer consumer) {
        if(running.getAndSet(true)) {
            throw new IllegalStateException("only 1 consumer per MQ");
        }
        while (running.get()) {
            Message message = next(consumer);
            if (message != null) {
                JqLog.d("[%s] consuming message of type %s", LOG_TAG, message.type);
                consumer.handleMessage(message);
                factory.release(message);
            }
        }
    }

如果running已经被设置成true,则抛出异常,每一个MessageQueue只能存在一个consumer。获取下一个Message,交给consumer进行处理,处理完进行释放。

获取下一个Message。

public Message next(MessageQueueConsumer consumer) {
        boolean calledOnIdle = false;
        while (running.get()) {
            //暂时去除无关代码
            ...
            for (int i = Type.MAX_PRIORITY; i >= 0; i--) {
                    UnsafeMessageQueue mq = queues[i];
                    if (mq == null) {
                        continue;
                    }
                    Message message = mq.next();
                    if (message != null) {
                        return message;
                    }
                }

            //暂时去除无关代码
            ...
        }
        return null;
    }

因为Message类型是ADD_JOB,执行handleAddJob。

private void handleAddJob(AddJobMessage message) {
        //暂时去除无用代码
        ...
        final boolean insert = oldJob == null || consumerManager.isJobRunning(oldJob.getId());
        if (insert) {
            JobQueue queue = job.isPersistent() ? persistentJobQueue : nonPersistentJobQueue;
            if (oldJob != null) { //the other job was running, will be cancelled if it fails
                consumerManager.markJobsCancelledSingleId(TagConstraint.ANY, new String[]{job.getSingleInstanceId()});
                queue.substitute(jobHolder, oldJob);
            } else {
                queue.insert(jobHolder);
            }
        } else {
            JqLog.d("another job with same singleId: %s was already queued", job.getSingleInstanceId());
        }
        
        jobHolder.getJob().onAdded();
        //暂时去除无用代码
        ...
    }

如果要求持久化,则进行数据库保存,否则进行内存缓存。回调Job的onAdded
至此完成新增Job,但一直没找到Job的onRun,别急,我们回头再去看下next。

public Message next(MessageQueueConsumer consumer) {
        boolean calledOnIdle = false;
        while (running.get()) {
            //暂时去除无关代码
            ...
            if (!calledOnIdle) {
                consumer.onIdle();
                calledOnIdle = true;
            }
            //暂时去除无关代码
            ...
        }
        return null;
    }

原来当next找不到下一个Message时,会通知consumer,目前处于闲置状态。

final MessageQueueConsumer queueConsumer = new MessageQueueConsumer() {
            //暂时去除无用代码
            ...
            @Override
            public void onIdle() {
                JqLog.d("consumer manager on idle");
                JobConsumerIdleMessage idle = factory.obtain(JobConsumerIdleMessage.class);
                idle.setWorker(Consumer.this);
                idle.setLastJobCompleted(lastJobCompleted);
                parentMessageQueue.post(idle);
            }
        };

发出JobConsumerIdleMessage消息,在JobManagerThread线程里进行处理。

@Override
    public void run() {
        messageQueue.consume(new MessageQueueConsumer() {
            @Override
            public void handleMessage(Message message) {
                switch (message.type) {
                    case JOB_CONSUMER_IDLE:
                        boolean busy = consumerManager.handleIdle((JobConsumerIdleMessage) message);
                        if (!busy) {
                            invokeSchedulersIfIdle();
                        }
                        break;
                        \\去除无关代码
                        ...
                }
            }
            \\去除无关代码
            ...
        });
    }
boolean handleIdle(@NonNull JobConsumerIdleMessage message) {
        //暂时去除无用代码
        ...
        if (nextJob != null) {
            consumer.hasJob = true;
            runningJobGroups.add(nextJob.getGroupId());
            RunJobMessage runJobMessage = factory.obtain(RunJobMessage.class);
            runJobMessage.setJobHolder(nextJob);
            runningJobHolders.put(nextJob.getJob().getId(), nextJob);
            if (nextJob.getGroupId() != null) {
                runningJobGroups.add(nextJob.getGroupId());
            }
            consumer.messageQueue.post(runJobMessage);
            return true;
        } else {
            //暂时去除无用代码
            ...
            }
            return false;
        }
    }

发出RunJobMessage消息。

final MessageQueueConsumer queueConsumer = new MessageQueueConsumer() {
            @Override
            public void handleMessage(Message message) {
                switch (message.type) {
                    case RUN_JOB:
                        handleRunJob((RunJobMessage) message);
                        lastJobCompleted = timer.nanoTime();
                        removePokeMessages();
                        break;
                    //暂时去除无用代码
                    ...
                }
            }
            //暂时去除无用代码
            ...
        };

private void handleRunJob(RunJobMessage message) {
            JqLog.d("running job %s", message.getJobHolder().getClass().getSimpleName());
            JobHolder jobHolder = message.getJobHolder();
            //运行Job
            int result = jobHolder.safeRun(jobHolder.getRunCount(), timer);
            RunJobResultMessage resultMessage = factory.obtain(RunJobResultMessage.class);
            resultMessage.setJobHolder(jobHolder);
            resultMessage.setResult(result);
            resultMessage.setWorker(this);
            parentMessageQueue.post(resultMessage);
        }

int safeRun(int currentRunCount, Timer timer) {
        return job.safeRun(this, currentRunCount, timer);
    }

final int safeRun(JobHolder holder, int currentRunCount, Timer timer) {
        //暂时去除无用代码
        ...
        try {
            onRun();
        } catch (Throwable t) {
            //暂时去除无用代码
        ...
        }
        if (!failed) {
            return JobHolder.RUN_RESULT_SUCCESS;
        }
        if (holder.isCancelledSingleId()) {
            return JobHolder.RUN_RESULT_FAIL_SINGLE_ID;
        }
        if (holder.isCancelled()) {
            return JobHolder.RUN_RESULT_FAIL_FOR_CANCEL;
        }
        if (reRun) {
            return JobHolder.RUN_RESULT_TRY_AGAIN;
        }
        if (cancelForDeadline) {
            return JobHolder.RUN_RESULT_HIT_DEADLINE;
        }
        if (currentRunCount < getRetryLimit()) {
            holder.setThrowable(throwable);
            return JobHolder.RUN_RESULT_FAIL_SHOULD_RE_RUN;
        } else {
            holder.setThrowable(throwable);
            return JobHolder.RUN_RESULT_FAIL_RUN_LIMIT;
        }
    }

private void handleRunJob(RunJobMessage message) {
            JobHolder jobHolder = message.getJobHolder();
            int result = jobHolder.safeRun(jobHolder.getRunCount(), timer);
            RunJobResultMessage resultMessage = factory.obtain(RunJobResultMessage.class);
            resultMessage.setJobHolder(jobHolder);
            resultMessage.setResult(result);
            resultMessage.setWorker(this);
            parentMessageQueue.post(resultMessage);
        }

如果运行成功,则返回RUN_RESULT_SUCCESS,如果失败,分别返回失败原因为被取消,需要再次运行,运行次数超过限制等,发出RunJobResultMessage消息。

private void handleRunJobResult(RunJobResultMessage message) {
        final int result = message.getResult();
        final JobHolder jobHolder = message.getJobHolder();
        callbackManager.notifyOnRun(jobHolder.getJob(), result);
        RetryConstraint retryConstraint = null;
        switch (result) {
            case JobHolder.RUN_RESULT_SUCCESS:
                removeJob(jobHolder);
                break;
            //暂时去除无用代码
            ...
            case JobHolder.RUN_RESULT_TRY_AGAIN:
                retryConstraint = jobHolder.getRetryConstraint();
                insertOrReplace(jobHolder);
                break;
            //暂时去除无用代码
            ...
        }
        consumerManager.handleRunJobResult(message, jobHolder, retryConstraint);
        callbackManager.notifyAfterRun(jobHolder.getJob(), result);
        //暂时去除无用代码
            ...
    }

如果执行Job成功,则移除该Job。至此我们大致分析完整个Job从新增到执行的全部流程。

3.5 监控网络连接

当网络连接时继续执行Job,具体实现在NetworkUtilImpl里。

public NetworkUtilImpl(Context context) {
        context = context.getApplicationContext();、
        //如果SDK>=21
        if (VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                //如果SDK>=23,则监听IDLE模式
                listenForIdle(context);
            }
            //监听网络连接状态
            listenNetworkViaConnectivityManager(context);
        } else {
            context.registerReceiver(new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    //处理网络状态改变
                    dispatchNetworkChange(context);
                }
            }, getNetworkIntentFilter());
        }
    }

private void listenForIdle(Context context) {
        context.registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                dispatchNetworkChange(context);
            }
        }, new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
    }

private void listenNetworkViaConnectivityManager(final Context context) {
        ConnectivityManager cm = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkRequest request = new NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .build();
        cm.registerNetworkCallback(request, new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                dispatchNetworkChange(context);
            }
        });
    }

void dispatchNetworkChange(Context context) {
        if(listener == null) {//shall not be but just be safe
            return;
        }
        listener.onNetworkChange(getNetworkStatus(context));
    }

一旦网络连接,则回调到JobManagerThread。

@Override
    public void onNetworkChange(@NetworkUtil.NetworkStatus int networkStatus) {
        ConstraintChangeMessage constraint = messageFactory.obtain(ConstraintChangeMessage.class);
        messageQueue.post(constraint);
    }

发送ConstraintChangeMessage消息,回调到JobManagerThread。

@Override
    public void run() {
        messageQueue.consume(new MessageQueueConsumer() {
            @Override
            public void handleMessage(Message message) {
                switch (message.type) {
                    //暂时去除无用代码
                    ...
                    case CONSTRAINT_CHANGE:
                        consumerManager.handleConstraintChange();
                        break;
                    //暂时去除无用代码
                    ...
                }
            }

            //暂时去除无用代码
            ...
        });
    }
void handleConstraintChange() {
        considerAddingConsumers(true);
    }

private void considerAddingConsumers(boolean pokeAllWaiting) {
        //暂时去除无用代码
        ...
        boolean isAboveLoadFactor = isAboveLoadFactor();
        if (isAboveLoadFactor) {
            addWorker();
        }
    }

    private void addWorker() {
        //新增Consumer
        Consumer consumer = new Consumer(jobManagerThread.messageQueue,
                new SafeMessageQueue(timer, factory, "consumer"), factory, timer);
        final Thread thread;
        if (threadFactory != null) {
            thread = threadFactory.newThread(consumer);
        } else {
            thread = new Thread(threadGroup, consumer, "job-queue-worker-" + UUID.randomUUID());
            thread.setPriority(threadPriority);
        }
        consumers.add(consumer);
        thread.start();
    }

在Consumer里执行。

@Override
        public void run() {
            messageQueue.consume(queueConsumer);
        }

至此,又开始继续执行Jobs。

4. 设计之美

AndroidPriorityJobQueue设计了2个消息队列,一个是存在优先级,用来保存新增Job、处理空闲状态、处理Job结果、取消等消息,一个不存在优先级,用来保存运行Job的消息,分别用2个线程来对于处理这2个队列,这样使处理Job的线程职责更加清晰。目前看到里面用到了3中设计模式,分别是工厂模式、代理模式和Builder模式。

4.1 工厂模式

MessageFactory

public <T extends Message> T obtain(Class<T> klass) {
        final Type type = Type.mapping.get(klass);
        synchronized (type) {
            Message message = pools[type.ordinal()];
            if (message != null) {
                pools[type.ordinal()] = message.next;
                counts[type.ordinal()] -= 1;
                message.next = null;
                return (T) message;
            }
            try {
                return klass.newInstance();
            } catch (InstantiationException e) {
                JqLog.e(e, "Cannot create an instance of " + klass + ". Make sure it has a empty" +
                        " constructor.");
            } catch (IllegalAccessException e) {
                JqLog.e(e, "Cannot create an instance of " + klass + ". Make sure it has a public" +
                        " empty constructor.");
            }
        }
        return null;
    }

通过newInstance来创建对象,值得一提的是,当消息处理完成,会通过release释放Message,保存到pools,这样下次创建Message时,如果存在则直接使用。

DefaultQueueFactory

@Override
    public JobQueue createPersistentQueue(Configuration configuration, long sessionId) {
        return new CachedJobQueue(new SqliteJobQueue(configuration, sessionId, jobSerializer));
    }

    @Override
    public JobQueue createNonPersistent(Configuration configuration, long sessionId) {
        return new CachedJobQueue(new SimpleInMemoryPriorityQueue(configuration, sessionId));
    }

4.2 代理模式

CachedJobQueue

public class CachedJobQueue implements JobQueue {
    private JobQueue delegate;
    private Integer cachedCount;

    public CachedJobQueue(JobQueue delegate) {
        this.delegate = delegate;
    }

    @Override
    public boolean insert(@NonNull JobHolder jobHolder) {
        invalidateCache();
        return delegate.insert(jobHolder);
    }

    private void invalidateCache() {
        cachedCount = null;
    }

    //省略代码

    @Override
    public int count() {
        if(cachedCount == null) {
            cachedCount = delegate.count();
        }
        return cachedCount;
    }

    private boolean isEmpty() {
        return cachedCount != null && cachedCount == 0;
    }

    @Override
    public int countReadyJobs(@NonNull Constraint constraint) {
        if (isEmpty()) {
            return 0;
        }
        return delegate.countReadyJobs(constraint);
    }

    @Override
    public JobHolder nextJobAndIncRunCount(@NonNull Constraint constraint) {
        if(isEmpty()) {
            return null;//we know we are empty, no need for querying
        }
        JobHolder holder = delegate.nextJobAndIncRunCount(constraint);
        if (holder != null && cachedCount != null) {
            cachedCount -= 1;
        }
        return holder;
    }
    
    //省略代码
    
}

该代理模式的设计主要是为了缓存等待Jobs的数量。

4.3 Builder模式

Configuration

public class Configuration {
    //省略代码

    @Nullable
    public ThreadFactory getThreadFactory() {
        return threadFactory;
    }

    @SuppressWarnings("unused")
    public static final class Builder {
        private Configuration configuration;

        public Builder(@NonNull Context context) {
            this.configuration = new Configuration();
            this.configuration.appContext = context.getApplicationContext();
        }

        //省略代码
        @NonNull
        public Builder threadFactory(@Nullable final ThreadFactory threadFactory) {
            configuration.threadFactory = threadFactory;
            return this;
        }

        @NonNull
        public Configuration build() {
            if(configuration.queueFactory == null) {
                configuration.queueFactory = new DefaultQueueFactory();
            }
            if(configuration.networkUtil == null) {
                configuration.networkUtil = new NetworkUtilImpl(configuration.appContext);
            }
            if (configuration.timer == null) {
                configuration.timer = new SystemTimer();
            }
            return configuration;
        }
    }
}

我们可以看到配置类很适合用Builder模式来设计,对配置项和使用类进行解耦,便于配置项的拓展。

5. 总结

AndroidPriorityJobQueue从13年开源到现在,经历了2个大的版本,完整的看下来也花费了不少时间,看到了一些设计后台任务队列框架的思路,特别是对任务队列进行分组确保串行,对网络恢复时继续进行Job等设计,可能理解的还不够深入,有错误的地方还大家望指正。

6. 参考资料

AndroidPriorityJobQueue Github

可以随意转发,也欢迎关注我的简书,我会坚持给大家带来分享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容