AFNetworking源碼探究(十一) —— 數(shù)據(jù)解析之子類中協(xié)議方法的實現(xiàn)(二)

版本記錄

版本號 時間
V1.0 2018.03.01

前言

我們做APP發(fā)起網(wǎng)絡(luò)請求,都離不開一個非常有用的框架AFNetworking,可以說這個框架的知名度已經(jīng)超過了蘋果的底層網(wǎng)絡(luò)請求部分,很多人可能不知道蘋果底層是如何發(fā)起網(wǎng)絡(luò)請求的,但是一定知道AFNetworking,接下來幾篇我們就一起詳細(xì)的解析一下這個框架。感興趣的可以看上面寫的幾篇。
1. AFNetworking源碼探究(一) —— 基本介紹
2. AFNetworking源碼探究(二) —— GET請求實現(xiàn)之NSURLSessionDataTask實例化(一)
3. AFNetworking源碼探究(三) —— GET請求實現(xiàn)之任務(wù)進(jìn)度設(shè)置和通知監(jiān)聽(一)
4. AFNetworking源碼探究(四) —— GET請求實現(xiàn)之代理轉(zhuǎn)發(fā)思想(一)
5. AFNetworking源碼探究(五) —— AFURLSessionManager中NSURLSessionDelegate詳細(xì)解析(一)
6. AFNetworking源碼探究(六) —— AFURLSessionManager中NSURLSessionTaskDelegate詳細(xì)解析(一)
7. AFNetworking源碼探究(七) —— AFURLSessionManager中NSURLSessionDataDelegate詳細(xì)解析(一)
8. AFNetworking源碼探究(八) —— AFURLSessionManager中NSURLSessionDownloadDelegate詳細(xì)解析(一)
9. AFNetworking源碼探究(九) —— AFURLSessionManagerTaskDelegate中三個轉(zhuǎn)發(fā)代理方法詳細(xì)解析(一)
10. AFNetworking源碼探究(十) —— 數(shù)據(jù)解析之?dāng)?shù)據(jù)解析架構(gòu)的分析(一)

回顧

上一篇我們主要介紹了有關(guān)數(shù)據(jù)解析類和協(xié)議,以及實現(xiàn)解析的架構(gòu),這一篇就分開講述各個類是如何實現(xiàn)對應(yīng)的數(shù)據(jù)解析的。


AFURLResponseSerialization協(xié)議

我們先看一下這個協(xié)議的接口

/**
 The `AFURLResponseSerialization` protocol is adopted by an object that decodes data into a more useful object representation, according to details in the server response. Response serializers may additionally perform validation on the incoming response and data.

 For example, a JSON response serializer may check for an acceptable status code (`2XX` range) and content type (`application/json`), decoding a valid JSON response into an object.
 */
@protocol AFURLResponseSerialization <NSObject, NSSecureCoding, NSCopying>

/**
 The response object decoded from the data associated with a specified response.

 @param response The response to be processed.
 @param data The response data to be decoded.
 @param error The error that occurred while attempting to decode the response data.

 @return The object decoded from the specified response data.
 */
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
                           data:(nullable NSData *)data
                          error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;

@end

根據(jù)服務(wù)器響應(yīng)中的細(xì)節(jié),AFURLResponseSerialization協(xié)議被一個對象采用,該對象將數(shù)據(jù)解碼為更有用的對象表示。 Response序列化器還可以對傳入響應(yīng)和數(shù)據(jù)執(zhí)行驗證。例如,JSON響應(yīng)序列化器可以檢查可接受的狀態(tài)碼(2XX范圍)和內(nèi)容類型(application / json),將有效的JSON響應(yīng)解碼成對象


AFHTTPResponseSerializer

這個是所有其他解析類的父類,他遵守上面的AFURLResponseSerialization協(xié)議。

我們看一下協(xié)議在這個類中的實現(xiàn)

- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    [self validateResponse:(NSHTTPURLResponse *)response data:data error:error];

    return data;
}

這里調(diào)用了一個方法,進(jìn)行了指定response和數(shù)據(jù)的驗證。

/**
 Validates the specified response and data.

 In its base implementation, this method checks for an acceptable status code and content type. Subclasses may wish to add other domain-specific checks.

 @param response The response to be validated.
 @param data The data associated with the response.
 @param error The error that occurred while attempting to validate the response.

 @return `YES` if the response is valid, otherwise `NO`.
 */
- (BOOL)validateResponse:(nullable NSHTTPURLResponse *)response
                    data:(nullable NSData *)data
                   error:(NSError * _Nullable __autoreleasing *)error;

在其基本實現(xiàn)中,此方法檢查可接受的狀態(tài)碼和內(nèi)容類型。 子類可能希望添加其他域特定的檢查。

下面我們看一下驗證過程,主要對應(yīng)下面這段代碼

- (BOOL)validateResponse:(NSHTTPURLResponse *)response
                    data:(NSData *)data
                   error:(NSError * __autoreleasing *)error
{
    BOOL responseIsValid = YES;
    NSError *validationError = nil;

    if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
        if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
            !([response MIMEType] == nil && [data length] == 0)) {

            if ([data length] > 0 && [response URL]) {
                NSMutableDictionary *mutableUserInfo = [@{
                                                          NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
                                                          NSURLErrorFailingURLErrorKey:[response URL],
                                                          AFNetworkingOperationFailingURLResponseErrorKey: response,
                                                        } mutableCopy];
                if (data) {
                    mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
                }

                validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
            }

            responseIsValid = NO;
        }

        if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
            NSMutableDictionary *mutableUserInfo = [@{
                                               NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
                                               NSURLErrorFailingURLErrorKey:[response URL],
                                               AFNetworkingOperationFailingURLResponseErrorKey: response,
                                       } mutableCopy];

            if (data) {
                mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
            }

            validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);

            responseIsValid = NO;
        }
    }

    if (error && !responseIsValid) {
        *error = validationError;
    }

    return responseIsValid;
}

這是一個具有返回值類型為BOOL的方法,但是這里對于返回值并沒有使用。

(a) 最外層的判斷

最外層的判斷主要是

if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) 

就是如果response不是nil,并且response的類型是NSHTTPURLResponse

(b) 第一個if判斷

在上面最外層判斷的內(nèi)部是兩個if判斷,根據(jù)不同的條件判斷數(shù)據(jù)是否有效以及在無效時應(yīng)該拋出怎樣的異常。

主要對應(yīng)下面這段代碼

if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
    !([response MIMEType] == nil && [data length] == 0)) {

    if ([data length] > 0 && [response URL]) {
        NSMutableDictionary *mutableUserInfo = [@{
                                                  NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
                                                  NSURLErrorFailingURLErrorKey:[response URL],
                                                  AFNetworkingOperationFailingURLResponseErrorKey: response,
                                                } mutableCopy];
        if (data) {
            mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
        }

        validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
    }

    responseIsValid = NO;
}

responseIsValid = NO,我們可以看出來,這一定是拋出異常,沒有驗證通過的,但是為什么拋出異常呢?我們看一下。

如果有接受數(shù)據(jù)類型,如果不匹配response,而且響應(yīng)類型不為空,數(shù)據(jù)長度不為0。接著進(jìn)行判斷,如果數(shù)據(jù)長度大于0,而且有響應(yīng)URL,那么就生成mutableUserInfo信息,調(diào)用下面的方法生成錯誤信息。

static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) {
    if (!error) {
        return underlyingError;
    }

    if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) {
        return error;
    }

    NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy];
    mutableUserInfo[NSUnderlyingErrorKey] = underlyingError;

    return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo];
}

這里要注意,NSURLResponse中這個MIMEType屬性。

/*! 
    @abstract Returns the MIME type of the receiver.
    @discussion The MIME type is based on the information provided
    from an origin source. However, that value may be changed or
    corrected by a protocol implementation if it can be determined
    that the origin server or source reported the information
    incorrectly or imprecisely. An attempt to guess the MIME type may
    be made if the origin source did not report any such information.
    @result The MIME type of the receiver.

     @abstract返回接收者的MIME類型。
     @討論MIME類型基于提供的信息
     來源。 但是,該值可能會改變或
     如果可以確定原始服務(wù)器或來源報告了信息
     不正確或不準(zhǔn)確,則由協(xié)議實施糾正
     。如果原始資料來源未報告任何此類信息, 
     可以嘗試猜測MIME類型
     @result接收者的MIME類型。
*/
@property (nullable, readonly, copy) NSString *MIMEType;

(c) 第二個if判斷

主要對應(yīng)下邊這段代碼

if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
    NSMutableDictionary *mutableUserInfo = [@{
                                       NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
                                       NSURLErrorFailingURLErrorKey:[response URL],
                                       AFNetworkingOperationFailingURLResponseErrorKey: response,
                               } mutableCopy];

    if (data) {
        mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
    }

    validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);

    responseIsValid = NO;
}

判斷自己可接受的狀態(tài)碼,如果和response的狀態(tài)碼不匹配,則進(jìn)入if塊,生成錯誤和標(biāo)識。

(d) error和responseIsValid判斷

主要是下面一段代碼

if (error && !responseIsValid) {
    *error = validationError;
}

這里,如果error不為空,并且responseIsValid == NO,也就是說上面兩個if判斷至少走過了一個,這時候給error進(jìn)行了賦值。

*error = validationError;

這個方法就是來判斷返回數(shù)據(jù)與咱們使用的解析器是否匹配,需要解析的狀態(tài)碼是否匹配。

兩個屬性值,一個acceptableContentTypes,一個acceptableStatusCodes,兩者在初始化的時候有給默認(rèn)值,如果給acceptableContentTypes定義了不匹配的類型,那么數(shù)據(jù)仍舊會解析錯誤。


AFJSONResponseSerializer

AFJSONResponseSerializerAFHTTPResponseSerializer的一個子類,用于驗證和解碼JSON響應(yīng)。

默認(rèn)情況下,AFJSONResponseSerializer接受以下MIME類型,其中包括官方標(biāo)準(zhǔn),application / json以及其他常用類型:

  • application / json
  • text / json
  • text / javascript

我們看一下協(xié)議在這個類中的實現(xiàn)

- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
        if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
            return nil;
        }
    }

    id responseObject = nil;
    NSError *serializationError = nil;
    // Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.
    // See https://github.com/rails/rails/issues/1742
    BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];
    if (data.length > 0 && !isSpace) {
        responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
    } else {
        return nil;
    }

    if (self.removesKeysWithNullValues && responseObject) {
        responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
    }

    if (error) {
        *error = AFErrorWithUnderlyingError(serializationError, *error);
    }

    return responseObject;
}

下面就看一下,這里都做了a什么

(a) 有效性的驗證

我們看一下如何進(jìn)行有效性的驗證的。

if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
    if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
        return nil;
    }
}

就是調(diào)用我們上面解析的,驗證有效性的方法。如果無效,進(jìn)入判斷,接著if判斷,如果error為空,或者有錯誤,去函數(shù)里判斷。

static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) {
    if ([error.domain isEqualToString:domain] && error.code == code) {
        return YES;
    } else if (error.userInfo[NSUnderlyingErrorKey]) {
        return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain);
    }

    return NO;
}

數(shù)據(jù)無效后,返回nil。

(b) 幾個條件判斷

下面就是幾個條件判斷,滿足的話直接序列化對應(yīng)的JSON數(shù)據(jù),不滿足的話返回nil。

id responseObject = nil;
NSError *serializationError = nil;
// Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.
// See https://github.com/rails/rails/issues/1742
BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];
if (data.length > 0 && !isSpace) {
    responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
} else {
    return nil;
}

if (self.removesKeysWithNullValues && responseObject) {
    responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}

if (error) {
    *error = AFErrorWithUnderlyingError(serializationError, *error);
}
  • 第一組條件判斷
BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];
if (data.length > 0 && !isSpace) {
    responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
} else {
    return nil;
}

這里首先判斷數(shù)據(jù)是否為空,利用isEqualToData:方法進(jìn)行判斷,如果不為空,并且數(shù)據(jù)長度大于0,那么就進(jìn)行JSON數(shù)據(jù)的序列化。

responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];

如果不滿足上面條件就返nil。

  • 第二組條件判斷
if (self.removesKeysWithNullValues && responseObject) {
    responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}

這里有一個屬性

/**
 Whether to remove keys with `NSNull` values from response JSON. Defaults to `NO`.
 */
@property (nonatomic, assign) BOOL removesKeysWithNullValues;

是否從響應(yīng)JSON中刪除具有NSNull值的鍵。 默認(rèn)為NO。如果需要移除這個鍵并且上面的responseObject已經(jīng)序列化成功,那么就要調(diào)用下面的函數(shù)移除具有NSNull值的鍵。

static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {
    if ([JSONObject isKindOfClass:[NSArray class]]) {
        NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];
        for (id value in (NSArray *)JSONObject) {
            [mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
        }

        return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
    } else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
        NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
        for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
            id value = (NSDictionary *)JSONObject[key];
            if (!value || [value isEqual:[NSNull null]]) {
                [mutableDictionary removeObjectForKey:key];
            } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
                mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);
            }
        }

        return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
    }

    return JSONObject;
}

這里有一個屬性和枚舉,一起來看一下

/**
 Options for reading the response JSON data and creating the Foundation objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONReadingOptions". `0` by default.
 */
用于讀取響應(yīng)JSON數(shù)據(jù)并創(chuàng)建Foundation對象的選項。 有關(guān)可能的值,請參閱“NSJSONSerialization”文檔部分“NSJSONReadingOptions”。 默認(rèn)為'0'

@property (nonatomic, assign) NSJSONReadingOptions readingOptions;

typedef NS_OPTIONS(NSUInteger, NSJSONReadingOptions) {
    NSJSONReadingMutableContainers = (1UL << 0),
    NSJSONReadingMutableLeaves = (1UL << 1),
    NSJSONReadingAllowFragments = (1UL << 2)
} API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
  • 第三組條件判斷
if (error) {
    *error = AFErrorWithUnderlyingError(serializationError, *error);
}

如果error不為空,那么就利用函數(shù)AFErrorWithUnderlyingError生成NSError對象并賦值。

后記

本篇講述了一個AFURLResponseSerialization協(xié)議以及AFHTTPResponseSerializerAFJSONResponseSerializer類中父類那個協(xié)議方法的實現(xiàn)。

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

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