Toast源碼學(xué)習(xí)

Toast的作用主要是快速的展示相關(guān)信息給用戶,同時(shí)又不占據(jù)太多屏幕位置褐奴,也不奪取焦點(diǎn)按脚,更重要的時(shí)其調(diào)用非常簡(jiǎn)單,一行代碼就可以實(shí)現(xiàn)敦冬。所以學(xué)習(xí)一下Toast的源碼還是很有必要的辅搬。
Toast的源碼在這個(gè)位置:

frameworks\base\core\java\android\widget\Toast.java

先看我們常用的調(diào)用方式

Toast.makeText(context,"Hello ",Toast.LENGTH_SHORT).show();

一段非常簡(jiǎn)潔的鏈?zhǔn)秸{(diào)用代碼,一個(gè)方法就是設(shè)置內(nèi)容和時(shí)長(zhǎng)脖旱,第二個(gè)方法就是顯示堪遂。
我們先從第一個(gè)方法入手介蛉。用過(guò)的都知道m(xù)akeText有一個(gè)重載方法,主要就是所傳內(nèi)容的參數(shù)類型不同溶褪,一個(gè)可以直接傳入字符串币旧,一個(gè)則可以傳入資源ID,如下:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        ...
}

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
                                throws Resources.NotFoundException {
        ...
}

但是從8.0開(kāi)始猿妈,又多了一個(gè)重載方法吹菱,如下:

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
}

可以看到多了一個(gè)Looper參數(shù),用過(guò)的朋友都知道Toast是不可以直接在子線程調(diào)用的彭则,否則會(huì)有如下錯(cuò)誤:

Can't toast on a thread that has not called Looper.prepare()

具體原因后面在分析鳍刷,根據(jù)其提示信息,我們可以分別調(diào)用 Looper.prepare()和 Looper.loop()來(lái)實(shí)現(xiàn)在子線程使用Toast贰剥。但這樣比較麻煩倾剿,所以8.0中又多了一個(gè)這樣的方法,可以直接傳入一個(gè)主線程的looper蚌成,然后在子線程調(diào)用Toast前痘。但是這個(gè)方法目前是hide狀態(tài)的,我們無(wú)法直接調(diào)用担忧,為了試試這個(gè)方法芹缔,我們可以通過(guò)反射調(diào)用一下,看看效果瓶盛。

final Class clazz = Class.forName("android.widget.Toast");
final Method method = clazz.getMethod("makeText", Context.class, Looper.class,CharSequence.class,int.class);
new Thread(){
         @Override
         public void run() {
              super.run();
              try {
                 Toast toast = (Toast) method.invoke(null,context,Looper.getMainLooper(),"Hello",0);
                 toast.show();
              } catch (Exception e) {
                  Log.d("MTC",e.getMessage());
              }
         }
}.start();

結(jié)果證明是可以正常調(diào)用的最欠,至于為什么要隱藏這個(gè)方法,目前還不清楚惩猫。
現(xiàn)在我們開(kāi)始看這個(gè)方法的具體實(shí)現(xiàn):

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
}

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
                                throws Resources.NotFoundException {
        return makeText(context, context.getResources().getText(resId), duration);
}

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
}

可以看到前兩個(gè)makeText方法都在間接調(diào)用第三個(gè)重載芝硬,只是默認(rèn)將looper方法設(shè)為null而已。在第三個(gè)重載中轧房,主要做了這幾件事拌阴,1.構(gòu)造一個(gè)Toast對(duì)象。2.引入一個(gè)布局并且給Textview設(shè)置內(nèi)容奶镶。3.設(shè)置顯示時(shí)間迟赃。

相應(yīng)的在Toast中也提供了一些get,set方法來(lái)獲取和設(shè)置布局和文字厂镇。如:setView纤壁,getView,setText捺信。注意沒(méi)有g(shù)etText方法酌媒。其中有setDuration方法,但是并不是想象中那樣自定義事件,而是只能設(shè)置為L(zhǎng)ENGTH_SHORT和LENGTH_LONG兩種馍佑。對(duì)應(yīng)的也有g(shù)etDuration斋否。

我們可以簡(jiǎn)單看一下這個(gè)布局文件,布局很簡(jiǎn)單,就是一個(gè)TextView

frameworks\base\core\res\res\layout\transient_notification.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="?android:attr/toastFrameBackground">

    <TextView
        android:id="@android:id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginHorizontal="24dp"
        android:layout_marginVertical="15dp"
        android:layout_gravity="center_horizontal"
        android:textAppearance="@style/TextAppearance.Toast"
        android:textColor="@color/primary_text_default_material_light"
        />

</LinearLayout>

接下來(lái)看一下Toast的構(gòu)造方法:

public Toast(Context context) {
        this(context, null);
}

public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
}

同樣拭荤,相對(duì)于之前版本的代碼茵臭,8.0中也多出了一個(gè)帶Looper參數(shù)的構(gòu)造函數(shù),同樣這個(gè)構(gòu)造函數(shù)也是隱藏的舅世。第一個(gè)構(gòu)造也是調(diào)用第二個(gè)構(gòu)造旦委。在構(gòu)造函數(shù)中,初始化了Context變量雏亚,構(gòu)造了一個(gè)TN對(duì)象缨硝,并設(shè)置TN對(duì)象中的一些參數(shù)。我們?yōu)g覽Toast一些布局設(shè)置的方法時(shí)發(fā)現(xiàn)罢低,比如setGravity查辩,setMargin等,都是間接的設(shè)置給了TN网持,說(shuō)明TN是用來(lái)控制Toast的宜岛。我們就來(lái)看一下這個(gè)類。

它是Toast里的一個(gè)靜態(tài)內(nèi)部類功舀,父類是ITransientNotification萍倡,構(gòu)造函數(shù)如下:

TN(String packageName, @Nullable Looper looper) {
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

            mPackageName = packageName;

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
}

在構(gòu)造中,先是設(shè)置許多布局屬性辟汰,在這里我們看到在設(shè)置flag時(shí)設(shè)置為不可觸摸和不可獲得焦點(diǎn)列敲。然后設(shè)置looper變量,如果使用不帶Looper參數(shù)的makeText方法帖汞,這里的looper會(huì)用Looper.myLooper()方法初始化戴而,這也就是在子線程中,myLooper()返回為空翩蘸,導(dǎo)致報(bào)錯(cuò)的原因填硕。這個(gè)looper使用在Handler中的,所以不能為空鹿鳖。之后實(shí)例化了一個(gè)Handler對(duì)象,用于處理show壮莹,hide和cancel動(dòng)作翅帜。

在TN里面也定義了顯示的時(shí)長(zhǎng):

static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;

我們也看到TN里的show,hide命满,cancel方法也都是通過(guò)mHandler傳遞消息涝滴,在Handler對(duì)象調(diào)用對(duì)應(yīng)方法實(shí)現(xiàn)的。

        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

        public void cancel() {
            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }

我們首先看一下handleShow方法:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

可以看出還是很簡(jiǎn)單的,主要是設(shè)置窗口參數(shù)歼疮,然后通過(guò)WindowManager添加視圖即可杂抽。由此可以想到hide就是通過(guò)WindowManager移除視圖,具體看代碼:

public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }

                mView = null;
            }
        }

這里總結(jié)一下韩脏,Toast的makeText方法只是實(shí)例化出來(lái)一個(gè)Toast對(duì)象和TN對(duì)象缩麸,其中Toast只是用來(lái)提供接口讓我們?cè)O(shè)置各種參數(shù),TN則是實(shí)際上用來(lái)控制Toast的顯示隱藏及布局等操作赡矢。

最后看一下Toast的show方法:

public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

這個(gè)就很清楚了杭朱,首先獲得INotificationManager對(duì)象,然后將顯示toast的請(qǐng)求加入隊(duì)列吹散,等待顯示弧械。
INotificationManager的源碼在以下位置:

frameworks\base\services\core\java\com\android\server\notification\NotificationManagerService.java

由于Toast的顯示和隱藏是由INotificationManager管理的,所以我們具體看一下相關(guān)的幾個(gè)方法空民。

@Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

            if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                    (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                            || isPackageSuspended)) {
                return;
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index;
                    if (!isSystemToast) {
                        index = indexOfToastPackageLocked(pkg);
                    } else {
                        index = indexOfToastLocked(pkg, callback);
                    }
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                        record.update(callback);
                    } else {
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }
                    keepProcessAliveIfNeededLocked(callingPid);
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

這就是進(jìn)隊(duì)列的方法刃唐,其中mToastQueue就是一個(gè)ArrayList,里面保存著一個(gè)個(gè)ToastRecord界轩,ToastRecord也是一個(gè)靜態(tài)內(nèi)部類画饥,負(fù)責(zé)保存進(jìn)程ID,ITransientNotification實(shí)例耸棒,時(shí)長(zhǎng)等信息荒澡。基本流程就是先判斷是否存在隊(duì)列中(這個(gè)判斷主要是基于包名的与殃,詳見(jiàn)indexOfToastPackageLocked方法)单山,若存在則更新時(shí)長(zhǎng)和TN信息(用于更新內(nèi)容等),否則加入隊(duì)列末尾幅疼。若當(dāng)前是隊(duì)列第一個(gè)米奸,則調(diào)用showNextToastLocked()來(lái)顯示,方法如下:

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                record.callback.show(record.token);
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

基本流程就是不斷從隊(duì)列中取第一個(gè)ToastRecord爽篷,然后調(diào)用TN實(shí)例中的show方法顯示悴晰。也就回到之前我們看的handleShow方法中,利用WindowManager添加視圖進(jìn)行顯示逐工。之后調(diào)用了scheduleTimeoutLocked铡溪,主要用于移除Toast

private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }

在這里我們看到了Toast顯示時(shí)長(zhǎng)的實(shí)現(xiàn),就是發(fā)送一個(gè)延遲消息泪喊,延遲期間就是顯示的時(shí)機(jī)棕硫。當(dāng)Handler收到MESSAGE_TIMEOUT消息時(shí),執(zhí)行下面方法:

private void handleTimeout(ToastRecord record)
    {
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

又調(diào)用了cancelToastLocked方法袒啼,附帶該Toast在隊(duì)列的位置:

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
        }

        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            showNextToastLocked();
        }
    }

可見(jiàn)顯示調(diào)用了TN中的hide方法哈扮,然后將ToastRecord移出隊(duì)列纬纪,然后循環(huán)去顯示下一個(gè)Toast。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末滑肉,一起剝皮案震驚了整個(gè)濱河市包各,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌靶庙,老刑警劉巖问畅,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惶洲,居然都是意外死亡按声,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門恬吕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)签则,“玉大人,你說(shuō)我怎么就攤上這事铐料〗チ眩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵钠惩,是天一觀的道長(zhǎng)柒凉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)篓跛,這世上最難降的妖魔是什么膝捞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮愧沟,結(jié)果婚禮上蔬咬,老公的妹妹穿的比我還像新娘。我一直安慰自己沐寺,他們只是感情好林艘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著混坞,像睡著了一般狐援。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上究孕,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天啥酱,我揣著相機(jī)與錄音,去河邊找鬼厨诸。 笑死懈涛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的泳猬。 我是一名探鬼主播批钠,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼得封!你這毒婦竟也來(lái)了埋心?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤忙上,失蹤者是張志新(化名)和其女友劉穎拷呆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體疫粥,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茬斧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了梗逮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片项秉。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慷彤,靈堂內(nèi)的尸體忽然破棺而出娄蔼,到底是詐尸還是另有隱情,我是刑警寧澤底哗,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布岁诉,位于F島的核電站,受9級(jí)特大地震影響跋选,放射性物質(zhì)發(fā)生泄漏涕癣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一前标、第九天 我趴在偏房一處隱蔽的房頂上張望坠韩。 院中可真熱鬧,春花似錦候生、人聲如沸同眯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)须蜗。三九已至,卻和暖如春目溉,著一層夾襖步出監(jiān)牢的瞬間明肮,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工缭付, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柿估,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓陷猫,卻偏偏與公主長(zhǎng)得像秫舌,于是被迫代替她去往敵國(guó)和親的妖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容