SharedPreferences ANR問題分析和解決 & Android 8.0的優(yōu)化

在日志后臺上ANR的Top1問題品嚣,SharedPreferences相關的anr問題悠夯,我們經(jīng)常會遇到是晨。

主要anr日志:

 "main" prio=5 tid=1 WAIT
  | group="main" sCount=1 dsCount=0 cgrp=default handle=1074614660
  | sysTid=10796 nice=-4 sched=0/0 cgrp=default handle=1074614660
  | state=S schedstat=( 7395789134 225970925 16305 ) utm=616 stm=123 core=0
 at java.lang.Object.wait(Native Method)
 at java.lang.Thread.parkFor(Thread.java:1212)
 at sun.misc.Unsafe.park(Unsafe.java:325)
 at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
 at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
 at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:973)
 at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281)
 at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
 at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:364)
 at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
 at android.app.ActivityThread.handleStopActivity(ActivityThread.java:3561)
 at android.app.ActivityThread.access$1100(ActivityThread.java:172)

問題分析:

該問題是與SharedPreferences操作相關的癌椿。在我們的代碼中健蕊,使用sp讀寫配置文件,都是采用了官方的推薦做法踢俄,調用apply提交绊诲,調用這個方法時,會首先寫入內存中褪贵,然后將落盤的任務加入隊列中掂之,會在異步線程中做落盤的操作,這個操作一般來說是沒有問題的脆丁,也是google官方推薦的做法世舰。但是另一方面android的系統(tǒng)會在Activity的onStop,onPause等生命周期中,調用QueuedWork.waitToFinish槽卫,等待落盤的任務隊列執(zhí)行完成跟压,如果任務隊列中的任務很多,或者待寫入的數(shù)據(jù)量很大時(sp文件是全量讀寫的)歼培,在一些io性能差的中低端機型上就會很容易出現(xiàn)anr.

SharedPreferences的源碼流程震蒋,可以參考鏈接:http://gityuan.com/2017/06/18/SharedPreferences/
下面主要分析apply方法的流程:

final class SharedPreferencesImpl implements SharedPreferences {
 
 public void apply() {
          //將數(shù)據(jù)提交到內存中
           final MemoryCommitResult mcr = commitToMemory();
           final Runnable awaitCommit = new Runnable() {
                   public void run() {
                       try {
                         // 等待寫入任務完成
                           mcr.writtenToDiskLatch.await();
                       } catch (InterruptedException ignored) {
                       }
                   }
               };
           // 將等待任務加入到列表中
           QueuedWork.add(awaitCommit);

           Runnable postWriteRunnable = new Runnable() {
                   public void run() {
                       awaitCommit.run();
                       QueuedWork.remove(awaitCommit);
                   }
               };
           // 將寫入任務加入到隊列中
           SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
           // 通知回調
           notifyListeners(mcr);
       }

apply的基本流程是:

  1. 首先調用commitToMemory將數(shù)據(jù)改動同步到內存中,也就是SharedPreferencesImpl的mMap(HashMap)
  2. 然后調用 QueuedWork.add(awaitCommit);將一個等待的任務加入到列表中躲庄,在Activity等的生命周期中查剖,就是以這個為判斷條件,等待寫入任務執(zhí)行完成的噪窘。
  3. 調用enqueueDiskWrite方法的實現(xiàn)笋庄,將寫入任務加入到隊列中,寫入磁盤的操作會在子線程中執(zhí)行。

enqueueDiskWrite方法的實現(xiàn):

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                 final Runnable postWriteRunnable) {
       final Runnable writeToDiskRunnable = new Runnable() {
               public void run() {
                   synchronized (mWritingToDiskLock) {
                     // 真正執(zhí)行寫入文件的操作
                       writeToFile(mcr);
                   }
                   synchronized (SharedPreferencesImpl.this) {
                       mDiskWritesInFlight--;
                   }
                   if (postWriteRunnable != null) {
                       postWriteRunnable.run();
                   }
               }
           };

       final boolean isFromSyncCommit = (postWriteRunnable == null);

       // Typical #commit() path with fewer allocations, doing a write on
       // the current thread.
       if (isFromSyncCommit) {
           boolean wasEmpty = false;
           synchronized (SharedPreferencesImpl.this) {
               wasEmpty = mDiskWritesInFlight == 1;
           }
           if (wasEmpty) {
               writeToDiskRunnable.run();
               return;
           }
       }
       //將寫入磁盤的任務加入到單線程的線程池中(8.0之前)
       QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
   }

加入到任務隊列的處理中直砂,android8.0之前菌仁,是將runnable任務加入到單線程的線程池中, android 8.0之后做了很大的調整静暂,幾乎是對QueuedWork類做了重寫济丘。android 8.0中是將任務加入到LinkedList鏈表中,而且是在HandlerThread中做異步處理洽蛀,而不是使用線程池闪盔。

android 8.0 QueuedWork.java:


public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
           //將任務加入到鏈表中
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
              //延時100ms執(zhí)行
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
//執(zhí)行寫入磁盤任務
 private static void processPendingWork() {
        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();
                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
          // 將任務從鏈表中依次取出執(zhí)行
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }
            }
        }
    }

調用QueuedWork.waitToFinish()方法的代碼:

ActivityThread.java:


image.png

Activity 的 onStop,以及 Service 的 onStop 和 onStartCommand 都是通過 ActivityThread 觸發(fā)的辱士。在一些組件的生命周期回調中泪掀,比如Service.onStartCommand,Service.onDestroy,Activity.onPause,Activity.onStop時,會調用QueuedWork.waitToFinish();去等待所有寫入任務的執(zhí)行完成颂碘。

在android 8.0之前异赫,這個方法的實現(xiàn):

public static void waitToFinish() {
       Runnable toFinish;
       //等待所有的任務執(zhí)行完成
       while ((toFinish = sPendingWorkFinishers.poll()) != null) {
           toFinish.run();
       }
   }

sPendingWorkFinishers并不是寫入任務的列表,而是等待狀態(tài)的列表头岔,這個方法的作用就是如名字所代表的塔拳,就是在等待完成,阻塞主線程峡竣,干等著靠抑。
這里的toFinish.run方法,其實就只是執(zhí)行一行代碼:mcr.writtenToDiskLatch.await(); 在等待寫入完成.
android 8.0 之前的實現(xiàn)QueuedWork.waitToFinish是有缺陷的适掰。在多個生命周期方法中颂碧,在主線程等待任務隊列去執(zhí)行完畢,而由于cpu調度的關系任務隊列所在的線程并不一定是處于執(zhí)行狀態(tài)的类浪,而且當apply提交的任務比較多時载城,等待全部任務執(zhí)行完成,會消耗不少時間费就,這就有可能出現(xiàn)anr.

android 8.0的優(yōu)化

而android 8.0以后诉瓦,這個方法的實現(xiàn)做了很大的改變;

public static void waitToFinish() {
 
       Handler handler = getHandler();

       synchronized (sLock) {
           if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
               // Delayed work will be processed at processPendingWork() below
               handler.removeMessages(QueuedWorkHandler.MSG_RUN);

           }

 
           sCanDelay = false;
       }

   ...
     // 觸發(fā)依次調用所有的寫入任務
           processPendingWork();
    
   ...
       try {
         //等待任務執(zhí)行完成
           while (true) {
               Runnable finisher;

               synchronized (sLock) {
                 
                   finisher = sFinishers.poll();
               }

               if (finisher == null) {
                   break;
               }
       
               finisher.run();
           }
       } finally {
           sCanDelay = true;
       }
       }
   }

在這個版本的實現(xiàn)中力细,會主動觸發(fā)processPendingWork取出寫任務列表中依次執(zhí)行睬澡,而不是只在在等待。

SharedPreferences的實現(xiàn)中眠蚂,除了線程調度做的改動外煞聪,android8.0還做了一個很重要的優(yōu)化:
我們知道在調用apply方法時,會將改動同步提交到內存中map中河狐,然后將寫入磁盤的任務加入的隊列中米绕,在工作線程中從隊列中取出寫入任務瑟捣,依次執(zhí)行寫入馋艺。注意栅干,不管是內存的寫入還是磁盤的寫入,對于一個xml格式的sp文件來說捐祠,都是全量寫入的碱鳞。
這里就存在優(yōu)化的空間,比如對于同一個sp文件踱蛀,連續(xù)調用n次apply,就會有n次寫入磁盤任務執(zhí)行窿给,實際上只需要最后執(zhí)行最后那次就可以了,最后那次提交對應內存的map是持有最新的數(shù)據(jù)率拒,所以就可以省掉前面n-1次的執(zhí)行崩泡,這個就是android 8.0中做的優(yōu)化,看下代碼是如何實現(xiàn)的:

SharedPreferencesImpl.writeToFile()方法:

  // Only need to write if the disk state is older than this commit
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

android8.0中猬膨,增加了版本號控制的邏輯角撞,版本號數(shù)值都是要遞增的。mDiskStateGeneration表示當前磁盤最新的版本號勃痴, mcr.memoryStateGeneration是指本次內存提交的版本號谒所,很明顯只有滿足mDiskStateGeneration < mcr.memoryStateGeneration 這個條件才是有意義的提交,所以加了這個判斷沛申。
mCurrentMemoryStateGeneration 是指當前內存中最新的版本號劣领,調用commit或者apply時,這兩個方法都會調用commitToMemory()铁材,在這個方法里會將這個值遞增1

  if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

當滿足mCurrentMemoryStateGeneration == mcr.memoryStateGeneration 這個條件時尖淘,表示是最新的提交任務。
所以當工作線程要依次執(zhí)行寫入任務列表中的任務時著觉,只會執(zhí)行最后的德澈、最新的寫入任務,這樣就通過少做不必要的事情來實現(xiàn)了優(yōu)化固惯。

Android8.0對Sp的優(yōu)化主要是有兩個方面:

改變原來被動等待線程調度執(zhí)行寫入的方式梆造,改為主動去調用,涉及主要方法是SharedPreferencesImpl.waitToFinish
增加版本號控制的邏輯葬毫,原來是所有的提交都會執(zhí)行寫入磁盤一遍镇辉,現(xiàn)在是只執(zhí)行最后、最新的提交寫入磁盤贴捡,涉及的主要方法是:SharedPreferencesImpl.writeToFile

在問題日志的平臺上忽肛,也可以看到,該問題在android8.0以上就沒有出現(xiàn)烂斋,都分布在android8.0以下屹逛。

復現(xiàn)方式:

在當前activity中础废,調用apply,寫入多次,大量的數(shù)據(jù)到sp中罕模,再進行頁面跳轉,觸發(fā)onPause评腺、onStop方法,則在一些低端機(如紅米note 1)很容易復現(xiàn)該問題淑掌,出現(xiàn)anr.

private void applyInfo(){
        SharedPreferences applySp = mActivity.getSharedPreferences("apply",Context.MODE_PRIVATE);
        SharedPreferences.Editor applyEdit =  applySp.edit();
        String content = "很長的文本";
        for(int i = 1 ;i <= 1000; i++ ){
            String strKey = "str"+i;
            applyEdit.putString(strKey,content);
            applyEdit.apply();
        }
    }

解決方法

問題直接來自于在系統(tǒng)在主線程的幾個生命周期中去等待任務列表執(zhí)行完成蒿讥,那么android為什么要這樣設計呢?android的應用是被托管運行的抛腕,應用在運行過程中有可能被系統(tǒng)回收芋绸、殺死、或者用戶主動殺死担敌,其實是在一個不確定的環(huán)境中運行摔敛,apply提交的任務,不是立即執(zhí)行的全封,而是會加入到列表中马昙,在未來的某一個時刻去執(zhí)行,那么就存在不確定性了售貌,有可能在執(zhí)行之前應用進程被殺死了给猾,那么寫入任務就失敗了。所以就在應用進程的存續(xù)時颂跨,抓緊找到一些時機去完成寫入磁盤的事情敢伸,也就是在上面的幾個生命周期方法中。

這個設計整體上是沒有大問題的恒削,但是QueuedWork.waitToFinish的方法在老版的實現(xiàn)上存在很大的缺陷池颈,它使得主線程只是在等待,而沒有做推動钓丰,這種情況下導致應用出現(xiàn)anr,進而被用戶或者系統(tǒng)殺死進程躯砰,這樣寫入任務還是不能執(zhí)行完成,還影響用戶體驗携丁,這個是得不償失的琢歇。8.0的版本才修復了這個缺陷。

在google的android issue平臺上梦鉴,也有類似的問題報告:
https://issuetracker.google.com/issues/62206685

老版本 的QueuedWork.waitToFinish方法實現(xiàn)有缺陷李茫,可以去規(guī)避這個方法來解決這個問題,就是去清除等待鎖的隊列肥橙,主線程在調用這個方法時魄宏,不必去等待〈娣ぃ可以只在Android8.0以下加入此處理宠互。
該解決方案參考自: https://mp.weixin.qq.com/s/IFgXvPdiEYDs5cDriApkxQ

代碼實現(xiàn)

ActivityThread 中有一個 Handler 變量味榛,我們通過 Hook 拿到此變量,給此 Handler 設置一個 callback予跌,Handler 的 dispatchMessage 中會先處理 callback搏色。

try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Method currentAtyThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
            Object activityThread = currentAtyThreadMethod.invoke(null);

            Field mHField = activityThreadClass.getDeclaredField("mH");
            mHField.setAccessible(true);
            Handler handler = (Handler) mHField.get(activityThread);

            Field mCallbackField = Handler.class.getDeclaredField("mCallback");
            mCallbackField.setAccessible(true);
            mCallbackField.set(handler,new SpCompatCallback());
            Log.d(TAG,"hook success");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (Throwable e){
            e.printStackTrace();
        }

自定義callbak:SpCompatCallback,在這個方法中做清理等待鎖列表的操作:

public class SpCompatCallback implements Handler.Callback {


    public SpCompatCallback(){
    }

    //handleServiceArgs
    private static final int SERVICE_ARGS = 115;
    //handleStopService
    private static final int STOP_SERVICE = 116;
    //handleSleeping
    private static final int SLEEPING = 137;
    //handleStopActivity
    private static final int STOP_ACTIVITY_SHOW = 103;
    //handleStopActivity
    private static final int STOP_ACTIVITY_HIDE = 104;
    //handlePauseActivity
    private static final int PAUSE_ACTIVITY = 101;
    //handlePauseActivity
    private static final int PAUSE_ACTIVITY_FINISHING = 102;

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what){
            case SERVICE_ARGS:
                SpHelper.beforeSpBlock("SERVICE_ARGS");
                break;
            case STOP_SERVICE:
                SpHelper.beforeSpBlock("STOP_SERVICE");
                break;
            case SLEEPING:
                SpHelper.beforeSpBlock("SLEEPING");
                break;
            case STOP_ACTIVITY_SHOW:
                SpHelper.beforeSpBlock("STOP_ACTIVITY_SHOW");
                break;
            case STOP_ACTIVITY_HIDE:
                SpHelper.beforeSpBlock("STOP_ACTIVITY_HIDE");
                break;
            case PAUSE_ACTIVITY:
                SpHelper.beforeSpBlock("PAUSE_ACTIVITY");
                break;
            case PAUSE_ACTIVITY_FINISHING:
                SpHelper.beforeSpBlock("PAUSE_ACTIVITY_FINISHING");
                break;
            default:
                break;
        }
        return false;
    }
}

清理等待列表的操作:

public class SpHelper {
    private static final String TAG = "SpHelper";
    private static boolean init = false;
    private static String CLASS_QUEUED_WORK = "android.app.QueuedWork";
    private static String FIELD_PENDING_FINISHERS = "sPendingWorkFinishers";
    private static ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;

    public static void beforeSpBlock(String tag){
        if(!init){
            getPendingWorkFinishers();
            init = true;
        }
        Log.d(TAG,"beforeSpBlock "+tag);
        if(sPendingWorkFinishers != null){
            sPendingWorkFinishers.clear();
        }
    }

    private static void getPendingWorkFinishers() {
        Log.d(TAG,"getPendingWorkFinishers");
        try {
            Class clazz = Class.forName(CLASS_QUEUED_WORK);
            Field field = clazz.getDeclaredField(FIELD_PENDING_FINISHERS);
            field.setAccessible(true);
            sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);
            Log.d(TAG,"getPendingWorkFinishers success");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Throwable e){
            e.printStackTrace();
        }

    }
}

另外一種解決思路

濫用apply的情況下,會將任務堆積匕得,在后面造成anr;而在主線程調用commit,又會在提交時造成主線程的anr.那么可以將所有的sp提交都實現(xiàn)為子線程中調用commit,就避免了apply任務的堆積問題舰攒。

但這個方案帶來的副作用比清理等待鎖要更明顯:
1.系統(tǒng)apply是先同步更新緩存再異步寫文件嗜诀,調用方在同一線程內讀寫緩存是同步的,無需關心上下文數(shù)據(jù)讀寫同步問題

2.commit異步化之后直接在子線程中更新緩存再寫文件魂务,調用方需要關注上下文線程切換集币,異步有可能引發(fā)讀寫數(shù)據(jù)不一致問題

因此還是推薦用第一種方案

SP推薦實踐

1.在工作線程中寫入sp時考阱,直接調用commit就可以,不必調用apply,這種情況下鞠苟,commit的開銷更小
2.在主線程中寫入sp時乞榨,不要調用commit,要調用apply
3.sp對應的文件盡量不要太大当娱,按照模塊名稱去讀寫對應的sp文件吃既,而不是一個整個應用都讀寫一個sp文件
4.sp的適合讀寫輕量的、小的配置信息跨细,不適合保存大數(shù)據(jù)量的信息鹦倚,比如長串的json字符串。

  1. 當有連續(xù)的調用PutXxx方法操作時(特別是循環(huán)中)冀惭,當確認不需要立即讀取時震叙,最后一次調用commit或apply即可。

參考鏈接:

http://gityuan.com/2017/06/18/SharedPreferences/
https://mp.weixin.qq.com/s/IFgXvPdiEYDs5cDriApkxQ

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末散休,一起剝皮案震驚了整個濱河市媒楼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌戚丸,老刑警劉巖划址,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異限府,居然都是意外死亡夺颤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門谣殊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拂共,“玉大人,你說我怎么就攤上這事姻几∫撕” “怎么了势告?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抚恒。 經(jīng)常有香客問我咱台,道長,這世上最難降的妖魔是什么俭驮? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任回溺,我火速辦了婚禮,結果婚禮上混萝,老公的妹妹穿的比我還像新娘遗遵。我一直安慰自己,他們只是感情好逸嘀,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布车要。 她就那樣靜靜地躺著,像睡著了一般崭倘。 火紅的嫁衣襯著肌膚如雪翼岁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天司光,我揣著相機與錄音琅坡,去河邊找鬼。 笑死残家,一個胖子當著我的面吹牛榆俺,可吹牛的內容都是我干的。 我是一名探鬼主播跪削,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谴仙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了碾盐?” 一聲冷哼從身側響起晃跺,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毫玖,沒想到半個月后掀虎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡付枫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年烹玉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阐滩。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡二打,死狀恐怖,靈堂內的尸體忽然破棺而出掂榔,到底是詐尸還是另有隱情继效,我是刑警寧澤症杏,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站瑞信,受9級特大地震影響厉颤,放射性物質發(fā)生泄漏。R本人自食惡果不足惜凡简,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一逼友、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秤涩,春花似錦帜乞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽状植。三九已至浊竟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間津畸,已是汗流浹背振定。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肉拓,地道東北人后频。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像暖途,于是被迫代替她去往敵國和親卑惜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355