iOS的Crash捕獲和防護(hù)策略


題外話:近來工作閑暇之余把以前看的網(wǎng)易大神寫的crash防護(hù)手動實(shí)現(xiàn)了。紙上得來終覺淺崔兴,絕知此事要躬行华烟。記錄一下思路宿稀,大部分還是參考大神的經(jīng)驗(yàn)伴奥∠裨疲框架內(nèi)部可接入日志上報(bào)系統(tǒng)肃叶,結(jié)合服務(wù)端進(jìn)行收集夯辖。
Baymax:網(wǎng)易iOS App運(yùn)行時(shí)Crash自動防護(hù)實(shí)踐
自己實(shí)現(xiàn)的OCShield

代碼捕獲crash

Crash一般產(chǎn)生自 iOS 的微內(nèi)核 Mach蕴茴,然后在 BSD 層轉(zhuǎn)換成 UNIX SIGABRT 信號劝评,以標(biāo)準(zhǔn) POSIX 信號的形式提供給用戶。NSException 是使用者在處理 App 邏輯時(shí)倦淀,用編程的方法拋出蒋畜。


crash的捕獲的方式

1.Mach 異常與 Unix 信號

Mach 異常捕獲∽策矗基于Mach內(nèi)核編程姻成,需要對內(nèi)核有一定了解插龄。
Unix 信號捕獲。對于Mach 異常科展,操作系統(tǒng)會將其轉(zhuǎn)換為對應(yīng)的 Unix信號均牢,可以通過注冊signalHandler的方式來做信號異常。

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
Exception Subtype:      KERN_INVALID_ADDRESS at 0x041a6f3

Mach 異常是什么才睹?它又是如何與 Unix 信號建立聯(lián)系的徘跪?
Mach 是一個 XNU 的微內(nèi)核核心,Mach 異常是指最底層的內(nèi)核級異常琅攘,被定義在 <mach/exception_types.h>下 垮庐。每個 thread,task坞琴,host 都有一個異常端口數(shù)組哨查,Mach 的部分 API 暴露給了用戶態(tài),用戶態(tài)的開發(fā)者可以直接通過 Mach API 設(shè)置 thread剧辐,task寒亥,host 的異常端口,來捕獲 Mach 異常浙于,抓取 Crash 事件护盈。
所有 Mach 異常都在 host 層被ux_exception轉(zhuǎn)換為相應(yīng)的 Unix 信號,并通過threadsignal將信號投遞到出錯的線程羞酗。iOS 中的 POSIX API 就是通過 Mach 之上的 BSD 層實(shí)現(xiàn)的腐宋。
因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach 層的EXC_BAD_ACCESS異常檀轨,在 host 層被轉(zhuǎn)換成 SIGSEGV 信號投遞到出錯的線程胸竞。既然最終以信號的方式投遞到出錯的線程,那么就可以通過注冊 signalHandler 來捕獲信號:

signal(SIGSEGV,signalHandler);

捕獲 Mach 異巢翁眩或者 Unix 信號都可以抓到 crash 事件卫枝,這兩種方式哪個更好呢?優(yōu)選 Mach 異常讹挎,因?yàn)?Mach 異常處理會先于 Unix 信號處理發(fā)生校赤,如果 Mach 異常的 handler 讓程序 exit 了,那么 Unix 信號就永遠(yuǎn)不會到達(dá)這個進(jìn)程了筒溃。轉(zhuǎn)換 Unix 信號是為了兼容更為流行的 POSIX 標(biāo)準(zhǔn) (SUS 規(guī)范)马篮,這樣不必了解 Mach 內(nèi)核也可以通過 Unix 信號的方式來兼容開發(fā)。

因?yàn)橛布a(chǎn)生的信號 (通過 CPU 陷阱) 被 Mach 層捕獲怜奖,然后才轉(zhuǎn)換為對應(yīng)的 Unix 信號浑测;蘋果為了統(tǒng)一機(jī)制,于是操作系統(tǒng)和用戶產(chǎn)生的信號 (通過調(diào)用kill和pthread_kill) 也首先沉下來被轉(zhuǎn)換為 Mach 異常歪玲,再轉(zhuǎn)換為 Unix 信號迁央。

signal(SIGABRT, SignalExceptionHandler) 

2.NSException 捕獲掷匠。

應(yīng)用層,通過 NSUncaughtExceptionHandler捕獲岖圈,因?yàn)槎褩V胁粫谐鲥e代碼讹语,所以需要獲取NSException對象中的reason、name幅狮、callStackSymbols募强。然后把細(xì)節(jié)寫入Crash日志,上傳到后臺做數(shù)據(jù)分析崇摄。

NSSetUncaughtExceptionHandler(UncaughtExceptionHandler) //程序啟動代理方法

void UncaughtExceptionHandler(NSException *exception) {
      NSArray *callStack = [exception callStackSymbols];
      NSString *reason = [exception reason];
      NSString *name = [exception name];
      
      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
      [formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"];
      NSString * dateStr = [formatter stringFromDate:[NSDate date]];
      
      NSString * iOS_Version = [[UIDevice currentDevice] systemVersion];
      NSString * PhoneSize    =   NSStringFromCGSize([[UIScreen mainScreen] bounds].size);
      NSString * App_Version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
      NSString * iPhoneType = @"當(dāng)前設(shè)備名字";
      NSString *uploadString = @"所有拼接信息";
      // 存儲到本地沙盒.下次啟動找尋
    }

iOS的Crash分類

1.unrecognized selector crash【實(shí)現(xiàn)】
2.KVO/KVC crash【實(shí)現(xiàn)】
3.NSNotification crash
4.NSTimer crash【實(shí)現(xiàn)】
5.Container crash(數(shù)組越界擎值,插nil等)【實(shí)現(xiàn)】
6.NSString crash (字符串操作的crash)【實(shí)現(xiàn)】
7.Bad Access crash (野指針)【實(shí)現(xiàn)】
8.UI not on Main Thread Crash (非主線程刷UI(機(jī)制待改善))

Unrecognized Selector

調(diào)用方法時(shí)會轉(zhuǎn)換成objc_msgSend()函數(shù)調(diào)用。

1.首先逐抑,在相應(yīng)操作的對象中的緩存方法列表中找調(diào)用的方法鸠儿,如果找到,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)并執(zhí)行厕氨。
2.如果沒找到进每,在相應(yīng)操作的對象中的方法列表中找調(diào)用的方法,如果找到命斧,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)執(zhí)行田晚。
3.如果沒找到,去父類指針?biāo)赶虻膶ο笾袌?zhí)行1国葬,2贤徒。
4.以此類推,如果一直到根類還沒找到汇四,轉(zhuǎn)向攔截調(diào)用接奈,走消息轉(zhuǎn)發(fā)機(jī)制。
5.如果沒有重寫攔截調(diào)用的方法通孽,程序報(bào)錯序宦。

消息轉(zhuǎn)發(fā)流程

1.調(diào)用resolveInstanceMethod給個機(jī)會讓類添加這個實(shí)現(xiàn)這個函數(shù)。
2.調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)背苦。
3.調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行互捌。

基于此,我選擇2行剂、3都去實(shí)現(xiàn)對比方案疫剃。
方案一:重寫NSObject的forwardingTargetForSelector方法。雖然不會造成NSInvocation對象的開銷硼讽,但是會攔截到系統(tǒng)的其他方法,導(dǎo)致該方法調(diào)用多次問題牲阁。

1.動態(tài)創(chuàng)建一個樁類固阁。
2.動態(tài)為樁類添加對應(yīng)的Selector壤躲,用一個通用的返回0的函數(shù)來實(shí)現(xiàn)該SEL的IMP。
3.將消息直接轉(zhuǎn)發(fā)到這個樁類對象上备燃。

流程圖

方案二:與方案一思路類似碉克,hook NSObject的methodSignatureForSelectorforwardInvocation,雖然頻繁創(chuàng)建NSInvocation對象并齐,但是到了這里已經(jīng)過濾掉系統(tǒng)的方法漏麦。

KVO類型

kvo一般crash原因是

1.KVO的被觀察者dealloc時(shí)仍然注冊著KVO。
2.添加KVO重復(fù)添加觀察者或重復(fù)移除觀察者(KVO注冊觀察者與移除觀察者不匹配)况褪。

基于管理混亂問題撕贞,可以讓被觀察對象持有一個KVO的delegate,所有和KVO相關(guān)的操作均通過delegate來進(jìn)行管理测垛,delegate通過建立一張map來維護(hù)KVO整個關(guān)系捏膨。

hook addObserver:方法

通過上面的流程,將observerd對象的所有kvo相關(guān)的observer信息全部轉(zhuǎn)移到KVOdelegate上食侮,并且避免了相同kvoinfo被重復(fù)添加多次的可能性号涯。

hook removeObserver:方法

移除一個keypath的Observer時(shí),當(dāng)delegate的kvoInfoMap中找不到key為該keypath的時(shí)候锯七,說明此時(shí)delegate并沒有持有對應(yīng)keypath的observer链快,即說明移除了一個不匹配的觀察者,此時(shí)如果再繼續(xù)操作會導(dǎo)致app崩潰眉尸,所以應(yīng)該及時(shí)中斷流程域蜗,然后統(tǒng)計(jì)異常信息。
當(dāng)keypath對應(yīng)的KVOInfo列表(infoArray)為空的時(shí)候效五,說明此時(shí)delegate已經(jīng)不再持有任何和keypath相關(guān)的observer了地消。這時(shí)應(yīng)該調(diào)用原有removeObserver的方法將delegate對應(yīng)的觀察者移除。
注意到在檢查遍歷infoArray的時(shí)侯畏妖,除了要刪除對應(yīng)的info信息脉执,還多了一步檢查info.observer == nil的過程,是因?yàn)槿绻鹢bserver為nil戒劫,那么此時(shí)如果keypath對應(yīng)的值變化的話半夷,也會因?yàn)檎也坏給bserver而崩潰,所以需要做這一步來阻止該種情況的發(fā)生迅细。

hook observeValueForKeyPath:方法


delegate對于observeValueForKeyPath方法的修改最主要的地方是巫橄,在于將對應(yīng)的響應(yīng)方法轉(zhuǎn)移給真正的KVO Observer,通過keyInfoMap找到keypath對應(yīng)的KVOInfo里面預(yù)先存儲好的observer茵典,然后調(diào)用observer原本的響應(yīng)方法湘换。
同時(shí)在遍歷InfoArray的時(shí)候,發(fā)現(xiàn)info.observerw == nil的時(shí)候,需要及時(shí)將其清除掉彩倚,避免KVO的觀察者observer被釋放后value變化導(dǎo)致的crash.
最后筹我,針對 KVO的被觀察者dealloc時(shí)仍然注冊著KVO導(dǎo)致的crash 的情況,可以將NSObject的dealloc swizzle帆离, 在object dealloc的時(shí)候自動將其對應(yīng)的kvodelegate所有和kvo相關(guān)的數(shù)據(jù)清空蔬蕊,然后將kvodelegate也置空。避免出現(xiàn)KVO的被觀察者dealloc時(shí)仍然注冊著KVO而產(chǎn)生的crash哥谷。

KVC類型

hook常用的方法岸夯,用Try catch方式守護(hù)。


NSNotification類型

主要針對iOS9系統(tǒng)之前不移除通知们妥。蘋果在iOS9之后專門針對于這種情況做了處理猜扮,所以在iOS9之后,即使開發(fā)者沒有移除observer王悍,Notification crash也不會再產(chǎn)生了破镰。
hook NSObject的dealloc函數(shù),在對象真正dealloc之前先調(diào)用一下
[[NSNotificationCenter defaultCenter] removeObserver:self]即可压储。
注意到并不是所有的對象都需要做以上的操作鲜漩,如果一個對象從來沒有被NSNotificationCenter 添加為observer的話,在其dealloc之前調(diào)用removeObserver完全是多此一舉集惋。 所以我們hook了NSNotificationCenter的addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject函數(shù)孕似,在其添加observer的時(shí)候,對observer動態(tài)添加標(biāo)記flag刮刑。這樣在observer dealloc的時(shí)候喉祭,就可以通過flag標(biāo)記來判斷其是否有必要調(diào)用removeObserver函數(shù)了。

NSTimer類型

使用NSTimer的scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重復(fù)性的定時(shí)任務(wù)時(shí)存在一個問題:NSTimer會強(qiáng)引用target實(shí)例雷绢,所以需要在合適的時(shí)機(jī)invalidate定時(shí)器泛烙,否則就會由于定時(shí)器timer強(qiáng)引用target的關(guān)系導(dǎo)致target不能被釋放,造成內(nèi)存泄露翘紊,甚至在定時(shí)任務(wù)觸發(fā)時(shí)導(dǎo)致crash蔽氨。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關(guān)。與此同時(shí)帆疟,如果NSTimer是無限重復(fù)的執(zhí)行一個任務(wù)的話鹉究,也有可能導(dǎo)致target的selector一直被重復(fù)調(diào)用且處于無效狀態(tài),對app的CPU踪宠,內(nèi)存等性能方面均是沒有必要的浪費(fèi)自赔。

swizzle NSTimer的scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:相關(guān)的方法


在新方法中動態(tài)創(chuàng)建stubTarget對象,stubTarget對象弱引用持有原有的target柳琢,selector绍妨,timer润脸,targetClass等properties。然后將原target分發(fā)stubTarget上他去,selector回調(diào)函數(shù)為stubTarget的fireProxyTimer津函。

通過stubTarget的fireProxyTimer:來具體處理回調(diào)函數(shù)selector的處理和分發(fā)


當(dāng)NSTimer的回調(diào)函數(shù)fireProxyTimer:被執(zhí)行的時(shí)候,會自動判斷原target是否已經(jīng)被釋放孤页,如果釋放了,意味著NSTimer已經(jīng)無效涩馆,此時(shí)如果還繼續(xù)調(diào)用原有target的selector很有可能會導(dǎo)致crash行施,而且是沒有必要的。所以此時(shí)需要將NSTimer invalidate魂那,然后統(tǒng)計(jì)上報(bào)錯誤數(shù)據(jù)蛾号。如此一來就做到了NSTimer在合適的時(shí)機(jī)自動invalidate。

Container類型

針對于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的會導(dǎo)致崩潰的API進(jìn)行method swizzling涯雅,然后在swizzle的新方法中加入一些條件限制和判斷鲜结,從而讓這些API變的安全。

NSString類型

NSString/NSMutableString 類型的crash的產(chǎn)生原因和防護(hù)方案與Container crash很相像活逆。

野指針類型

method swizzling替換NSObject的allocWithZone方法

在新的方法中判斷該類型對象是否需要加入野指針防護(hù)精刷,如果需要,則通過objc_setAssociatedObject為該對象設(shè)置flag標(biāo)記蔗候,被標(biāo)記的對象后續(xù)會進(jìn)入zombie流程怒允。


做flag標(biāo)記是因?yàn)楹芏嘞到y(tǒng)類,比如NSString锈遥,UIView等創(chuàng)建纫事,釋放非常頻繁,而這些實(shí)例發(fā)生野指針概率非常低所灸±龌蹋基本都是我們自己寫的類才會有野指針的相關(guān)問題,所以通過在創(chuàng)建時(shí)爬立,設(shè)置一個標(biāo)記用來過濾不必要做野指針防護(hù)的實(shí)例钾唬,提高方案的效率。
同時(shí)做判斷是否要加入標(biāo)記的條件里面懦尝,我們加入了黑名單機(jī)制知纷,是因?yàn)橐恍┨囟ǖ念愂遣贿m用于添加到zombie機(jī)制的,會發(fā)生崩潰(例如:NSBundle)陵霉,而且所以和zombie機(jī)制相關(guān)的類也不能加入標(biāo)記琅轧,否則會在釋放過程中循環(huán)引用和調(diào)用,導(dǎo)致內(nèi)存泄漏甚至棧溢出踊挠。

method swizzling替換NSObject的dealloc方法

對flag標(biāo)記的對象實(shí)例調(diào)用objc_destructInstance乍桂,釋放該實(shí)例引用的相關(guān)屬性冲杀,然后將實(shí)例的isa修改為ShieldZombieObject。通過objc_setAssociatedObject 保存將原始類名保存在該實(shí)例中睹酌。


dealloc最后會調(diào)到objectdispose函數(shù)权谁,在這個函數(shù)里面其實(shí)也做了三件事情。

1.調(diào)用objc_destructInstance釋放該實(shí)例引用的相關(guān)實(shí)例憋沿。
2.將該實(shí)例的isa修改為stubClass旺芽,接受任意方法調(diào)用。
3.釋放該內(nèi)存辐啄。

在ShieldZombieSub 通過消息轉(zhuǎn)發(fā)機(jī)制forwardingTargetForSelector處理所有攔截的方法

根據(jù)selector動態(tài)添加能夠處理方法的響應(yīng)者ShieldZombieSub 實(shí)例采章,然后通過 objc_getAssociatedObject 獲取之前保存該實(shí)例對應(yīng)的原始類名,統(tǒng)計(jì)錯誤數(shù)據(jù)壶辜。

當(dāng)退到后臺或者達(dá)到未釋放實(shí)例的上限時(shí)悯舟,則調(diào)用free函數(shù)釋被引用zombie化的實(shí)例。

注:
1.做了野指針防護(hù)砸民,通過動態(tài)插入一個空實(shí)現(xiàn)的方法來防止出現(xiàn)Crash抵怎,但是業(yè)務(wù)層面的表現(xiàn)難以確定,可能會進(jìn)入業(yè)務(wù)異常的狀態(tài)岭参。需要擬定一下如何展現(xiàn)該問題給用戶的方案反惕。
2.由于做了延時(shí)釋放若干實(shí)例,對系統(tǒng)總內(nèi)存會產(chǎn)生一定影響冗荸,目前將內(nèi)存的緩沖區(qū)開到5M左右承璃,所以應(yīng)該沒有很大的影響,但還是可能潛在一些風(fēng)險(xiǎn)蚌本。
3.延時(shí)釋放實(shí)例是根據(jù)相關(guān)功能代碼會聚焦在某一個時(shí)間段調(diào)用的假設(shè)前提下盔粹,所以野指針的zombie保護(hù)機(jī)制只能在其實(shí)例對象仍然緩存在zombie的緩存機(jī)制時(shí)才有效,若在實(shí)例真正釋放之后程癌,再調(diào)用野指針還是會出現(xiàn)crash,所以不能達(dá)到真正防止crash的目的舷嗡。

據(jù)面試阿里的面試官說可以用計(jì)算內(nèi)存堆棧信息的方式,作者表示不理解嵌莉。

非主線程刷UI類型

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
在這三個方法調(diào)用的時(shí)候判斷一下當(dāng)前的線程进萄,如果不是主線程的話,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //調(diào)用原本方法 });
來將對應(yīng)的刷UI的操作轉(zhuǎn)移到主線程上锐峭,同時(shí)統(tǒng)計(jì)錯誤信息中鼠。
但是真正實(shí)施了之后,發(fā)現(xiàn)這三個方法并不能完全覆蓋UIView相關(guān)的所有刷UI到操作沿癞,但是如果要將全部到UIView的刷UI的方法統(tǒng)計(jì)起來并且swizzle援雇,感覺略笨拙而且不高效。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椎扬,一起剝皮案震驚了整個濱河市惫搏,隨后出現(xiàn)的幾起案子具温,更是在濱河造成了極大的恐慌,老刑警劉巖筐赔,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铣猩,死亡現(xiàn)場離奇詭異,居然都是意外死亡茴丰,警方通過查閱死者的電腦和手機(jī)达皿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贿肩,“玉大人鳞绕,你說我怎么就攤上這事∈” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵萄焦,是天一觀的道長控轿。 經(jīng)常有香客問我,道長拂封,這世上最難降的妖魔是什么茬射? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮冒签,結(jié)果婚禮上在抛,老公的妹妹穿的比我還像新娘。我一直安慰自己萧恕,他們只是感情好刚梭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著票唆,像睡著了一般朴读。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上走趋,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天衅金,我揣著相機(jī)與錄音,去河邊找鬼簿煌。 笑死氮唯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的姨伟。 我是一名探鬼主播惩琉,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼授滓!你這毒婦竟也來了琳水?” 一聲冷哼從身側(cè)響起肆糕,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎在孝,沒想到半個月后诚啃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡私沮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年始赎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仔燕。...
    茶點(diǎn)故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡造垛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晰搀,到底是詐尸還是另有隱情五辽,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布外恕,位于F島的核電站杆逗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鳞疲。R本人自食惡果不足惜罪郊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尚洽。 院中可真熱鬧悔橄,春花似錦、人聲如沸腺毫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潮酒。三九已至争舞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澈灼,已是汗流浹背竞川。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叁熔,地道東北人委乌。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像荣回,于是被迫代替她去往敵國和親遭贸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評論 2 354