知識點3

26. 什么是KVC和KVO?

KVO:

iOS開發-KVO的奧秘

http://www.lxweimin.com/p/742b4b248da9

iOS KVO(鍵值觀察) 總覽

http://www.lxweimin.com/p/7e0ddc5f4e78

iOS--KVO的實現原理與具體應用

http://www.cnblogs.com/azuo/p/5442319.html

KVC:

iOS開發技巧系列---詳解KVC(我告訴你KVC的一切)

http://www.lxweimin.com/p/45cbd324ea65

【iOS】KVC 和 KVO 的使用場景

http://blog.csdn.net/chenglibin1988/article/details/38259865

KVC(Key-Value-Coding):鍵 - 值編碼是一種通過字符串間接訪問對象的方式。

而不是通過調用setter方法或通過實例變量訪問的機制。很多情況下可以簡化程序代碼。

例如:

@interface MeiLing:NSObject

@property NSString *name;

@property UILabel *label;

@end

對于name的賦值 可以使用 meiLing.name = @"笑玲"; 這是點語法。調用的是setName方法。

KVC的寫法是? [meiLing setValue:@"夢玲" forKey:@"name"];? 通過name字符串賦值。

當然也可以跨層賦值,例如為label的text屬性賦值

點語法: meiLing.label.text = @"笑玲";

KVC: [meiLing setValue:@"夢玲" forKeyPath:@"label.text"];

KVO:鍵值觀察機制,他提供了觀察某一屬性變化的方法,極大的簡化了代碼。

KVO 只能被 KVC觸發, 包括使用setValue:forKey:方法 和 點語法。

通過下方方法為屬性添加KVO觀察

- (void)addObserver:(NSObject *)observer

forKeyPath:(NSString *)keyPath

options:(NSKeyValueObservingOptions)options

context:(nullable void *)context;

當被觀察的屬性發生變化時,會自動觸發下方方法

- (void)observeValueForKeyPath:(NSString *)keyPath

ofObject:(id)object

change:(NSDictionary *)change

context:(void *)context

41. 不手動指定autoreleasepool的前提下,一個autorealese對象在什么時刻釋放?(比如在一個vc的viewDidLoad中創建)

分兩種情況:手動干預釋放時機、系統自動去釋放。

手動干預釋放時機--指定autoreleasepool 就是所謂的:當前作用域大括號結束時釋放。

系統自動去釋放--不手動指定autoreleasepool

Autorelease對象會在當前的 runloop 迭代結束時釋放。

如果在一個vc的viewDidLoad中創建一個 Autorelease對象,

那么該對象會在 viewDidAppear 方法執行前就被銷毀了。

不手動指定autoreleasepool的前提下,一個autorealese對象在什么時刻釋放?(比如在一個vc的viewDidLoad中創建)

分兩種情況:手動干預釋放時機、系統自動去釋放。

1.手動干預釋放時機--指定autoreleasepool 就是所謂的:當前作用域大括號結束時釋放。

系統自動去釋放--不手動指定autoreleasepool

2.Autorelease對象出了作用域之后,會被添加到最近一次創建的自動釋放池中,并會在當前的 runloop 迭代結束時釋放。

釋放的時機總結起來,可以用下圖來表示:



下面對這張圖進行詳細的解釋:

從程序啟動到加載完成是一個完整的運行循環,然后會停下來,等待用戶交互,用戶的每一次交互都會啟動一次運行循環,來處理用戶所有的點擊事件、觸摸事件。

我們都是知道:所有 autorelease 的對象,在出了作用域之后,會被自動添加到最近創建的自動釋放池中。

但是如果每次都放進應用程序的 main.m

中的 autoreleasepool 中,遲早有被撐滿的一刻。這個過程中必定有一個釋放的動作。何時?

在一次完整的運行循環結束之前,會被銷毀。

那什么時間會創建自動釋放池?運行循環檢測到事件并啟動后,就會創建自動釋放池。

子線程的 runloop 默認是不工作,無法主動創建,必須手動創建。

自定義的 NSOperation 和 NSThread 需要手動創建自動釋放池。比如: 自定義的 NSOperation 類中的 main 方法里就必須添加自動釋放池。否則出了作用域后,自動釋放對象會因為沒有自動釋放池去處理它,而造成內存泄露。

但對于 blockOperation 和 invocationOperation 這種默認的Operation ,系統已經幫我們封裝好了,不需要手動創建自動釋放池。

@autoreleasepool 當自動釋放池被銷毀或者耗盡時,會向自動釋放池中的所有對象發送 release 消息,釋放自動釋放池中的所有對象。

如果在一個vc的viewDidLoad中創建一個 Autorelease對象,那么該對象會在 viewDidAppear 方法執行前就被銷毀了。

參考鏈接:《黑幕背后的Autorelease》

這個問題拿來做面試題,問過很多人,沒有幾個能答對的。很多答案都是“當前作用域大括號結束時釋放”,顯然木有正確理解Autorelease機制。在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop

迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop

__weak idreference =nil;

- (void)viewDidLoad {?

[superviewDidLoad];

NSString*str = [NSStringstringWithFormat:@"sunnyxx"];

// str是一個autorelease對象,設置一個weak的引用來觀察它

reference = str;

}

- (void)viewWillAppear:(BOOL)animated {

?[superviewWillAppear:animated];

NSLog(@"%@", reference);

// Console: sunnyxx

}

- (void)viewDidAppear:(BOOL)animated {

?[superviewDidAppear:animated];

NSLog(@"%@", reference);

// Console: (null)

}

由于這個vc在loadView之后便add到了window層級上,所以viewDidLoad和viewWillAppear是在同一個runloop調用的,因此在viewWillAppear中,這個autorelease的變量依然有值。

當然,我們也可以手動干預Autorelease對象的釋放時機:

- (void)viewDidLoad{

?[superviewDidLoad];

@autoreleasepool{

NSString*str = [NSStringstringWithFormat:@"sunnyxx"];?

}

NSLog(@"%@", str);// Console: (null)}


我是前言

Autorelease機制是iOS開發者管理對象內存的好伙伴,MRC中,調用[obj autorelease]來延遲內存的釋放是一件簡單自然的事,ARC下,我們甚至可以完全不知道Autorelease就能管理好內存。而在這背后,objc和編譯器都幫我們做了哪些事呢,它們是如何協作來正確管理內存的呢?刨根問底,一起來探究下黑幕背后的Autorelease機制。

Autorelease對象什么時候釋放?

這個問題拿來做面試題,問過很多人,沒有幾個能答對的。很多答案都是“當前作用域大括號結束時釋放”,顯然木有正確理解Autorelease機制。

在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop

小實驗

__weak id reference = nil;

- (void)viewDidLoad {

[super viewDidLoad];

NSString *str = [NSString stringWithFormat:@"sunnyxx"];

// str是一個autorelease對象,設置一個weak的引用來觀察它

reference = str;

}

- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];

NSLog(@"%@", reference); // Console: sunnyxx

}

- (void)viewDidAppear:(BOOL)animated {

[super viewDidAppear:animated];

NSLog(@"%@", reference); // Console: (null)

}

由于這個vc在loadView之后便add到了window層級上,所以viewDidLoad和viewWillAppear是在同一個runloop調用的,因此在viewWillAppear中,這個autorelease的變量依然有值。

當然,我們也可以手動干預Autorelease對象的釋放時機:

- (void)viewDidLoad {

[super viewDidLoad];

@autoreleasepool {

NSString *str = [NSString stringWithFormat:@"sunnyxx"];

}

NSLog(@"%@", str); // Console: (null)

}

Autorelease原理

AutoreleasePoolPage

ARC下,我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨后編譯器將其改寫成下面的樣子:

void *context = objc_autoreleasePoolPush();

// {}中的代碼

objc_autoreleasePoolPop(context);

而這兩個函數都是對AutoreleasePoolPage的簡單封裝,所以自動釋放機制的核心就在于這個類。

AutoreleasePoolPage是一個C++實現的類


AutoreleasePool并沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應結構中的parent指針和child指針)

AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)

AutoreleasePoolPage每個對象會開辟4096字節內存(也就是虛擬內存一頁的大小),除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址

上面的id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置

一個AutoreleasePoolPage的空間被占滿時,會新建一個AutoreleasePoolPage對象,連接鏈表,后來的autorelease對象在新的page加入

所以,若當前線程中只有一個AutoreleasePoolPage對象,并記錄了很多autorelease對象地址時內存如下圖:


圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page對象,與這一頁鏈表連接完成后,新page的next指針被初始化在棧底(begin的位置),然后繼續向棧頂添加新對象。

所以,向一個對象發送- autorelease消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置

釋放時刻

每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象,值為0(也就是個nil),那么這一個page就變成了下面的樣子:


objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作為入參,于是:

根據傳入的哨兵對象地址找到哨兵對象所處的page

在當前page中,將晚于哨兵對象插入的所有autorelease對象都發送一次- release消息,并向回移動next指針到正確位置

補充2:從最新加入的對象一直向前清理,可以向前跨越若干個page,直到哨兵所在的page

剛才的objc_autoreleasePoolPop執行后,最終變成了下面的樣子:


嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常簡單了,pop的時候總會釋放到上次push的位置為止,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣,每次一層,互不影響。

【附加內容】

Autorelease返回值的快速釋放機制

值得一提的是,ARC下,runtime有一套對autorelease返回值的優化策略。

比如一個工廠方法:

+ (instancetype)createSark {

return [self new];

}

// caller

Sark *sark = [Sark createSark];

秉著誰創建誰釋放的原則,返回值需要是一個autorelease對象才能配合調用方正確管理內存,于是乎編譯器改寫成了形如下面的代碼:

+ (instancetype)createSark {

id tmp = [self new];

return objc_autoreleaseReturnValue(tmp); // 代替我們調用autorelease

}

// caller

id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調用retain

Sark *sark = tmp;

objc_storeStrong(&sark, nil); // 相當于代替我們調用了release

一切看上去都很好,不過既然編譯器知道了這么多信息,干嘛還要勞煩autorelease這個開銷不小的機制呢?于是乎,runtime使用了一些黑魔法將這個問題解決了。

黑魔法之Thread Local Storage

Thread Local Storage(TLS)線程局部存儲,目的很簡單,將一塊內存作為某個線程專有的存儲,以key-value的形式進行讀寫,比如在非arm架構下,使用pthread提供的方法實現:

void* pthread_getspecific(pthread_key_t);

int pthread_setspecific(pthread_key_t , const void *);

說它是黑魔法可能被懂pthread的笑話- -

在返回值身上調用objc_autoreleaseReturnValue方法時,runtime將這個返回值object儲存在TLS中,然后直接返回這個object(不調用autorelease);同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue里,發現TLS中正好存了這個對象,那么直接返回這個object(不調用retain)。

于是乎,調用方和被調方利用TLS做中轉,很有默契的免去了對返回值的內存管理。

于是問題又來了,假如被調方和主調方只有一邊是ARC環境編譯的該咋辦?(比如我們在ARC環境下用了非ARC編譯的第三方庫,或者反之)

只能動用更高級的黑魔法。

黑魔法之__builtin_return_address

這個內建函數原型是char *__builtin_return_address(int level),作用是得到函數的返回地址,參數表示層數,如__builtin_return_address(0)表示當前函數體返回地址,傳1是調用這個函數的外層函數的返回值地址,以此類推。

- (int)foo {

NSLog(@"%p", __builtin_return_address(0)); // 根據這個地址能找到下面ret的地址

return 1;

}

// caller

int ret = [sark foo];

看上去也沒啥厲害的,不過要知道,函數的返回值地址,也就對應著調用者結束這次調用的地址(或者相差某個固定的偏移量,根據編譯器決定)

也就是說,被調用的函數也有翻身做地主的機會了,可以反過來對主調方干點壞事。

回到上面的問題,如果一個函數返回前知道調用方是ARC還是非ARC,就有機會對于不同情況做不同的處理

黑魔法之反查匯編指令

通過上面的__builtin_return_address加某些偏移量,被調方可以定位到主調方在返回值后面的匯編指令:

// caller

int ret = [sark foo];

// 內存中接下來的匯編指令(x86,我不懂匯編,瞎寫的)

movq ??? ???

callq ???

而這些匯編指令在內存中的值是固定的,比如movq對應著0x48。

于是乎,就有了下面的這個函數,入參是調用方__builtin_return_address傳入值

static bool callerAcceptsFastAutorelease(const void * const ra0) {

const uint8_t *ra1 = (const uint8_t *)ra0;

const uint16_t *ra2;

const uint32_t *ra4 = (const uint32_t *)ra1;

const void **sym;

// 48 89 c7? ? movq? %rax,%rdi

// e8? ? ? ? ? callq symbol

if (*ra4 != 0xe8c78948) {

return false;

}

ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l;

ra2 = (const uint16_t *)ra1;

// ff 25? ? ? jmpq *symbol@DYLDMAGIC(%rip)

if (*ra2 != 0x25ff) {

return false;

}

ra1 += 6l + (long)*(const int32_t *)(ra1 + 2);

sym = (const void **)ra1;

if (*sym != objc_retainAutoreleasedReturnValue)

{

return false;

}

return true;

}

它檢驗了主調方在返回值之后是否緊接著調用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC環境,反之就走沒被優化的老邏輯。

其他Autorelease相關知識點

使用容器的block版本的枚舉器時,內部會自動添加一個AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

// 這里被一個局部@autoreleasepool包圍著

}];

當然,在普通for循環和for in循環中沒有,所以,還是新版的block版本枚舉器更加方便。for循環中遍歷產生大量autorelease變量時,就需要手加局部AutoreleasePool咯。

黑幕背后的Autorelease

http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

42. 使用block時什么情況會發生引用循環,如何解決?

一個對象中強引用了block,在block中又使用了該對象,就會發射循環引用。

解決方法是將該對象使用__weak或者__block修飾符修飾之后再在block中使用。

id weak weakSelf = self;

或者

__weak __typeof(&*self)weakSelf = self該方法可以設置為宏,便于使用

例如

#define WS(weakSelf)? __weak __typeof(&*self)weakSelf = self;

WS(weakSelf)

[self.tableView addHeaderWithCallback:^{

[weakself requestMemberList];

}];

如果要在block內部改變外部變量的值,則需要如下定義

id __block weakSelf = self;

43. 以下代碼運行結果如何?

-(void)viewDidLoad

{

[super viewDidLoad];

NSLog(@"1");

dispatch_sync(dispatch_get_main_queue(), ^{

NSLog(@"2");

});

NSLog(@"3");

}

答:

發生主線程鎖死。程序出現假死狀態.

//死鎖原因

//1:dispatch_sync在等待block語句執行完成,而block語句需要在主線程里執行,所以dispatch_sync如果在主線程調用就會造成死鎖

//2:dispatch_sync是同步的,本身就會阻塞當前線程,也即主線程。而又往主線程里塞進去一個block,所以就會發生死鎖。

//});

//dispatch_async(dispatch_get_global_queue(), ^{

//async 在主線程中 創建了一個異步線程 加入 全局并發隊列,async 不會等待block 執行完成,立即返回

NSLog(@2);//不會造成死鎖;

});

}

分析這段代碼:view DidLoad 在主線程中,也即dispatch_get_main_queue()中,執行到sync時向dispatch_get_main_queue()插入同步thread,sync會等到后面的block執行完成才返回。sync又在主隊列里面,是個串行隊列,sync是后面才加入的,前面一個是主線程,所以sync想執行block必須等待前一個主線程執行完成,而主線程卻在等待sync返回,去執行后續工作,從而造成死鎖。

2:

dispatch_sync 和 dispatch_async 區別:

dispatch_async(queue,block) async 異步隊列,dispatch_async 函數會立即返回, block會在后臺異步執行。

dispatch_sync(queue,block) sync 同步隊列,dispatch_sync 函數不會立即返回,即阻塞當前線程,等待 block同步執行完成。

44. 若一個類有實例變量 NSString *_foo ,調用setValue:forKey:時,可以以foo還是 _foo 作為key?

都可以。這個考察了KVC機制。

45. 什么情況使用 weak 關鍵字,相比 assign 有什么不同?

什么情況使用 weak 關鍵字?

1)在ARC中,在有可能出現循環引用的時候,往往要通過讓其中一端使用weak來解決,

比如:delegate代理屬性

2)自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用weak,

自定義IBOutlet控件屬性一般也使用weak;當然,也可以使用strong。

不同點:

1)weak 此特質表明該屬性定義了一種“非擁有關系” (nonowning relationship)。

為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似,

然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。

而 assign 的“設置方法”只會執行針對“純量類型”

(scalar type,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作。

2)assign 可以用非OC對象,而weak必須用于OC對象

46.怎么用 copy 關鍵字?

1) 用@property聲明 NSString、NSArray、NSDictionary 經常使用copy關鍵字,

是因為他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,

他們之間可能進行賦值操作,為確保對象中的字符串值不會無意間變動,應該在設置新屬性值時拷貝一份。

不過通常為了節省系統資源,選擇使用strong代替copy

2)block也經常使用copy關鍵字, 不過使用strong也無傷大雅.

47. @protocol 和 category 中如何使用 @property

1)在protocol中使用property只會生成setter和getter方法聲明,我們使用屬性的目的,

是希望遵守我協議的對象能實現該屬性

2)category 使用 @property 也是只會生成setter和getter方法的聲明,

如果我們真的需要給category增加屬性的實現,需要借助于運行時的兩個runtime函數:

①objc_setAssociatedObject

②objc_getAssociatedObject

48. @synthesize和@dynamic分別有什么作用?

1)@property有兩個對應的詞,一個是@synthesize,一個是@dynamic。

如果@synthesize和@dynamic都沒寫,那么默認的就是@synthesize var = _var;

2)@synthesize的語義是如果你沒有手動實現setter方法和getter方法,

那么編譯器會自動為你加上這兩個方法。

3)@dynamic告訴編譯器:屬性的setter與getter方法由用戶自己實現,不自動生成。(

當然對于readonly的屬性只需提供getter即可)。假如一個屬性被聲明為@dynamic var,

然后你沒有提供@setter方法和@getter方法,編譯的時候沒問題,

但是當程序運行到instance.var = someVar,由于缺setter方法會導致程序崩潰;

或者當運行到 someVar = var時,由于缺getter方法同樣會導致崩潰。

編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定。

49. ARC下,不顯式指定任何屬性關鍵字時,默認的關鍵字都有哪些?

對應基本數據類型默認關鍵字是

atomic,readwrite,assign

對于普通的OC對象

atomic,readwrite,strong

MRC:手動內存釋放。遵循誰申請誰釋放的原則,需要手動的處理內存計數的增加和修改。從12年開始,逐步被ARC(自動內存釋放)模式取代。 ?

點語法: “self.屬性 = obj” 調用屬性的setter方法。”self.屬性” 調用屬性的getter方法區別在于是否有等號 。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容