深復制、淺復制、copy、mutableCopy

1. 屬性中copystrong特性的區別

在開始學習淺復制(Shallow Copy)、深復制(Deep Copy)之前,先了解下屬性中copystrong特性的區別。

copy特性如下:

  • copy:創建一個對象的副本。在創建的那一刻新對象與原始對象內容相同。
  • 新的對象引用計數為1,與原始對象引用計數無關,且原始對象引用計數不會改變。
  • 使用copy創建的新對象也是強引用,使用完成后需要負責釋放該對象。
  • copy特性可以減少對象對上下文的依賴。新對象、原始對象中任一對象的值改變不會影響另一對象的值。
  • 要想設置該對象的特性為copy,該對象必須遵守NSCopying協議,Foundation類默認實現了NSCopying協議,所以只需要為自定義的類實現該協議即可。

strong特性如下:

  • 創建一個強引用的指針,引用對象引用計數加1。
  • strong特性表示兩個對象內存地址相同(建立一個指針,進行指針拷貝),內容會一直保持相同,直到更改一方內存地址,或將其設置為nil
  • 如果有多個對象同時引用一個屬性,任一對象對該屬性的修改都會影響其他對象獲取的值。

如果想要對屬性中特性進行更全面了解,可以查看 iOS中定義屬性時的atomic、nonatomic、copy、assign、strong、weak等幾個特性的區別 這篇文章。

2. 淺復制與深復制

對象的拷貝有淺復制和深復制兩種方式。淺復制只復制指向對象的指針,并不復制對象本身;深復制是直接復制整個對象到另一塊內存中。即淺復制是復制指針,深復制是復制內容。

NSObject提供了copymutableCopy 方法,copy復制后對象是不可變對象(immutable),mutableCopy復制后的對象是可變對象(mutable),與原始對象是否可變無關。

下面針對非集合類、集合類對象的深復制、淺復制進行說明。

2.1 非集合類對象的copymutableCopy

非集合類對象指的是NSStringNSNumber之類的對象,深復制會復制引用對象的內容,而淺復制只復制引用這些對象的指針。因此,如果對象A被淺復制到對象B,對象B和對象A引用的是同一內存地址的實例變量或屬性。

CopyObjectCopying.png

2.1.1 不可變對象的copymutableCopy

創建一個Single View Application模板的demo,demo名稱為copy&mutableCopy。進入ViewController.m,在實現部分添加以下方法。

// 非容器類 不可變對象
- (void)immutableObject {
    // 1.創建一個string字符串。
    NSString *string = @"github.com/pro648";
    NSString *stringB = string;
    NSString *stringCopy = [string copy];
    NSMutableString *stringMutableCopy = [string mutableCopy];
    
    // 2.輸出指針指向的內存地址。
    NSLog(@"Memory location of string = %p",string);
    NSLog(@"Memory location of stringB = %p",stringB);
    NSLog(@"Memory location of stringCopy = %p",stringCopy);
    NSLog(@"Memory location of stringMutableCopy = %p",stringMutableCopy);
}

上述代碼分步說明如下:

  1. 創建一個string字符串,之后通過賦值,調用copymutableCopy方法進行復制操作。
  2. 通過使用%p,輸出指針所指向內容的內存地址。

然后在viewDidLoad中調用該方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1.非容器類 不可變對象
    [self immutableObject];
}

運行demo,可以看到控制臺輸出如下:

Memory location of string = 0x1060e6068
Memory location of stringB = 0x1060e6068
Memory location of stringCopy = 0x1060e6068
Memory location of stringMutableCopy = 0x600000072000

可以看到,stringstringBstringCopy內存地址一致,即指向的是同一塊內存區域,進行了淺復制操作。而stringMutableCopy與另外三個變量內存地址不同,系統為其分配了新內存,即進行了深復制操作。

2.1.2 可變對象的copymutableCopy

繼續在實現部分添加以下方法,并記得在viewDidLoad中調用。

// 2.非容器類 可變對象
- (void)mutableObject {
    // 1.創建一個可變字符串。
    NSMutableString *mString = [NSMutableString stringWithString:@"github.com/pro648"];
    NSString *mStringCopy = [mString copy];
    NSMutableString *mutablemString = [mString copy];
    NSMutableString *mStringMutableCopy = [mString mutableCopy];
    
    // 2.在可變字符串后添加字符串。
    [mString appendString:@"AA"];
    [mutablemString appendString:@"BB"];  // 運行時,這一行會報錯。
    [mStringMutableCopy appendString:@"CC"];
    
    // 3.輸出指針指向的內存地址。
    NSLog(@"Memory location of \n mString = %p,\n mstringCopy = %p,\n mutablemString = %p,\n mStringMutableCopy = %p",mString, mStringCopy, mutablemString, mStringMutableCopy);
}

在上面代碼中,注釋2部分為可變字符串拼接字符串,運行到為mutablemString拼接字符串這一行代碼時,程序會崩潰,因為通過copy方法獲得的字符串是不可變字符串。所以在運行前要注釋掉這一行。

運行demo,可以看到控制臺輸出如下:

Memory location of 
 mString = 0x60000007bc00,
 mstringCopy = 0x600000051940,
 mutablemString = 0x6000000517c0,
 mStringMutableCopy = 0x60000007bec0

可以看到四個對象內存地址各不相同。所以,這里的copymutableCopy執行的均為深復制。

綜合上面兩個例子,我們可以得出這樣結論:

  • 對不可變對象執行copy操作,是指針復制,執行mutableCopy操作是內容復制。
  • 對可變對象執行copy操作和mutableCopy操作都是內容復制。

用代碼表示如下:

[immutableObject copy];         // 淺復制
[immutableObject mutableCopy];          // 深復制
[mutableObject copy];           // 深復制
[mutableObject mutableCopy] ;   // 深復制

2.2 容器類對象的深復制、淺復制

容器類對象指NSArrayNSDictionary等。容器類對象的深復制、淺復制如下圖所示:

CopyCollectionCopy.png

對于容器類,需要探討的是復制后容器內元素的變化,而非容器本身內存地址是否發生了變化。

2.2.1 容器類對象的淺復制

有許多方法可以對集合進行淺復制。當對集合進行淺復制時,將復制原始集合中元素指針到新的集合,即原始集合中元素引用計數加一。

<a id="YES">

在實現部分添加以下方法,并在viewDidLoad中調用該方法。

// 3.淺復制容器類對象。
- (void)shallowCopyCollections {
    // 1.創建一個不可變數組,數組內元素為可變字符串。
    NSMutableString *red = [NSMutableString stringWithString:@"Red"];
    NSMutableString *green = [NSMutableString stringWithString:@"Green"];
    NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
    NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
    
    // 2.進行淺復制。
    NSArray *myArray2 = [myArray1 copy];
    NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
    NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1 copyItems:NO];
    
    // 3.修改myArray2的第一個元素。
    NSMutableString *tempString = myArray2.firstObject;
    [tempString appendString:@"Color"];
    
    // 4.輸出四個數組內存地址及四個數組內容。
    NSLog(@"Memory location of \n myArray1 = %p, \n myArray2 %p, \n myMutableArray3 %p, \n myArray4 %p",myArray1, myArray2, myMutableArray3, myArray4);
    NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}

</a>

運行demo,可以看到控制臺輸出如下:

Memory location of 
 myArray1 = 0x60800004f240, 
 myArray2 0x60800004f240, 
 myMutableArray3 0x60800004ef40, 
 myArray4 0x60800004f090
Contents of 
 myArray1 (
    RedColor,
    Green,
    Blue
), 
 myArray2 (
    RedColor,
    Green,
    Blue
), 
 myMutableArray3 (
    RedColor,
    Green,
    Blue
), 
 myArray4 (
    RedColor,
    Green,
    Blue
)

可以看到myArray1myArray2數組內存地址相同,myMutableArray3myArray4與其它數組內存地址各不相同。這是因為mutableCopy的對象會被分配新的內存,alloc會為對象分配新的內存空間。

觀察數組內元素,發現修改myArray2數組內第一個元素,四個數組第一個元素都發生了改變,所以這里只進行了淺復制。

2.2.2 容器類對象的深復制

有兩種方式對容器類對象進行深復制:

  • 第一種方法是:使用initWithArray: copyItems:類型方法,其中,第二個參數為YES
  • 第二種方法是:使用歸檔、解檔。

下面先看如何使用initWithArray: copyItems:類型方法。使用該方法進行深復制時,第二個參數為YES。如果使用該方法對集合進行深復制,那么集合內每個元素都會收到copyWithZone: 消息,我們平常使用copymutableCopy方法時,系統會把copymutableCopy自動替換為copyWithZone:mutableCopyWithZone:。即copymutableCopy只是簡便方法。如果集合內元素遵守NSCopying協議,元素被復制到新的集合。如果集合內元素不遵守NSCopying協議,用這樣的方式進行深復制,會在運行時產生錯誤。

copyWithZone: 產生的是淺復制,所以,這種方法只能產生一層深復制 one-level-deep copy,如果集合內元素仍然是集合,則子集合內元素不會被深復制,只對子集合內元素指針進行復制。

如果集合內元素為不可變對象,發送copyWithZone:消息后進行指針復制,該對象仍然不可變,因此只進行指針復制即可滿足需求。

如果集合內元素為可變對象,發送copyWithZone:消息后進行的是內容復制,復制后該元素不可變,此時,完成了一層深復制。

上面代碼注釋2部分中,initWithArray: copyItems:第二個參數修改為YES,注釋4中輸出部分修改為輸出數組第一個元素內存地址。更新后如下:

// 4.容器類一層深復制
- (void)oneLevelDeepCopy {
    // 1.創建一個不可變數組,數組內元素為可變字符串。
    NSMutableString *red = [NSMutableString stringWithString:@"Red"];
    NSMutableString *green = [NSMutableString stringWithString:@"Green"];
    NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
    NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
    
    // 2.進行淺復制。
    NSArray *myArray2 = [myArray1 copy];
    NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
    NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1 copyItems:YES];
    
    // 3.修改myArray2的第一個元素。
    NSMutableString *tempString = myArray2.firstObject;
    [tempString appendString:@"Color"];
    
    // 4.輸出數組內第一個元素內存地址,輸出四個數組。
    NSLog(@"Memory location of \n myArray1.firstObject = %p, \n myArray2.firstObject %p, \n myMutableArray3.firstObject %p, \n myArray4.firstObject %p",myArray1.firstObject, myArray2.firstObject, myMutableArray3.firstObject, myArray4.firstObject);
    NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
}

運行demo,可以看到控制臺輸出如下:

Memory location of 
 myArray1.firstObject = 0x600000079980, 
 myArray2.firstObject 0x600000079980, 
 myMutableArray3.firstObject 0x600000079980, 
 myArray4.firstObject 0xa000000006465523
Contents of 
 myArray1 (
    RedColor,
    Green,
    Blue
), 
 myArray2 (
    RedColor,
    Green,
    Blue
), 
 myMutableArray3 (
    RedColor,
    Green,
    Blue
), 
 myArray4 (
    Red,
    Green,
    Blue
)

可以看到myArray4數組內第一個元素與其它數組第一個元素內存地址不同,即進行了一層深復制。

這種對集合進行深復制的方法,對其它類型集合也有效。如詞典中initWithDictionary: withItems:方法。

如果你的數組內元素是另一個數組,想要進行完全深復制,可以使用歸檔、解歸檔方法。使用該方法時,歸檔對象要遵守NSCoding協議。如果你對歸檔不熟悉,可以查看我的另一篇文章:數據存儲之歸檔解檔 NSKeyedArchiver NSKeyedUnarchiver

下面使用歸檔、解檔的方法進行完全深復制。

// 5.使用歸檔進行完全深復制。
- (void)trueDeepCopy {
    // 1.創建一個可變數組,數組第一個元素是另一個可變數組,第二個元素是另一個不可變數組。
    NSMutableString *hue = [NSMutableString stringWithString:@"hue"];
    NSMutableString *saturation = [NSMutableString stringWithString:@"saturation"];
    NSMutableString *brightness = [NSMutableString stringWithString:@"brightness"];
    NSMutableArray *hsbArray1 = [NSMutableArray arrayWithObjects:hue, saturation, brightness, nil];
    NSArray *hsbArray2 = [NSArray arrayWithObjects:hue, saturation, brightness, nil];
    NSMutableArray *hsbArray3 = [NSMutableArray arrayWithObjects:hsbArray1, hsbArray2, nil];
    
    // 2.通過歸檔、解檔進行完全深復制。
    NSData *dataArea = [NSKeyedArchiver archivedDataWithRootObject:hsbArray3];
    NSMutableArray *hsbArray4 = [NSKeyedUnarchiver unarchiveObjectWithData:dataArea];
    
    // 3.輸出hsbArray3和hsbArray4數組第一個元素內存地址。
    NSLog(@"Memory location of \n hsbArray3.firstObject = %p, \n hsbArray4.firstObject = %p",hsbArray3.firstObject, hsbArray4.firstObject);
}

上面代碼中,可變數組hsbArray3第一個元素是可變數組hsbArray1,第二個元素是不可變數組hsbArray2

使用歸檔、讀取歸檔方法深復制后,在控制臺輸出hsbArray3hsbArray4第一個元素內存地址。輸出如下:

Memory location of 
 hsbArray3.firstObject = 0x60000004b100, 
 hsbArray4.firstObject = 0x60000004b1f0

可以看到hsbArray3hsbArray4數組內元素內存地址不同,即進行了一層深復制。

trueDeepCopy方法內,繼續為hsbArray4數組內第一個元素tempArray1可變數組添加字符串對象。為hsbArray4第二個元素hsbArray2數組添加字符串對象。最后輸出hsbArray3hsbArray4數組內容。

// 5.使用歸檔進行完全深復制。
- (void)trueDeepCopy {
    ...
    // 4.為hsbArray4第一個元素添加字符串。
    NSMutableArray *tempArray1 = hsbArray4.firstObject;
    [tempArray1 addObject:@"hsb"];
    
    // 5.hsbArray4第二個元素是hsbArray2,而hsbArray2是不可變數組,這一步將產生錯誤。
//    NSMutableArray *tempArray2 = hsbArray4[1];
//    [tempArray2 addObject:@"Color"];
    
    // 6.輸出數組內容。
    NSLog(@"Contents of \n hsbArray3 %@, \n hsbArray4 %@",hsbArray3, hsbArray4);
}

因為hsbArray4第二個元素是hsbArray2副本,而hsbArray2是不可變數組,這一步將產生錯誤。注釋掉5部分代碼后,控制臺輸出如下:

Contents of 
 hsbArray3 (
        (
        hue,
        saturation,
        brightness
    ),
        (
        hue,
        saturation,
        brightness
    )
), 
 hsbArray4 (
        (
        hue,
        saturation,
        brightness,
        hsb
    ),
        (
        hue,
        saturation,
        brightness
    )
)

可以看到只有hsbArray4數組第一個元素內對象發生了改變,所以,使用歸檔、讀取歸檔進行的是完全深復制。

復制集合時,該集合、集合內元素的可變性可能會受到影響。每種方法對任意深度集合中對象的可變性有稍微不同的影響。

  1. copyWithZone:創建對象的最外層 surface level不可變,所有更深層次對象的可變性不變。
  2. mutableCopyWithZone:創建對象的最外層 surface level可變,所有更深層次對象的可變性不變。
  3. initWithArray: copyItems:第二個參數為NO,此時,所創建數組最外層可變性與初始化的可變性相同,所有更深層級對象可變性不變。
  4. initWithArray: copyItems:第二個參數為YES,此時,所創建數組最外層可變性與初始化的可變性相同,下一層級是不可變的,所有更深層級對象可變性不變。
  5. 歸檔、解檔復制的集合,所有層級的可變性與原始對象相同。

2.3 自定義對象的深復制、淺復制

自定義的類需要我們自己實現NSCopyingNSMutableCopying協議,這樣才可以調用copymutableCopy方法。

添加父類為NSObject,名稱為Person的類。進入Person.h,添加以下屬性和方法,同時讓該類遵守NSCopying協議。

@interface Person : NSObject <NSCopying>

@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSUInteger age;

- (void)setName:(NSString *)name withAge:(NSUInteger)age;

@end

NSCopying協議只有一個必須實現的copyWithZone:方法。進入Person.m,實現屬性中setName: withAge:方法和copyWithZone:方法。

- (void)setName:(NSString *)name withAge:(NSUInteger)age {
    _name = name;
    _age = age;
}

- (id)copyWithZone:(NSZone *)zone {
    Person *person = [[Person allocWithZone:zone] init];
    [person setName:self.name withAge:self.age];
    return person;
}

如果Person類會被繼承,那么copyWithZone:方法將被繼承,這時應將上面的

Person *person = [[Person allocWithZone:zone] init];

替換為

id person = [[[self class] allocWithZone: zone] init];

這樣,可以從該類分配一個新對象,而這個類是copy的接收者。例如:如果Person類有一個名為NewPerson的子類,那么應該在繼承的方法中分配了新的NewPerson對象,而不是Person對象。

如果Person類的父類也實現了NSCopying協議,那么應該先調用父類的copy方法,以復制繼承來的實例變量。如果需要實現可變復制,還需要遵守NSMutableCopying協議。

進入ViewController.m,導入Person.h,在實現部分添加以下方法,并在viewDidLoad中調用。

// 6.自定義類的復制
- (void)copyCustomClass {
    Person *person = [[Person alloc] init];
    [person setName:@"A" withAge:1];
    
    Person *personCopy = [person copy];
    [personCopy setName:@"B" withAge:2];
    // 斷點位置
}

copyCustomClass方法最后一行設置斷點,運行demo,可以看到控制臺輸出如下圖:

CopyBreakpoint.png

通過上圖可以看到,personpersonCopy內存地址不同,且personpersonCopy中的nameage屬性的值各不相同。

3. 修改指針指向

現在看最后一個示例,在ViewController.m的實現部分添加以下方法,并在viewDidLoad中調用該方法。

// 7.更改指針指向地址
- (void)pointToAnotherMemoryAddress {
    // 1.指針a、b同時指向字符串pro
    NSString *a = @"pro";
    NSString *b = a;
    NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
    // 斷點1位置
    
    // 2.指針a指向字符串pro648
    a = @"pro648";
    NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
    // 斷點2位置
}

上述代碼分步說明如下:

  1. 指針a指向字符串pro內存地址,b = a表示ba的淺復制,指針b也指向字符串pro內存地址。NSLog語句可以說明這一問題。也可以在注釋斷點1位置所在行設置斷點,查看指針指向的內容。
  2. 修改指針a指向字符串pro648,此時輸出ab指針所指的向內存地址。并在斷點2位置所在行設置斷點。運行后可以看到控制臺輸出如下:
CopyModifyPointer.png

可以看到,ab指針指向不同內存地址,a指向字符串pro648b指向字符串pro

這是因為

a = @"pro648";

等同于

a = [[NSString alloc] initWithString:@"pro648"];

a = @"pro648"修改了a指針指向的內存地址,而b指針依然指向之前的內存地址。

NSStringNSMutableString的區別主要是:NSMutableString對象所指向內存地址中的內容可以被修改,而NSString對象所指向內存地址中內容不能被修改,但NSString對象不是常量,可以通過為NSString對象重新分配一塊內存來改變其指向的內容。

總結

淺拷貝盡可能少的復制對象,集合的淺拷貝副本只是集合結構的副本,而不是集合內元素的副本。淺拷貝獲得的副本與原始集合共享各個元素。

深拷貝復制一切內容。集合的深拷貝會復制集合的結構和元素,但如果集合內元素也是集合,則涉及到一層深拷貝、完全深拷貝。

Demo名稱:copy&mutableCopy
源碼地址:https://github.com/pro648/BasicDemos-iOS

參考資料:

  1. Copying Collections
  2. Object copying
  3. Deep Copy and Shallow Copy in Objective C
  4. What is the difference between a deep copy and a shallow copy?

歡迎更多指正:https://github.com/pro648/tips/wiki

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

推薦閱讀更多精彩內容