Android 性能優(yōu)化 06---UI卡頓優(yōu)化

一.CPU/GPU

CPU的任務(wù)繁多雹拄,做邏輯計算外收奔,還要做內(nèi)存管理、顯示操作滓玖,因此 在實(shí)際運(yùn)算的時候性能會大打折扣坪哄,在沒有 GPU 的時代,不能顯示復(fù) 雜的圖形势篡,其運(yùn)算速度遠(yuǎn)跟不上今天復(fù)雜三維游戲的要求翩肌。即使 CPU 的工作頻率超過 2GHz 或更高,對它繪制圖形提高也不大禁悠。這時 GPU 的設(shè)計就出來了 念祭。


image.png

二.XML布局顯示到屏幕流程

image.png

image.png

三.卡頓原理

image.png

四.16ms主要處理兩件事

  • 將UI對象轉(zhuǎn)換成多邊形和紋理
  • CPU傳遞數(shù)據(jù)到GPU碍侦,GPU進(jìn)行繪制粱坤。

五.如何減少時間

  • CPU減少XML轉(zhuǎn)換成對象時間
  • GPU減少重復(fù)繪制(GPU傻)

六.卡頓的原因

兩個單位的運(yùn)行時間超出16.66ms就會跳幀

  • XML文件加載解析,到傳輸至底層到最終post到通知surfaceflinger的時間
  • GPU繪制產(chǎn)生數(shù)據(jù)的時間
    所以想要處理就要控制這兩塊時間在16.66mm內(nèi)為最優(yōu)瓷产。
    卡頓優(yōu)化的唯一核心就是讓CPU的數(shù)據(jù)處理和GPU的數(shù)據(jù)處理降低至16.66ms內(nèi)全部處理完站玄。

七.常用問題解決方案01-布局優(yōu)化

層級越深--->infalate--->遞歸走法---》內(nèi)存--->棧去里面也要消耗。層級越多CPU算力需要越多濒旦。

  • 常用標(biāo)簽
    include:頭尾等同質(zhì)化嚴(yán)重的可復(fù)用XML最好做成一個獨(dú)立布局文件引用蜒什。
    merge:被include的具體布局采用merge,作用是會在加載時直接嵌入到父布局中,和include配合使用疤估。
    viewstub:常規(guī)像密碼提示框那些東西只有在需要特定條件下觸發(fā)才展示的用ViewStub這個組件只用在狀態(tài)為visible時才會加載。
  • 常用方案
    1.調(diào)整布局結(jié)構(gòu)霎冯。
    2.背景色匹配铃拇。
    3.使用約束布局。
    約束布局優(yōu)點(diǎn):
    a.極大程度減少布局層級
    b.可以實(shí)現(xiàn)一些其他布局管理器不能實(shí)現(xiàn)的樣式
    約束布局缺點(diǎn):
    每個被參考的控件都需要設(shè)置id

八.常用問題解決方案02-過度繪制

  • GPU過度繪制檢查
    手機(jī)開發(fā)者功能中自帶檢測工具沈撞。
  • 解決方案
    1.移除布局中不必要的背景慷荔。
    2.是視圖層次結(jié)構(gòu)扁平化。
    3.裁剪不必要的繪制元素缠俺。
    src = 一次opengl繪制
    background = 一次opengl繪制
    過度繪制的幾種情況:
    1: 布局層級太深显晶, 用戶看不到的區(qū)域也會被繪制
    2: 自定義控件中贷岸,onDraw方法做了過多的繪制

九.檢測工具01layout inspector

查看布局層次結(jié)構(gòu),主要用于布局優(yōu)化磷雇。
具體使用請查看:

https://blog.csdn.net/cadi2011/article/details/85212762

十.檢測工具02systrace

具體使用請查看:

https://www.cnblogs.com/wangjie1990/p/11327220.html

十一.檢測工具03Looper機(jī)制

因?yàn)樵贚ooper進(jìn)行消息轉(zhuǎn)發(fā)的時候偿警,會涉及到打印問題,且在執(zhí)行前后都會打印唯笙,利用這個機(jī)制在Looper中他提供能夠自定義Loging相關(guān)機(jī)制螟蒸。

public class LogMonitor implements Printer {


    private StackSampler mStackSampler;
    private boolean mPrintingStarted = false;
    private long mStartTimestamp;
    // 卡頓閾值
    private long mBlockThresholdMillis = (long) (5 * 16.66);
    //采樣頻率
    private long mSampleInterval = 1000;

    private Handler mLogHandler;

    public LogMonitor() {
        mStackSampler = new StackSampler(mSampleInterval);
        HandlerThread handlerThread = new HandlerThread("block-canary-io");
        handlerThread.start();
        mLogHandler = new Handler(handlerThread.getLooper());
    }

    @Override
    public void println(String x) {
        //從if到else會執(zhí)行 dispatchMessage,如果執(zhí)行耗時超過閾值崩掘,輸出卡頓信息
        if (!mPrintingStarted) {
            //記錄開始時間
            mStartTimestamp = System.currentTimeMillis();
            mPrintingStarted = true;
            mStackSampler.startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //出現(xiàn)卡頓
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            mStackSampler.stopDump();
        }
    }

    private void notifyBlockEvent(final long endTime) {
        mLogHandler.post(new Runnable() {
            @Override
            public void run() {
                //獲得卡頓時主線程堆棧
                List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
                for (String stack : stacks) {
                    Log.e("block-canary", stack);
                }
            }
        });
    } 

    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    } 
}

/**
 * 適用于耗時代碼檢測
 */
public class BlockCanary {
    public static void install() {
        LogMonitor logMonitor = new LogMonitor();
        Looper.getMainLooper().setMessageLogging(logMonitor);
    }
}

/**
 * 適用于耗時代碼檢測
 */
public class BlockCanary {
    public static void install() {
        LogMonitor logMonitor = new LogMonitor();
        Looper.getMainLooper().setMessageLogging(logMonitor);
    }
}

public class StackSampler {
    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
            new SimpleDateFormat("MM-dd HH:mm:ss.SSS");


    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>();
    private int mMaxCount = 100;
    private long mSampleInterval;
    //是否需要采樣
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
        mSampleInterval = sampleInterval;
        HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
    }

    /**
     * 開始采樣 執(zhí)行堆棧
     */
    public void startDump() {
        //避免重復(fù)開始
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);

        mHandler.removeCallbacks(mRunnable);
        mHandler.postDelayed(mRunnable, mSampleInterval);
    }

    public void stopDump() {
        if (!mShouldSample.get()) {
            return;
        }
        mShouldSample.set(false);

        mHandler.removeCallbacks(mRunnable);
    }


    public List<String> getStacks(long startTime, long endTime) {
        ArrayList<String> result = new ArrayList<>();
        synchronized (mStackMap) {
            for (Long entryTime : mStackMap.keySet()) {
                if (startTime < entryTime && entryTime < endTime) {
                    result.add(TIME_FORMATTER.format(entryTime)
                            + SEPARATOR
                            + SEPARATOR
                            + mStackMap.get(entryTime));
                }
            }
        }
        return result;
    }

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString()).append("\n");
            }
            synchronized (mStackMap) {
                //最多保存100條堆棧信息
                if (mStackMap.size() == mMaxCount) {
                    mStackMap.remove(mStackMap.keySet().iterator().next());
                }
                mStackMap.put(System.currentTimeMillis(), sb.toString());
            }

            if (mShouldSample.get()) {
                mHandler.postDelayed(mRunnable, mSampleInterval);
            }
        }
    };

}

十二.ChoreograhperHelper編舞者監(jiān)聽幀率

注意目的是看整個運(yùn)行情況定位到大塊七嫌,然后結(jié)合上面設(shè)定閾值進(jìn)行處理。

/**
 * 細(xì)化幀數(shù)
 * 適用于快速定位幀率監(jiān)控
 */
public class ChoreographerHelper {
    private static final String TAG = "ChoreographerHelper";
    static long lastFrameTimeNanos = 0;

    public static void start() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long frameTimeNanos) {
                    //上次回調(diào)時間
                    if (lastFrameTimeNanos == 0) {
                        lastFrameTimeNanos = frameTimeNanos;
                        Choreographer.getInstance().postFrameCallback(this);
                        return;
                    }
                    long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
                    if (diff > 16.6f) {

                        //掉幀數(shù)
                        int droppedCount = (int) (diff / 16.6);
                        if (droppedCount > 2) {
                            Log.w(TAG, "UI線程超時(超過16ms)當(dāng)前:" + diff + "ms" + " , 丟幀:" + droppedCount);
                        }
                    }
                    lastFrameTimeNanos = frameTimeNanos;
                    Choreographer.getInstance().postFrameCallback(this);
                }
            });
        }
    }
}

十三.UI優(yōu)化思路與核心決策

  • 如何研判是否需要做調(diào)整
    優(yōu)化的方案一定是摳空間或者時間:
    加入20個Fragment:優(yōu)化每一個Fragment到極致(1:數(shù)據(jù)能不能少加載點(diǎn)苞慢?2:流程能不能優(yōu)化诵原?視覺能不能優(yōu)化?)
    20個Fragment能不能少加載點(diǎn)挽放?流程優(yōu)化绍赛?
    空間重要還是時間 ?流程上能不能優(yōu)化骂维?
  • 方案上如何去做妥協(xié)
    1.穩(wěn)定第一位
    2.成本/用戶體驗(yàn)

卡頓分析與布局優(yōu)化

卡頓分析

Systrace

Systrace 是Android平臺提供的一款工具惹资,用于記錄短期內(nèi)的設(shè)備活動。該工具會生成一份報告航闺,其中匯總了
Android 內(nèi)核中的數(shù)據(jù)褪测,例如 CPU 調(diào)度程序、磁盤活動和應(yīng)用線程潦刃。Systrace主要用來分析繪制性能方面的問題侮措。在發(fā)生卡頓時,通過這份報告可以知道當(dāng)前整個系統(tǒng)所處的狀態(tài)乖杠,從而幫助開發(fā)者更直觀的分析系統(tǒng)瓶頸分扎,改進(jìn)性能。

App層面監(jiān)控卡頓

systrace可以讓我們了解應(yīng)用所處的狀態(tài)胧洒,了解應(yīng)用因?yàn)槭裁丛驅(qū)е碌奈废拧H粜枰獪?zhǔn)確分析卡頓發(fā)生在什么函數(shù),
資源占用情況如何卫漫,目前業(yè)界兩種主流有效的app監(jiān)控方式如下:

1菲饼、 利用UI線程的Looper打印的日志匹配;
2列赎、 使用Choreographer.FrameCallback

Looper日志檢測卡頓
Android主線程更新UI宏悦。如果界面1秒鐘刷新少于60次,即FPS小于60,用戶就會產(chǎn)生卡頓感覺饼煞。簡單來說源葫,
Android使用消息機(jī)制進(jìn)行UI更新,UI線程有個Looper砖瞧,在其loop方法中會不斷取出message息堂,調(diào)用其綁定的
Handler在UI線程執(zhí)行。如果在handler的dispatchMesaage方法里有耗時操作芭届,就會發(fā)生卡頓储矩。
其實(shí)這種方式也就是 BlockCanary 原理。

Choreographer.FrameCallback
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號褂乍,來通知界面進(jìn)行重繪持隧、渲染,每一次同步的周期約為16.6ms逃片,代表一幀
的刷新頻率屡拨。通過Choreographer類設(shè)置它的FrameCallback函數(shù),當(dāng)每一幀被渲染時會觸發(fā)回調(diào)FrameCallback.doFrame (long frameTimeNanos) 函數(shù)褥实。frameTimeNanos是底層VSYNC信號到達(dá)的時間戳 呀狼。
通過 ChoreographerHelper 可以實(shí)時計算幀率和掉幀數(shù),實(shí)時監(jiān)測App頁面的幀率數(shù)據(jù)损离,發(fā)現(xiàn)幀率過低哥艇,還可以自動保存現(xiàn)場堆棧信息。

Looper比較適合在發(fā)布前進(jìn)行測試或者小范圍灰度測試然后定位問題僻澎,ChoreographerHelper適合監(jiān)控線上環(huán)境的 app 的掉幀情況來計算 app 在某些場景的流暢度然后有針對性的做性能優(yōu)化貌踏。

布局優(yōu)化

層級優(yōu)化

measure、layout窟勃、draw這三個過程都包含自頂向下的View Tree遍歷耗時祖乳,如果視圖層級太深自然需要更多的時間來完成整個繪測過程,從而造成啟動速度慢秉氧、卡頓等問題眷昆。而onDraw在頻繁刷新時可能多次出發(fā),因此onDraw更不能做耗時操作汁咏,同時需要注意內(nèi)存抖動亚斋。對于布局性能的檢測,依然可以使用systrace與traceview按照繪制流程檢查繪制耗時函數(shù)攘滩。
Layout Inspector
在較早的時代SDK中有一個hierarchy viewer 工具帅刊,但是早在 Android Studio 3.1 配套的SDK中(具體SDK版本不記得了)就已經(jīng)被棄用。現(xiàn)在應(yīng)在運(yùn)行時改用 Layout Inspector來檢查應(yīng)用的視圖層次結(jié)構(gòu)
使用merge標(biāo)簽
當(dāng)我們有一些布局元素需要被多處使用時轰驳,這時候我們會考慮將其抽取成一個單獨(dú)的布局文件。在需要使用的地方通過 include 加載。
使用ViewStub 標(biāo)簽
當(dāng)我們布局中存在一個View/ViewGroup级解,在某個特定時刻才需要他的展示時冒黑,可能會有同學(xué)把這個元素在xml中定義為invisible或者gone,在需要顯示時再設(shè)置為visible可見勤哗。比如在登陸時抡爹,如果密碼錯誤在密碼輸入框上顯示提示。
invisible
view設(shè)置為invisible時芒划,view在layout布局文件中會占用位置冬竟,但是view為不可見,該view還是會創(chuàng)建對
象民逼,會被初始化泵殴,會占用資源。
gone
view設(shè)置gone時拼苍,view在layout布局文件中不占用位置笑诅,但是該view還是會創(chuàng)建對象,會被初始化疮鲫,會占
用資源吆你。

如果view不一定會顯示,此時可以使用 ViewStub 來包裹此View 以避免不需要顯示view但是又需要加載view消耗資
源俊犯。
viewstub是一個輕量級的view妇多,它不可見,不用占用資源燕侠,只有設(shè)置viewstub為visible或者調(diào)用其inflater()方法
時者祖,其對應(yīng)的布局文件才會被初始化。

過度渲染

過度繪制是指系統(tǒng)在渲染單個幀的過程中多次在屏幕上繪制某一個像素贬循。例如咸包,如果我們有若干界面卡片堆疊在一
起,每張卡片都會遮蓋其下面一張卡片的部分內(nèi)容杖虾。但是烂瘫,系統(tǒng)仍然需要繪制堆疊中的卡片被遮蓋的部分随闽。

GPU 過度繪制檢查
手機(jī)開發(fā)者選項中能夠顯示過度渲染檢查功能氧卧,通過對界面進(jìn)行彩色編碼來幫我們識別過度繪制号胚。
解決過度繪制問題
可以采取以下幾種策略來減少甚至消除過度繪制:

  • 移除布局中不需要的背景玫恳。
    移除不必要的背景可以快速提高渲染性能损谦。不必要的背景可能永遠(yuǎn)不可見刁绒,因?yàn)樗鼤粦?yīng)用在該視圖上
    繪制的任何其他內(nèi)容完全覆蓋婚惫。例如屡谐,當(dāng)系統(tǒng)在父視圖上繪制子視圖時皮仁,可能會完全覆蓋父視圖的背
    景籍琳。

  • 使視圖層次結(jié)構(gòu)扁平化菲宴。
    可以通過優(yōu)化視圖層次結(jié)構(gòu)來減少重疊界面對象的數(shù)量,從而提高性能趋急。

  • 降低透明度喝峦。
    對于不透明的 view ,只需要渲染一次即可把它顯示出來呜达。但是如果這個 view 設(shè)置了 alpha 值谣蠢,則至少需要渲染兩次。這是因?yàn)槭褂昧?alpha 的 view 需要先知道混合 view 的下一層元素是什么查近,然后再結(jié)合上層的 view 進(jìn)行Blend混色處理眉踱。透明動畫、淡入淡出和陰影等效果都涉及到某種透明度霜威,這就會造成了過度繪制谈喳。可以通過減少要渲染的透明對象的數(shù)量侥祭,來改善這些情況下的過度繪制叁执。例如,如需獲得灰色文本矮冬,可以在 TextView 中繪制黑色文本谈宛,再為其設(shè)置半透明的透明度值。但是胎署,簡單地通過用灰色繪制文本也能獲得同樣的效果吆录,而且能夠大幅提升性能。

布局加載優(yōu)化

異步加載
LayoutInflater加載xml布局的過程會在主線程使用IO讀取XML布局文件進(jìn)行XML解析琼牧,再根據(jù)解析結(jié)果利用反射創(chuàng)建布局中的View/ViewGroup對象恢筝。這個過程隨著布局的復(fù)雜度上升,耗時自然也會隨之增大巨坊。Android為我們提供了 Asynclayoutinflater 把耗時的加載操作在異步線程中完成撬槽,最后把加載結(jié)果再回調(diào)給主線程。

dependencies { 
  implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" 
}
new AsyncLayoutInflater(this)
 .inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() { 
@Override 
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
 setContentView(view); //...... } 
});

1趾撵、使用異步 inflate侄柔,那么需要這個 layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的;
2占调、所有構(gòu)建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.myLooper暂题;(因?yàn)槭窃诋惒骄€程中加載的,異步線程默認(rèn)沒有調(diào)用 Looper.prepare )究珊;
3薪者、AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
4剿涮、不支持加載包含 Fragment 的 layout

5言津、如果 AsyncLayoutInflater 失敗攻人,那么會自動回退到UI線程來加載布局;

掌閱X2C思路
https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悬槽,一起剝皮案震驚了整個濱河市贝椿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌陷谱,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑟蜈,死亡現(xiàn)場離奇詭異烟逊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)铺根,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進(jìn)店門宪躯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人位迂,你說我怎么就攤上這事访雪。” “怎么了掂林?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵臣缀,是天一觀的道長。 經(jīng)常有香客問我泻帮,道長精置,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任锣杂,我火速辦了婚禮脂倦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘元莫。我一直安慰自己赖阻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布踱蠢。 她就那樣靜靜地躺著火欧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朽基。 梳的紋絲不亂的頭發(fā)上布隔,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天,我揣著相機(jī)與錄音稼虎,去河邊找鬼衅檀。 笑死,一個胖子當(dāng)著我的面吹牛霎俩,可吹牛的內(nèi)容都是我干的哀军。 我是一名探鬼主播沉眶,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼杉适!你這毒婦竟也來了谎倔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤猿推,失蹤者是張志新(化名)和其女友劉穎片习,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蹬叭,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡藕咏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了秽五。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孽查。...
    茶點(diǎn)故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坦喘,靈堂內(nèi)的尸體忽然破棺而出盲再,到底是詐尸還是另有隱情,我是刑警寧澤瓣铣,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布答朋,位于F島的核電站,受9級特大地震影響棠笑,放射性物質(zhì)發(fā)生泄漏绿映。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一腐晾、第九天 我趴在偏房一處隱蔽的房頂上張望叉弦。 院中可真熱鬧,春花似錦藻糖、人聲如沸淹冰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽樱拴。三九已至,卻和暖如春洋满,著一層夾襖步出監(jiān)牢的瞬間晶乔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工牺勾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留正罢,地道東北人。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓驻民,卻偏偏與公主長得像翻具,于是被迫代替她去往敵國和親履怯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,566評論 2 349

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